extends Node2D ## Keeps a floor count of grunts alive, spawning at the arena's inset ## edges. Endless trickle — the tracer needs targets, not waves. signal grunt_spawned(grunt: Node) const GRUNT := preload("res://scenes/grunt.tscn") const BURSTER := preload("res://scenes/burster.tscn") @export var target_count := 8 @export var spawn_interval := 0.8 ## Every Nth spawn is a burster. @export var burster_every := 4 @export var spawn_rect := Rect2(32, 32, 576, 296) @export var min_dist_from_mech := 120.0 var _timer := 0.0 var _spawned := 0 # Room budget: > 0 caps total spawns this room; 0 = endless trickle. var room_total := 0 var room_spawned := 0 func begin_room(total: int, grace := 1.5) -> void: room_total = total room_spawned = 0 _timer = grace func room_exhausted() -> bool: return room_total > 0 and room_spawned >= room_total func _physics_process(delta: float) -> void: _timer -= delta if _timer > 0.0: return _timer = spawn_interval if room_exhausted(): return if get_tree().get_nodes_in_group("enemies").size() < target_count: _spawn() if room_total > 0: room_spawned += 1 func _spawn() -> void: _spawned += 1 var scene := BURSTER if _spawned % burster_every == 0 else GRUNT var g := scene.instantiate() g.position = _pick_point() add_child(g) grunt_spawned.emit(g) func _pick_point() -> Vector2: var mech := get_tree().get_first_node_in_group("mech") as Node2D var p := Vector2.ZERO for i in 12: match randi() % 4: 0: p = Vector2(randf_range(spawn_rect.position.x, spawn_rect.end.x), spawn_rect.position.y) 1: p = Vector2(randf_range(spawn_rect.position.x, spawn_rect.end.x), spawn_rect.end.y) 2: p = Vector2(spawn_rect.position.x, randf_range(spawn_rect.position.y, spawn_rect.end.y)) 3: p = Vector2(spawn_rect.end.x, randf_range(spawn_rect.position.y, spawn_rect.end.y)) if mech == null or p.distance_to(mech.global_position) >= min_dist_from_mech: return p return p