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; }
/* 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
</div>
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
<div id="stats">
<div class="hdr"><button class="chev" id="stBack">&lt;</button><h2 id="stTitle">BODY COUNT</h2></div>
<!-- WHICH MODE, then which period. The three modes do not measure the same thing (see the
record-shape note in stats.endRun), so this strip changes the whole screen under it —
the title, the three figures, the roster's heading and the footer. -->
<div class="tabs" id="stModes">
<button class="on" data-m="g">SPLATTER</button>
<button data-m="z">TENNIS</button>
<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 class="hdr"><button class="chev" id="stBack">&lt;</button><h2>BODY COUNT</h2></div>
<!-- NO TABS (2026-08-31). This screen carried three MODE tabs times three PERIOD tabs, and
the same three boxes meant something different in every one of the nine states. It is
your career now, in four numbers that always mean the same thing, plus the two things
the screen is actually good at: the bodies you have collected, and the rack you fired. -->
<div class="figs pair" id="stFigs"></div>
<h3>BY OPPONENT<span>ALL TIME</span></h3>
<div id="stZ"></div>
<h3 id="stWHdr">BY WEAPON</h3>
<h3>BY WEAPON</h3>
<div id="stW"></div>
<div class="foot" id="stFoot"></div>
</div>
<!-- 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
heading with the tab, because the number means something different on each one. -->
<div id="ranks">
<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">
<button class="on" data-m="g">SPLATTER</button>
<button data-m="z">TENNIS</button>
<button data-m="c">CLASSIC</button>
<button data-m="z">THE CLIMB</button>
</div>
<h3 id="rkHdr">BEST SINGLE RUN<span>KILLS · GAMES</span></h3>
<div id="rkList"></div>
+12 -22
View File
@@ -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; }
+31 -40
View File
@@ -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('') : `<p class="none">${empty}</p>`;
};
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) =>
`<div class="fig${i === badIdx ? ' bad' : ''}"><b>${k}</b><i>${val}</i></div>`).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]) =>
`<div class="fig"><b>${k}</b><i>${val}</i></div>`).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]) =>
`<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>';
$('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 —