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)