A match run is a different kind of run, and stops polluting the gore stats

The two modes share no currency: gore counts kills, multi-kills and weapons;
zombie tennis counts opponents felled and has exactly one kill per opponent,
always credited to 'bomb' (matchGameWon's explode). Every one of those was
landing in splatterRuns as a gore run, on the roster as THE CLUB PRO, and on
the KILLS world board — where a five-body match would post a row next to a
300-kill massacre and mean something else entirely by it.

One character on the record fixes all of it: `m`, absent meaning gore (which
is what every existing record is, so no migration). read() takes a mode and
so does endRun, which now compares you only against your own mode and refuses
a name prompt from a match.

The record's two headline slots carry each MODE's two headline numbers — gore
g=games s=sets, match g=felled s=games — so `g` stays the thing everything
ranks on and the match best is derived from the record instead of living in a
second key that drifts. That retires the matchBest key from the last commit.

Found on the way, pre-existing and visible: endRun returned `{...best, kills,
games}` and the later key won, so G.best.games was the run that had just died.
The gore death screen has been printing "BEST <this run> GAMES · <the record>
SETS". The return now carries the best and nothing else.

And stats.check() — the module's own fixture check — was exported and called
by nothing. Wired into the suite, with the mode split added to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 08:58:04 +02:00
1 parent 8011186cda
commit 66fdad3d74
6 files changed
+84 -34

No files matched your search

+11 -13
View File
@@ -4,12 +4,12 @@
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { G, P, ball, sendBoy, setupPoint } from './state.js';
import { G, P, ball, score, sendBoy, setupPoint } from './state.js';
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame } from './audio.js';
import { drillKill, drillCaught } from './drill.js';
import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js';
import { resolveInterlude, endBossFight } from './beats.js'; // runtime-only cycle, like the gameplay one
import { tally, multiKill, endRun } from './stats.js';
import { tally, multiKill, endRun, MODE_MATCH } from './stats.js';
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
export const gore = []; // lawn decals {x, z, r, a} — the flat ground layer
@@ -300,19 +300,17 @@ export function killPlayer() {
// splatterBest read/write here: the best is now derived from the record rather than stored twice
// and left to drift. Timing is safe — restartRun() only fires on the tap AFTER death, so
// runGames/sets/splatT are all still intact at this point.
const b = endRun(G.runGames, G.sets[0], G.splatT);
G.best = b; G.newBest = b.newBest; // b is {games, sets, newBest}; render reads games/sets
// THE MATCH MEASURES SOMETHING ELSE. Blood-only scoring (model A) means a lost game costs nothing
// but the blood already spent, so "games survived" is not the achievement — OPPONENTS FELLED is.
// Kept on G.match (which survives to the death screen; restartRun is what clears it) with its own
// localStorage best, the pattern drillBest already set. Games ride along as the length of the run.
if (G.match) {
const felled = G.match.opp - 1;
let best = 0;
try { best = Number(localStorage.getItem('matchBest')) || 0;
if (felled > best) localStorage.setItem('matchBest', String(felled)); } catch {}
G.match.felled = felled; G.match.best = best; G.match.newBest = felled > best;
}
// It goes through the SAME endRun with the mode tagged and its own two headline numbers in the
// record's two headline slots (see the shape note there), so the best is derived from the record
// rather than stored a second time in its own key and left to drift.
const m = G.match;
const felled = m ? m.opp - 1 : 0, gamesPlayed = score.games[0] + score.games[1];
const b = m ? endRun(felled, gamesPlayed, G.splatT, true, MODE_MATCH)
: endRun(G.runGames, G.sets[0], G.splatT);
G.best = b; G.newBest = b.newBest; // b is {games, sets, newBest}; render reads games/sets
if (m) { m.felled = felled; m.games = gamesPlayed; m.best = b.games; } // ...the death screen's words
}
// One hostile ball, lobbed at where the player is standing. `spread` fans a volley sideways.