Six calls the umpire got wrong
- The duel misread a spent ('X') ball: bounce out = instant win, two bounces
= a loss plus a surge, for a shot that should just be re-served.
- Duel serves routed through the temp weapon riding slot 0 — a laser fired
instead of the ball. Tennis rules now mean plain balls, temp or no temp.
- A tap during the death animation tossed a ball that hung frozen midair.
- The EPIC boss card was clobbered by the SET card when game 30 was both.
- The telegraph glow divided by the wrong wind: negative alpha, no fade-in.
- The crowd clapped forever over the death screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f22d3d5a0
commit
f1f0d05a61
3 files changed
+33
-8
No files matched your search
+26
-5
@@ -282,8 +282,11 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
// the SELECTED tin arms the serve; slot 0 (PLAIN) throws a normal ball and banks your
|
||||
// specials for later. Placement still follows the flick — only WHICH ball flies is the
|
||||
// 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;
|
||||
// ...and the DUEL is plain balls, full stop. startDuel forces slot 0, but slot 0 routes through
|
||||
// G.temp — so with a temp weapon riding it every duel serve armed and spent the temp (a laser
|
||||
// fired instead of the ball and aborted the point). Tennis rules means 'ball', temp or no temp.
|
||||
const wsel = G.mode === 'splatter' && !G.interlude ? G.weapon : 0;
|
||||
const wid = G.interlude || (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.
|
||||
@@ -703,6 +706,11 @@ export function endBossFight() {
|
||||
zombieDuck(false);
|
||||
shots.length = 0;
|
||||
cheer(3.4, 0.9); voice('unbelievable', 0.3);
|
||||
// hand back the game beat the boss interrupted FIRST — same contract resolveInterlude uses. It
|
||||
// used to run LAST, and its 'set' branch writes a routine SET card and a short overT: on a game
|
||||
// that is both a set and a boss chapter (game 30), the EPIC card below was overwritten before it
|
||||
// had drawn a single frame, and the beat was cut to setCardT. The boss card wins now.
|
||||
if (G.afterDuel) { G.afterClear = G.afterDuel; G.afterDuel = null; proceedAfterClear(); }
|
||||
// The longest fight in the game deserves more than the two seconds a routine set gets. Big word,
|
||||
// long hold — the chapter and the crowd move down to the subs where they belong.
|
||||
// (no 'GAME n NEXT' line: the bar under the scoreboard prints the game number permanently, and a
|
||||
@@ -710,9 +718,10 @@ export function endBossFight() {
|
||||
G.card = { title: 'EPIC!', sub1: 'CHAPTER ' + c + ' DOWN',
|
||||
sub2: 'THE STANDS ARE ON THEIR FEET', color: UI.pick, t: TUNE.bossWinT,
|
||||
wait: TUNE.bossDeathT }; // ...after the body has finished coming apart
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.bossWinT + TUNE.bossDeathT;
|
||||
// hand back the game beat the boss interrupted — same contract resolveInterlude uses
|
||||
if (G.afterDuel) { G.afterClear = G.afterDuel; G.afterDuel = null; proceedAfterClear(); }
|
||||
ball.active = false;
|
||||
// a duel the handback just started owns the phase; otherwise hold the boss beat, and never
|
||||
// SHORTEN a longer one the handback set (a changeover's 5 s outlasts this)
|
||||
if (!G.interlude) { G.phase = 'over'; G.overT = Math.max(G.overT, TUNE.bossWinT + TUNE.bossDeathT); }
|
||||
}
|
||||
|
||||
// ---- THE DEADLOCK WATCHDOG.
|
||||
@@ -912,6 +921,11 @@ function onBounce() {
|
||||
spentBall(); return;
|
||||
}
|
||||
if (G.mode === 'splatter' && G.interlude) { // sudden death: the lines are LIVE again
|
||||
// A ball SPENT on a body ('X' — it wounded the racket zombie without killing him) gets no line
|
||||
// call in either direction: it is nobody's shot anymore. Without this case the P-or-not test
|
||||
// below read 'X' as HIS ball — bounce out and you were handed the duel, bounce twice in-court
|
||||
// and you LOST it for hitting him. Same rule as the dead-roller path in update().
|
||||
if (ball.lastHit === 'X') { spentBall(); return; }
|
||||
if (ball.bounces === 1) {
|
||||
const okSide = ball.lastHit === 'P' ? ball.z < COURT.netZ : ball.z > COURT.netZ;
|
||||
if (!inCourt(ball.x, ball.z) || !okSide) { ball.active = false; resolveInterlude(other(ball.lastHit)); voice('out'); }
|
||||
@@ -1158,6 +1172,11 @@ function meleeCooldown() {
|
||||
}
|
||||
|
||||
export function keyPressed(code) {
|
||||
// Dead men toss no balls. The restart tap lives in main.js (maybeRestart); everything below —
|
||||
// melee, parry, and especially the pretoss branch — must not run while the death animation plays,
|
||||
// or a panic tap conjured a ball that hung frozen midair over the corpse (update early-returns
|
||||
// in eaten mode, so a tossed ball never got a single frame of physics).
|
||||
if (G.mode === 'eaten') return;
|
||||
// Desktop sidestep: an arrow/A/D EDGE outside 'rally', where the walk axes do nothing anyway.
|
||||
// Inside a rally the same keys still walk, so one key never means two things at one moment.
|
||||
if (code === 'ArrowLeft' || code === 'KeyA' || code === 'ArrowRight' || code === 'KeyD') {
|
||||
@@ -1279,6 +1298,8 @@ export function update(dt) {
|
||||
// phase C: the summary IS the end menu — hand the soundtrack back to the menu theme. Not at
|
||||
// killPlayer: the death itself already has the massacre call and the crowd's groan over it.
|
||||
if (was < EATEN_SUMMARY && G.eatenT >= EATEN_SUMMARY) { zombieDuck(true); menuDuck(false); }
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt); // the stand sits back down over the death too —
|
||||
// a cheer live at the kill used to clap forever
|
||||
if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null;
|
||||
if (G.boomFlash > 0) G.boomFlash -= dt;
|
||||
updateZombies(dt);
|
||||
|
||||
@@ -428,7 +428,7 @@ function drawMachine(zb) {
|
||||
const h = MACH_H * (img.height / SPRITE_H);
|
||||
drawStatic(name, zb.x, zb.z, h);
|
||||
if (firing) { // the TELEGRAPH still needs to read at 20px tall, so the muzzle glow stays procedural
|
||||
const k = 1 - zb.windT / TUNE.bossWind, p = proj(zb.x, h * 0.62, zb.z); // bossWind, not machWind: the boss telegraphs faster
|
||||
const k = Math.max(0, 1 - zb.windT / (zb.windMax || TUNE.machWind)), p = proj(zb.x, h * 0.62, zb.z); // fade over the ACTUAL wind
|
||||
ctx.globalAlpha = 0.3 + k * 0.7;
|
||||
ctx.fillStyle = k > 0.6 ? '#fff2c0' : UI.warm;
|
||||
const r = Math.max(1.5, p.s * 0.05 * (0.5 + k));
|
||||
@@ -544,7 +544,7 @@ function drawZombie(zb) {
|
||||
drawStatic(name, zb.x, zb.z, h, bob, zb.face > 0 && !'cfmB'.includes(v));
|
||||
if (zb.burnT > 0) drawFlames(zb, h);
|
||||
if (zb.windT > 0 && zb.variant === 'B') { // the boss telegraphs too: it rears up, glowing, then throws
|
||||
const k = 1 - zb.windT / TUNE.bossWind, p = proj(zb.x, h * 0.62, zb.z); // bossWind, not machWind: the boss telegraphs faster
|
||||
const k = Math.max(0, 1 - zb.windT / (zb.windMax || TUNE.bossWind)), p = proj(zb.x, h * 0.62, zb.z); // fade over the design's own wind
|
||||
ctx.globalAlpha = 0.25 + k * 0.55; ctx.fillStyle = k > 0.6 ? '#fff2c0' : UI.bad;
|
||||
const r = Math.max(2, p.s * 0.1 * (0.5 + k));
|
||||
ctx.beginPath(); ctx.arc(Math.round(p.x), Math.round(p.y), r, 0, 7); ctx.fill();
|
||||
|
||||
+5
-1
@@ -858,7 +858,11 @@ export function updateZombies(dt) {
|
||||
}
|
||||
} else if ((zb.fireT -= dt) <= 0) {
|
||||
zb.fireT = boss ? (A?.cycle ?? TUNE.bossCycle) : TUNE.machCycle;
|
||||
zb.windT = boss ? (A?.wind ?? TUNE.bossWind) : TUNE.machWind;
|
||||
// windMax rides along for render: the telegraph glow fades in over the ACTUAL wind. It used
|
||||
// to divide by TUNE.bossWind whatever the shooter, so the machine (0.9 s) and any design
|
||||
// with wind > 0.5 spent the first stretch at an invalid negative alpha — full brightness,
|
||||
// no ramp, on the one effect documented as the fairness contract.
|
||||
zb.windT = zb.windMax = boss ? (A?.wind ?? TUNE.bossWind) : TUNE.machWind;
|
||||
pock(0.3, 1500); // the tell: a mechanical click as it loads
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user