Skip to content

feat(dungeon): hidden post-credit stinger 'The Vanishing' (demo-only) - #33

Merged
silver-snoopy merged 16 commits into
mainfrom
feat/post-credit-stinger
May 31, 2026
Merged

feat(dungeon): hidden post-credit stinger 'The Vanishing' (demo-only)#33
silver-snoopy merged 16 commits into
mainfrom
feat/post-credit-stinger

Conversation

@silver-snoopy

Copy link
Copy Markdown
Owner

What

A hidden, demo-only post-credit stinger for the dungeon game — the "…and one more thing" closing flourish for the 2026-06-03 Slay the Cert talk. The warlock walks in, reads the golden parchment, teleports out in a white flash, and the screen falls to black with "Our hero will return…" beneath a pulsing arcane rune + a tiny 4th-wall wink.

Launched by a camouflaged trigger: a permanent decorative rune sigil in the HUD that is inert chrome for every normal player and only becomes a one-click launch in demo mode.

🚫 Do not merge yet — pending local testing by @silver-snoopy. See "For the reviewer to do locally" below.

Design & plan

  • Spec: docs/superpowers/specs/2026-05-31-post-credit-stinger-design.md
  • Plan: docs/superpowers/plans/2026-05-31-post-credit-stinger.md

How it works

  • demoMode flag (src/game/demoMode.ts) — sticky session flag via registry + ?demo=1 URL param + localStorage. Deliberately separate from the per-run demoRun flag (which is cleared on Hub return), so the trigger stays armed. Never touches bank/scoring/saves/NG+.
  • decorativeSigil.ts — mounts the rune for all players; only attaches a click handler (no hand-cursor) when isDemoMode. Click → PostCreditScene.
  • PostCreditScene.ts — the cinematic. Pure tween/flash/text choreography over the existing static hero image (no new art); the "teleport" is a white flash masking alpha/scale→0. Always returns to the Hub on input (after the text lands) or via a 20s failsafe.
  • Wiring — sigil mounted in both the Hub (beside the title) and the Boss HUD (beside the boss banner); enableDemoMode called on demo-campaign launch.

Pre-arm for the talk

Open localhost:5173/?demo=1 and the sigil is live from the first screen — no campaign needed.

Testing

  • 126/126 unit tests green (Vitest), strict tsc --noEmit clean, vite build succeeds.
  • New tests cover the demoMode source paths + the localStorage-throws (private-mode) fallback + the inert-vs-armed sigil contract.
  • Choreography has no unit test by design (repo convention: scenes verified manually; no committed Playwright harness).

Reviewed

Ran the pr-review-toolkit (code / tests / silent-failures / comments). No Critical issues. Applied fixes in 19aa1cf: hardened fadeToScene against unknown-scene-key black-screens, added demoMode resilience tests, fixed a strict-typecheck regression, removeAllEvents() on leave, comment cleanups.

For the reviewer to do locally (deferred Task 5)

  1. cd public/dungeon; npm run dev → open http://localhost:5173/?demo=1.
  2. A/B the two sigil mounts (Hub title vs. Boss banner) and decide which reads better as camouflaged chrome, then prune the loser (the plan predicts keeping the Hub mount). Both are intentionally shipped here so you can compare.
  3. Confirm: rune visible but inert without ?demo=1; full cinematic plays + returns to Hub; legible at projector res (~1600×900).

🤖 Generated with Claude Code

silver-snoopy and others added 16 commits May 31, 2026 09:29
Demo-only hidden post-credit cinematic for the dungeon game, triggerable
in one click via a permanent-decorative-but-demo-armed rune sigil. Built
as the closing flourish for the 2026-06-03 "Slay the Cert" talk.

- Treatment A "The Vanishing": walk-in -> golden scroll -> white-flash
  teleport -> "Our hero will return..." + rune + tiny 4th-wall wink
- Sticky demoMode flag (registry + localStorage + ?demo=1) distinct from
  per-run demoRun; gates the sigil's interactivity only, never progression
- Build BOTH Hub + Boss-HUD mounts, A/B locally, prune to one (research
  predicts Hub: combat-HUD clutter vs. menu as canonical easter-egg home)
- Zero new art; teleport sold by flash masking alpha/scale->0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-by-task plan for "The Vanishing": demoMode flag, PostCreditScene
cinematic, demo-armed decorative sigil, dual Hub+Boss mounts with local
A/B prune. Full runnable code + Vitest tests per step; choreography and
A/B verified manually (no committed Playwright harness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cene

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o-black works

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts, typecheck

- transitions.fadeToScene: guard unknown scene key (restore from black + error)
  instead of silently stranding on a black screen — protects the live-talk
  return-home path and every other caller.
- PostCreditScene.leave(): removeAllEvents() so the 20s failsafe can't outlive
  the dismiss decision (defensive against future refactors).
- demoMode: comment the read-side localStorage catch; add 4 tests covering the
  localStorage-throws fallback (private mode) and cross-source composition.
- decorativeSigil.test: fix makeFakeText typing so strict tsc --noEmit is clean
  again (was a regression on the previously-green baseline).
- BossFightScene: reword "A/B mount" comment (no bucketing logic exists).

126/126 tests, tsc clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrate the post-credit stinger with main's redesigned demo system
(scripted ?demo run on the real bank + boss-fight HUD bar). Resolutions:

- HubScene: dropped the obsolete async beginDemoCampaign (fake demo bank;
  demo-questions.json was deleted on main) in favor of main's
  beginScriptedDemo; injected enableDemoMode there so the stinger sigil
  stays armed. The sigil also arms directly via the shared ?demo param.
- BossFightScene: dropped the deleted demoBadge import; kept the
  mountDecorativeSigil mount (now beside main's new mountBossHud bar).
- Merged imports (DEMO_SEED/DEMO_BOSS_ORDER + isScriptedDemo/seed-override
  from main; enableDemoMode + decorativeSigil from this branch).

144/144 tests, tsc --noEmit clean, vite build green.
Per design feedback, the boss-fight sigil read as stray chrome out by the
audio icons. Anchor it just past the "Floor N/M · domain" run label on the
left of the HUD bar instead, so it belongs to the run/domain cluster.

mountBossHud now returns its run-label Text so the sigil can be placed off
the label's true right edge — keeping a consistent gap for any domain-name
length (MCP … Claude Code) rather than a fixed x that over/under-shoots.

144/144 tests, tsc clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per design feedback, restructure the HUD run label into three pieces —
floor info, the decorative rune as the separator (the old "·" dot is
dropped), domain info — so the bar reads "Floor N/M ◈ domain". The rune
is now genuine permanent chrome (a separator) for normal players and the
demo-armed stinger trigger.

mountBossHud lays out floor → reserved separator slot → domain and returns
the slot centre; BossFightScene mounts the sigil there. formatRunLabel
(dot-joined) becomes formatFloorLabel; tests updated.

144/144 tests, tsc clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Hub auto-relaunches the scripted demo whenever ?demo is in the URL
(HubScene.create), so clicking the in-fight door during a demo went to the
Hub and immediately boomeranged back into the fight — the door appeared
dead. exitToHub now strips the ?demo param (which also carries the seed)
before fading to the Hub, so an explicit "leave" actually lands there.

144/144 tests, tsc clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting

Make the scripted ?demo a navigable journey and fix the post-credit sigil's
demo gating and placement.

Journey model (stored on the run save as journeyMode: 'normal' | 'demo'):
- Always land on the Hub in demo mode; the demo is started from the begin
  button (startNewRun routes to beginScriptedDemo when ?demo, else a real run),
  which also fixes "abandon demo -> New Game" starting a new demo.
- The in-fight door persists the demo run and returns to the Hub; the existing
  Continue button resumes it (resumeActiveRun re-arms demoRun for demo saves).
- Journey resets (clearActiveRun) when the saved run's journeyMode disagrees
  with the current mode (switch in / reload out) or after the stinger plays
  (PostCreditScene sets postCreditPlayed; the Hub consumes it).
- writeSave now persists demo runs tagged demo; the Hub leak-guards them so a
  demo save never surfaces as a Continue offer in normal play.

Sigil:
- Arm strictly on isScriptedDemo() (the ?demo URL) — the single source of truth
  for demo mode. Removes the buggy localStorage-persisted demo flag that left
  the sigil clickable in normal play; deletes the now-redundant demoMode module.
- Becomes the Floor/domain separator in the boss HUD ("Floor N/M ◈ domain"),
  14px to match the labels, nudged to centre in its slot.

131 tests, tsc --noEmit clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the diamond

The Hub had a separate decorative ◈ icon, but the agreed trigger is the
castle-gate title itself. Remove the standalone sigil from the Hub and arm
the "🏰 Gates of the Archive" title as the post-credit launcher in demo mode.

Extracted armStingerTriggerIfDemo so the boss-HUD sigil and the Hub title
share one arming path (interactive + pointerdown → stinger, only under ?demo,
no hand-cursor). The boss-HUD ◈ separator is unchanged.

133 tests, tsc clean, build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cannot desync

The scripted ?demo run keys its question picks on campaign.seed via
demoRngForFloor. The seed was never persisted in RunSave and was dropped when
resumeActiveRun rebuilt the campaign, so a between-boss demo resume re-picked
with demoRngForFloor(undefined, floor) -> NaN collapses to seed 1 -- silently
diverging from the committed cheat-sheet key on every floor past 0. The
"fail loud" guard only caught a missing campaign, not a lost seed.

- RunSave.campaign gains optional `seed`; isRunSave requires it for demo saves
  (corruption tripwire) while staying back-compat for legacy/normal saves so
  live players' in-progress runs still load.
- New pure campaignFromSave() reconstructs a full Campaign incl. seed;
  resumeActiveRun routes through it instead of an inline seed-dropping literal.
- writeSave persists campaign.seed.
- Tests (written first, watched fail): seed round-trip, legacy back-compat,
  demo-without-seed rejection, campaignFromSave unit coverage, and an
  end-to-end resume re-pick guard pinning floor 1's golden key.

Also track the thermo-nuclear-code-quality-review skill (un-ignore
.claude/skills/), the review pass that surfaced this bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Biome check (CI lint gate) flagged organizeImports in dungeon.test.ts and
formatting in runSave.test.ts on the previous commit. Pure formatting /
import-sorting — no behavior change. Tests 140/140, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@silver-snoopy
silver-snoopy merged commit e554b7d into main May 31, 2026
1 check passed
@silver-snoopy
silver-snoopy deleted the feat/post-credit-stinger branch May 31, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant