diff --git a/HANDOFF.md b/HANDOFF.md index ffffecd..dfeb6aa 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -454,11 +454,29 @@ 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 overlay is a three-stage machine**, not a screen per DOM node: `#intro[data-stage]` swaps +`splash -> {courts, tutorial}` while the backdrop and card frame stay mounted, because a screen that +unmounts the overlay flashes the live court through between menus. **The title screen IS the menu** — +a separate menu stage behind it was the same logo on the same backdrop with two buttons, and read as +the title screen failing to advance. 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 in +`assets/`, and his lines — adding a page is a row. + +**DEATH NO LONGER RELOADS THE PAGE.** `location.reload()` was an honest full reset and for a long +time the right call — but it also tore down the AudioContext, so the menu theme restarted from +silence every time you died, when it is supposed to carry straight on from the death summary into +the title screen. `gameplay.restartRun()` replaces it: + +- `state.resetState()` restores `G`, `P`, `O`, `ball` and `score` from a `structuredClone` taken at + module load. Everything in those bags is plain data, so the clone is the whole job — nothing has + to enumerate fields, and a field added later is covered for free. +- The two modules with timers of their own put them back themselves: `zombies.resetZombies()` (every + array plus `spawnT`/`moanT`/`mgTick`) and `input.resetInput()` — a key still held or a + half-finished drag would otherwise be inherited by the next run's first frame. +- The overlay is **hidden, not removed**, so it can come straight back. `#intro[hidden]` needs its + own rule because the `display: grid` on `#intro` would otherwise beat the hidden attribute. + +**If you add module-level mutable state anywhere, it belongs in one of those reset functions.** **Menu and tutorial art is generated DIRECTLY, not through `_regen.py`.** That pipeline keys magenta and slices figures out of a sheet; these are whole illustrated panels with backgrounds, so they go @@ -478,13 +496,10 @@ Generated type is a lottery and would not match the rest of the HUD. `btn_start` a taller plate: by there the drips have run out to the ends and the centre is clear enough to print over, and the plate's own bottom border comes with it. -**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 menu theme starts on the FIRST TAP, never at boot.** 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. `loopBed`'s `beds.has` guard makes every later `menuMusic()` a no-op, which is what lets +the title screen pick up the track already playing instead of restarting it. **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, diff --git a/gameplay.js b/gameplay.js index 840583d..ac0c129 100644 --- a/gameplay.js +++ b/gameplay.js @@ -1,11 +1,11 @@ // Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update. import { TUNE, UI, BOSS_ART, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE } from './config.js'; -import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js'; +import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js'; import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck } from './audio.js'; -import { axisX, axisZ, takeFlickAim, touchPX } from './input.js'; +import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js'; import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies -import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed } from './zombies.js'; +import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies } from './zombies.js'; // ------------------------------------------------------------- ballistics ---- // Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g. @@ -551,6 +551,15 @@ function startHorde() { } // hidden shortcut: tapping the umpire three times summons the horror early +// Back to a fresh run WITHOUT reloading the page. The reload was the honest reset, but it also tore +// down the AudioContext, so the menu theme restarted from silence every time you died — and the menu +// is supposed to pick up the track that was already playing over the death summary. Every mutable +// bag has a pristine copy (state.resetState) and the two modules with timers of their own put them +// back themselves, so nothing here has to know the shape of any of it. +export function restartRun() { + resetZombies(); resetInput(); resetState(); setupPoint(); +} + export function forceZombies() { if (G.mode === 'splatter') { startHorde(); return; } // in splatter the cheat forces a horde wave if (G.mode !== 'tennis') return; diff --git a/index.html b/index.html index c76e859..83fe3f1 100644 --- a/index.html +++ b/index.html @@ -57,7 +57,7 @@ /* desktop key hint — up in the empty top-right corner now that the rack owns the bottom edge */ #hint { position: fixed; top: 6px; right: 10px; text-align: right; font: 400 10px Silkscreen, monospace; color: #6b7688; } - @media (pointer: coarse) { #hint, #intro .kb { display: none; } } + @media (pointer: coarse) { #hint { display: none; } } #intro { position: fixed; inset: 0; z-index: 10; touch-action: none; display: grid; place-items: center; background: radial-gradient(circle at 50% 34%, rgba(34,16,20,0.92), rgba(8,10,16,0.96)); } #intro .card { font: 400 11px Silkscreen, monospace; color: #dfe6f2; text-align: center; line-height: 1.9; @@ -66,24 +66,21 @@ /* the logo is generated pixel art now (assets/logo.png), not stacked CSS text */ #intro .logo { display: block; width: min(320px, 78vw); height: auto; margin: 0 auto 8px; image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); } - #intro .slogan { margin: 8px 0 16px; font-size: 12px; color: #b6a0a4; letter-spacing: 0.5px; } + /* the slogan carries most of the gap between the logo's blood and the first button */ + #intro .slogan { margin: 10px 0 26px; font-size: 12px; color: #b6a0a4; letter-spacing: 0.5px; } #intro .slogan b { color: #c9564a; } - #intro .how { display: inline-block; text-align: left; color: #cbd4e2; line-height: 2; } - #intro .how p { margin: 0; } - #intro .how span { display: inline-block; width: 30px; } - #intro em { color: #8a96ab; font-style: normal; font-size: 12px; } /* which build is served — filled in by main.js, empty (and invisible) if the header is missing */ #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; } } /* 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; } + #splash, #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; } + /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the + display:grid above would otherwise win over the hidden attribute */ + #intro[hidden] { display: none; } /* 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; } @@ -102,9 +99,6 @@ image-rendering: pixelated; text-shadow: 0 2px 0 rgba(0,0,0,0.9); } .btn.plate { background-image: url('assets/btn_start.png?v=94'); color: #f7ecb8; } .btn.plate2 { background-image: url('assets/btn_tut.png?v=94'); color: #d6dfec; } - /* the logo sits exactly as it does on the title screen — same width, and enough room under it that - the blood running off its bottom edge never touches the first button */ - #menu .logo { margin-bottom: 30px; } .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. */ @@ -139,24 +133,15 @@
-
Fifteen. Thirty. Forty. Dead.
-👆 tap — serve + swing
-👉 flick — aimed shot
-✋ drag — run (auto-run is on)
-⌨️ WASD / arrows + SPACE
-TAP TO START
- -