v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole Implementation Plan
v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole Implementation Plan
Section titled “v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: For v0.1, survival only ever spawns the Warden boss; the first time it dies each run, a teaser wormhole opens that (when flown through) shows a full-screen “more awaits” overlay — a random locked boss + up to 3 existing enemies previewed against a spiral-galaxy backdrop — then resumes the same run untouched.
Architecture: A V01_WARDEN_ONLY flag short-circuits the survival boss picker to always spawn the Warden. A second, fully independent wormhole system (separate from the real area-warp system, which stays locked) spawns once per run on that Warden’s death, and on fly-through fills a one-shot teaser_event with a random boss + enemies drawn from two small config arrays (TEASER_BOSS_POOL/TEASER_ENEMY_POOL) built specifically so v0.2 can repoint them at v0.3’s content later. main.gd resolves the event into display values (colour/name/labels) and hands them to a new pure-presentation BossTeaserOverlay, which freezes the run the same way WeaponInfoOverlay/CodexOverlay already do.
Tech Stack: Godot 4.6 / typed GDScript, GUT 9.6.0 for tests.
Global Constraints
Section titled “Global Constraints”- Determinism baseline must stay byte-identical:
snapshot_string().hash() = 2730172591,state_checksum() = 4075578713(pinned intests/test_determinism_checksum.gd+test_determinism_crystals.gd). Every new code path in this plan is gated well past the pinned 10s/600-tick window (the Warden itself never spawns beforeBOSS_FIRST_TIME = 210.0), so this must require zero re-pinning — treat any movement as a bug in the change, not an expected re-pin. /simstays pure logic: no Node/render/Input/Engine/Time APIs insim/sim.gdchanges.BossTeaserOverlayand allmain.gdchanges are render-side only.- No GUT test for
BossTeaserOverlay— matches this project’s established convention (render/UI classes are verified by boot-check + playtest, not unit tests; seefunzo_renderer.gd,boss2_renderer.gd,weapon_panel.gdfor precedent). - Every new
class_namefile requiresgodot --headless --path . --importbefore the next boot-check or test run, or the stale global-class-cache silently drops its tests. - Boot-check after every task:
godot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR"must be empty. Never wrap withtimeout(not on macOS PATH). - Full suite + count guard after every task:
godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexitthenbash scripts/check-test-count.sh— trust the script count, not just “all passed”. - No new art assets, no new bosses, no new enemies — everything teased already exists in full; this plan only builds the picking/presentation logic.
- Commit at the end of each task, on this worktree’s branch only (
worktree-warden-only-boss-teaser) — the user already approved this plan and chose subagent-driven execution, and the branch is isolated frommainuntil an explicit merge step later, so per-task commits here don’t need a separate confirmation.
File Structure
Section titled “File Structure”New files:
sim/...— no new sim files, all sim changes live in the existingsim/sim.gd(matches the codebase’s existing pattern of adding boss/wormhole logic directly there rather than splitting a single boss-picker concern into its own file).ui/boss_teaser_overlay.gd— the new full-screen teaser overlay (BossTeaserOverlay extends CanvasLayer), self-contained, zeroSim/EnemyPoolcoupling.tests/test_v01_warden_only.gd— new test file for the boss-lock.tests/test_teaser_wormhole.gd— new test file for the teaser wormhole + event.
Modified files:
sim/sim.gd—_maybe_spawn_survival_boss()gate, new teaser state/consts,_spawn_teaser_wormhole(),_update_teaser_wormhole(),consume_teaser_event(), two one-line call sites (_sweep_dead,tick).main.gd— instantiateBossTeaserOverlay, add it to the physics freeze gate and the codex-encounter guard, feed the teaser wormhole into the existingWormholeRenderer, add_check_teaser_event()and call it from_process.
Task 1: v0.1 Warden-only boss gate
Section titled “Task 1: v0.1 Warden-only boss gate”Files:
- Modify:
sim/sim.gd(the_maybe_spawn_survival_bossfunction, currently around line 2578, and its preceding line) - Test:
tests/test_v01_warden_only.gd(new)
Interfaces:
-
Consumes:
EnemyPool.TYPE_BOSS,Sim._boss_index(),Sim._spawn_boss(s),Sim.dev_clear_enemies(),Sim.BOSS_FIRST_TIME(all pre-existing). -
Produces:
Sim.V01_WARDEN_ONLY: bool(const, defaulttrue) — later tasks and any future session read this to know the v0.1 gate is active. -
Step 1: Write the failing test
Create tests/test_v01_warden_only.gd:
extends GutTest
# v0.1 launch: survival only ever spawns the Warden. Flip Sim.V01_WARDEN_ONLY to false to# restore the full 5-boss rotation post-launch — see# docs/superpowers/specs/2026-07-01-warden-only-boss-teaser-design.md.
func _sim() -> Sim: return Sim.new(1, SimContentFixture.db())
func test_v01_warden_only_defaults_on() -> void: assert_true(Sim.V01_WARDEN_ONLY, "the v0.1 gate is on by default")
func test_survival_boss_never_spawns_other_boss_types() -> void: var sim := _sim() for k in range(20): sim._boss_spawn_count = k # exercises every remainder of the old `% 5` rotation sim.dev_clear_enemies() sim.run_time = Sim.BOSS_FIRST_TIME sim._next_boss_time = Sim.BOSS_FIRST_TIME sim._maybe_spawn_survival_boss() var bi := sim._boss_index() assert_ne(bi, -1, "a boss spawned for _boss_spawn_count=%d" % k) assert_eq(sim.enemies.type_id[bi], EnemyPool.TYPE_BOSS, "_boss_spawn_count=%d must still spawn the Warden while V01_WARDEN_ONLY is on" % k)- Step 2: Run test to verify it fails
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_v01_warden_only.gd -gexit
Expected: FAIL — Sim.V01_WARDEN_ONLY does not exist yet (parse/attribute error), or (if the const doesn’t exist) a compile error surfaces for the whole file. Either failure mode is acceptable proof the test exercises not-yet-written code — do not proceed until you see a genuine failure, not a false pass.
- Step 3: Write minimal implementation
In sim/sim.gd, find:
func _maybe_spawn_survival_boss() -> void: if story != null: return if run_time < _next_boss_time: return if _any_boss_alive(): # one boss at a time return var s := _boss_hp_scale() # later bosses spawn tougher (time-based) var pick := _boss_spawn_count % 5 match pick: 0: _spawn_boss(s) 1: _spawn_boss2(player.pos + rng.rand_unit_dir() * 640.0, s) 2: _spawn_funzo(player.pos + rng.rand_unit_dir() * 640.0, s) 3: _spawn_graviton(player.pos + rng.rand_unit_dir() * 640.0, s) 4: _spawn_eye(player.pos + rng.rand_unit_dir() * 640.0, s)Replace with:
# v0.1 launch: survival only ever spawns the Warden — none of the other 4 bosses. Flip to# false to restore the full 5-boss rotation post-launch (Boss2/FunZo/Graviton/Eye code below# is untouched, just unreachable while this is true). Seeconst V01_WARDEN_ONLY := true
func _maybe_spawn_survival_boss() -> void: if story != null: return if run_time < _next_boss_time: return if _any_boss_alive(): # one boss at a time return var s := _boss_hp_scale() # later bosses spawn tougher (time-based) if V01_WARDEN_ONLY: _spawn_boss(s) return var pick := _boss_spawn_count % 5 match pick: 0: _spawn_boss(s) 1: _spawn_boss2(player.pos + rng.rand_unit_dir() * 640.0, s) 2: _spawn_funzo(player.pos + rng.rand_unit_dir() * 640.0, s) 3: _spawn_graviton(player.pos + rng.rand_unit_dir() * 640.0, s) 4: _spawn_eye(player.pos + rng.rand_unit_dir() * 640.0, s)- Step 4: Run test to verify it passes
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit
Expected: PASS (both new tests green; total test count is prior count + 2).
- Step 5: Boot-check + count guard
Run: godot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR" — expect empty output.
Run: bash scripts/check-test-count.sh — expect test-count guard OK with the new file counted.
- Step 6: Determinism check
Run the full suite again (it includes tests/test_determinism_checksum.gd and
test_determinism_crystals.gd) — both must still report the pinned values
2730172591 / 4075578713. This task changes only code paths that run at/after
BOSS_FIRST_TIME = 210.0, far outside the pinned 10s window, so zero movement is
expected. If either test fails, stop and investigate before continuing — do not
re-pin to make it pass.
- Step 7: Commit
git add sim/sim.gd tests/test_v01_warden_only.gdgit commit -m "feat(sim): v0.1 survival boss lock — Warden only
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"Task 2: Teaser wormhole spawn + once-per-run latch
Section titled “Task 2: Teaser wormhole spawn + once-per-run latch”Files:
- Modify:
sim/sim.gd(new vars nearvar wormholes: Array = []at line ~508; new function near_spawn_area_wormholeat line ~3970; one call site inside_sweep_deadat line ~3917-3918) - Test:
tests/test_teaser_wormhole.gd(new — this task adds to it; Task 3 extends the same file)
Interfaces:
-
Consumes:
Sim.areas_enabled,Sim.wormholes,Sim._spawn_area_wormhole(pos),Sim.WORMHOLE_RADIUS,Sim._sweep_dead(),Sim.V01_WARDEN_ONLY(from Task 1). -
Produces:
Sim.teaser_wormhole: Dictionary({"pos": Vector2}when active,{}otherwise) andSim._spawn_teaser_wormhole(pos: Vector2) -> void— Task 3’s_update_teaser_wormhole()reads/clearsteaser_wormhole. -
Step 1: Write the failing test
Create tests/test_teaser_wormhole.gd:
extends GutTest
# v0.1 teaser wormhole: fully independent of the real area-warp system (Sim.wormholes /# Sim.areas_enabled / Sim.enter_area), spawned once per run on the first Warden kill. See# docs/superpowers/specs/2026-07-01-warden-only-boss-teaser-design.md.
func _sim() -> Sim: return Sim.new(1, SimContentFixture.db())
func _kill_warden(sim: Sim) -> void: sim.run_time = Sim.BOSS_FIRST_TIME sim._spawn_boss() var bi := sim._boss_index() sim.enemies.data[bi] = 0.0 sim._sweep_dead()
func test_warden_kill_spawns_exactly_one_teaser_wormhole() -> void: var sim := _sim() _kill_warden(sim) assert_eq(sim.teaser_wormhole.size(), 1, "teaser_wormhole holds one key (pos)") assert_true(sim.teaser_wormhole.has("pos"))
func test_real_wormhole_system_stays_independent() -> void: var sim := _sim() sim.areas_enabled = false # what main.gd sets for v0.1 (V01_LOCK_AREAS) _kill_warden(sim) assert_true(sim.wormholes.is_empty(), "the real area-warp wormhole stays locked") assert_eq(sim.teaser_wormhole.size(), 1, "the teaser wormhole still spawns independently")
func test_second_warden_kill_same_run_does_not_spawn_another() -> void: var sim := _sim() _kill_warden(sim) assert_eq(sim.teaser_wormhole.size(), 1, "first kill spawns it") sim.teaser_wormhole = {} # simulate it having been consumed by fly-through (Task 3 covers the real path) _kill_warden(sim) # _spawn_boss() bypasses the run_time-gated picker, so this just spawns+kills another Warden directly assert_true(sim.teaser_wormhole.is_empty(), "the once-per-run latch blocks a second teaser wormhole even though the slot is free again")- Step 2: Run test to verify it fails
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_teaser_wormhole.gd -gexit
Expected: FAIL — sim.teaser_wormhole does not exist yet.
- Step 3: Write minimal implementation
In sim/sim.gd, find:
var areas_enabled: bool = truevar wormholes: Array = [] # {pos, dest}; ≤1 at a time, spawned on a boss kill (Task 2)Add immediately after:
# v0.1 teaser wormhole — a system fully separate from the real area-warp one above (see# docs/superpowers/specs/2026-07-01-warden-only-boss-teaser-design.md). Never touches# wormholes/areas_enabled/enter_area. Repointable at new content for future versions by# editing TEASER_BOSS_POOL/TEASER_ENEMY_POOL only (Task 3).var teaser_wormhole: Dictionary = {} # {pos} when active, {} otherwisevar _teaser_shown_this_run: bool = falseThen find (near the end of _sweep_dead):
i -= 1 if boss_died_at != Vector2.INF: _spawn_area_wormhole(boss_died_at) # a boss fell → open the gateway to the next areaReplace with:
i -= 1 if boss_died_at != Vector2.INF: _spawn_area_wormhole(boss_died_at) # a boss fell → open the gateway to the next area _spawn_teaser_wormhole(boss_died_at) # v0.1: a "more awaits" teaser, independent of the aboveThen find:
# Spawn the area-gateway wormhole at a boss's death spot — ≤1 at a time, destination = the other area.# Boss deaths only happen long after the determinism baseline window, so this never runs in it.func _spawn_area_wormhole(pos: Vector2) -> void: if not areas_enabled: return # v0.1: the explorable-areas system is gated off if not wormholes.is_empty(): return wormholes.append({"pos": pos, "dest": other_area()})Add immediately after (before _update_wormholes):
# v0.1 teaser wormhole (see the TEASER_* consts in Task 3) — independent of# _spawn_area_wormhole above. Once per run: the first Warden kill spawns it, later Warden# kills that run do not (the run's own Sim instance resets _teaser_shown_this_run each run).func _spawn_teaser_wormhole(pos: Vector2) -> void: if not V01_WARDEN_ONLY or _teaser_shown_this_run or not teaser_wormhole.is_empty(): return _teaser_shown_this_run = true teaser_wormhole = {"pos": pos}- Step 4: Run test to verify it passes
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit
Expected: PASS.
-
Step 5: Boot-check + count guard (same commands as Task 1, Step 5)
-
Step 6: Determinism check (same as Task 1, Step 6 — zero movement expected)
-
Step 7: Commit
git add sim/sim.gd tests/test_teaser_wormhole.gdgit commit -m "feat(sim): v0.1 teaser wormhole spawn (once per run, independent of area-warp)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"Task 3: Teaser pools + fly-through detection + one-shot event
Section titled “Task 3: Teaser pools + fly-through detection + one-shot event”Files:
- Modify:
sim/sim.gd(new consts near the Task 2 vars; new functions near_update_wormholesat line ~3979; one call site insidetick()at line ~886) - Test:
tests/test_teaser_wormhole.gd(extend from Task 2)
Interfaces:
-
Consumes:
Sim.teaser_wormhole(Task 2),Sim.WORMHOLE_RADIUS,Sim.rng,Sim.tick(). -
Produces:
Sim.TEASER_BOSS_POOL: Array[int],Sim.TEASER_ENEMY_POOL: Array[String],Sim.teaser_event: Dictionary({"boss_type": int, "enemy_ids": Array[String]}),Sim.consume_teaser_event() -> Dictionary— Task 5’smain._check_teaser_event()calls this. -
Step 1: Write the failing test
Append to tests/test_teaser_wormhole.gd:
func _fly_through_teaser(sim: Sim) -> void: sim.player.pos = sim.teaser_wormhole["pos"] sim._update_teaser_wormhole()
func test_flying_through_teaser_wormhole_produces_event_from_pools() -> void: var sim := _sim() _kill_warden(sim) _fly_through_teaser(sim) assert_true(sim.teaser_wormhole.is_empty(), "the wormhole is consumed on fly-through") assert_true(sim.teaser_event.has("boss_type")) assert_true(Sim.TEASER_BOSS_POOL.has(int(sim.teaser_event["boss_type"])), "the teased boss is drawn from TEASER_BOSS_POOL") var enemy_ids: Array = sim.teaser_event["enemy_ids"] assert_lte(enemy_ids.size(), 3, "at most 3 enemies teased") var seen := {} for id in enemy_ids: assert_true(Sim.TEASER_ENEMY_POOL.has(String(id)), "each teased enemy id is drawn from TEASER_ENEMY_POOL") assert_false(seen.has(id), "no duplicate enemy ids in one teaser") seen[id] = true
func test_consume_teaser_event_returns_once_then_empty() -> void: var sim := _sim() _kill_warden(sim) _fly_through_teaser(sim) var ev := sim.consume_teaser_event() assert_true(ev.has("boss_type"), "first consume returns the event") var ev2 := sim.consume_teaser_event() assert_true(ev2.is_empty(), "second consume reads empty (already cleared)")- Step 2: Run test to verify it fails
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_teaser_wormhole.gd -gexit
Expected: FAIL — Sim.TEASER_BOSS_POOL/_update_teaser_wormhole/consume_teaser_event do not exist yet.
- Step 3: Write minimal implementation
In sim/sim.gd, extend the block added in Task 2:
# v0.1 teaser wormhole — a system fully separate from the real area-warp one above (see# docs/superpowers/specs/2026-07-01-warden-only-boss-teaser-design.md). Never touches# wormholes/areas_enabled/enter_area. Repointable at new content for future versions by# editing TEASER_BOSS_POOL/TEASER_ENEMY_POOL only.const TEASER_BOSS_POOL: Array[int] = [EnemyPool.TYPE_BOSS2, EnemyPool.TYPE_FUNZO, EnemyPool.TYPE_GRAVITON, EnemyPool.TYPE_EYE]const TEASER_ENEMY_POOL: Array[String] = ["ghost", "accumulator", "orbiter"]const TEASER_ENEMY_SAMPLE: int = 3 # up to this many enemies previewed per teaservar teaser_wormhole: Dictionary = {} # {pos} when active, {} otherwisevar _teaser_shown_this_run: bool = falsevar teaser_event: Dictionary = {} # {boss_type, enemy_ids} once fly-through fires; one-shot(This replaces the smaller block Task 2 added — same vars, with the three new consts and
teaser_event added alongside.)
Then find:
# Player flies over the wormhole → emit a warp event (render orchestrates the transition + enter_area)# and consume it. Iterates an empty array in the baseline → no-op.func _update_wormholes() -> void: var r := WORMHOLE_RADIUS + player.radius for w in wormholes: if player.pos.distance_squared_to(w["pos"]) <= r * r: area_events.append({"kind": "warp", "dest": String(w["dest"])}) wormholes.clear() returnAdd immediately after:
# Player flies over the TEASER wormhole → pick a random locked boss + up to# TEASER_ENEMY_SAMPLE distinct enemies from the pools, fill teaser_event, clear the wormhole.# Draws from `rng` (a spawn-flavor pick, not an upgrade choice — never desyncs the upgrade# stream). Never runs in the baseline window: the Warden itself never spawns before# BOSS_FIRST_TIME (210s), so teaser_wormhole is always empty inside the pinned 10s window.func _update_teaser_wormhole() -> void: if teaser_wormhole.is_empty(): return var r := WORMHOLE_RADIUS + player.radius var pos: Vector2 = teaser_wormhole["pos"] if player.pos.distance_squared_to(pos) > r * r: return var boss_type: int = TEASER_BOSS_POOL[rng.randi_range(0, TEASER_BOSS_POOL.size() - 1)] var pool: Array[String] = TEASER_ENEMY_POOL.duplicate() for i in range(pool.size()): var j := rng.randi_range(i, pool.size() - 1) var tmp := pool[i] pool[i] = pool[j] pool[j] = tmp var enemy_ids: Array[String] = [] for i in range(mini(TEASER_ENEMY_SAMPLE, pool.size())): enemy_ids.append(pool[i]) teaser_event = {"boss_type": boss_type, "enemy_ids": enemy_ids} teaser_wormhole = {}
# Render calls this once when it actually shows the overlay — a dropped frame (another# overlay is up) doesn't lose the event, it just stays queued until read.func consume_teaser_event() -> Dictionary: var ev := teaser_event teaser_event = {} return evThen find, inside tick():
_update_wormholes() # fly over a boss-spawned wormhole → emit a warp eventReplace with:
_update_wormholes() # fly over a boss-spawned wormhole → emit a warp event _update_teaser_wormhole() # v0.1: fly over the teaser wormhole → emit teaser_event- Step 4: Run test to verify it passes
Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit
Expected: PASS.
-
Step 5: Boot-check + count guard (same commands as Task 1, Step 5)
-
Step 6: Determinism check (same as Task 1, Step 6 — zero movement expected;
_update_teaser_wormholeonly draws fromrngwhenteaser_wormholeis non-empty, which never happens beforeBOSS_FIRST_TIME) -
Step 7: Commit
git add sim/sim.gd tests/test_teaser_wormhole.gdgit commit -m "feat(sim): teaser wormhole fly-through event + config-driven pools
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"Task 4: BossTeaserOverlay UI class
Section titled “Task 4: BossTeaserOverlay UI class”Files:
- Create:
ui/boss_teaser_overlay.gd
Interfaces:
- Consumes:
NeonTheme.title_font(),NeonTheme.mono_font()(existing). - Produces:
BossTeaserOverlay.show_for(boss_color: Color, boss_name: String, enemy_labels: Array) -> void,BossTeaserOverlay.is_open() -> bool— Task 5’smain.gdcalls both.
This class takes already-resolved presentation values, never a raw Sim/EnemyPool reference,
so it stays reusable for a v0.2 teaser with different content. No GUT test (matches this
project’s established convention for render/UI classes — see Global Constraints).
- Step 1: Write the file
Create ui/boss_teaser_overlay.gd:
class_name BossTeaserOverlayextends CanvasLayer
# v0.1 "more awaits" teaser: a full-screen takeover shown once per run, the first time the# player flies through the post-Warden-kill teaser wormhole. Pure presentation — takes# already-resolved values (colour/name/labels), never touches Sim/EnemyPool directly, so it# stays trivially reusable for a v0.2 teaser pointed at different content. Freezes the run via# is_open() (main.gd adds it to the _physics_process gate, same idiom as WeaponInfoOverlay).# See docs/superpowers/specs/2026-07-01-warden-only-boss-teaser-design.md.
const AUTO_DISMISS_S: float = 6.0
# Procedural spiral-galaxy backdrop + boss glow — additive vector art, same idiom as# funzo_renderer.gd/boss2_renderer.gd. No image assets.class _GalaxyBackdrop extends Node2D: var _t := 0.0 var _radius := 420.0 var glow_color := Color(1.0, 0.3, 0.3)
func _init() -> void: var mat := CanvasItemMaterial.new() mat.blend_mode = CanvasItemMaterial.BLEND_MODE_ADD material = mat
func _process(dt: float) -> void: _t += dt queue_redraw()
func _draw() -> void: var arms := 4 for arm in range(arms): var base_a := _t * 0.08 + float(arm) * TAU / float(arms) var pts := PackedVector2Array() var steps := 64 for s in range(steps): var t := float(s) / float(steps - 1) var a := base_a + t * TAU * 1.4 var rr := t * _radius pts.append(Vector2(cos(a), sin(a)) * rr) var col := Color(0.55, 0.35, 1.0, 0.22) if arm % 2 == 0 else Color(0.35, 0.65, 1.0, 0.16) draw_polyline(pts, col, 26.0, true) draw_circle(Vector2.ZERO, 90.0, Color(glow_color.r, glow_color.g, glow_color.b, 0.14)) draw_circle(Vector2.ZERO, 55.0, Color(glow_color.r, glow_color.g, glow_color.b, 0.55)) draw_circle(Vector2.ZERO, 30.0, Color(1.0, 1.0, 1.0, 0.85).lerp(glow_color, 0.3))
var _open := falsevar _timer := 0.0var _backdrop: _GalaxyBackdropvar _title: Labelvar _boss_name: Labelvar _enemy_row: HBoxContainer
func _ready() -> void: layer = 23 # above WeaponInfoOverlay (22) — this is a rarer, higher-priority takeover visible = false
var dim := ColorRect.new() dim.set_anchors_preset(Control.PRESET_FULL_RECT) dim.color = Color(0.01, 0.0, 0.03, 0.94) dim.mouse_filter = Control.MOUSE_FILTER_STOP add_child(dim)
_backdrop = _GalaxyBackdrop.new() add_child(_backdrop)
_title = Label.new() _title.text = "MORE AWAITS" _title.size = Vector2(400, 40) _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _title.add_theme_font_override("font", NeonTheme.title_font()) _title.add_theme_font_size_override("font_size", 26) _title.add_theme_color_override("font_color", Color(0.75, 0.9, 1.0)) add_child(_title)
_boss_name = Label.new() _boss_name.size = Vector2(400, 30) _boss_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _boss_name.add_theme_font_override("font", NeonTheme.title_font()) _boss_name.add_theme_font_size_override("font_size", 22) _boss_name.add_theme_color_override("font_color", Color(1.0, 0.95, 0.98)) add_child(_boss_name)
_enemy_row = HBoxContainer.new() _enemy_row.add_theme_constant_override("separation", 24) add_child(_enemy_row)
func is_open() -> bool: return _open
func show_for(boss_color: Color, boss_name: String, enemy_labels: Array) -> void: _open = true visible = true _timer = AUTO_DISMISS_S var vp := get_viewport().get_visible_rect().size var center := vp * 0.5 _backdrop.position = center _backdrop.glow_color = boss_color _title.position = Vector2(center.x - 200.0, 60.0) _boss_name.text = boss_name.to_upper() _boss_name.position = Vector2(center.x - 200.0, center.y + 100.0) for c in _enemy_row.get_children(): c.queue_free() for label_text in enemy_labels: var lbl := Label.new() lbl.text = String(label_text).to_upper() lbl.add_theme_font_override("font", NeonTheme.mono_font()) lbl.add_theme_font_size_override("font_size", 14) lbl.add_theme_color_override("font_color", Color(0.65, 0.85, 1.0)) _enemy_row.add_child(lbl) _enemy_row.position = Vector2(center.x - 200.0, vp.y - 90.0)
func _close() -> void: _open = false visible = false
func _process(delta: float) -> void: if not _open: return _timer -= delta if _timer <= 0.0: _close()
func _input(event: InputEvent) -> void: if not _open: return var confirm := event.is_action_pressed("ui_accept") \ or (event is InputEventJoypadButton and event.pressed and event.button_index == JOY_BUTTON_A) if confirm: _close() get_viewport().set_input_as_handled()- Step 2: Re-import (new
class_namefile)
Run: godot --headless --path . --import
Expected: completes without error; this refreshes the global class cache so BossTeaserOverlay is registered.
- Step 3: Boot-check
Run: godot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR"
Expected: empty (this file isn’t instantiated by anything yet, so this mainly confirms it
parses cleanly; Task 5 is where it’s actually wired in and exercised).
-
Step 4: Full suite + count guard (same commands as Task 1, Step 5 — the new file adds no tests of its own per the Global Constraints note, so the test count is unchanged from Task 3’s).
-
Step 5: Commit
git add ui/boss_teaser_overlay.gdgit commit -m "feat(ui): BossTeaserOverlay — v0.1 full-screen 'more awaits' takeover
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"Task 5: Wire into main.gd
Section titled “Task 5: Wire into main.gd”Files:
- Modify:
main.gd(new member var nearvar weapon_info: WeaponInfoOverlay; instantiation near whereweapon_info = WeaponInfoOverlay.new()runs; the_physics_processfreeze-gate line; thewormhole_renderer.update_wormholes(...)call; the_check_codex_encounters()guard; a new_check_teaser_event()function + its call site in_process)
Interfaces:
- Consumes:
Sim.teaser_wormhole,Sim.teaser_event,Sim.consume_teaser_event()(Tasks 2-3),BossTeaserOverlay.show_for(...)/.is_open()(Task 4),main._base_color_for_type(tid, el)(existing, added earlier this session for enemy-projectile colouring),main.codex_db(existingBestiaryDB),WormholeRenderer.update_wormholes(Array)(existing, unchanged signature). - Produces: nothing new consumed by later tasks — this is the integration point.
No GUT test for this task (it’s all main.gd/render wiring — matches the established
convention). Verified by boot-check + an extended headless smoke run.
- Step 1: Declare the member variable
In main.gd, find:
var weapon_info: WeaponInfoOverlay # in-play weapon details (Y button / V), freezes the sim while openAdd immediately after:
var boss_teaser: BossTeaserOverlay # v0.1 "more awaits" full-screen takeover, freezes the sim while open- Step 2: Instantiate it
In main.gd, find:
weapon_info = WeaponInfoOverlay.new() add_child(weapon_info)Add immediately after:
boss_teaser = BossTeaserOverlay.new() add_child(boss_teaser)- Step 3: Add it to the physics freeze gate
In main.gd, find:
if sim.game_over or _paused_for_levelup or _story_won or _paused_for_menu \ or weapon_info.is_open() or codex.is_open() or _warping: returnReplace with:
if sim.game_over or _paused_for_levelup or _story_won or _paused_for_menu \ or weapon_info.is_open() or codex.is_open() or boss_teaser.is_open() or _warping: return- Step 4: Feed the teaser wormhole into the existing renderer
In main.gd, find:
wormhole_renderer.update_wormholes(sim.wormholes)Replace with:
var wormhole_list := sim.wormholes.duplicate() if not sim.teaser_wormhole.is_empty(): wormhole_list.append(sim.teaser_wormhole) wormhole_renderer.update_wormholes(wormhole_list)- Step 5: Guard the codex first-encounter check against a stacked overlay
In main.gd, find (inside _check_codex_encounters):
if sim == null or sim.game_over or _paused_for_levelup or _paused_for_menu \ or _story_won or weapon_info.is_open() or codex.is_open() or meta == null: returnReplace with:
if sim == null or sim.game_over or _paused_for_levelup or _paused_for_menu \ or _story_won or weapon_info.is_open() or codex.is_open() or boss_teaser.is_open() \ or meta == null: return- Step 6: Add the check-and-show function
In main.gd, find _check_codex_encounters (around line 1320) and add this new function
immediately after it:
# v0.1: the first time the player flies through the post-Warden teaser wormhole, show the# full-screen "more awaits" overlay. Won't fire while another overlay is up (mirrors# _check_codex_encounters' stacking guard above); the sim-side event survives until then.func _check_teaser_event() -> void: if sim == null or sim.game_over or _paused_for_levelup or _paused_for_menu \ or _story_won or weapon_info.is_open() or codex.is_open() or boss_teaser.is_open(): return if sim.teaser_event.is_empty(): return var event := sim.consume_teaser_event() var boss_type := int(event.get("boss_type", -1)) var boss_color := _base_color_for_type(boss_type, -1) var boss_name := String(codex_db.entry_for_type(boss_type).get("name", "???")) var enemy_labels: Array = [] for id in event.get("enemy_ids", []): enemy_labels.append(String(codex_db.entry_for_key(String(id)).get("name", String(id)))) boss_teaser.show_for(boss_color, boss_name, enemy_labels)- Step 7: Call it from
_process
In main.gd, find:
_check_codex_encounters()Replace with:
_check_codex_encounters() _check_teaser_event()- Step 8: Boot-check
Run: godot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR"
Expected: empty.
-
Step 9: Full suite + count guard (same commands as Task 1, Step 5 — no new tests added this task, count unchanged from Task 3).
-
Step 10: Determinism check (same as Task 1, Step 6 — this task is 100% render-side
main.gdwiring, zero/simchanges, so zero movement is expected). -
Step 11: Extended headless smoke
Run: godot --headless --path . --quit-after 3600 2>&1 | grep -iE "SCRIPT ERROR|error|Invalid|Nonexistent|out of bounds|Index" | sort -u
Expected: empty. This won’t reach BOSS_FIRST_TIME (210s), so it doesn’t exercise the teaser
path itself (that’s covered by Tasks 1-3’s unit tests) — it confirms the new main.gd wiring
(the extra .duplicate()/.append() per frame, the new guard conditions) doesn’t break normal
play.
- Step 12: Commit
git add main.gdgit commit -m "feat(main): wire BossTeaserOverlay into the run loop
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"Task 6: Final verification + hand-off note
Section titled “Task 6: Final verification + hand-off note”Files: none (verification only).
- Step 1: Full ritual, one more time, on the whole feature together
godot --headless --path . --importgodot --headless --path . --quit-after 90 2>&1 | grep "SCRIPT ERROR"godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexitbash scripts/check-test-count.shExpected: all green, SCRIPT ERROR grep empty, test count matches tests/test_*.gd file
count exactly (prior count + 4: 2 in test_v01_warden_only.gd, 4 in the extended
test_teaser_wormhole.gd… i.e. whatever the actual new total is — the script asserts this
automatically, just confirm it reports OK).
- Step 2: Determinism, one more time
Confirm tests/test_determinism_checksum.gd and test_determinism_crystals.gd both still
report 2730172591 / 4075578713 — zero movement across the whole feature.
- Step 3: Note what’s NOT covered by this plan
This plan does not include: bumping Sim_Const.BUILD, committing a docs(claude.md) catch-up
entry, syncing to the tvOS repo, or building/installing to the Apple TV. Those follow the same
bh-deploy ritual used throughout this session (sync the changed files — sim/sim.gd,
main.gd, ui/boss_teaser_overlay.gd, and the two new/extended test files — plus the whole
tests/ dir, verify independently there, export/build/install) and should only happen once the
user explicitly asks for the commit + deploy, matching every prior chunk this session.
Plan self-review
Section titled “Plan self-review”Spec coverage:
- Decision 1 (Warden-only + flag) → Task 1. ✓
- Decision 2 (separate teaser system, real area system untouched) → Tasks 2-3 (explicitly
tested in
test_real_wormhole_system_stays_independent). ✓ - Decision 3 (once per run) → Task 2 (
test_second_warden_kill_same_run_does_not_spawn_another). ✓ - Decision 4 (random pick, config-driven pools) → Task 3 (
TEASER_BOSS_POOL/TEASER_ENEMY_POOL, tested for pool-membership + no-duplicates). ✓ - Decision 5 (reuses existing content, no new enemies/bosses) → Tasks 3-5 use only existing
EnemyPool.TYPE_*ids and existing bestiary keys. ✓ - Decision 6 (full-screen takeover layout) → Task 4. ✓
- Decision 7 (pauses the run, auto-dismiss/confirm-dismiss) → Tasks 4-5
(
is_open()/freeze gate,AUTO_DISMISS_Stimer +ui_accept/JOY_BUTTON_A). ✓ - Load-bearing determinism constraint → re-checked at the end of every task, not just once. ✓
- Data-flow diagram → matches Tasks 2, 3, 5 exactly (spawn → fly-through → event → consume → resolve → show_for → dismiss → resume). ✓
- Scope guard (no new art/bosses/enemies, no persistence of “which boss was teased”, no skip-tracking, real area system untouched, no new audio) → honored throughout; nothing in this plan adds any of those. ✓
Placeholder scan: no TBD/TODO; every step has complete, runnable code; no “similar to Task N” — Task 3 explicitly re-states the full var block rather than pointing back to Task 2.
Type consistency: teaser_wormhole: Dictionary / teaser_event: Dictionary / TEASER_BOSS_POOL: Array[int] /
TEASER_ENEMY_POOL: Array[String] are declared once (Tasks 2-3) and used with the same names/types
in Task 5’s main.gd wiring and both test files. BossTeaserOverlay.show_for(boss_color: Color, boss_name: String, enemy_labels: Array) matches its Task 5 call site exactly (positional args,
same order, same resolved types).