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

+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 —