#113: a wave converges on the nearest structure from any distance
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>
This commit is contained in:
parent
7a78d1b1a6
commit
1a2a27b6e8
6 changed files with 68 additions and 3 deletions
|
|
@ -5,6 +5,16 @@ 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
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@ func tick(delta: float) -> void:
|
|||
# #101: a structure is a target now, not scenery. EnemyTargets keeps a
|
||||
# ship in the enemy's face ahead of a building further off.
|
||||
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||
if target == null \
|
||||
or enemy.global_position.distance_to(target.global_position) > detection_range:
|
||||
if target == null:
|
||||
return
|
||||
# #113: a wave enemy closes on what the player built from wherever it
|
||||
# arrived. Everything else keeps the radius that stops it reading as
|
||||
# omniscient (#69).
|
||||
if not EnemyTargets.ignores_range(enemy) \
|
||||
and enemy.global_position.distance_to(target.global_position) > detection_range:
|
||||
return
|
||||
var to_target: Vector2 = target.global_position - enemy.global_position
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ func _launch_wave() -> void:
|
|||
var enemy: Node2D = enemy_scene.instantiate()
|
||||
# Before add_child: Enemy._ready() resolves enemy_id into a definition.
|
||||
enemy.enemy_id = composition[i]
|
||||
# #113: a wave is an attack on what the player has built. It converges
|
||||
# on the nearest structure from wherever it spawned rather than
|
||||
# waiting to be flown near.
|
||||
enemy.relentless = true
|
||||
enemy.position = centre + Vector2(cos(angle), sin(angle)) * radius
|
||||
get_parent().add_child(enemy)
|
||||
wave_enemies.append(enemy)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ func tick(delta: float) -> void:
|
|||
return
|
||||
var to_target: Vector2 = target.global_position - enemy.global_position
|
||||
var distance: float = to_target.length()
|
||||
if distance < 1.0 or distance > detection_range:
|
||||
if distance < 1.0:
|
||||
return
|
||||
# #113: see EnemyPursuitComponent - a wave arrives with somewhere to be.
|
||||
if not EnemyTargets.ignores_range(enemy) and distance > detection_range:
|
||||
return
|
||||
var direction: Vector2 = to_target / distance
|
||||
var retreat_range: float = preferred_range * retreat_range_factor
|
||||
|
|
|
|||
|
|
@ -64,3 +64,10 @@ static func nearest_target(node: Node, from: Vector2) -> Node2D:
|
|||
|
||||
static func is_building(target: Node) -> bool:
|
||||
return target != null and target.is_in_group("buildings")
|
||||
|
||||
|
||||
# #113: whether this enemy may act on a target however far away it is.
|
||||
# Asked here rather than read directly, so the three movement components
|
||||
# cannot drift into disagreeing about what "relentless" means.
|
||||
static func ignores_range(enemy: Node) -> bool:
|
||||
return enemy != null and bool(enemy.get("relentless"))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ extends SceneTree
|
|||
# one hit on entry and then sitting there harmlessly
|
||||
# E. an enemy projectile damages a structure
|
||||
# F. enough of it levels the building, and the node goes with the record
|
||||
# G. #113: a WAVE enemy converges on the nearest structure from any
|
||||
# distance, where one placed by a map author keeps the detection radius
|
||||
# that stops it reading as omniscient
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
|
|
@ -35,6 +38,11 @@ var stage: int = 0
|
|||
var elapsed: float = 0.0
|
||||
var hp_watermark: int = 0
|
||||
var destroyed_fired: bool = false
|
||||
var roamer: Node2D
|
||||
var raider: Node2D
|
||||
var far_id: int = 0
|
||||
var roamer_from: float = 0.0
|
||||
var raider_from: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -198,6 +206,34 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the crafter node outlived the record")
|
||||
print("the crafter came down, and its node went with it")
|
||||
|
||||
# --- G. a wave arrives with somewhere to be -----------------
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(0.0, 0.0))
|
||||
far_id = int(game_state.buildings[-1].id)
|
||||
# Well past every component's detection_range.
|
||||
var far_away: Vector2 = Vector2(6000.0, 0.0)
|
||||
roamer = _spawn_enemy("chaser", far_away)
|
||||
raider = _spawn_enemy("chaser", far_away + Vector2(0.0, 400.0))
|
||||
raider.relentless = true
|
||||
roamer_from = roamer.global_position.distance_to(Vector2.ZERO)
|
||||
raider_from = raider.global_position.distance_to(Vector2.ZERO)
|
||||
elapsed = 0.0
|
||||
stage = 6
|
||||
return false
|
||||
6:
|
||||
if elapsed < 1.5:
|
||||
return false
|
||||
if not is_instance_valid(raider) or not is_instance_valid(roamer):
|
||||
return _fail("one of the two test chasers is gone")
|
||||
var raider_now: float = raider.global_position.distance_to(Vector2.ZERO)
|
||||
var roamer_now: float = roamer.global_position.distance_to(Vector2.ZERO)
|
||||
if raider_now >= raider_from - 100.0:
|
||||
return _fail("a wave chaser %.0fpx from a crafter did not close on it" % raider_from)
|
||||
if roamer_now < roamer_from - 20.0:
|
||||
return _fail("a chaser nobody sent moved %.0fpx toward a crafter far outside its detection range" \
|
||||
% (roamer_from - roamer_now))
|
||||
print("wave chaser closed %.0fpx from %.0f away; a placed one held station" \
|
||||
% [raider_from - raider_now, raider_from])
|
||||
|
||||
print("TEST_WAVES_ATTACK_BUILDINGS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
Loading…
Reference in a new issue