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

6-7 Gag Research (2026-08)

The 6-7 gag as a “Meanwhile…” interlude — build-ready draft (2026-08-08)

Follow-on to CAMEO-COMEDY-2026-08.md §5, which already settled the what: build kn00t_67.gif’s unbuilt “6-7” easter egg as a level-transition-screen gag, not a 5th in-level environmental cameo, timed to the existing Soccer→Tanzstunde level advance. This doc takes that one paragraph and turns it into something buildable: what’s actually on screen, what the sting sounds like, which state machine it hooks into, and how it relates to docs/src/story.md’s “Meanwhile…” narrative device and the sibling rickroll-finale thread (FINALE-DESIGN-2026-08.md, NEXT.md). Same spirit as the other three 2026-08 docs — a set of concrete options for Torsten to react to, nothing here is decided. No code changes, no asset generation — this doc only.


1. What’s actually in the asset today

Opened both frames directly (assets/src/kn00t/kn00t_67.gif, coalesced), not going by name alone:

  • 200×200, 89a GIF, 2 frames, 32-color palette.
  • Straw hat, brown beard/mustache, blue eyes (the harmless twin of EvilOriginalkn00t’s red-eyed “same costume” per CAMEO-COMEDY §1), rainbow-striped shirt visible at the shoulders/chest.
  • The animated part is small: both hands sit at chest height holding a bit of the rainbow shirt fabric; frame-to-frame the hand position shifts slightly — read as an idle “hey, over here” wave, not a big arm swing. It’s a subtle loop, not a broad gesture — worth knowing going in, since “2-frame idle wave” undersells how little actually moves.
  • Not yet pixelized. assets/gen/kn00t/ has outputs for all 8 gdrive portraits (pixelize_kn00t.sh) plus camo/pirate (pixelize_kn00t_extra.sh, for kn00t_1/kn00t_3), but nothing for kn00t_67. This is a real, first build step, not a formality — see §2.

2. Visual elaboration

2a. Extracting the two frames (new tooling step, following the existing pattern)

kn00t_1/kn00t_3 needed pixelize_kn00t_extra.sh because their source webps are non-16-divisible upscales of a native pixel-art grid (854×858 → 16×16, a documented non-integer-block-boundary case solved with -filter point). kn00t_67 is a cleaner version of the same problem: 200×200 is an exact ×10 scale of a 20×20 native grid (confirmed by test-resizing frame 0 to both 16×16 and 20×20 with -filter point — 20×20 preserves the eyes/mustache detail more cleanly than 16×16, which is expected since 200/20 = 10 exactly vs. 200/16 = 12.5). This means kn00t_67’s own native grid (20×20) doesn’t match the rest of the kn00t family’s 16×16 — worth flagging as its own visual fact, not a mistake to correct: the family isn’t pixel-grid-uniform to begin with (kn00t_1/kn00t_3 are also drawn on a different physical resolution than the gdrive set, just documented rather than pixel-perfect-matched).

Proposal: a new tools/pixelize_kn00t_67.sh, same shape as the other two scripts — convert kn00t_67.gif -coalesce to split the 2 frames, then per frame: flood-fill white background to transparent from all 4 corners (same -fuzz 8% recipe already proven on this exact source family), -filter point -resize 20x20!, plus an _hd.png upscale for any UI use that wants it bigger than native. Output assets/gen/kn00t/sixseven_a.png / sixseven_b.png (frame naming, not wave_1/ wave_2 — keeps it visually distinct from the ambush cameo family’s open_hair-style descriptive names, since this one isn’t a costume identity, it’s an animation frame pair).

2b. Making a 1-2s screen moment read as staged, not a gif drop-in

The raw asset is two static frames of a small, low-contrast gesture. Enlarging it and flipping frames for 1-2 seconds, alone, risks reading as exactly what CAMEO-COMEDY was worried about — “a picture appeared for a second,” not a beat. Proposals, layered (not exclusive — pick the subset that’s worth the build cost):

  • Scale it up a lot. At native 20×20 it’ll disappear on a 384×216 canvas (VIEW_W/VIEW_H). The other cameos work small because they’re meant to blend into level geometry (CAMEO-COMEDY’s R4). This is the opposite case — it’s the only thing on screen, so it should be the opposite of subtle. A _hd.png-scale render (say, 120-160px tall, roughly a third to half the canvas height) centered or slightly off-center reads as a deliberate portrait, not a sprite.
  • A held background, not a black screen. drawBackdropSequence() already exists as a generic “hold a background, crossfade, caption” primitive (used for the gameComplete montage and title/demo attract mode) — reusing it here means the gag gets a moving-parallax backdrop (Soccer’s or Tanzstunde’s own bgNear, or a plain solid color) essentially for free, rather than a flat fill behind the character. Concretely: a short, non-looping call into the same drawing code, holding on one frame instead of cycling levels.
  • Procedural motion beyond the frame-swap, staying inside the project’s no-external-art-pipeline rule (tools/’s scripts are all ImageMagick shell pipelines or numpy-driven Python, no hand-drawn frames beyond what’s already sourced):
    • A slow sine-driven vertical bob (few px, ~1Hz) on top of the 2-frame swap — same “cheap procedural life” trick already used elsewhere in this engine for idle animation (e.g. the dance partner’s beat-driven sway, danceDemoBeatProgress).
    • A radiating “ta-da” burst — a few small expanding circles/stars drawn with plain ctx.arc() calls timed to the sting’s hits (no new asset, matches the game’s existing habit of hand-coded Canvas2D flourishes rather than sprite-sheet FX, e.g. the confetti/sparkle-style elements implied by chest_open’s sparkle SFX pairing).
    • A simple vignette or radial spotlight (reusing the existing startTransition() vignette math, VIGNETTE_MAX_R, just as a static spotlight rather than an animated close/open) to frame him rather than pasting him on a flat rect.
  • Caption/text overlay. docs/src/story.md names the device “Meanwhile…” but that literal word has never actually been rendered anywhere in the shipped game — every other overlay speaks in-world text (LEVEL COMPLETE, kn00t blocks the way..., RECALLED!). This would be the first real use of the narrative label itself. Two options, genuinely open (see §6):
    • Literal: "Meanwhile..." as a top caption, establishing the device by name for the first time — the FINALE/rickroll payoff, whenever it’s built, would then be read as another instance of an established pattern rather than each interlude inventing its own text convention.
    • In-world: something that plays the “6-7” internet-meme joke straight without naming the device, e.g. a small "...6" / "...7" split across the two frame beats, or nothing at all — letting the 2-frame wave be the entire joke, silent and small, on the theory that over-captioning a meme reference kills it.

What I’d actually do: scale it up (2a), hold it against a level backdrop via drawBackdropSequence rather than a flat fill, and skip new procedural FX beyond maybe the sine bob — the sting (§3) is doing most of the “this is a real beat” work, and a family-audience meme cameo doesn’t need much visual decoration to land. Caption choice is genuinely open, flagged in §6.


3. Music: a short original sting

What’s already there

Two existing tools, two different registers:

  • tools/make_sfx.py — short (~0.1-1.5s) one-shot stings via note()/sequence()/ noise() + a simple attack-decay envelope(), written straight to assets/gen/sfx/{name}.wav, wired into a fixed SfxName union (src/engine/audio.ts) and played via playSfx(name) — genuinely one-shot, no looping. This is where every existing moment-scale sting lives: ambush_reveal (a dissonant stab + descending “cackle” for the kn00t ambush-twist reveal — the closest existing sibling to this gag, both being a “kn00t reveal” beat), chest_open (creak + ascending sparkle), goal (whistle + fanfare), level_complete (4-note ascending fanfare).
  • tools/make_music.py — full step-sequenced loops (bass/chords/melody/percussion via render_voice(), kick(), hihat(), mix()), written to assets/gen/music/{name}.wav, played via playMusic(name, layer?). Looping is hardcodedstartMusicNow() always sets src.loop = true; there is no one-shot playback path through the music system today. This file also holds bytebeat(), the raw integer-formula synthesis technique explicitly earmarked for the actual rickroll joke sound (NEXT.md’s rickroll section, season3_exploratory’s bytebeat_1/2/3.wav R&D sketches) — a different, deliberately “broken-sounding” register from what this gag wants.

The 6-7 gag’s sting is a 1-2s one-shot, timed to a specific on-screen moment — that’s exactly the make_sfx.py shape (ambush_reveal, chest_open), not the make_music.py shape (which would need new one-shot plumbing in audio.ts to even play back without looping).

Proposal

Add make_sixseven_wave() to tools/make_sfx.py’s main(), saved as assets/gen/sfx/sixseven_wave.wav, added to SFX_NAMES in src/engine/audio.ts. Musically: the existing stings split cleanly into “triumphant” (goal, level_complete — clean ascending diatonic runs), “sneer” (ambush_reveal — dissonant beating tones + descending cackle), and “treasure” (chest_open — creak + bright sparkle). None of those registers fit a friendly, goofy meme cameo — it should sound silly, not victorious or sinister. Concretely: a simple two-phrase “call and answer” matching the two animation frames — e.g. a bright rising interval on frame A (note() with a short, wide-duty pulse for a slightly reedy/kazoo-ish timbre, matching the straw-hat/rainbow-shirt “beachy” read rather than this game’s usual tight 8-bit chiptune duty cycle) answered by a droll descending “womp womp”-adjacent two-note close on frame B — total length matched to the on-screen hold (~1.2-1.8s, in DANCE_FANFARE_DURATION’s ballpark, not ambush_reveal’s shorter ~0.3s stab).

Open question (see §6): whether to keep it purely inside make_sfx.py’s simple note()/sequence() primitives (cheapest, matches the sting-family precedent exactly) or borrow make_music.py’s richer multi-voice render_voice()/mix() composition for a fuller little “jingle” feel (closer to what “compose some music” suggests, but means either duplicating a couple of helper functions across the two tool files or importing between them, which the existing tools don’t do today — they’re independent, single-purpose scripts). Leaning toward the simpler make_sfx.py path unless a richer jingle is worth that plumbing.


4. Input-blocking mechanic

What exists today (checked directly in src/main.ts)

update() is one large function that branches on state near its top, and every non-'playing' branch returns immediately after input.endFrame() — this is the actual mechanism by which the game already blocks player control during a scripted beat, not a separate flag:

  • state === 'won' — the LEVEL COMPLETE screen (drawWinOverlay). Set the instant boss.checkReached() fires (line ~1656); the just-finished level’s own data is still loaded (level.bossSprite is why the boss portrait shown here is that level’s own boss, not the next level’s). Only input.jumpPressed() does anything, calling advanceLevel(). No timer — it waits indefinitely for a press.
  • state === 'transition' — the vignette closing/opening beat wrapped around enterUnderworld()/exitUnderworld() (startTransition(), TRANSITION_CLOSE_S/ TRANSITION_OPEN_S = 0.5s each). Fully scripted, timer-driven, no input read at all during the beat. Hardcoded to the Underworld swaptr.kind is only 'enter' | 'exit', and the phase-complete branch calls enterUnderworld(tr.destination!)/exitUnderworld() directly, not a generic “run this content” hook.
  • state === 'dancing' — the Simon-Says QTE (updateDanceSequence), also fully taking over update().
  • state === 'gameComplete' — the finale screen; also blocks normal gameplay, though (uniquely) it still reads jumpPressed() for a couple of live choices (start Season 2, reset idle timer).

There is no generic “play a scripted, timed, input-blocking beat with custom content, then resolve to X” state. PLAN.md’s own P5 line item (“prequel/interlude/ sequel cutscene player, caption system, narration slots”) has never been built — confirmed by PLAN.md:314’s explicit note that the Season 1→2 celebration interlude was skipped for exactly this reason, and by the two video assets earmarked for exactly this purpose (VID-20210802-WA0000.mp4 prequel, VDSL_Video_1622044168870.mp4 “Meanwhile…” interlude — docs/src/assets.md) sitting completely unwired into the game to this day. drawBackdropSequence() is the closest thing to reusable interlude rendering machinery that exists, but it’s a rendering helper called from inside drawGameCompleteOverlay()/the title screen, not a state of its own with its own update-loop gating.

What this means for the 6-7 gag

Say it plainly, per the brief: this generic mechanism doesn’t exist yet. Building it just for the 6-7 gag (e.g. bolting a one-off timer onto the 'won'advanceLevel() path, conditioned on level.id === 'soccer') would work for this one gag, but the brief’s own framing — “both are interludes in the ‘Meanwhile…’ storytelling style,” both need to block input — plus the rickroll payoff being explicitly the next thing in this same design family (still unbuilt per FINALE-DESIGN-2026-08.md’s scope note: triggerRickrollStub() is a no-op today) means a one-off would likely get half-rebuilt again in a few weeks for the rickroll animation. Proposal: add one new generic state, once, sized for both.

Sketch (naming/shape, not final): a state === 'interlude' branch, structurally a sibling to 'transition' — timer-driven (intervalT/duration fields, same shape as transition’s t/phase), takes a small descriptor object (which draw function to call each frame, how long to hold, what to do on completion — a callback or a next-state string) rather than hardcoding “Underworld swap” the way 'transition' does. On completion it calls whatever was passed in — for the 6-7 gag, advanceLevel()’s normal loadLevel(levelIndex) (Tanzstunde’s own state already becomes 'playing' immediately per loadLevelData’s convention; the interlude would run as an extra beat inserted between advanceLevel() finishing its load and the game actually handing control back) — for the rickroll payoff, whatever the eventual 8-bit animation + sting sequence needs. Both content payloads (6-7’s two-frame wave + sting, rickroll’s animation + music) are just different data fed into the same timed-blocking-state machinery, matching the “build it once for the interlude type in general” instruction directly.

This also gives docs/src/story.md’s “Meanwhile…” concept and the never-wired prequel/interlude video assets a real home for the first time — worth a one-line flag to whoever picks this up that a generic 'interlude' state is also the thing PLAN.md’s P5 line item has been waiting on since the original design brief, not a bespoke solution invented just for a meme cameo.

Where exactly to splice it in for Soccer→Tanzstunde specifically (the placement question CAMEO-COMEDY-2026-08.md §5 left implicit): after advanceLevel()’s loadLevel(levelIndex) call, conditioned on the level just left being Soccer (activeLevels[levelIndex - 1].id === 'soccer', checked once, since advanceLevel() already runs for every level transition and only this one pair should trigger it) — override the state loadLevel just set to 'playing' back to 'interlude' for the gag’s duration before really handing control back. This keeps PLAN.md:209’s existing rule intact (“normal level-to-level advancement… stays instant”) for every other level pair, making Soccer→Tanzstunde a deliberate, narrow exception rather than a general behavior change.


5. Narrative framing

docs/src/story.md’s “Narrative Sequences” section names the device directly: “Short prequel, sequel, and interlude (‘Meanwhile …’) graphical sequences drive the narrative.” Two things earmarked for exactly this role (docs/src/assets.md/PLAN.md’s asset table) have sat unbuilt since the original design brief. The 6-7 gag, built as proposed here, would be the first shipped instance of this narrative device — not a one-off cameo gag wearing “Meanwhile…” as a label of convenience. That’s worth being deliberate about, because whatever shape it takes here (state name, timing convention, caption style) becomes the precedent the rickroll payoff and any future prequel/sequel beat inherits, whether or not that’s stated explicitly at build time.

Relationship to the finale/rickroll thread specifically: FINALE-DESIGN-2026-08.md and NEXT.md’s rickroll section describe a much bigger sibling — a full 8-bit disco stage payoff, gated behind the Tanzstunde-taught dance sequence recalled at a trove in the Clouds “Upperworld,” already merged as a mechanic (recallSucceeded/triggerRickrollStub()) but with its actual animation+music payoff still 100% unbuilt, blocked on sourcing royalty-free-cleared audio. The 6-7 gag is comedically and mechanically the small version of the same idea: a kn00t-costume callback (harmless-now/dangerous-later, same as kn00t_67/EvilOriginalkn00t) landing as a “Meanwhile…” beat that blocks input for a couple of seconds. Sequencing-wise, since the 6-7 gag is far smaller in scope (no royalty-free audio blocker, existing assets, no new game mechanic — just a timed cutscene state) it’s the natural one to build first, both because it’s ready to build now and because it would validate the generic 'interlude' state (§4) before the much heavier rickroll payoff has to lean on it. That sequencing note is mine, not Torsten’s — flagged as an assumption in §6.


6. Open questions (flagged, not guessed at)

  1. Caption text. Literal "Meanwhile..." (establishes the device by name, consistent going forward) vs. no caption / an in-joke caption vs. something else entirely. See §2b.
  2. Sting composition path. Simple make_sfx.py-style note()/sequence() sting (cheap, matches every existing sting’s precedent) vs. borrowing make_music.py’s richer multi-voice composition for a fuller “jingle,” which has no existing cross-file precedent to build on. See §3.
  3. Build order relative to the rickroll payoff. This doc assumes 6-7 goes first and the generic 'interlude' state gets validated on the cheaper gag before the rickroll animation leans on it — that’s an inference from scope size, not something Torsten has said directly. Worth confirming, especially since a concurrent agent is researching the rickroll animation/music in a separate worktree right now; the two docs should ideally agree on the shared 'interlude' state’s shape rather than each proposing its own, and that reconciliation is explicitly out of scope for this pass per the brief.
  4. Exact on-screen duration and vertical placement. “1-2 seconds” is the brief’s own range; nothing here pins it to a specific number, nor to where on the 384×216 canvas the enlarged portrait sits (centered vs. off to one side, e.g. peeking in from a screen edge the way a full-body character-select portrait might).
  5. Whether the gag should be skippable. Every other input-blocking state in this game either has no skip (the vignette transition, the fixed-length dance demo phase) or resolves on a specific press ('won'’s jump-to-advance). A 1-2s beat is short enough that “always plays, never skippable” is probably fine, but it’s worth deciding on purpose rather than by default, especially since a family audience will see this exact transition on every single Season 1 and Season 2 playthrough (nothing currently proposes gating it to first-time-only).