diff --git a/index.html b/index.html index 5dcf92b..98a9d45 100644 --- a/index.html +++ b/index.html @@ -263,6 +263,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat -webkit-tap-highlight-color: transparent; } /* the BODY COUNT headline row: three boxed figures sharing one frame */ .figs { display: flex; border: 2px solid var(--line); background: var(--panel); } + /* four numbers read better two-up than four-across on a phone — at four the labels wrap and + the row becomes a wall of two-line captions */ + .figs.pair { display: grid; grid-template-columns: 1fr 1fr; } .fig { flex: 1; min-width: 0; padding: 7px 6px; text-align: center; } .fig + .fig { border-left: 2px solid var(--line); } .fig b { display: block; font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; } @@ -974,36 +977,27 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
-

BODY COUNT

- -
- - - -
-
- - - -
-
-

BY OPPONENT

+

BODY COUNT

+ +
+

BY OPPONENTALL TIME

-

BY WEAPON

+

BY WEAPON

-

WORLD BOARD

+
- - +

BEST SINGLE RUNKILLS · GAMES

diff --git a/src/main.js b/src/main.js index 4f9bd7a..2a73a3e 100644 --- a/src/main.js +++ b/src/main.js @@ -646,12 +646,14 @@ function buildCard() { c = stats.read('all', undefined, stats.MODE_CLASSIC); // the ledger climbs: rows are [box id, [label, target, format]...]; every value starts at 0 and // counts up through the same easing the bars sweep with + // THE CARD IS WHO YOU ARE, not what you have done (2026-08-31, the stats streamline): the + // lifetime ledger lives on BODY COUNT, which is the screen for it. What stays here is the + // CAREER — how far up the club you have got, and how far each cup let you. for (const [boxId, rows] of [ - ['cdFigs1', [['KILLS', g.kills + z.kills + c.kills], - ['MULTI-KILLS', g.multi + z.multi + c.multi], - ['DEATHS', g.deaths + z.deaths + c.deaths]]], - ['cdFigs2', [['BEST RUN', g.longest, v => v + ' G'], - ['MOST FELLED', z.longest], + ['cdFigs1', [['LEAGUE', profile.prof.lg], ['CUPS WON', profile.prof.lg - 1 + profile.prof.ch], + ['BEST CUP', profile.cupBest(profile.prof.lg)]]], + ['cdFigs2', [['BODIES FELLED', g.kills + z.kills + c.kills], + ['DEEPEST CLIMB', z.longest], ['RUNS', g.matches + z.matches + c.matches]]]]) { const box = document.getElementById(boxId); box.innerHTML = rows.map(([label]) => fig(label, '0')).join(''); @@ -767,13 +769,11 @@ function start(modeId = pickedMode) { // training: the picked rung rides the match machinery with the ladder parked (beats.startMatch) if (modeId === 'train') { intoSplatter(); startMatch(false, null, trainRung); } } -let stPeriod = 'all', stMode = stats.MODE_GORE; + // the stats screen opens on the mode you last PICKED to play — that is the one whose numbers you // came to look at, and it saves a tap on the way in every single time const openStats = () => { - stMode = boardFor(pickedMode); - for (const el of document.getElementById('stModes').children) el.classList.toggle('on', el.dataset.m === stMode); - stats.drawStats(stPeriod, SPRV, stMode); go('stats'); + stats.drawStats('all', SPRV); go('stats'); }; // entering a question screen rebuilds it, so the LAST tag always names the run actually played — // and the FIRST visit ever detours through the identity screen (the leagues): the club wants a @@ -976,18 +976,6 @@ for (let ch = 1; ch <= 10; ch++) { } // the three period tabs are one listener on the strip — the button carries the period in data-p // the two tab strips are one listener each — the button carries its value in a data attribute -document.getElementById('stTabs').addEventListener('click', e => { - const b = e.target.closest('button'); if (!b) return; - stPeriod = b.dataset.p; - for (const el of e.currentTarget.children) el.classList.toggle('on', el === b); - stats.drawStats(stPeriod, SPRV, stMode); -}); -document.getElementById('stModes').addEventListener('click', e => { - const b = e.target.closest('button'); if (!b) return; - stMode = b.dataset.m; - for (const el of e.currentTarget.children) el.classList.toggle('on', el === b); - stats.drawStats(stPeriod, SPRV, stMode); -}); refreshSplash(); // the profile mirror: adopt a server copy that is ahead (a wiped phone, a second device), then // let the plaque show whatever came back. The BOOT GATE holds the splash locked (skeleton @@ -1106,7 +1094,9 @@ const maybeRestart = e => { if (promo) { buildPromo(promo, run); go('promo'); return; } // ...on the BOARD's number, not on kills: a classic match has none, so the gore-shaped test here // silently barred two of the three modes from ever reaching the prompt - if (run && run.newKillBest && run.score > 0) { askName(run); return; } + // ...on a board that still EXISTS: classic tennis left the world board with the stats streamline + // (out of competition, by its own room's rule), so a classic best has nowhere to be written + if (run && run.newKillBest && run.score > 0 && run.mode !== stats.MODE_CLASSIC) { askName(run); return; } // the fixture button restarts the canonical path without a detour through the menus; PLAY AGAIN // re-deals the mode that just ended (pickedMode is what started it, cup included) if (door === 'next') { pickMode('cup'); start('cup'); return; } diff --git a/src/stats.js b/src/stats.js index 6dfda02..439e56d 100644 --- a/src/stats.js +++ b/src/stats.js @@ -150,15 +150,21 @@ export function endRun(games, sets, secs, dead = true, mode = MODE_GORE) { // ------------------------------------------------------------- aggregation ---- // `runs` is injectable so check() can assert against a fixture without touching localStorage. +// `mode` may be MODE_ALL: the BODY COUNT screen counts your whole career now, not one mode's +// column of it (2026-08-31 — the stats streamline). The per-mode read stays for the boards, which +// genuinely cannot share a currency. +export const MODE_ALL = '*'; export function read(period, runs = load(), mode = MODE_GORE) { - const own = runs.filter(r => modeOf(r) === mode); // ...and only ever one mode's runs at a time + const own = mode === MODE_ALL ? runs : runs.filter(r => modeOf(r) === mode); const rs = period === 'last' ? own.slice(-1) : period === '7d' ? own.filter(r => Date.now() - r.t < 7 * 864e5) : own; - const out = { kills: 0, multi: 0, deaths: 0, longest: 0, matches: rs.length, z: {}, w: {} }; + const out = { kills: 0, multi: 0, deaths: 0, longest: 0, best: 0, matches: rs.length, z: {}, w: {} }; + const B = boardOf(mode === MODE_ALL ? MODE_GORE : mode); for (const r of rs) { out.kills += r.k || 0; out.multi += r.mk || 0; out.deaths += r.dead || 0; out.longest = Math.max(out.longest, r.g || 0); + out.best = Math.max(out.best, B.score(r)); // the best single run IN THIS BOARD'S currency for (const k in r.z) out.z[k] = (out.z[k] || 0) + r.z[k]; for (const k in r.w) out.w[k] = (out.w[k] || 0) + r.w[k]; } @@ -268,50 +274,35 @@ const roster = (el, rows, v, empty) => { }).join('') : `

${empty}

`; }; -export function drawStats(period, v, mode = MODE_GORE) { - const d = read(period, undefined, mode); +// WHAT YOU HAVE DONE — one screen, no tabs (2026-08-31, Oliver: "es ist total verwirrend, ich habe +// keine ahnung was dort eigentlich angezeigt wird"). It carried three MODE tabs times three PERIOD +// tabs — nine states, and the same three boxes meant something different in every one. What it +// shows now is your career, in four numbers that never change their meaning, plus the two things +// this screen is actually good at: the roster you have collected and the rack you have fired. +// MULTI-KILLS retired (a vanity number on no board), DEATHS with it (every run ends in one, so +// it was RUNS again), and the period filter with them — LAST RUN is the summary you just tapped +// away and 7 DAYS is a fitness metric in a permadeath game. `read` keeps the parameter. +export function drawStats(period, v) { + const all = read('all', undefined, MODE_ALL); + const gore = read('all', undefined, MODE_GORE); + const match = read('all', undefined, MODE_MATCH); const $ = id => document.getElementById(id); - $('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME'; - $('stTitle').textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED' - : mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT'; - const figs = mode === MODE_MATCH - ? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]] - : mode === MODE_CLASSIC - ? [['PLAYED', n(d.matches)], ['WON', n(d.matches - d.deaths)], ['LOST', n(d.deaths)]] - : [['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)]]; - // the third figure is the RED one in gore and in classic (deaths, matches lost); in the match it - // is just how many runs you have played, so nothing on that strip is bad news - const badIdx = mode === MODE_MATCH ? -1 : 2; - $('stFigs').innerHTML = figs.map(([k, val], i) => - `
${k}${val}
`).join(''); - + // every body you have put down, in any mode — the climb's own record stands beside it, so it + // does not need adding twice (its opponents are counted in the same kill tally) + const figs = [['BODIES FELLED', n(all.kills)], + ['BEST GORE RUN', n(gore.best)], + ['DEEPEST CLIMB', n(match.longest)], + ['RUNS', n(all.matches)]]; + $('stFigs').innerHTML = figs.map(([k, val]) => + `
${k}${val}
`).join(''); // sorted by count, and only bodies you have actually met — an empty roster of everything would // read as a checklist of things to go and do, which this screen is not - const rows = Object.entries(d.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]); - $('stZHdr').firstChild.textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED' : 'BY OPPONENT'; - roster($('stZ'), rows, v, mode === MODE_MATCH ? 'NOBODY HAS GONE DOWN YET.' : 'NOTHING YET. GO AND EARN A ROW.'); - - // THE WEAPON SHELF IS THE GORE MODE'S ALONE. Both tennis modes are played with plain balls (the - // rack is hidden in both), so the section is hidden rather than printed empty — a shelf that says - // NO WEAPONS FIRED for a mode that has none reads as something you failed to do. - const ws = Object.entries(d.w).filter(([id, c]) => c > 0 && wIcon(id)).sort((a, b) => b[1] - a[1]); - // style.display, not the `hidden` attribute: #stW carries a display rule of its own and any - // declared display beats [hidden]'s, so the shelf stayed on screen under a mode with no weapons. - const off = mode !== MODE_GORE ? 'none' : ''; - $('stWHdr').style.display = off; $('stW').style.display = off; - // ...and CLASSIC TENNIS has no roster either: nothing dies in it. A section printing that is a - // line about something the mode does not do, which is worse than the section not being there. - const noZ = mode === MODE_CLASSIC ? 'none' : ''; - $('stZHdr').style.display = noZ; $('stZ').style.display = noZ; + const rows = Object.entries(all.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]); + roster($('stZ'), rows, v, 'NOTHING YET. GO AND EARN A ROW.'); + const ws = Object.entries(all.w).filter(([id, c]) => c > 0 && wIcon(id)).sort((a, b) => b[1] - a[1]); $('stW').innerHTML = ws.length ? ws.map(([id, c]) => `
${n(c)}
`).join('') : '

NO WEAPONS FIRED.

'; - - $('stFoot').innerHTML = mode === MODE_MATCH - ? `DEEPEST RUN${d.longest} FELLED` - : mode === MODE_CLASSIC - ? `LONGEST MATCH${d.longest} GAME${d.longest === 1 ? '' : 'S'}` - : `LONGEST RUN${d.longest} GAME${d.longest === 1 ? '' : 'S'}`; } // The real board. Async, and every failure lands on a line of copy rather than an empty screen —