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 —