A wave is an attack on what the player has built, not a hazard sitting in space waiting to be flown near. Wave enemies now ignore the detection radius their components otherwise respect and close on the nearest building from wherever they arrived. The flag is per INSTANCE rather than per definition: the same chaser is a wave when the director launches it and a garrison when a map author places it, and a planet garrison that chased the player across the system would be a very different thing from what the editor is used to place. The test pins both halves - a wave chaser 6000px out closed on a crafter, and an identically built one nobody sent held its station. What it fights on the way is unchanged: EnemyTargets already prefers a vessel inside 520px over a building further off, and already searches the player_ships group, so the fleet and an outpost's garrison both count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
3.5 KiB
GDScript
109 lines
3.5 KiB
GDScript
extends Area2D
|
|
|
|
signal destroyed
|
|
|
|
const ENEMY_DEFINITIONS = preload("res://enemies/EnemyDefinitions.gd")
|
|
|
|
@export var enemy_id: String = "chaser"
|
|
# #113: this one came in with a WAVE, and a wave is an attack on what the
|
|
# player has built rather than a hazard sitting in space. It converges on the
|
|
# nearest structure from wherever it spawned, ignoring the detection range
|
|
# every component otherwise respects.
|
|
#
|
|
# Per instance rather than per definition: the same chaser is a wave when the
|
|
# director launches it and a garrison when a map author places it, and a
|
|
# planet garrison that chased the player across the system would be a very
|
|
# different thing from what the editor was used to place.
|
|
@export var relentless: bool = false
|
|
|
|
var definition: Resource
|
|
var hp: int
|
|
|
|
# Mirrors Ship.gd's own max_speed/acceleration: copied from the definition
|
|
# once, then mutable, so a test/preview can pin an enemy in place (e.g.
|
|
# `enemy.speed = 0.0`) without needing to know about definitions at all.
|
|
var speed: float
|
|
var acceleration: float
|
|
var rotation_speed: float
|
|
var max_hp: int
|
|
|
|
|
|
func _ready() -> void:
|
|
add_to_group("enemies")
|
|
definition = ENEMY_DEFINITIONS.get_definition(enemy_id)
|
|
speed = definition.speed
|
|
acceleration = definition.acceleration
|
|
rotation_speed = definition.rotation_speed
|
|
max_hp = definition.max_hp
|
|
hp = max_hp
|
|
for component_scene in definition.component_scenes:
|
|
var component: Node = component_scene.instantiate()
|
|
add_child(component)
|
|
if component.has_method("setup"):
|
|
component.setup()
|
|
queue_redraw()
|
|
|
|
|
|
func get_component(capability: String) -> Node:
|
|
for child in get_children():
|
|
if child.get("capability_id") == capability:
|
|
return child
|
|
return null
|
|
|
|
|
|
func has_capability(capability: String) -> bool:
|
|
return get_component(capability) != null
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
for child in get_children():
|
|
if child.has_method("tick"):
|
|
child.tick(delta)
|
|
|
|
|
|
# Called by a player vessel this has just collided with. Whether the impact
|
|
# destroys it is the definition's call - see EnemyDefinition.dies_on_contact.
|
|
# Routed through take_damage so it dies exactly as it dies to gunfire: same
|
|
# explosion, same `destroyed` signal, same bookkeeping for whatever is
|
|
# listening.
|
|
func collide_with_player_vessel() -> void:
|
|
_resolve_ram()
|
|
|
|
|
|
# #101: a structure resolves an impact by the same rule - whether it costs
|
|
# the enemy its life is the definition's call, not the target's type.
|
|
func collide_with_player_building() -> void:
|
|
_resolve_ram()
|
|
|
|
|
|
func _resolve_ram() -> void:
|
|
if definition != null and bool(definition.dies_on_contact):
|
|
take_damage(hp)
|
|
|
|
|
|
# What one strike against a structure takes off it. Data on the definition,
|
|
# for the same reason dies_on_contact is: a lancer arriving at 950px/s is not
|
|
# a sniper drifting into a wall.
|
|
func contact_damage() -> int:
|
|
return 6 if definition == null else int(definition.contact_damage)
|
|
|
|
|
|
func take_damage(amount: int) -> void:
|
|
hp -= amount
|
|
if hp <= 0:
|
|
EnemyExplosionVFX.spawn(get_tree().root, position)
|
|
destroyed.emit()
|
|
queue_free()
|
|
|
|
|
|
func _draw() -> void:
|
|
# Drawn through EnemyDefinitions.draw_definition, the same routine the
|
|
# map editor's markers and palette use, so a definition cannot end up
|
|
# looking one way where it is placed and another where it is fought.
|
|
if definition == null:
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0),
|
|
]), Color(1.0, 0.15, 0.2), 1.0)
|
|
return
|
|
# Constant glow - see Ship.gd. Nothing animates, so no per-frame redraw.
|
|
ENEMY_DEFINITIONS.draw_definition(self, definition, definition.glow_color)
|