Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 grepping main.ts for season2Extra* and season === 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.ts in 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.ts stops accumulating scattered if (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/Player model than what exists today? Would it make a “Season 2 adds a windPush component to some entities” change local and additive instead of a new if (season === 2) branch in main.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.__ps33dDebug hook 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 citsc --noEmitvitest 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:

  1. Same as today - Claude edits a constant/config server-side and restarts whichever server needs to pick it up.
  2. 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, per MECHANICS.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:

  1. 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.”
  2. 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_COMBO is only 4 moves; fully blind risked reading as guess-the-combo rather than a fair callback).
  3. 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’s gameComplete fires at its boss, after where the Upperworld portal sits). The Upperworld’s backdrop is added to DEMO_BACKDROPS so it shows up in the existing gameComplete crossfade 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:

  1. 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.
  2. 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.
  3. Rickroll’s kn00t67_sting SFX tone — recompose as silly/kazoo-ish. Built triumphant (reads as “you accomplished something”); both the original design intent and RICKROLL-AUDIO-2026-08.md wanted something that reads as a joke instead. Action needed: rework the kn00t67_sting block in tools/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-render assets/gen/sfx/kn00t67_sting.wav after.
  4. Caption text (“MEANWHILE…”) — keep as built. Not contested; matches docs/src/story.md’s existing “Meanwhile…” interlude convention directly. No further action.
  5. 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.
  6. 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, fitting tools/make_music.py’s existing primitives (render_voice, pulse/triangle, kick/hihat/clap) via a new make_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 the rickroll MusicName, 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/Mamakn00t 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.txt are signed and will expire — harmless today since everything’s already pulled into assets/src/, only matters if a re-download is ever needed.