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:
1 parent
5927664905
commit
e96b4374d8
3 files changed
+57
-82
No files matched your search
+14
-20
@@ -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"><</button><h2 id="stTitle">BODY COUNT</h2></div>
|
<div class="hdr"><button class="chev" id="stBack"><</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"><</button><h2>WORLD BOARD</h2></div>
|
<div class="hdr"><button class="chev" id="rkBack"><</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
@@ -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
@@ -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 —
|
||||||
|
|||||||
Reference in new issue
Block a user