Files
vh fc4fc59a2b feat: M2 run skeleton — belt, card draft, node fork, room sequencing, run tally
Belt (canon: charges are the cost, the belt is the ammo): cards are
CARD_TYPES indices consumed on play, next slides into the 2-slot hand;
belt cap 8; HUD shows hand glyphs + upcoming-card ticks. Card pool
grows to 4: SHOCKWAVE, HEATWAVE, DASH (impulse along held movement
direction — aimable while frozen, no aim input), IGNITE (nearest-enemy
heat spike past threshold). Rooms: spawn-budget waves (base 8 +4/room,
4 rooms/run); clear -> interstitial fork (CARD vs SCRAP +8) -> CARD
opens a 3-option draft + scrap decline (+4) -> next room; final clear
or mech destruction -> tally overlay (rooms/kills/scrap/chem) with
relaunch on 1. Interstitial owns the pause while active; the hand
yields input and time authority to it. Mech death now signals the run
manager instead of hard-reloading.

Chain semantics (same-tag surge / cross-tag chemistry trigger)
deliberately deferred to M3.

Smoke gate: belt consumption white-boxed in the card phase, _until
moved to process_frame (physics awaits deadlock under pause), run
phase covers clear->fork->draft->belt growth->room advance->tally.
3/3 green on nh3-dev headless 4.7.1.
2026-08-07 20:30:43 -07:00

68 lines
1.9 KiB
GDScript

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