Files
urm03/scripts/spawner.gd
T
vh 1976149ce5 feat: KINETIC+HEAT chemistry row + self-heating burster — greybox build complete
Chemistry row *Spread* (canon CONCEPT.md § tag grammar): a heated
enemy slammed into another (knockback impact >= threshold, not a
walking bump) spreads its heat on contact and passes momentum, so
chains cascade with decaying force — chemistry feeding chemistry.
Authorship-loud feedback stack per canon: ~80ms hitstop (owned by the
hand, capped so chain procs can't lock the world; rides on top of
freeze/thaw states), the two tag hues meeting at the point of impact
(oriented fx, no proc text), and a per-pair audio sting (procedural
placeholder, _ph-marked per asset policy).

Burster: second enemy type, HEAT-tagged walking bomb (orange base,
literal tag mirror) — self-heats ~0.5/s toward threshold, big
detonation. Every 4th spawn. Detonations are now symmetric: they
damage enemies AND the mech in radius (one grammar on both sides;
heat play prices standing close). Grunt scene exports drive both
enemy types — no subclass.

Smoke gate extended: burster self-heat observed (retry-tolerant),
deterministic staged chemistry proc (heated striker slammed into cold
neighbor outside weapon range -> proc counter, heat spread). 3/3
green on nh3-dev headless 4.7.1. Known cosmetic: 2 ObjectDB instances
reported leaked at exit (constant, exit-order artifact — audio stream
alive at quit), not a runtime leak.
2026-08-07 11:47:21 -07:00

51 lines
1.5 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
func _physics_process(delta: float) -> void:
_timer -= delta
if _timer > 0.0:
return
_timer = spawn_interval
if get_tree().get_nodes_in_group("enemies").size() < target_count:
_spawn()
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