spaceship/enemies/EnemySpawner.gd
Loïc Wisniewski 1a2a27b6e8 #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>
2026-08-23 21:38:09 +02:00

66 lines
3 KiB
GDScript

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)