class_name EnemySpawnerComponent extends Node # Continuously spawns another enemy type beside its host, at a fixed # interval, capped at max_alive concurrently. The spawned enemies are # added as children of the spawner's OWN PARENT - not the spawner itself - # so killing the spawner never frees its already-spawned children (they # are siblings, not descendants) and so a surface-hosted spawner keeps its # spawns under the same host a `../Fleet`-style path assumption would miss # (see #44's note on why that hazard exists for anything parented under a # planet surface's PlanetSetupHost rather than directly under Main.tscn). var capability_id: String = "spawner" @export var spawn_interval: float = 4.0 @export var spawned_enemy_id: String = "chaser" @export var max_alive: int = 6 @export var spawn_radius: float = 90.0 var spawn_timer: float = 0.0 var spawned: Array = [] var enemy_scene: PackedScene = preload("res://Enemy.tscn") func tick(delta: float) -> void: spawned = spawned.filter(func(node): return is_instance_valid(node)) if spawned.size() >= max_alive: return spawn_timer -= delta if spawn_timer > 0.0: return spawn_timer = spawn_interval var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet") if fleet == null or fleet.ships.is_empty(): return var spawner: Node2D = get_parent() var host: Node = spawner.get_parent() if host == null: return var angle: float = randf() * TAU var offset: Vector2 = Vector2(cos(angle), sin(angle)) * spawn_radius var new_enemy: Node2D = enemy_scene.instantiate() new_enemy.enemy_id = spawned_enemy_id # spawner.position is already expressed in `host`'s local space, and # new_enemy is about to be added to that same `host` - unlike # EnemyRangedWeaponComponent's projectile, which reparents to root and # needs global_position, there is no space conversion needed here. new_enemy.position = spawner.position + offset host.add_child(new_enemy) spawned.append(new_enemy)