v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole — Design
v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole — Design
Section titled “v0.1 Warden-Only Boss + Post-Kill Teaser Wormhole — Design”Date: 2026-07-01
Status: Approved (design); ready for implementation plan
Mode: Survival only (the only mode reachable in v0.1 — story mode is gated off by StartMenu.V01_CRYSTALS_ONLY)
For the v0.1 launch, survival only ever spawns the Warden (the original boss) — none of the other four bosses (Sentinel/Boss2, FunZo, Graviton, The Eye). The first time the Warden dies in a run, a wormhole opens at its death spot (reusing the existing portal visual). Flying through it shows a full-screen “teaser” overlay — a procedural spiral-galaxy backdrop, a glowing silhouette + name of one of the four locked bosses (picked at random), and a small strip of 2-3 existing enemy names as a “coming soon” preview — then the overlay dismisses and the run continues exactly where it left off. Purely a flavor/marketing moment: no area change, no gameplay effect, nothing unlocked.
Chris’s explicit forward intent: this same system gets repointed for v0.2 → v0.3 (new bosses/enemies teased next). The teased content pools must therefore be trivial to swap later, not hardcoded into logic.
Decisions (locked with Chris)
Section titled “Decisions (locked with Chris)”- Warden-only for v0.1, gated by a single flag next to the existing
V01_LOCK_AREAS/V01_LOCK_COOP/V01_CRYSTALS_ONLYflags — flip it to restore the full 5-boss rotation post-launch. Boss2/FunZo/Graviton/Eye code is untouched, just unreachable from survival’s boss picker while the flag is on. - Teaser wormhole is a system fully separate from the real explorable-areas
wormhole/warp system (option 2 from the approach discussion) — same portal
visual, but its own spawn condition, its own event, its own overlay class. The
real area system (
Sim.wormholes,Sim.areas_enabled, the"warp"event,Sim.enter_area) is not touched at all and stays fully locked. This is deliberately a throwaway-safe boundary: nothing here has to be unwound when the real explorable-areas feature eventually ships. - Once per run. Only the first Warden kill in a run spawns the teaser wormhole; subsequent Warden kills that run (it still respawns on the usual boss-interval cadence) do not spawn another.
- Random pick, config-driven pools. One boss is picked at random from a
TEASER_BOSS_POOLconst array; up to 3 enemies are picked at random from aTEASER_ENEMY_POOLconst array (bestiary ids). For v0.2, repointing this system at v0.3’s content is editing those two arrays — nothing else changes. - Reuses existing content — no new enemies or bosses are designed for this. The four locked bosses and the enemy pool already exist in full; the teaser only presents their existing name/color/bestiary text.
- Full-screen takeover layout (validated via mockup): spiral-galaxy backdrop fills the screen, a large glowing boss silhouette + name centered, a small row of enemy name badges along the bottom, a short title banner at the top.
- Pauses the run, same idiom as the level-up/pause-menu freeze gates. Auto-dismisses after a few seconds, or immediately on any confirm press; the run then resumes exactly where it left off (no i-frame grace needed — nothing was mid-fight when the player deliberately flew into the portal).
Load-bearing constraint — determinism baseline unaffected
Section titled “Load-bearing constraint — determinism baseline unaffected”The Warden already only spawns past BOSS_FIRST_TIME (210s) and the teaser fires
only on its death — both far outside the pinned 10s/600-tick determinism window.
No new rng draws occur before that window. The pinned baseline
(snapshot_string().hash() = 2730172591, state_checksum() = 4075578713 as of
the 2026-07-01 swarmer-buff re-pin) must stay byte-identical; re-verified as part
of the build ritual with zero expected movement.
Components
Section titled “Components”/sim changes (pure logic, no Node/render APIs)
Section titled “/sim changes (pure logic, no Node/render APIs)”sim/sim.gd—_maybe_spawn_survival_boss(): addconst V01_WARDEN_ONLY := truenear the existing boss-rotation logic. When true, always calls_spawn_boss(s)and returns before the% 5rotation match. Flip tofalseto restore the full rotation.sim/sim.gd— new teaser state: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"](bestiary ids; a starting set Chris can extend)var teaser_wormhole: Dictionary = {}—{pos}when active, empty otherwise (separate fromwormholes, which stays reserved for the real area system)var _teaser_shown_this_run: bool = false— the once-per-run latchvar teaser_event: Dictionary = {}— set once when the player flies through ({boss_type: int, enemy_ids: Array[String]}); render side reads + clears it each frame it’s non-empty (same one-shot-event idiom asfx_events, just not cleared unconditionally every tick since it must survive until read)_spawn_teaser_wormhole(pos): no-ops ifnot V01_WARDEN_ONLYor_teaser_shown_this_runor a teaser wormhole is already out; otherwise sets_teaser_shown_this_run = trueandteaser_wormhole = {"pos": pos}. Called from_sweep_deadalongside (NOT instead of) the existing_spawn_area_wormhole(boss_died_at)call — both are independently gated (the real one still no-ops onareas_enabled == false), so they can never both actually spawn a wormhole in v0.1._update_teaser_wormhole(): proximity check (sameWORMHOLE_RADIUSas the real one) againstteaser_wormhole; on entry, picksrng.randi_range(0, TEASER_BOSS_POOL.size()-1)for the boss and samples up to 3 distinct entries fromTEASER_ENEMY_POOL(shuffle-and-take, mirroring the existing crystal-pool shuffle pattern already inroll_upgrade_choices), fillsteaser_event, clearsteaser_wormhole. Called fromtick()alongside the existing_update_wormholes().consume_teaser_event() -> Dictionary: returns and clearsteaser_event(render calls this once when it actually shows the overlay, so a dropped frame — e.g. another overlay is up — doesn’t lose the event; mirrors how the enemy-codex feature guards against “seen” being marked before the card actually shows).
Render-side changes
Section titled “Render-side changes”main.gd: feedsim.teaser_wormholeinto the existingWormholeRenderer.update_wormholes()call alongsidesim.wormholes(simple concat — the renderer already just draws whatever positions it’s given, no changes needed there). Each_process, ifsim.teaser_wormholeis empty butsim.teaser_eventis non-empty and no other overlay/freeze is active: callsim.consume_teaser_event(), resolve_base_color_for_type(event.boss_type, -1)andEnemyPool.TYPE_NAMES[event.boss_type]for the boss, resolve eachevent.enemy_idsentry to its bestiary display name, and callboss_teaser.show_for(color, name, labels). Addboss_teaser.is_open()to the_physics_processfreeze gate (same idiom asweapon_info.is_open()/codex.is_open()).ui/boss_teaser_overlay.gd(new,BossTeaserOverlay extends CanvasLayer):show_for(boss_color: Color, boss_name: String, enemy_labels: Array[String]): builds the takeover — title banner, a spiral galaxy_draw()backdrop (procedural, additive arcs/dust rotating slowly — same vector-art idiom asfunzo_renderer.gd/boss2_renderer.gd, not a new image asset), the teased boss’s glowing silhouette inboss_color+boss_namecentered, and a row ofenemy_labelsbadges (bestiary name text, no new art) along the bottom. Takes already-resolved presentation values, not a rawsim.teaser_event—main.gdresolvesboss_colorvia the existing_base_color_for_type()helper before calling in. This keeps the overlay a pure presentation class with zero coupling toSim/EnemyPool, so it stays trivially reusable for a v0.2 teaser with entirely different content.- Auto-dismiss timer (a few seconds) OR any confirm press dismisses early.
is_open() -> boolfor the freeze gate.
Data flow
Section titled “Data flow”Warden dies (first time this run) ─▶ Sim._spawn_teaser_wormhole(pos) │player flies over it ─▶ Sim._update_teaser_wormhole() │ picks random boss + up to 3 enemies ▼ Sim.teaser_event = {boss_type, enemy_ids} │main._process (no other overlay open) ─▶ consume_teaser_event() │ resolve colour/name/labels ▼ BossTeaserOverlay.show_for(color, name, labels) │ (sim paused) auto-dismiss timer / confirm press │ run resumes, unchangedOne-way, render-only past the single teaser_event handoff. No area change, no
meta-save write, no gameplay effect.
Testing (TDD, per bh-dev-chunk)
Section titled “Testing (TDD, per bh-dev-chunk)”tests/test_v01_warden_only.gd(new):_maybe_spawn_survival_bossnever spawns Boss2/FunZo/Graviton/Eye across many calls whileV01_WARDEN_ONLYis true; always spawnsTYPE_BOSS.tests/test_teaser_wormhole.gd(new):- A Warden death spawns exactly one teaser wormhole; a second Warden death the
same run does not spawn another (
_teaser_shown_this_runlatch). - Flying over it produces a
teaser_eventwithboss_typedrawn fromTEASER_BOSS_POOLandenemy_idsa subset (≤3, all distinct) ofTEASER_ENEMY_POOL. consume_teaser_event()returns the event once then reads empty.- The real
wormholesarray andareas_enabledare untouched by any of the above (confirms the two systems are genuinely independent).
- A Warden death spawns exactly one teaser wormhole; a second Warden death the
same run does not spawn another (
- Determinism — re-run
tests/test_determinism_checksum.gd+test_determinism_crystals.gdand confirm zero movement (Warden-only + teaser logic both fire far outside the pinned window). - Test-count guard —
scripts/check-test-count.shafter adding the new test files. - Boot smoke —
--headless --quit-after+ grep stderr forSCRIPT ERROR; an extended smoke run is not expected to reach 210s (BOSS_FIRST_TIME), so the teaser path itself is verified by the unit tests above, not the smoke run. - No GUT test for
BossTeaserOverlayitself (render/UI, matches this project’s established convention — verified by boot-check + eventually a live playtest).
Scope guard (YAGNI)
Section titled “Scope guard (YAGNI)”- No new bosses, no new enemies, no new art assets — everything teased already exists in full.
- No persistence of “which boss was teased” — it’s a fresh random pick every run, nothing tracked in the meta save.
- No skip-tracking (“don’t show me this again”) — it’s once-per-run by design, low enough frequency not to need a permanent opt-out for v0.1.
- The real explorable-areas/wormhole system is not modified, extended, or partially unlocked in any way.
- No audio cue beyond whatever the existing portal/reaction stinger already covers (reuse, don’t add).
Deferred / future
Section titled “Deferred / future”- v0.2: repoint
TEASER_BOSS_POOL/TEASER_ENEMY_POOLat v0.3’s new content once it exists (the whole point of building this generically now). - Eventually replacing “random pick” with a fixed narrative order, if the story team wants the tease to always show a specific “next” boss rather than a surprise.
- Real area unlock (already specced separately in
docs/superpowers/specs/2026-06-28-explorable-areas-design.md) is unaffected and unblocked by this work — when it ships,V01_LOCK_AREAS/V01_WARDEN_ONLYcan be retired independently of each other.