diff --git a/.gitignore b/.gitignore index d53b26b..c2ddf69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .env scores.json scores.json.tmp +profiles.json +profiles.json.tmp +__pycache__/ assets/_gen/ assets/_refcheck/ assets/__pycache__/ diff --git a/PROJECT.md b/PROJECT.md index ff28fc2..344e3fa 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -491,7 +491,41 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu `RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone. -## THE LEAGUES — the meta-progression plan (2026-08-29, AGREED, NOT YET BUILT) +## Session log: THE LEAGUES, PHASE 1 — profile, xp, the identity screen (2026-08-29, STAGING ONLY) + +Phase 1 of the plan below, built exactly to it. **STAGING ONLY for now** (Oliver: he tests himself +before the wider group) — do not merge to main until his verdict. + +- **`src/profile.js`** is the profile: `{xp, lg, ch, setup}` in localStorage `splatterProfile`, + mirrored to the server (`/api/profile`, keyed by stats' existing client id). Name/tour/kit/skin + deliberately KEEP their existing homes and are only mirrored outbound, so nothing has two owners. + The mirror never goes backward on xp (server-side guard) and `pull()` adopts it only when ahead — + a wiped phone gets its identity AND its number back. +- **XP hooks are deliberately few**: `advancePoint` (the one place every mode's point converges — + points and games cannot be missed or double-counted), the `killZombie` funnel, duel win, cup + round, cup won. `award()` carries the no-stakes guard ITSELF (drill, bench, testMode, training + court), so a call site cannot forget it. Curve: `80·(l−1)^1.8`, cap 50 — first levels inside the + first run, late ones sessions apart. All dials in `profile.XP`. +- **UI**: the splash carries a LEVEL strip (#xpRow, filled by refreshSplash); the death/summary + screen shows `+N XP · LEVEL a → b` plus a progress bar (render.js, beside the best-run lines). + A mid-run level-up announces as a CARD (popup policy: never a modal on a live court). +- **THE IDENTITY SCREEN** (`#player`, first `PLAY MATCH` tap only): name prefilled with a ZOMBIE + NAME (`profile.zombieName()`, two-part roll, always ≤12 chars and board-legal), a die button + rolls another; tour/kit/skin pickers write the SAME picked* state and localStorage keys the + setup screen reads. **`syncSetup()` (called by `go('setup')`) repaints the setup rows from + picked\*** — without it the rows only tracked their own taps and showed the boot-time selection + after an identity-screen change (caught live in the pane). +- **Server**: `/api/profile` GET/POST beside the scores API, same stance (whitelist re-derive, + bounded, atomic write, 5000 profiles cap, oldest-touched evicted). `profiles.json` beside + `scores.json` under `/data` — the volume that already exists. +- **Drive-by fix**: `pagehide` now returns on `G.drill` — drill kills leak into the stats run bag + (tally sits above the drill hook in killZombie) and a tab close mid-drill banked them as a gore + run. Death was already guarded; the tab-close path was not. +- Tests: +4 (curve shape, the award funnel + every mute, run-delta reset, name legality), suite + 118, seeds fuzzed. Pane-verified end to end (identity flow, POST 200, death screen with +16 XP + and the bar; NOTE this pane session has rAF dead again — `T.step(1/60)` loops drove the clock). + +## THE LEAGUES — the meta-progression plan (2026-08-29, AGREED, PHASE 1 BUILT — see log above) Worked out with Oliver in discussion, decisions settled below. The goal in his words: an incentive to keep opening the game beyond beating the next opponent — levelling through play, with a clearly diff --git a/index.html b/index.html index f3d4d88..39223f9 100644 --- a/index.html +++ b/index.html @@ -174,7 +174,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat /* 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, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #bracket, #vs, #train { display: none; } + #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #bracket, #vs, #train, #player { display: none; } #intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; } /* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a screen whose content does not fill it */ @@ -188,6 +188,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #intro[data-stage="bracket"] #bracket, #intro[data-stage="train"] #train, #intro[data-stage="vs"] #vs, + #intro[data-stage="player"] #player, #intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; animation: drop 0.3s ease-out; } /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the @@ -236,22 +237,35 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat font-style: normal; color: var(--dim); letter-spacing: 0.5px; } /* THE PLAYER BLOCK: the live man on the left, everything that changes him on the right, so a tap and its result are never more than a thumb apart and the whole of "who am I" is one screenful. */ - #who { display: flex; gap: 12px; align-items: flex-start; } - #whoCtl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; } + #who, #who2 { display: flex; gap: 12px; align-items: flex-start; } + #whoCtl, #pCtl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; } /* The live player. `position: static` is not decoration: the bare `canvas` rule above is the COURT — fixed, full-bleed, 100dvh — and it catches any canvas in the document, so this one flew to the top-left corner of the screen until it was told otherwise. Every geometry property that rule sets has to be undone here. The buffer is the sprite's own 96x224, so width follows height. */ - #look { position: static; display: block; flex: none; margin: 0; + #look, #pLook { position: static; display: block; flex: none; margin: 0; width: auto; height: 176px; touch-action: auto; image-rendering: pixelated; } /* the tour is now TWO WORDS, not two portraits — the live player next to them is the picture, and a second drawing of the same man (front-facing, and deaf to every colour on this screen) was both a duplicate and the reason the court list started below the fold */ - #tours { display: flex; gap: 7px; } - #tours button { flex: 1; padding: 10px 2px; background: var(--panel); border: 2px solid var(--line); + #tours, #pTours { display: flex; gap: 7px; } + #tours button, #pTours button { flex: 1; padding: 10px 2px; background: var(--panel); border: 2px solid var(--line); color: var(--dim); font: 400 10px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer; -webkit-tap-highlight-color: transparent; } - #tours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); color: var(--cream); } + #tours button.on, #pTours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); color: var(--cream); } + /* THE IDENTITY SCREEN (the leagues, phase 1): name row = the input plus the die. */ + #nameRow { display: flex; gap: 8px; } + #nameRow .tin { flex: 1; margin: 0; } + #pDice { width: 52px; flex: none; margin: 0; padding: 0; font-size: 20px; line-height: 1; } + /* THE LEVEL STRIP: the one meta number over a thin bar, riding the splash foot. The fill colour + is the acid accent for now; the leagues will hand it the skill colour in phase 3. */ + #xpRow { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; } + #xpRow u { font: 700 9px Silkscreen, monospace; color: var(--cream); text-decoration: none; + letter-spacing: 1px; white-space: nowrap; } + #xpRow s { flex: 1; height: 6px; background: rgba(255,255,255,0.10); border: 1px solid var(--line); + text-decoration: none; display: block; } + #xpRow s i { display: block; height: 100%; width: 0%; background: var(--acid); + transition: width 0.4s ease; } /* KIT and SKIN: a row of colour squares, under a label that names whatever is selected. That is the whole UI — the pick has no meters and no art to show, and a labelled tile per colour would be five times the screen for a choice you make by looking at the colour. */ @@ -503,6 +517,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat you can press before the sheets have landed is how you reach the stats screen with half the zombies missing. Swapped by #splash.busy, see main.js. -->
THE CLUB WILL LEARN YOUR NAME.
+ +