fc4fc59a2b
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.
139 lines
3.8 KiB
GDScript
139 lines
3.8 KiB
GDScript
extends Node2D
|
|
## One greybox arena, one screen (640x360), fixed camera. Draws its own
|
|
## floor and wall slabs; tracks kills and chemistry procs; conducts the
|
|
## authorship-loud chemistry feedback stack (canon: brief hitstop + the
|
|
## two tag hues meeting at the point of contact + a per-pair sting —
|
|
## no proc text, the light and the sting are the noun).
|
|
|
|
const CHEM_FX := preload("res://scripts/chemistry_fx.gd")
|
|
const SALVAGE := preload("res://scripts/salvage.gd")
|
|
|
|
## Run structure: rooms with a spawn budget; clear a room -> node fork
|
|
## (card draft vs scrap) -> next room; final room or death -> tally.
|
|
@export var rooms_enabled := true
|
|
@export var rooms_per_run := 4
|
|
@export var room_budget_base := 8
|
|
@export var room_budget_per_room := 4
|
|
|
|
var kills: int = 0
|
|
var chem_procs: int = 0
|
|
var room: int = 1
|
|
var rooms_cleared: int = 0
|
|
|
|
|
|
func _ready() -> void:
|
|
add_to_group("arena")
|
|
$Spawner.grunt_spawned.connect(_on_grunt_spawned)
|
|
$Mech.hull_changed.connect(_on_hull_changed)
|
|
$Mech.destroyed.connect(_on_mech_destroyed)
|
|
$Interstitial.fork_chosen.connect(_on_fork_chosen)
|
|
$Interstitial.draft_chosen.connect(_on_draft_chosen)
|
|
$Interstitial.restart_requested.connect(func() -> void: get_tree().reload_current_scene())
|
|
if rooms_enabled:
|
|
_begin_room()
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
if not rooms_enabled or $Interstitial.active:
|
|
return
|
|
if $Spawner.room_exhausted() and get_tree().get_nodes_in_group("enemies").is_empty():
|
|
_room_cleared()
|
|
|
|
|
|
func _begin_room() -> void:
|
|
$Spawner.begin_room(room_budget_base + (room - 1) * room_budget_per_room)
|
|
|
|
|
|
func _room_cleared() -> void:
|
|
rooms_cleared += 1
|
|
$Hand.force_close()
|
|
if room >= rooms_per_run:
|
|
$Interstitial.open_tally(_stats(), false)
|
|
else:
|
|
$Interstitial.open_fork()
|
|
|
|
|
|
func _on_fork_chosen(kind: String) -> void:
|
|
if kind == "scrap":
|
|
$Hand.add_salvage($Interstitial.fork_scrap_reward)
|
|
_advance()
|
|
else:
|
|
var opts: Array[int] = []
|
|
for i in 3:
|
|
opts.append(randi() % $Hand.CARD_TYPES.size())
|
|
$Interstitial.open_draft(opts)
|
|
|
|
|
|
func _on_draft_chosen(card_id: int) -> void:
|
|
if card_id >= 0:
|
|
$Hand.belt_append(card_id)
|
|
else:
|
|
$Hand.add_salvage($Interstitial.draft_decline_scrap)
|
|
_advance()
|
|
|
|
|
|
func _advance() -> void:
|
|
room += 1
|
|
_begin_room()
|
|
|
|
|
|
func _on_mech_destroyed() -> void:
|
|
$Hand.force_close()
|
|
$Interstitial.open_tally(_stats(), true)
|
|
|
|
|
|
func _stats() -> Dictionary:
|
|
return {
|
|
"rooms": rooms_cleared,
|
|
"kills": kills,
|
|
"scrap": $Hand.salvage_total,
|
|
"chem": chem_procs,
|
|
}
|
|
|
|
|
|
func chemistry_proc(at: Vector2, axis: Vector2) -> void:
|
|
chem_procs += 1
|
|
var hand := get_tree().get_first_node_in_group("hand")
|
|
if hand != null:
|
|
hand.hitstop()
|
|
var fx: Node2D = CHEM_FX.new()
|
|
fx.position = at
|
|
fx.axis = axis
|
|
add_child(fx)
|
|
$Sting.play()
|
|
|
|
|
|
func _on_grunt_spawned(grunt: Node) -> void:
|
|
grunt.died.connect(func() -> void: _on_grunt_died(grunt))
|
|
|
|
|
|
func _on_grunt_died(grunt: Node2D) -> void:
|
|
# died fires before queue_free, so position/value are still valid.
|
|
kills += 1
|
|
var hand := get_tree().get_first_node_in_group("hand")
|
|
if hand != null:
|
|
hand.on_kill()
|
|
var s: Area2D = SALVAGE.new()
|
|
s.position = grunt.global_position
|
|
s.value = grunt.salvage_value
|
|
# Deferred: death can fire inside a physics signal flush (projectile
|
|
# body_entered), where Area2D collision setup is blocked.
|
|
add_child.call_deferred(s)
|
|
|
|
|
|
func _on_hull_changed(hull: float, hull_max: float) -> void:
|
|
$HUD/HullFill.size.x = 118.0 * (hull / hull_max)
|
|
|
|
|
|
func _draw() -> void:
|
|
draw_rect(Rect2(0, 0, 640, 360), Color(0.1, 0.11, 0.13))
|
|
for x in range(0, 641, 32):
|
|
draw_line(Vector2(x, 0), Vector2(x, 360), Color(0.13, 0.145, 0.17))
|
|
for y in range(0, 361, 32):
|
|
draw_line(Vector2(0, y), Vector2(640, y), Color(0.13, 0.145, 0.17))
|
|
var wall := Color(0.3, 0.32, 0.36)
|
|
draw_rect(Rect2(0, 0, 640, 16), wall)
|
|
draw_rect(Rect2(0, 344, 640, 16), wall)
|
|
draw_rect(Rect2(0, 0, 16, 360), wall)
|
|
draw_rect(Rect2(624, 0, 16, 360), wall)
|