Trade the tutorial for a screen that counts the dead

The menus were placeholders next to the art. Three screens now: a title over
an empty, dimmed, blood-stained court; a MATCH SETUP screen where each court
wears meters read straight off its own bounce numbers; and BODY COUNT, which
needed the game to start measuring anything at all.

stats.js keeps one capped array of per-run records — all time, last match and
seven days are three filters over it rather than three counters that can drift
apart. Kills file under the body AND the weapon: killZombie only ever knew an
index, so every damage loop stamps the cause on its way past.

The tutorial is gone. Marvin explained what the in-game modals already teach,
and his stage is where the stats live now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-04 16:01:59 +02:00
1 parent 411bff8724
commit f3b0b791d6
21 files changed
+619 -210

No files matched your search

+4 -3
View File
@@ -143,7 +143,7 @@ function doHit(ch, quality, aim) {
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
ball.kills = 0; // fresh shot: multi-kill streak restarts
ball.bomb = false; ball.flame = false; // only a consuming serve arms these; any other hit clears them
ball.bomb = false; ball.flame = false; ball.wid = 'ball'; // only a consuming serve arms these; any other hit clears them
if (ch.side === 'P') { // shot marker: where this will land — red means it's going out
// TENNIS ONLY. It answers "did that land in?", and splatter does not ask: `out` could never be
// true there, so it was a permanent yellow ring on the grass carrying no information — and
@@ -284,6 +284,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
// rack's call now. GUN (slot 4) never reaches here; it starts a burst before the serve.
const wsel = G.mode === 'splatter' ? G.weapon : 0;
const wid = wsel === 0 && !G.temp ? 'ball' : slotId(wsel), armed = slotStock(wsel) > 0;
ball.wid = armed ? wid : 'ball'; // an unstocked tin throws a plain ball — file the kills that way
// THE LASER fires instead of the ball, down the lane this serve was aimed at. Everything else
// about the gesture is untouched — the pull, the aim, the release — which is the whole point of it.
if (wid === 'laser' && armed) {
@@ -304,7 +305,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
// g: ball.g — a serve carries up to 2.3x gravity in topspin; clones integrated at flat
// TUNE.gravity floated long while the centre ball dived, which is the "wings lob off at
// the wrong height" report. They are the SAME shot, fanned.
extras.push({ x: ball.x, y: ball.y, z: ball.z, vy: ball.vy, pow: quality, t: 2.5, g: ball.g,
extras.push({ x: ball.x, y: ball.y, z: ball.z, vy: ball.vy, pow: quality, t: 2.5, g: ball.g, wid: 'triple',
vx: ball.vx * c - ball.vz * sn, vz: ball.vx * sn + ball.vz * c });
}
}
@@ -1076,7 +1077,7 @@ export function dragServe(aim) {
// rally clock. This is what lets the drag mean ONE thing at all times: a second gesture that
// walked the player around between shots was the confusing part, not the fire rate.
if (ball.active && G.phase !== 'toss') // the old ball flies on as a ballistic clone
extras.push({ x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, vz: ball.vz, pow: ball.pow || 0.5, t: 2.5, g: ball.g });
extras.push({ x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, vz: ball.vz, pow: ball.pow || 0.5, t: 2.5, g: ball.g, wid: ball.wid });
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.x = P.x + 0.12; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity; // over his head, see tossBall
// a full pull is quality 1.0, but only a pull that STOPS at the top of the draw is praised