diff --git a/HANDOFF.md b/HANDOFF.md index d79641a..d5a409f 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -454,6 +454,20 @@ death summary is the end menu, and the zombie bed ducks out as it comes in. Two bed's gain at 0. Without it, a multi-megabyte file ducked mid-fetch comes up at full level several seconds later, over the match. +**The overlay is a four-stage machine**, not a screen per DOM node: `#intro[data-stage]` swaps +`splash -> menu -> {courts, tutorial}` while the backdrop and card frame stay mounted, because a +screen that unmounts the overlay flashes the live court through between menus. Court tiles SELECT +only; `START GAME` and `BACK` are their own buttons. The tutorial is a data table (`TUT` in main.js) +of Marvin poses, an optional picture already in `assets/`, and his lines — adding a page is a row. + +**The menu theme starts on the FIRST TAP, never at boot** (`go()` calls `menuMusic`). A title screen +that plays at you before you touch it is rude, and on a phone the autoplay policy would have held it +silent until that tap regardless. + +**Death returns to the MENU, not the splash.** `maybeRestart` still reloads — that is the honest +full reset and nothing has to unwind a run by hand — but it sets `sessionStorage.splatterStage` on +the way out, and boot consumes it to open at the menu. + **The theme is AAC** (`background-music.m4a`, 768 KB — the WAV was 8.3 MB, and this one downloads on every page load before the player has decided to play). `loopBed` takes the FULL filename now, because the beds are a mix of `.wav` and `.m4a`; `fadeBed`'s key must match it exactly, extension diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index 5660c28..02f3641 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -439,5 +439,19 @@ "check": "loose", "subject": "Three frames of the same tennis opponent, drawn as coarse low-resolution pixel art. FIRST a plain standing frame for scale, exactly like the reference. THEN TWO frames of him standing at the baseline BOUNCING A BALL on the ground before serving, with NO ball drawn. Every figure is the SAME man as the reference and the SAME size: RED polo shirt, dark grey shorts, RED cloth headband, blond hair, seen from the FRONT. Same face, same colours, same proportions. His headband is RED, the same red as his shirt \u2014 never white, cream or grey. In both bouncing frames his racket is held out to the side in his RIGHT hand \u2014 which is on the LEFT-HAND SIDE OF THE IMAGE \u2014 and his other arm is stretched DOWN AND OUT to the RIGHT-HAND SIDE OF THE IMAGE, well clear of his body, with the palm open and facing the ground as if patting a ball. CRITICAL: the two bouncing frames are otherwise the IDENTICAL drawing \u2014 same stance, same legs, same head, same racket, same size \u2014 and the ONLY difference between them is the height of that patting hand: in the first it is low, near knee height; in the second it is higher, near his waist. Draw NO ball in any frame.", "palette_ref": "o_idle1" + }, + "marvin": { + "sprites": [ + "marvin1", + "marvin2", + "marvin3", + "marvin4" + ], + "ref_only": [ + "x_boymop1" + ], + "anchor": 0, + "check": "loose", + "subject": "The reference is ONE young ball boy in a WHITE polo shirt and WHITE shorts with brown hair, seen from the FRONT, holding a floor mop. Draw FOUR frames of that SAME boy \u2014 same face, same brown hair, same white kit, same white shoes, same size, same front-facing view \u2014 with a gormless, dim-witted, slightly cross-eyed expression in every frame. THE FOUR FRAMES ARE FOUR DIFFERENT POSES: (1) WAVING hello with his free hand raised beside his head, big goofy open-mouthed grin, the mop standing upright in his other hand; (2) POINTING away to the side with one arm stuck straight out and his mouth open explaining something, mop upright in the other hand; (3) SHRUGGING with both palms turned up and his shoulders hunched, utterly baffled, eyebrows up, the mop tucked under one arm; (4) TERRIFIED \u2014 eyes wide as saucers, mouth open in a yell, both hands clutching the mop up across his chest like a weapon. Each figure stands upright at the same height with both feet on the ground. The whole boy and the whole mop are fully inside the canvas with clear space around them. The canvas contains EXACTLY FOUR figures and no others." } } \ No newline at end of file diff --git a/assets/marvin1.png b/assets/marvin1.png new file mode 100644 index 0000000..036927f Binary files /dev/null and b/assets/marvin1.png differ diff --git a/assets/marvin2.png b/assets/marvin2.png new file mode 100644 index 0000000..a2fd47f Binary files /dev/null and b/assets/marvin2.png differ diff --git a/assets/marvin3.png b/assets/marvin3.png new file mode 100644 index 0000000..f4215ca Binary files /dev/null and b/assets/marvin3.png differ diff --git a/assets/marvin4.png b/assets/marvin4.png new file mode 100644 index 0000000..1ac9130 Binary files /dev/null and b/assets/marvin4.png differ diff --git a/audio.js b/audio.js index 200c1cd..8137b7a 100644 --- a/audio.js +++ b/audio.js @@ -111,10 +111,10 @@ export const ambient = () => loopBed('tennis-ambient-loop.wav', 500, 0.035); // // 982597 frames at 48 kHz, which is the un-round 20.47077 s below — an AAC loop needs the SOURCE // length as loopEnd, not the decoder's padded one. See the note on menuMusic. export const zombieMusic = () => loopBed('zombie-mode.m4a', 800, 0.2, 20.47077); -// Menu theme: the splash, the court picker, the death summary. Barely highpassed — this one is -// MUSIC, not an atmosphere bed, so the low end is the point. Started at boot so it is already -// running by the time the title is on screen; on a phone the autoplay policy holds the context -// suspended until the first tap, and it becomes audible then (see loopBed). +// Menu theme: every menu screen and the death summary. Barely highpassed — this one is MUSIC, not +// an atmosphere bed, so the low end is the point. Started by the FIRST TAP (main.js `go`), never at +// boot: a title screen that starts playing at you before you have touched it is rude, and on a phone +// the autoplay policy would have held it silent until that tap anyway. // AAC, not WAV: 48 s of stereo music is 8.3 MB as a WAV and this one downloads on every page load, // before the player has decided to play anything. 768 KB encoded, and the master is exactly 48.000 s // — which is the loopEnd, because the decoded buffer is NOT (measured 48.0174 s in Chrome; see the diff --git a/index.html b/index.html index 4d92596..3ef920e 100644 --- a/index.html +++ b/index.html @@ -76,29 +76,54 @@ #build { margin: 10px 0 0; font-size: 9px; color: #5a6270; letter-spacing: 0.5px; } #intro .start { margin-top: 18px; color: #e0cc72; font-weight: bold; letter-spacing: 1px; animation: blink 1.1s step-end infinite; } @keyframes blink { 50% { opacity: 0.25; } } - /* court select — the intro's second stage. One overlay, two panels, .picking swaps them, so the - backdrop and the card frame never flash between screens. */ - #courts { display: none; } - #intro.picking #splash { display: none; } - #intro.picking #courts { display: block; animation: drop 0.3s ease-out; } - #courts h2 { margin: 0 0 14px; font: 400 11px Silkscreen, monospace; color: #e0cc72; letter-spacing: 2px; } + /* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame + alive across every screen is the point — a screen that unmounts the whole overlay flashes the + court through between menus. */ + #splash, #menu, #courts, #tutorial { display: none; } + #intro[data-stage="splash"] #splash { display: block; } + #intro[data-stage="menu"] #menu { display: block; animation: drop 0.3s ease-out; } + #intro[data-stage="courts"] #courts { display: block; animation: drop 0.3s ease-out; } + #intro[data-stage="tutorial"] #tutorial { display: block; animation: drop 0.3s ease-out; } + /* headline: taller than the body text, so a screen reads as a screen and not as a list */ + #intro h2 { margin: 0 0 20px; font: 400 15px Silkscreen, monospace; color: #e0cc72; + letter-spacing: 3px; line-height: 1.5; } + /* one button look for every menu screen */ + .btn { display: block; width: min(340px, 84vw); margin: 0 auto 10px; padding: 12px 10px; + background: rgba(255,255,255,0.04); border: 2px solid #3b4354; color: #dfe6f2; + font: 400 12px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer; + -webkit-tap-highlight-color: transparent; } + .btn.go { border-color: #e0cc72; color: #e0cc72; } + .btn.back { font-size: 10px; padding: 9px 10px; color: #8a96ab; } /* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */ - #courts button { display: flex; align-items: center; gap: 12px; width: min(340px, 84vw); + #courts button.court { display: flex; align-items: center; gap: 12px; width: min(340px, 84vw); margin: 0 auto 8px; padding: 6px; background: rgba(255,255,255,0.04); border: 2px solid #3b4354; color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; } - #courts button.on { border-color: #e0cc72; background: rgba(224,204,114,0.10); } + #courts button.court.on { border-color: #e0cc72; background: rgba(224,204,114,0.10); } #courts img { width: 108px; height: 72px; display: block; image-rendering: pixelated; flex: none; } #courts b { display: block; font: 400 11px Silkscreen, monospace; color: #dfe6f2; letter-spacing: 0.5px; } #courts i { display: block; margin-top: 5px; font: 400 9px Silkscreen, monospace; color: #8a96ab; font-style: normal; } + #courts .acts { margin-top: 16px; } + /* tutorial: Marvin on the left, his line and the picture on the right */ + #tutorial .page { display: flex; align-items: flex-end; gap: 14px; width: min(360px, 88vw); + margin: 0 auto; text-align: left; min-height: 190px; } + #tutorial .marvin { width: 84px; flex: none; image-rendering: pixelated; align-self: flex-end; } + #tutorial .say { flex: 1; } + #tutorial .say p { margin: 0 0 10px; font: 400 10px Silkscreen, monospace; color: #dfe6f2; line-height: 1.9; } + #tutorial .say em { color: #e0cc72; font-style: normal; } + #tutorial .shot { display: block; max-width: 100%; max-height: 108px; margin: 0 0 8px; + image-rendering: pixelated; } + #tutorial .dots { margin: 14px 0 10px; font: 400 10px Silkscreen, monospace; color: #5a6270; letter-spacing: 3px; } + #tutorial .acts { display: flex; gap: 8px; width: min(340px, 84vw); margin: 0 auto; } + #tutorial .acts .btn { margin: 0; flex: 1; }
@@ -113,10 +138,35 @@
TAP TO START
+
+