From d847c2e4a8c661dd23c599e0279b97f60814dd8c Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 6 Aug 2026 09:37:08 +0200 Subject: [PATCH] Sweep the dead code off the court MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/x_tin.png | Bin 8537 -> 0 bytes audio.js | 2 +- config.js | 2 +- gameplay.js | 21 ++++++++++----------- render.js | 14 +++++++------- state.js | 4 ++-- zombies.js | 4 ++-- 7 files changed, 23 insertions(+), 24 deletions(-) delete mode 100644 assets/x_tin.png diff --git a/assets/x_tin.png b/assets/x_tin.png deleted file mode 100644 index d8d31f081bea466b91abccb0ee5642bf166f0be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8537 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 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); diff --git a/config.js b/config.js index dff986a..358b06b 100644 --- a/config.js +++ b/config.js @@ -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 diff --git a/gameplay.js b/gameplay.js index 2749744..2d71486 100644 --- a/gameplay.js +++ b/gameplay.js @@ -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)); diff --git a/render.js b/render.js index 22474c0..c387daf 100644 --- a/render.js +++ b/render.js @@ -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') diff --git a/state.js b/state.js index 84392ff..79ed231 100644 --- a/state.js +++ b/state.js @@ -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'; diff --git a/zombies.js b/zombies.js index 121181d..719e395 100644 --- a/zombies.js +++ b/zombies.js @@ -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