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:
1 parent
ea6e50f8f8
commit
f67411e330
6 files changed
+83
-14
No files matched your search
+22
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -386,8 +386,13 @@ export const TUNE = {
|
||||
matchPlayerHp: 8, // your pool for the MATCH — richer than the gore mode's 4, because a real game
|
||||
// has rallies you deserve to lose without the run ending ("the player is too
|
||||
// weak in this mode"). Their point still costs 1; matchHeal is how you last
|
||||
matchSpeedPer: 0.12, // racketAI speed grows this much per opponent felled...
|
||||
matchFlubKeep: 0.82, // ...and its whiff chance keeps only this much — the ladder now that hp is flat
|
||||
matchSpeedBase: 1.4, // racketAI speed multiplier for opponent ONE — the duel's 2.8 m/s lumber was
|
||||
// tuned for a single sudden-death point and read as "very weak and slow"
|
||||
// across a whole game (~3.9 m/s now, rising per rung)
|
||||
matchSpeedPer: 0.12, // ...growing this much per opponent felled
|
||||
matchFlub: 0.08, // the match's own whiff base — the duel's 0.2 gives away a fifth of all
|
||||
// returns, which across a game is an opponent that beats itself
|
||||
matchFlubKeep: 0.82, // ...and it keeps only this much per rung — the ladder now that hp is flat
|
||||
matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate)
|
||||
// The timing windows, DRAWN (the match's whole ask: "what is the possible tap window, what is the
|
||||
// sweet spot"). Numbers shared with the mechanics they visualise, so the picture cannot lie:
|
||||
|
||||
+7
-3
@@ -950,8 +950,10 @@ function racketAI(rz, dt) {
|
||||
const dx = ball.x - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
||||
if (d > 0.15) {
|
||||
// THE MATCH LADDER ESCALATES HERE, not in hp: with regicide gone the pool is pure scoreboard,
|
||||
// so a harder opponent is a FASTER one that flubs less — the same knobs, scaled per body felled.
|
||||
const spd = TUNE.racketSpeed * (G.match ? 1 + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||
// so a harder opponent is a FASTER one that flubs less. The BASE is boosted too — the duel's
|
||||
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
||||
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
||||
const spd = TUNE.racketSpeed * (G.match ? TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||
const step = Math.min(spd * dt, d);
|
||||
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
||||
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||
@@ -961,7 +963,9 @@ function racketAI(rz, dt) {
|
||||
if (rz.swingCool <= 0 && rz.swingT < 0
|
||||
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||
const flub = TUNE.racketFlub * (G.match ? Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : 1);
|
||||
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
||||
// which across a game is an opponent that beats itself.
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : TUNE.racketFlub;
|
||||
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user