extends Node2D # The wave director (#100 / #97). Keeps its old node name on purpose: it is # still "the thing that puts enemies in the world", and every test that # wanted a quiet world already says # `main.get_node("EnemySpawner").set_process(false)`. Renaming it would have # changed 88 test files to say the same thing differently. # # What it no longer does is trickle. It used to spawn one enemy every 3-6 # seconds for ever, so there was no rhythm to the danger and nothing to # prepare for. Now the CLOCK lives in GameState (which survives the trip to # a planet surface) and this node only reacts to it: when a wave turns # active, it puts the wave in the world; when the wave is dead, it says so # and the quiet starts again. @export var spawn_margin: float = 150.0 # Waves arrive as a ring around the fleet rather than from one bearing: a # wave that all came from the left would be dodged by flying right. @export var wave_spread: float = 0.55 @onready var fleet: Node2D = get_node("../Fleet") var enemy_scene: PackedScene = preload("res://enemies/Enemy.tscn") # The enemies this director launched, so "the wave is dead" is a question # about THIS wave and not about every enemy on the map - a planet garrison # or an enemy mothership's own spawns must not hold the next wave back. var wave_enemies: Array = [] func _process(_delta: float) -> void: wave_enemies = wave_enemies.filter(func(e): return is_instance_valid(e)) if GameState.wave_pending: _launch_wave() return if GameState.wave_phase == GameState.WAVE_PHASE_ACTIVE and wave_enemies.is_empty(): GameState.wave_cleared() # Public so a test can put a wave in the world without waiting out the peace. func _launch_wave() -> void: if fleet == null or fleet.ships.is_empty(): # Nothing to attack. Leave it pending rather than spending the wave # on an empty map - the fleet-wipe safety net rebuilds within a frame # and the wave arrives then. return GameState.wave_pending = false var composition: PackedStringArray = GameState.wave_composition(fleet.ships.size()) var visible_rect: Rect2 = ViewUtils.get_visible_world_rect(self) var radius: float = visible_rect.size.length() / 2.0 + spawn_margin var centre: Vector2 = fleet.get_centroid() var bearing: float = randf() * TAU for i in range(composition.size()): # Spread around one bearing rather than scattered at random: a wave # should read as an attack arriving, not as the map filling up. var angle: float = bearing + randf_range(-wave_spread, wave_spread) \ + TAU * float(i) / float(maxi(composition.size(), 1)) * wave_spread 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)