Sweep the dead code off the court

EATEN_GRAB (write-only marker), ball.firstLand (written five places, read
nowhere), zombies' unused VOLLEY import, three scoreboard palette fields
from the pre-PNG board, the x_tin.png nothing references, and a dozen
export keywords on functions no module ever imported.

Kept on purpose: o_run3/o_run4.png — the SPR list excludes them so both
tours run on two frames (the women's set has no 3/4 twins), per the comment
at the preload list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-06 09:37:08 +02:00
1 parent e07196f247
commit d847c2e4a8
7 files changed
+23 -24

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

+1 -1
View File
@@ -138,7 +138,7 @@ export const zombieDuck = out => fadeBed('zombie-mode.m4a', out); // key must ma
export const menuDuck = out => fadeBed('background-music.m4a', out);
let sysVoice = null;
export function say(text, delay = 0) { // fallback when a recorded umpire line is missing
function say(text, delay = 0) { // fallback when a recorded umpire line is missing
try {
if (!sysVoice) sysVoice = speechSynthesis.getVoices().find(v => v.lang.startsWith('en-GB')) || null;
const u = new SpeechSynthesisUtterance(text);
+1 -1
View File
@@ -362,7 +362,7 @@ export const APPLAUSE_T = 4.2;
// and what is left is the mound · C (EATEN_SUMMARY): the summary fades in over them still feeding.
// It is deliberately long. Oliver: "the movies surround and bite them, blood spills, and then the
// person basically drowns in the zombies" — that cannot happen in half a second.
export const EATEN_GRAB = 0.9, EATEN_BURST = 2.3, EATEN_SUMMARY = 3.6, EATEN_RESTART = 4.6;
export const EATEN_BURST = 2.3, EATEN_SUMMARY = 3.6, EATEN_RESTART = 4.6;
export const EATEN_FEED_R = 1.15; // m — how tight the feeding ring packs around the body
// UI accent colours, one place so the whole HUD can be re-tuned at once. Deliberately
+10 -11
View File
@@ -29,11 +29,11 @@ export function predictLanding() { // first-bounce point, closed form — exact,
// further, which is the whole fix: a shambler at the far baseline costs the same short pull as
// one on top of you, so the gesture always fits on the screen. The first body standing in the
// lane is the target; the cone widens with range so distant lanes stay pickable.
export const pullFrac = mag => Math.max(0, Math.min(1, (mag - TUNE.pullMin) / (TUNE.pullFull - TUNE.pullMin)));
const pullFrac = mag => Math.max(0, Math.min(1, (mag - TUNE.pullMin) / (TUNE.pullFull - TUNE.pullMin)));
export const pullQuality = mag => TUNE.pullQMin + pullFrac(mag) * (1 - TUNE.pullQMin);
// pullQuality inverted: hitServe is handed a quality, not a drag, and the band it leaves behind is
// drawn at the same weight the live preview had.
export const qualityFrac = q => Math.max(0, Math.min(1, (q - TUNE.pullQMin) / (1 - TUNE.pullQMin)));
const qualityFrac = q => Math.max(0, Math.min(1, (q - TUNE.pullQMin) / (1 - TUNE.pullQMin)));
// A pull that STOPS in the band at the top of the draw, rather than merely reaching it. See
// TUNE.perfectBand for why the old "quality == 1" test praised almost every serve.
export const pullPerfect = mag => mag >= TUNE.pullFull && mag <= TUNE.pullFull * TUNE.perfectBand;
@@ -139,7 +139,7 @@ function doHit(ch, quality, aim) {
}
ball.g = g;
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
ball.lastHit = ch.side; ball.bounces = 0; ball.firstLand = null;
ball.lastHit = ch.side; ball.bounces = 0;
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
@@ -162,7 +162,7 @@ function doHit(ch, quality, aim) {
// ----------------------------------------------------------------- serve ----
function tossBall(sv) {
ball.active = true; ball.bounces = 0; ball.firstLand = null; ball.collected = false;
ball.active = true; ball.bounces = 0; ball.collected = false;
// The player is right-handed: he tosses with his LEFT hand (viewer's left) and hits with the
// racket on the viewer's right, so the ball goes up almost over his head — a big offset either way
// lands it on the wrong side of one of those two frames. The opponent is drawn from the front and
@@ -171,7 +171,7 @@ function tossBall(sv) {
ball.vx = 0; ball.vz = 0; ball.vy = TOSS.vy; ball.g = TUNE.gravity;
G.phase = 'toss';
}
export function serveApexY() { return TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity); } // ~2.95
function serveApexY() { return TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity); } // ~2.95
// how many 10%-pace cuts a shot may take to get over the net. 8 wasn't enough for a target
// sitting right behind the tape — that needs a near-vertical drop, so ~10 cuts (pace × 0.35).
@@ -522,10 +522,10 @@ function startChangeover() {
// that ("[0,1,2,3,4].find(...)"), so the moment the rack grew past five, a burst weapon sitting in
// slot 5..9 could never be found and never spent: beacon, flamer and rocket sat at their pickup
// count forever. One accessor, one place that knows the range.
export const slotEntry = i => (i === 0 ? G.temp : G.rack[i - 1]) || null;
const slotEntry = i => (i === 0 ? G.temp : G.rack[i - 1]) || null;
export const slotId = i => slotEntry(i)?.id || (i === 0 ? 'ball' : null);
export const slotStock = i => { const e = slotEntry(i); return e ? e.n : (i === 0 ? Infinity : 0); };
export const slotOf = id => { // -1 if you are not carrying it
const slotOf = id => { // -1 if you are not carrying it
for (let i = 0; i <= RACK_MAX; i++) if (slotId(i) === id) return i;
return -1;
};
@@ -549,7 +549,7 @@ export function grantWeapon(id, n = WEAPONS[id].tin) {
// Spend one from a slot, and hand you back plain balls when it runs dry. The temp weapon does not
// leave an empty dimmed slot behind the way a rack entry does — slot 0 simply becomes BALL again.
export function spendSlot(i) {
function spendSlot(i) {
const e = slotEntry(i);
if (!e) return;
if (--e.n > 0) return;
@@ -646,7 +646,7 @@ function stockTestRack() {
else if (G.rack.length < RACK_MAX) G.rack.push({ id, n: WEAPONS[id].tin });
}
}
export function tickTestMode(dt) {
function tickTestMode(dt) {
if (!G.testMode) return;
stockTestRack(); // no barrels: the rack is already full of everything they would hand you, and
} // one rolling across the sandbox was just something else to look at
@@ -934,7 +934,6 @@ function onBounce() {
}
if (G.mode !== 'tennis') return; // no lines, no faults — the umpire has other concerns
if (ball.bounces === 1) {
ball.firstLand = { x: ball.x, z: ball.z };
const wasServe = G.wasServe; G.wasServe = false;
if (wasServe) {
if (!inServeBox(ball.x, ball.z)) { ball.active = false; fault(); }
@@ -1156,7 +1155,7 @@ export function dragServe(aim) {
// 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, wid: ball.wid });
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.active = true; ball.collected = false; ball.bounces = 0;
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
hitServe(P, pullQuality(aim.mag), aim, pullPerfect(aim.mag));
+7 -7
View File
@@ -1,7 +1,7 @@
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
import { cv, ctx } from './dom.js';
import { TUNE, UI, EATEN_GRAB, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW } from './config.js';
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW } from './config.js';
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
import { zombies, gore, piles, parts, pellets, extras, shots } from './zombies.js';
@@ -67,7 +67,7 @@ export let W = 480, H = 270;
// BOTTOM — the canvas deliberately runs to the PHYSICAL bottom edge so the court art fills the
// screen (the home indicator is a slim bar, not opaque UI). Only the interactive RACK is
// lifted clear of it, by SAFE.bottom below.
export const SAFE = { top: 0, bottom: 0, topCss: 0 };
const SAFE = { top: 0, bottom: 0, topCss: 0 };
// Logical px of slack under a full draw, on top of the rack: the home indicator (~34 CSS px) plus
// enough that a pull ENDING near the edge is still nowhere near STARTING there. See fitViewport.
const PULL_ROOM = 30;
@@ -263,7 +263,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
}
// ------------------------------------------------------------------ crowd ----
export function buildCrowd() {
function buildCrowd() {
crowd.dots.length = 0;
const rows = Math.floor((proj(0, 0, 0).y - 26) / 5.5), cols = Math.ceil(W / 8);
for (let row = 0; row < rows; row++) for (let i = 0; i < cols; i++) crowd.dots.push({
@@ -1095,8 +1095,8 @@ const RACK_GRAB = 7; // px the TOUCH band extends above the drawn rack (below,
// UPWARD instead, and only once you are actually carrying more than five: the common case is still
// one row on the bottom edge, exactly as before.
const rackRows = () => (G.rack.length + 1 > RACK_ROW ? 2 : 1);
export const rackTop = () => H - TUNE.rackH * rackRows() - RACK_BOT - SAFE.bottom;
export const METER_Y = () => rackTop() - 17;
const rackTop = () => H - TUNE.rackH * rackRows() - RACK_BOT - SAFE.bottom;
const METER_Y = () => rackTop() - 17;
function rackGeom() {
const h = TUNE.rackH, tw = Math.min(64, (W - 4) / RACK_ROW), rw = tw * RACK_ROW;
const rows = rackRows();
@@ -1539,8 +1539,8 @@ export function render() {
// sunk into a recessed slot — purple for the server, gold digits. Splatter adds a recessed
// status well (kills-to-next-point pips + weapon stock). Hard edges, no radius = retro.
const splat = G.mode !== 'tennis';
const bd = { face: '#1e5b3a', hi: '#3f9560', lo: '#0e3624', slot: '#0b2b1c', slotSvr: '#2b1a46',
spine: '#472a70', spineHi: '#5f3d94', cream: '#f2ead6', dim: '#89a08f', gold: UI.pick, engHi: '#2c7a4f' };
const bd = { face: '#1e5b3a', hi: '#3f9560', lo: '#0e3624',
spine: '#472a70', spineHi: '#5f3d94', cream: '#f2ead6', dim: '#89a08f', gold: UI.pick };
const R = (x, y, w, h, c) => { ctx.fillStyle = c; ctx.fillRect(x, y, w, h); };
const ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3
? (score.pts[i] === score.pts[1 - i] ? '40' : score.pts[i] > score.pts[1 - i] ? 'AD' : '40')
+2 -2
View File
@@ -7,7 +7,7 @@ import { gore } from './zombies.js'; // runtime-only cycle (zombies imports stat
export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
export const ball = { x: 0, y: 1, z: 20, vx: 0, vy: 0, vz: 0, g: 9.2,
active: false, lastHit: 'P', bounces: 0, firstLand: null, collected: false, kills: 0, bomb: false, flame: false,
active: false, lastHit: 'P', bounces: 0, collected: false, kills: 0, bomb: false, flame: false,
// which weapon launched the ball in flight — the stats screen files a kill under it long after
// the WEAPONS row itself is out of scope. Lives on the ball so PRISTINE resets it for free.
wid: 'ball' };
@@ -147,7 +147,7 @@ export const other = s => s === 'P' ? 'O' : 'P';
export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even points → deuce (right) side
export function setupPoint() {
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.firstLand = null;
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0;
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
if (G.mode === 'splatter') { // always your serve, from wherever you're standing
score.server = 'P';
+2 -2
View File
@@ -3,7 +3,7 @@
// your baseline — one crossing it is a point for THEM.
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
import { TUNE, UI, COURT, VOLLEY, CONTACT, BOSS_ART, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { TUNE, UI, COURT, CONTACT, BOSS_ART, 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 { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh } from './audio.js';
import { splatPoint, cheer, spentBall, resolveInterlude, showModal, endBossFight, aside, grantWeapon, weaponModal, endBurst } from './gameplay.js';
@@ -461,7 +461,7 @@ function killZombie(i, quiet) {
// ---- fire. The flame ball doesn't just kill what it touches, it starts an epidemic: the kill
// ignites its neighbours, and every burning body keeps igniting whatever it shambles past.
// Read-only over zombies[] (it only sets burnT), so it is safe to call from inside a kill loop.
export function ignite(x, z, r) {
function ignite(x, z, r) {
for (const zb of zombies) {
if (!zb || zb.burnT > 0) continue; // the boss DOES catch now — just at TUNE.bossBurnMul the rate,
// so fire is a real answer to a big body instead of a no-op