The opponent shows up, and the death loop dies

"still very weak and slow opponent. it also spawns at places where it is
impossible to react to my serve. so i win like always. in the cases i dont
win there is a bug. when trying to serve after i got hurt i get hurt again
and again until i die".

The death loop was G.serveNum. Tennis's pointTo is the only thing that ever
reset it, and the match bypasses pointTo entirely — so the first fault of the
match armed "second serve" permanently, and every missed serve after it was a
double fault, a point, and blood, repeating to death. Exactly the spiral a
rattled player serves into. matchReady(), the match's own point boundary,
resets it now; a regression pins it.

The receiver starts every point on its mark: matchReady drops it onto the
exact receiver position the tennis branch computes for O, deuce/ad mirrored,
instead of a random spawn x or wherever the last rally left it. An opponent
that cannot reach the serve is not weak, it is absent.

And the base opponent is a player now: matchSpeedBase 1.4 (the duel's 2.8m/s
lumber was tuned for one sudden-death point), matchFlub 0.08 (0.2 gives away
a fifth of all returns). Measured with the same autopilot that previously won
seven straight games: it now loses the match. Possibly too hot — the two
dials are named, Oliver's thumb decides.

40 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 22:41:10 +02:00
1 parent ea6e50f8f8
commit f67411e330
6 files changed
+83 -14

No files matched your search

+22 -6
View File
@@ -8,7 +8,7 @@
// input <-> gameplay: the imported bindings are only touched inside functions, never at load.)
import { TUNE, UI, bossArt, COURT, HOME } from './config.js';
import { G, P, ball, score, setupPoint, sendBoy } from './state.js';
import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js';
import { voice, zombieDuck, aww } from './audio.js';
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, advancePoint, scoreKey } from './gameplay.js';
@@ -227,7 +227,7 @@ export function startMatch() {
G.boy = null;
score.pts = [0, 0];
matchOpponent();
ball.active = false; setupPoint();
matchReady();
zombieDuck(true);
voice('play_resume', 0.2);
if (!G.seen.match) {
@@ -238,6 +238,22 @@ export function startMatch() {
return true;
}
// Every point starts like a real one: the server behind his line (setupPoint's tennis branch) and
// THE RECEIVER ON ITS MARK — the zombie is dropped onto the receiver position the tennis branch
// just computed for O, instead of standing wherever the last rally abandoned it. Oliver: "it spawns
// at places where it is impossible to react to my serve. so i win like always." An opponent that
// cannot reach the serve is not weak, it is absent.
// ...and G.serveNum resets HERE, because the match bypasses tennis's pointTo (the only other place
// that resets it). Without this, the first fault of the match armed 'second serve' PERMANENTLY:
// every missed serve after it was a double fault, a point, and blood — the death loop Oliver hit.
function matchReady() {
ball.active = false;
G.serveNum = 1;
setupPoint();
const rz = zombies.find(z => z.variant === 'r');
if (rz) { rz.x = O.x; rz.z = 1.0; rz.pose = 'idle'; rz.swingT = -1; rz.swingCool = 0.3; }
}
function matchOpponent() {
spawnRacketZombie();
const zb = zombies.find(z => z.variant === 'r');
@@ -279,7 +295,7 @@ function matchPoint(side) {
if (over) return side === 'P' ? matchGameWon(zb, true) : matchGameLost();
voice(scoreKey(), 0.4);
if (side === 'P') cheer(0.8, 0.3);
ball.active = false; setupPoint(); // next point, same opponent
matchReady(); // next point, same opponent, both on their marks
}
function matchGameWon(zb, banked = false) {
@@ -294,9 +310,9 @@ function matchGameWon(zb, banked = false) {
voice('game_player', 0.3); cheer(2.4, 0.6);
G.card = { title: 'GAME.', sub1: 'NEXT OPPONENT', color: UI.pick, t: 2.2 };
d.opp++;
matchOpponent(); // fresh body, more hp, its own intro beat
matchOpponent(); // fresh body, its own intro beat
d.ending = false;
ball.active = false; setupPoint();
matchReady();
G.phase = 'over'; G.overT = 2.2;
}
@@ -304,7 +320,7 @@ function matchGameLost() {
// Reachable only through deuce (four straight points is exactly your hp) — the body keeps its
// wounds, the scoreboard banks their game, and the match simply plays on.
voice('game_opponent', 0.3); aww(1);
ball.active = false; setupPoint();
matchReady();
G.phase = 'over'; G.overT = 1.8;
}