BODY COUNT loses its nine states, the board loses a mode

Three mode tabs times three period tabs, and the same three boxes meant
something different in every one — Oliver had no idea what he was looking at,
which is the correct reaction to a screen with nine states. It is one screen
now: bodies felled, best gore run, deepest climb, runs. Never anything else.

MULTI-KILLS retired (a vanity number on no board) and DEATHS with it (every run
ends in one, so it was RUNS again). The period filter went too: LAST RUN is the
summary you just tapped away, and 7 DAYS is a fitness metric in a permadeath
game. The card stops repeating the ledger and keeps the CAREER instead —
league, cups won, best cup.

The world board drops CLASSIC: that mode is out of competition by its own
room's rule now, so ranking it contradicted the wall. A classic best no longer
opens the name prompt either — it would have had nowhere to go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-31 15:39:27 +02:00
1 parent 5927664905
commit e96b4374d8
3 files changed
+57 -82

No files matched your search

+14 -20
View File
@@ -263,6 +263,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
-webkit-tap-highlight-color: transparent; } -webkit-tap-highlight-color: transparent; }
/* the BODY COUNT headline row: three boxed figures sharing one frame */ /* the BODY COUNT headline row: three boxed figures sharing one frame */
.figs { display: flex; border: 2px solid var(--line); background: var(--panel); } .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 { flex: 1; min-width: 0; padding: 7px 6px; text-align: center; }
.fig + .fig { border-left: 2px solid var(--line); } .fig + .fig { border-left: 2px solid var(--line); }
.fig b { display: block; font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; } .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
</div> </div>
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. --> <!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
<div id="stats"> <div id="stats">
<div class="hdr"><button class="chev" id="stBack">&lt;</button><h2 id="stTitle">BODY COUNT</h2></div> <div class="hdr"><button class="chev" id="stBack">&lt;</button><h2>BODY COUNT</h2></div>
<!-- WHICH MODE, then which period. The three modes do not measure the same thing (see the <!-- NO TABS (2026-08-31). This screen carried three MODE tabs times three PERIOD tabs, and
record-shape note in stats.endRun), so this strip changes the whole screen under it — the same three boxes meant something different in every one of the nine states. It is
the title, the three figures, the roster's heading and the footer. --> your career now, in four numbers that always mean the same thing, plus the two things
<div class="tabs" id="stModes"> the screen is actually good at: the bodies you have collected, and the rack you fired. -->
<button class="on" data-m="g">SPLATTER</button> <div class="figs pair" id="stFigs"></div>
<button data-m="z">TENNIS</button> <h3>BY OPPONENT<span>ALL TIME</span></h3>
<button data-m="c">CLASSIC</button>
</div>
<div class="tabs" id="stTabs">
<button class="on" data-p="all">ALL TIME</button>
<button data-p="last">LAST RUN</button>
<button data-p="7d">7 DAYS</button>
</div>
<div class="figs" id="stFigs"></div>
<h3 id="stZHdr">BY OPPONENT<span id="stPeriod"></span></h3>
<div id="stZ"></div> <div id="stZ"></div>
<h3 id="stWHdr">BY WEAPON</h3> <h3>BY WEAPON</h3>
<div id="stW"></div> <div id="stW"></div>
<div class="foot" id="stFoot"></div>
</div> </div>
<!-- THREE BOARDS, one per game mode — the three do not share a currency, so they cannot share <!-- THREE BOARDS, one per game mode — the three do not share a currency, so they cannot share
a ranking. Same tab strip as the stats screen's, and stats.drawRanks rewrites the column a ranking. Same tab strip as the stats screen's, and stats.drawRanks rewrites the column
heading with the tab, because the number means something different on each one. --> heading with the tab, because the number means something different on each one. -->
<div id="ranks"> <div id="ranks">
<div class="hdr"><button class="chev" id="rkBack">&lt;</button><h2>WORLD BOARD</h2></div> <div class="hdr"><button class="chev" id="rkBack">&lt;</button><h2>WORLD BOARD</h2></div>
<!-- TWO boards, not three (2026-08-31): classic tennis is out of competition by design now
— no league, no record, no stakes — so ranking it contradicted the room it lives in. -->
<div class="tabs" id="rkModes"> <div class="tabs" id="rkModes">
<button class="on" data-m="g">SPLATTER</button> <button class="on" data-m="g">SPLATTER</button>
<button data-m="z">TENNIS</button> <button data-m="z">THE CLIMB</button>
<button data-m="c">CLASSIC</button>
</div> </div>
<h3 id="rkHdr">BEST SINGLE RUN<span>KILLS · GAMES</span></h3> <h3 id="rkHdr">BEST SINGLE RUN<span>KILLS · GAMES</span></h3>
<div id="rkList"></div> <div id="rkList"></div>
+12 -22
View File
@@ -646,12 +646,14 @@ function buildCard() {
c = stats.read('all', undefined, stats.MODE_CLASSIC); c = stats.read('all', undefined, stats.MODE_CLASSIC);
// the ledger climbs: rows are [box id, [label, target, format]...]; every value starts at 0 and // 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 // 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 [ for (const [boxId, rows] of [
['cdFigs1', [['KILLS', g.kills + z.kills + c.kills], ['cdFigs1', [['LEAGUE', profile.prof.lg], ['CUPS WON', profile.prof.lg - 1 + profile.prof.ch],
['MULTI-KILLS', g.multi + z.multi + c.multi], ['BEST CUP', profile.cupBest(profile.prof.lg)]]],
['DEATHS', g.deaths + z.deaths + c.deaths]]], ['cdFigs2', [['BODIES FELLED', g.kills + z.kills + c.kills],
['cdFigs2', [['BEST RUN', g.longest, v => v + ' G'], ['DEEPEST CLIMB', z.longest],
['MOST FELLED', z.longest],
['RUNS', g.matches + z.matches + c.matches]]]]) { ['RUNS', g.matches + z.matches + c.matches]]]]) {
const box = document.getElementById(boxId); const box = document.getElementById(boxId);
box.innerHTML = rows.map(([label]) => fig(label, '0')).join(''); 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) // training: the picked rung rides the match machinery with the ladder parked (beats.startMatch)
if (modeId === 'train') { intoSplatter(); startMatch(false, null, trainRung); } 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 // 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 // came to look at, and it saves a tap on the way in every single time
const openStats = () => { const openStats = () => {
stMode = boardFor(pickedMode); stats.drawStats('all', SPRV); go('stats');
for (const el of document.getElementById('stModes').children) el.classList.toggle('on', el.dataset.m === stMode);
stats.drawStats(stPeriod, SPRV, stMode); go('stats');
}; };
// entering a question screen rebuilds it, so the LAST tag always names the run actually played — // 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 // 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 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 // 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(); refreshSplash();
// the profile mirror: adopt a server copy that is ahead (a wiped phone, a second device), then // 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 // 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; } 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 // ...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 // 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 // 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) // re-deals the mode that just ended (pickedMode is what started it, cup included)
if (door === 'next') { pickMode('cup'); start('cup'); return; } if (door === 'next') { pickMode('cup'); start('cup'); return; }
+31 -40
View File
@@ -150,15 +150,21 @@ export function endRun(games, sets, secs, dead = true, mode = MODE_GORE) {
// ------------------------------------------------------------- aggregation ---- // ------------------------------------------------------------- aggregation ----
// `runs` is injectable so check() can assert against a fixture without touching localStorage. // `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) { 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) const rs = period === 'last' ? own.slice(-1)
: period === '7d' ? own.filter(r => Date.now() - r.t < 7 * 864e5) : period === '7d' ? own.filter(r => Date.now() - r.t < 7 * 864e5)
: own; : 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) { for (const r of rs) {
out.kills += r.k || 0; out.multi += r.mk || 0; out.deaths += r.dead || 0; 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.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.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]; 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('') : `<p class="none">${empty}</p>`; }).join('') : `<p class="none">${empty}</p>`;
}; };
export function drawStats(period, v, mode = MODE_GORE) { // WHAT YOU HAVE DONE — one screen, no tabs (2026-08-31, Oliver: "es ist total verwirrend, ich habe
const d = read(period, undefined, mode); // 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); const $ = id => document.getElementById(id);
$('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME'; // every body you have put down, in any mode — the climb's own record stands beside it, so it
$('stTitle').textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED' // does not need adding twice (its opponents are counted in the same kill tally)
: mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT'; const figs = [['BODIES FELLED', n(all.kills)],
const figs = mode === MODE_MATCH ['BEST GORE RUN', n(gore.best)],
? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]] ['DEEPEST CLIMB', n(match.longest)],
: mode === MODE_CLASSIC ['RUNS', n(all.matches)]];
? [['PLAYED', n(d.matches)], ['WON', n(d.matches - d.deaths)], ['LOST', n(d.deaths)]] $('stFigs').innerHTML = figs.map(([k, val]) =>
: [['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)]]; `<div class="fig"><b>${k}</b><i>${val}</i></div>`).join('');
// 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) =>
`<div class="fig${i === badIdx ? ' bad' : ''}"><b>${k}</b><i>${val}</i></div>`).join('');
// sorted by count, and only bodies you have actually met — an empty roster of everything would // 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 // 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]); const rows = Object.entries(all.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, 'NOTHING YET. GO AND EARN A ROW.');
roster($('stZ'), rows, v, mode === MODE_MATCH ? 'NOBODY HAS GONE DOWN YET.' : '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]);
// 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;
$('stW').innerHTML = ws.length ? ws.map(([id, c]) => $('stW').innerHTML = ws.length ? ws.map(([id, c]) =>
`<div class="wtile"><img src="assets/${wIcon(id)}.png?v=${v}" alt=""><b>${n(c)}</b></div>`).join('') `<div class="wtile"><img src="assets/${wIcon(id)}.png?v=${v}" alt=""><b>${n(c)}</b></div>`).join('')
: '<p class="none">NO WEAPONS FIRED.</p>'; : '<p class="none">NO WEAPONS FIRED.</p>';
$('stFoot').innerHTML = mode === MODE_MATCH
? `<b>DEEPEST RUN</b><i>${d.longest} FELLED</i>`
: mode === MODE_CLASSIC
? `<b>LONGEST MATCH</b><i>${d.longest} GAME${d.longest === 1 ? '' : 'S'}</i>`
: `<b>LONGEST RUN</b><i>${d.longest} GAME${d.longest === 1 ? '' : 'S'}</i>`;
} }
// The real board. Async, and every failure lands on a line of copy rather than an empty screen — // The real board. Async, and every failure lands on a line of copy rather than an empty screen —