diff --git a/enemies/Enemy.gd b/enemies/Enemy.gd index eb99a08..86cb26a 100644 --- a/enemies/Enemy.gd +++ b/enemies/Enemy.gd @@ -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 diff --git a/enemies/EnemyPursuitComponent.gd b/enemies/EnemyPursuitComponent.gd index 6558c5f..f8f752f 100644 --- a/enemies/EnemyPursuitComponent.gd +++ b/enemies/EnemyPursuitComponent.gd @@ -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 diff --git a/enemies/EnemySpawner.gd b/enemies/EnemySpawner.gd index 1ca5a12..2554887 100644 --- a/enemies/EnemySpawner.gd +++ b/enemies/EnemySpawner.gd @@ -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) diff --git a/enemies/EnemyStandoffComponent.gd b/enemies/EnemyStandoffComponent.gd index d1fd32e..8225357 100644 --- a/enemies/EnemyStandoffComponent.gd +++ b/enemies/EnemyStandoffComponent.gd @@ -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 diff --git a/enemies/EnemyTargets.gd b/enemies/EnemyTargets.gd index 31a6456..60df0dc 100644 --- a/enemies/EnemyTargets.gd +++ b/enemies/EnemyTargets.gd @@ -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")) diff --git a/tests/test_waves_attack_buildings.gd b/tests/test_waves_attack_buildings.gd index 42a7aa4..daee3c0 100644 --- a/tests/test_waves_attack_buildings.gd +++ b/tests/test_waves_attack_buildings.gd @@ -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