Next Up
What’s left, now that Season 1 and Season 2 are both fully built. PLAN.md stays as the build history (why every shipped mechanic exists, exact as-built detail) — this file is where the next things live instead of getting bolted onto the end of an already-huge document.
RESEARCH TASK — architecture + tooling review — DONE, awaiting decision
Written up 2026-08-07, researched same day (4 parallel agents, read-only). Findings +
a concrete proposal are in ARCHITECTURE-REVIEW-2026-08.md (repo root) — read that
before touching any of this. Original framing kept below for reference, but the actual
next step is: discuss the proposal with Torsten, then implement whatever’s agreed, not
re-research from scratch.
One artifact already exists from the research pass, uncommitted: a proof-of-concept
pure-function extraction + Vitest test in src/game/player.ts /
src/game/player.test.ts (plus vitest added to package.json) — see the doc’s §4
for what it demonstrates. Review and accept/tweak/discard before building on it.
Original task framing (2026-08-07)
Written up 2026-08-07 so a fresh session (no prior conversation) has everything needed to pick this up cold. This is a research/analysis task — read, dig in, come back with findings and a concrete proposal. Don’t start implementing a redesign until that proposal’s been discussed.
Why this is happening now: building Season 2 took much longer than building Season 1 did, and a single afternoon’s round of post-playthrough bugfixes (a portal bug, an unreachable pickup, HUD gaps, content tuning) felt disproportionately slow too, largely because verifying each fix meant a full Playwright browser round-trip (dev server + headless Chromium + scripted keypresses + screenshots/state polling), sometimes several iterations to get a single platform’s jump geometry right. Torsten’s own framing: “I rather invest a few hours in an architectural redesign than wait 30 minutes each for a bunch of bugfixes.” Three concrete things to look at:
1. The Season 1/2 split architecture may be structurally wrong
Read MECHANICS.md’s “Season 1 vs. Season 2” section first for how it works
today: most levels reuse their exact Season 1 LevelData object for Season
2, and escalations are spliced in via optional season2Extra* fields
(season2ExtraItems/season2ExtraHazards/season2ExtraPlatforms/
season2ExtraWindZones), read by loadLevelData()/resetHazards() in
src/main.ts only when season === 2. In practice this meant almost
every Season 2 mechanic touched main.ts directly, scattered across many
if (season === 2) checks at different call sites (the hazard-reveal
distance, the wind-push accumulation, the goal-scoring gate, the ambush
items, the music-layer lookup, and more) rather than living somewhere
Season-2-specific and self-contained. It also caused a real bug
found via a full playthrough 2026-08-07: PortalDef.used is mutable state
on the shared LevelData object, so finding an Underworld portal once in
Season 1 permanently marked it used for Season 2 too — nobody designed
that, it just fell out of sharing state across an accidental object
reference. (Fixed for now with a targeted reset in startSeason2(), but
that’s a patch, not evidence the architecture is right.)
The actual question: is “one LevelData object, conditionally
mutated/spliced based on a season variable read at dozens of call sites in
one giant main.ts” the wrong shape? Alternatives worth evaluating:
- Each level exposes its own explicit Season 1 and Season 2
LevelData(even if Season 2 mostly just spreads Season 1’s and overrides a few fields) — makes the diff between seasons readable in the level file itself, not reconstructed by greppingmain.tsforseason2Extra*andseason === 2. - A small “level variant” or “modifier” abstraction the engine understands
natively, so adding a Season 3 (or a difficulty variant, or a remix mode)
doesn’t mean editing
main.tsin N more places. - Keep the current shape but build real helpers that collect all the
season-conditional logic into one place per concern, so
main.tsstops accumulating scatteredif (season === 2)checks one bugfix at a time.
Don’t assume the fix is obvious - actually look at how main.ts grew this
session (it’s the single largest, most-touched file by far) and form a real
opinion about whether the shape of the split is the problem, not just its
current size.
2. Look at latticexyz’s MUD and OPCraft for architecture ideas
Clone these locally and read the code — ignore/skip anything Ethereum/ blockchain-specific (the on-chain state sync, contracts, wallet stuff), pixelsp33d has no on-chain component and none is planned. What’s actually worth mining: how MUD structures game state as an ECS (Entity-Component- System) — components, systems, world — and how OPCraft (a real shipped game built on MUD) organizes its game logic, client/rendering split, and dev tooling on top of that. Repos:
https://github.com/latticexyz/mud- OPCraft’s repo (search from the MUD org/docs if it’s not obviously named the same - it was Lattice’s own voxel-game demo built on MUD)
What to actually extract, concretely:
- Does an ECS-shaped core (entities = numeric/opaque IDs, components = plain
data keyed by entity, systems = pure functions operating on component
queries) look like a better fit for pixelsp33d’s own
ItemInstance/HazardDef/Playermodel than what exists today? Would it make a “Season 2 adds awindPushcomponent to some entities” change local and additive instead of a newif (season === 2)branch inmain.ts? - How do they structure “systems” so game logic is testable independent of rendering? (Directly relevant to the testing question below.)
- Any dev-tooling/hot-reload/debug-inspector patterns worth stealing
regardless of the ECS question - e.g. OPCraft likely has some kind of
live world inspector, which is conceptually similar to pixelsp33d’s own
ad-hoc
window.__ps33dDebughook that’s grown organically this session and could probably be more principled.
Come back with: what’s genuinely reusable (as inspiration/pattern, not literal code - MUD’s the wrong language/runtime shape for a Vite/vanilla-TS browser game), what’s blockchain-specific cruft to ignore, and a concrete recommendation on whether an ECS-shaped rewrite of the core game-state model is worth it here.
3. When does pixelsp33d outgrow Canvas2D?
Not urgent, but worth having a real answer instead of a vague feeling.
Think through: what are the actual signals that would justify moving off a
hand-rolled CanvasRenderingContext2D draw loop (current approach - no
engine, no WebGL) - entity count? layered-effect complexity (parallax +
shimmer + crossfades are already hand-rolled per-effect)? mobile
performance once touch controls exist? Actually check: startLoop()
(src/engine/ - find it) and how many draw calls a busy scene (e.g.
Tanzstunde S2’s finale, ~5 dancers + disco balls + water shimmer + HUD)
costs per frame today, and whether there’s headroom or it’s already close
to a budget. Propose a decision framework (a set of concrete thresholds -
not “rewrite it now,” not “never think about it again”) and, if relevant,
name a specific lightweight next step (a thin WebGL sprite-batching layer?
a real 2D lib like PixiJS? something else?) rather than jumping straight to
“use a full game engine.”
4. Testing strategy - Playwright doesn’t scale, what do real studios do?
The concrete pain: verifying the Dance Shoes platform fix this session took
several rounds of “edit level file → start dev server → launch headless
Chromium → script keypresses with hand-tuned millisecond delays → read
window.__ps33dDebug state → adjust numbers → repeat” - each round-trip
slow, and fundamentally testing the rendered browser game, not the
underlying game logic. This does not scale as the mechanics catalog grows.
Research what real game studios actually use for automated gameplay testing (distinct from web E2E testing, which is what Playwright is built for) - likely candidates to look into: deterministic simulation / “headless mode” that runs game logic without any rendering at all, replay-based testing (record real input sequences, replay and assert on resulting state), unit-testing pure game-logic functions in isolation (collision math, hazard state machines, item-pickup resolution) without a browser at all, snapshot/property-based testing for level geometry invariants (e.g. “every jump-required gap must be ≤ max jump range” - would have caught the Dance Shoes bug at level-definition time, not via manual playtesting).
Concrete question to answer: could pixelsp33d’s core game logic (collision resolution, hazard duty-cycles, item-pickup rules, the dance sequence’s grading math) be refactored to run and be asserted on in plain Node, with zero browser/Playwright/canvas involved - fast, synchronous unit tests colocated with the mechanic they test, the same PR/commit that adds a mechanic also adding a targeted test for it? Playwright would then be reserved for genuine end-to-end/rendering/input-integration smoke checks (a handful, not the primary verification method), not the only tool in the box. Come back with a concrete recommendation and, ideally, one small proof-of-concept (e.g. a pure function extracted from the jump-height math, unit-tested directly) demonstrating the shape of the answer.
Also worth considering as an outcome of this research, not just a code
change: offloading test-driving to dedicated hardware. Torsten has a
homelab/fleet (see tracker’s reference_fleet_naming/reference_home_tailnet_hosts
memory if this session has access to ~/txt/tracker/’s memory - otherwise
just ask him what’s available) - a spare box could run the Playwright/
headless-browser suite (or a future replay-based one) continuously or on a
schedule, off the main dev machine entirely, rather than every verification
loop competing for the same laptop that’s also running the dev server and
the editor. Whether that’s worth setting up depends partly on the answer to
the “does this even need a browser” question above - a pure-Node unit-test
suite is cheap enough to run anywhere/constantly; a full Playwright+Chromium
suite is exactly the kind of thing that benefits from living on its own box
instead of eating local resources during active development.
Done, 2026-08-07: tools/fleet_test.sh (local driver, run from the
laptop) + tools/remote_test_runner.sh (runs on the remote box) implement
this against trullala. rsync ships the current working tree over (no git
remote exists between this repo and trullala - verified, this repo is
local-only); the remote side runs npm ci → tsc --noEmit → vitest run
→ starts the dev server → both Playwright smoke suites → tears the server
down, inside the shared trullala-tasks tmux session per tracker’s fleet
convention. Verified end-to-end live: bootstrapped python3-venv +
Playwright + Chromium on trullala (none of that was present before), ran
the full pipeline twice, all 6 steps PASS both times. Result comes back to
tools/.fleet-results/.remote-test-status + .remote-test-log on the
laptop, no tmux attach required to check.
Mobile touch controls
PLAN.md’s P7 “Ship” phase always scoped this as the last thing before
deployment: a static dist/ build plus on-screen touch controls, everything
else (hosting, domain) left as a separate decision. Not started. The engine
already reads through one Input class (src/engine/input.ts) with keyboard
and gamepad both normalized to the same held/edge-triggered methods — touch
would be a third source feeding the same interface, not a parallel input
path.
Hosting choice for pixelsp33d.de
Done 2026-08-10 — full “as built” record moved to PLAN.md §7 (“Hosting, docs,
and public launch”) since it’s now frozen build history, not live work. Short
version: the game is live at https://pixelsp33d.de/ (Cloudflare Pages), the docs
site is public at https://docs.pixelsp33d.de/ and privately at
https://preview.pixelsp33d.de/ (login-gated, for staging doc changes before
promoting them), and trullala (the homelab box) serves a private LAN-only playtest
copy at http://192.168.178.42:8095/.
Idea, not built (2026-08-10): extend the existing TEST_REDIRECT_TARGET
redirect trick (vite.config.ts’s testRedirect() plugin, see
memory/reference_tv_testing.md) to make switching which host the TV’s one
bookmark points at (e.g. pad’s dev server vs. trullala’s static demo) as
easy as switching debug targets already is - so Torsten doesn’t have to
re-bookmark or hand-edit the IP on the TV remote every time. Two variants
floated, either could work, neither built:
- Same as today - Claude edits a constant/config server-side and restarts whichever server needs to pick it up.
- Self-serve via an in-game OSD, e.g. an “extras” menu item - Torsten picks the target himself from a list on the TV (gamepad-driven), no laptop-side edit needed at all. Would need its own small redirect/registry service (or piggyback on whichever host the OSD itself is served from) since a static build has no server-side plugin to update at runtime.
atproto score/trophy lexicons (de.pixelsp33d.*)
Conceptual only, never implemented. Pentaract’s own score/achievement
lexicons don’t exist yet upstream — de.pixelsp33d.* was always meant as a
stopgap, and may need migrating if/when upstream lands real equivalents
rather than being a permanent fixture.
Done 2026-08-10, unrelated to the lexicon question itself: the project
picked up a real ATProto identity, pixelsp33d.eurosky.social (a bsky
handle on the eurosky.social PDS) — a placeholder, intended to move to a
pixelsp33d.de-based DNS handle later now that the zone exists. See
ATPROTO for the fuller brainstorm this connects to.
Narration audio (Moritz)
Not blocking — the game is caption-first already. Distinct from background music (built, procedural, already playing under everything): this is spoken-word per-level commentary, still unrecorded. Waiting on Moritz, not on any engine work.
Tanzstunde dance sequence: relocate + tie to a chest, feeding the finale
Real design idea from a full playthrough (2026-08-07), not implemented yet. Torsten’s own framing: “the dance button action should move to the start of the level or even better to the underworld of the tanzstunde portal. The idea is that the dance move opens the chest. The chests content are the lead to the finale and also the rickroll later.”
What this actually proposes, spelled out since it’s a few linked ideas at once:
- Move the dance sequence’s trigger (currently x=1805 in
tanzstunde_s2.ts, right before the boss) somewhere earlier/different - either the start of the level, or (Torsten’s preferred option) a new Underworld sub-level reached via a Tanzstunde portal that doesn’t exist yet - Tanzstunde currently has no secret-portal/Underworld connection at all (only Lidl Lunch and Soccer do, perMECHANICS.md). Building this means a genuinely new portal + a new Underworld room, not just moving an x-coordinate. - Change what a successful dance does: instead of the current small
per-step energy bonus (
Player.danceSequenceStep()), a good dance opens a chest. - That chest’s contents become the lead-in to the game’s actual finale, and also connect to the rickroll gag below - i.e. this and the rickroll gag are the same underlying “grand finale” design thread, not two separate features.
This is a real reward-progression redesign (touches the dance sequence, the portal/Underworld system, the chest reward, and the finale screen all at once) - worth designing deliberately with Torsten before building, not implementing from a one-line paraphrase.
Decided 2026-08-08 (as envisioned by uervel): the dance-sequence chest is the rickroll trove — one object, not two. Tanzstunde’s dance QTE teaches the player a specific input sequence (Simon-Says style, not just a generic energy-bonus minigame); that same learned sequence is what has to be correctly repeated at the trove to open it and trigger the rickroll episode. “Learned, then repeated” is the actual mechanic connecting the two NEXT.md sections below.
Fully resolved 2026-08-08 (see the rickroll gag section for the full
rationale on each): the trove lives in a new Clouds secret room, “The
Upperworld” (Clouds is the only level reachable after Tanzstunde in the
fixed level order, so it’s a real recall gap, not an immediate repeat);
recall keeps the beat-bar/rhythm timing help but drops the move labels
(a real memory test without becoming guess-the-combo); and the payoff
fires mid-run, as soon as the player reaches Clouds — not gated behind
hasCompletedGame, which would make it structurally unreachable during
the run that’s supposed to lead into it. Implemented and merged to
main, 2026-08-08 — since playtested, with several follow-up fixes
(portal visibility/positioning, dance-lesson demo+loop redesign, wind
gauntlet) also merged; see PLAYTEST-FINDINGS.md for the full record.
The rickroll gag (was ROLL.txt, folded in here 2026-08-08 — nothing lost, see below)
Unbuilt. A treasure trove (reusing a “known good from an earlier level” prop) unlocked by an alternate-button sequence with some real timing precision to it — a visible lever/gear turning on each correct press so the unlock mechanic reads clearly, not a blind guess-the-combo. Payoff: an 8-bit rickroll animation + music sting.
Licensing note (from the original ROLL.txt): Torsten flagged needing
“royalty free licenced” audio for the sting, not the real song outright —
the bytebeat_*.wav sketches below are one way to sidestep that entirely
(a deliberately broken-sounding bytebeat reinterpretation of the joke,
not a needle-drop), but if a more literal audio cue is ever wanted, it
still needs a cleared/royalty-free source, not the original recording.
Torsten’s own framing ties this to the game’s eventual grand finale, not
a standalone easter egg dropped anywhere convenient — “will come back to
this for the grand finale, interludes and ROLL’in.” The bytebeat_*.wav
sketches in assets/gen/music/season3_exploratory/ were generated with this
specifically in mind (deliberately weird/broken-sounding is the joke) — see
that directory’s own README.md.
Decided 2026-08-08: this trove is the Tanzstunde dance-sequence chest (see the section above) — not a separate pickup. The “alternate-button sequence” required to open it is specifically the sequence the dance QTE already taught the player; opening it is passing a recall test, not guessing a combo.
Fully resolved 2026-08-08, closing out the three sub-questions this left open:
- Where the trove sits: Clouds (“The Upperworld”, a new portal-gated secret room), not next to Tanzstunde. Clouds is the only level reachable after Tanzstunde in the fixed, no-backtracking level order, so this is a real recall gap — the physical-distance idea from ROLL.txt’s original “known good from an earlier level” framing survives, just resolved to a specific place rather than “revised.”
- How recall is tested: the middle ground — keep
drawDanceSequenceOverlay()’s beat bar (rhythm/timing help) but drop the move labels. Tests whether the player remembers what to press without also demanding blind reflex-precision on top of memory (DANCE_COMBOis only 4 moves; fully blind risked reading as guess-the-combo rather than a fair callback). - When it pays off: mid-run, reachable as soon as the player reaches
Clouds — not gated behind
hasCompletedGame. That flag only gets set after finishing the game, which would make the trove unreachable during the very run it’s meant to lead into (Clouds is the last level; Season 2’sgameCompletefires at its boss, after where the Upperworld portal sits). The Upperworld’s backdrop is added toDEMO_BACKDROPSso it shows up in the existinggameCompletecrossfade montage as an echo, at no extra engine cost.
Implemented and merged to main, 2026-08-08 — see the section above
for the paired teach-side build (Tanzstunde’s own new Underworld room).
The rickroll animation/music payoff itself is also now built and merged,
2026-08-08 (same day, later session) — a generic state === 'interlude'
mechanism (title card + 2-frame animated scene, blocks input) serves both
the 6-7 gag (below) and the real rickroll payoff: a disco backdrop (disco
ball, spotlight beams, the existing Tanzstunde dancefloor tile) + a
procedural kn00t-in-trenchcoat sprite, with the existing bytebeat_1.wav
(from season3_exploratory/) graduated into a real rickroll MusicName.
triggerRickrollStub() is no longer just a flag-flip stub — it starts the
real interlude. Built in a worktree concurrently with two research docs
below; see “Reconciling the build” for what that surfaced and what’s still
open.
Reconciling the build against the concurrent research docs (2026-08-08)
The rickroll/6-7 build ran in its own worktree at the same time as
SIXSEVEN-GAG-2026-08.md and RICKROLL-AUDIO-2026-08.md were being
researched — neither saw the other’s output. Comparing after the fact
surfaced six divergences; Torsten decided all six in one pass:
- kn00t_67 sprite resolution — keep as built (16×16). The doc found the source gif’s native grid is actually 20×20 (not 16×16 like the rest of the kn00t family), which would be more faithful — but 16×16 keeps it visually consistent with every other kn00t variant, and the sprite is already built and tested. No further action.
- 6-7 gag backdrop — keep as built (flat black + spotlight). The doc
proposed holding a real level backdrop behind it instead (via
drawBackdropSequence()). The flat treatment reads as a clean, isolated “stage” moment, closer to a comic-strip cutaway. No further action. - Rickroll’s
kn00t67_stingSFX tone — recompose as silly/kazoo-ish. Built triumphant (reads as “you accomplished something”); both the original design intent andRICKROLL-AUDIO-2026-08.mdwanted something that reads as a joke instead. Action needed: rework thekn00t67_stingblock intools/make_sfx.py(~line 194 — currently an explicit “ta-da”-style rising run + landing flourish per its own comment) into a deliberately silly/kazoo-ish register instead, same pulse/noise/envelope primitives, different character. Re-renderassets/gen/sfx/kn00t67_sting.wavafter. - Caption text (“MEANWHILE…”) — keep as built. Not contested;
matches
docs/src/story.md’s existing “Meanwhile…” interlude convention directly. No further action. - Splice architecture — keep as built (defer the level load until the interlude resolves, not load-then-overlay). Purely internal; the doc’s alternate approach produces the same visible result. Not worth reworking for no user-facing difference. No further action.
- Richer original rickroll audio track — build it now. The built
payoff only wires the existing zero-risk
bytebeat_1.wav.RICKROLL-AUDIO-2026-08.md’s compositional brief (real melody/chords composed fresh, evoking ~113 BPM/B♭ minor/SAW-era instrumentation without copying it — see that doc’s “line drawn” section for exactly what’s safe vs. not) was recommended as an additional tier, not a replacement. Action needed: compose the new track per that brief, fittingtools/make_music.py’s existing primitives (render_voice,pulse/triangle,kick/hihat/clap) via a newmake_rickroll()- style function — this is real composing work (fresh melody + chords), not parameter tuning. Once rendered, wire it in alongside (not instead of) the existing bytebeat track — exact selection mechanism (replace at therickrollMusicName, or a second name picked between) still open, decide when actually building this.
Remaining actionable work from this thread: items 3 and 6 above (a sting re-render + a new composed track) — everything else is closed with no further changes needed.
Items 3 and 6 done, 2026-08-08. kn00t67_sting recomposed in
tools/make_sfx.py into the silly/kazoo-ish register (a nasal duty
cycle, a comedic “wah-wah-wah” triple toot, a droopy downward landing
instead of an ascending flourish) and re-rendered.
Item 6 ended up producing three rickroll tracks rather than one,
after Torsten reviewed the brief-compliant original and then explicitly
chose to also add a literal reproduction — see
RICKROLL-AUDIO-2026-08.md section 6 for the full decision record
(stated rationale: relying on meme-culture/parody norms for the legal
footing, not on avoidance-by-composition):
'rickroll'— the original zero-risk bytebeat track (unchanged).'rickroll_composed'(make_rickroll_composed()) — an original synth- pop pastiche per the brief in section 3: 113 BPM, Bb minor, fresh melody/chords, no vocal.'rickroll_hooktheory'(make_rickroll_hooktheory()) — a literal transcription of the real chorus’s melody and ii-V-iii-vi progression, sourced from Hooktheory data pasted directly into section 6 (the page itself 403’s automated fetching).
main.ts’s rickrollInterludeConfig() now picks one of the three
uniformly at random each time the interlude fires (RICKROLL_TRACKS),
and sizes sceneDurationS to that specific track’s real decoded length
via a new getMusicDuration() export in audio.ts — the three tracks
run 12.0s/21.24s/16.84s respectively, so a fixed duration (this config’s
shape before the third track existed) would have cut the two longer ones
off mid-phrase. tools/smoke_test_season2.py’s rickroll-interlude test
had its poll timeout widened accordingly (16s → 26s) to cover the
longest possible pick.
The gameComplete/leaderboard screen also picked up a small callback:
advanceLevel() plays 'rickroll_composed' there instead of the
player’s usual per-player bytebeat, but only for a run where
rickrollStubTriggered is true (the trove was actually found) —
everyone else’s leaderboard music is unchanged.
Verified: tsc --noEmit, vitest run (23 passed), and both Playwright
smoke suites green across several repeated runs (to exercise different
random track picks).
Also fixed in the same pass (unrelated bug, caught by inspection):
drawRickrollScene’s caption always read “kn00t found the vibe.” even
when the current player was pixelsp33d/uervel/Mama — kn00t is
just one of the four playable characters (scoreboard.ts’s
PLAYER_NAMES), not a fixed narrator. Now interpolates currentPlayer.
The kn00t_67 cameo sprite itself is unrelated and untouched (that’s a
separate NPC, not the player character).
Follow-up, same day: rickroll_hooktheory is a gag written
specifically for pixelsp33d — that character now always gets it
deterministically (rickrollInterludeConfig’s track check), no roll
of the dice; every other character still gets the random pick across
all three tracks. Separately, kn00t67_sting’s original pre-rework
“triumphant ta-da” version (recovered from git history, commit
111b2f8^) is back as kn00t67_sting_tada, wired alongside the kazoo
rework as a random pick in sixSevenInterludeConfig rather than one
replacing the other. Both changes exposed a real regression:
smoke_test_season2.py’s test_upperworld_recall_grants_chest_and_ rickroll_stub has its own separate _poll_until_state(..., max_ms= 16000) call (distinct from the dedicated rickroll-interlude test’s,
already widened earlier) that I’d missed — since that test’s
currentPlayer defaults to pixelsp33d (never goes through the
chooser), it now always draws rickroll_hooktheory’s ~18.4s total wait,
which blew past the old 16s cap. Widened to 26s; re-verified green
across multiple runs against a live dev server afterward.
Surge test, same day: both smoke suites gained a --player flag
(smoke_test.py/smoke_test_season2.py) that drives the real in-game
chooser (ArrowRight × N + Space, matching PLAYER_NAMES’s order -
not a debug/URL shortcut) before running any test, so
currentPlayer-dependent behavior gets exercised as an actual player
selecting that character would trigger it. tools/surge_test.sh runs
both suites once per one of the four characters against an
already-running dev server, resetting the scoreboard first via a new
tools/reset_scoreboard.py (a real page visit to ?resetScoreboard=1,
same debug param the OSD’s own reset uses - not direct localStorage
surgery). All 4 characters × both suites passed clean.
Idea for later (not being built now): the headless-Playwright method used for these smoke tests could double as a screenshot/ screencast capture pipeline for promo material, or even a “replay” section in the OSD extras — Torsten floated this while watching a smoke-test run and realizing it wasn’t a visible playthrough. Purely a someday idea, no design work done.
Season 3
Doesn’t exist as a game structure yet — no decision made on what a third
pass over these scenes would even mean mechanically (harder again, like
Season 1→2? A genuinely different angle per level? Something else?).
tools/explore_season3_music.py generated a first batch of candidate
textures (punchy synth, heavier atmo, three bytebeat one-liners) as pure R&D,
not tied to any committed direction — a menu to pick from once there’s an
actual Season 3 idea to score, not a plan in itself.
Small, low-priority housekeeping
- Discord CDN links in
ASSETS.txtare signed and will expire — harmless today since everything’s already pulled intoassets/src/, only matters if a re-download is ever needed.