diff --git a/config.js b/config.js index f36cb97..cb4cb9e 100644 --- a/config.js +++ b/config.js @@ -152,6 +152,14 @@ export const TUNE = { // from how far away it is (bossAI), so the run-up reads the same every time and // is long enough to see coming. Was 1.5 s at a flat 4.2 m/s, which only reached // you at all because the boss crept nearer every cycle until it was on top of you. + bossStrike: 0.4, // m SHORT of you the rush stops — its STRIKE LINE, reached exactly as the rush + // timer runs out, so the boss turns and withdraws on the same frame it arrives. + // A rush that stopped ON you left it standing there afterwards, and no amount of + // sidestepping gets you out from under that (dodgeCool 0.5 s vs a 0.28 s + // i-frame). It cannot run PAST you either: a body over the baseline is deleted + // and scores a point (see updateZombies), which quietly despawned the boss + // mid-fight the first time this ran through. At 0.4 m the contact test still + // reaches about 0.85 m either side of the lane, and the sidestep covers 1.9. bossChargeRest: 2.2, // s of held ground between charges — the window you get to hit it bossHopT: 0.55, // s per mascot hop (it moves in arcs, not in a slide) bossHopX: 2.0, // m of ground one hop covers (the eased arc lands it at about 2/pi of this) diff --git a/zombies.js b/zombies.js index 8d6a267..573df5b 100644 --- a/zombies.js +++ b/zombies.js @@ -174,10 +174,16 @@ function bossAI(zb, dt) { zb.moveT -= dt; const v = TUNE.bossChargeV * (A.speedMul ?? 1); if (zb.charging) { - const dx = P.x - zb.x; - zb.x += Math.sign(dx) * Math.min(Math.abs(dx), TUNE.bossStrafe * dt); // it tracks your lane SLOWLY: a sidestep beats it - zb.z = Math.min(P.z, zb.z + zb.chargeV * dt); // AT you and NO FURTHER: unclamped it ran - if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // off the back of the court + // IT COMMITTED TO A LANE AND IT IS RUNNING DOWN THAT LANE, whatever you do next. Tracking your + // x for the whole rush made the sidestep meaningless — it just followed you across and ran you + // over ("they overrun you without a chance"), and stopping the rush ON you left it standing + // there afterwards, where the 0.5 s dodge cooldown cannot outrun a 0.28 s i-frame. So: the + // lane is locked when the rush begins, you get the entire run-up to leave it, and the rush + // ends on a STRIKE LINE just in front of you, on the same frame the rush timer expires, so it + // turns and withdraws the instant it arrives. A rush that misses is over. + zb.x += Math.sign(zb.lane - zb.x) * Math.min(Math.abs(zb.lane - zb.x), TUNE.bossStrafe * 2 * dt); + zb.z += zb.chargeV * dt; + if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // spent: it turns and walks back } else { // AND IT FALLS BACK. A rush that ended where it landed left the boss parked on top of you for // the rest of the fight: dodge or die every two seconds, with no window to hit it and nothing @@ -187,13 +193,14 @@ function bossAI(zb, dt) { // PASSES: you dodge it, and the ground it gives up again is your turn to hit it. const home = TUNE.bossZMax, sp = v * (A.retreat ?? 1.3) * dt; // toward its LINE, from either side: zb.z += Math.sign(home - zb.z) * Math.min(Math.abs(home - zb.z), sp); // backward off a rush, forward on the way in - // THE RUSH IS TIMED, NOT PACED. Whatever ground the retreat left between you, it covers it in - // bossChargeT — so every charge arrives, and every charge takes the same readable run-up to do - // it. Paced at a flat speed instead, the same fix that stopped it camping on you also stopped - // it ever reaching you: it started each rush from the band and ran out of clock 8 m short. - if (zb.moveT <= 0) { + // THE RUSH IS TIMED, NOT PACED, and it always begins FROM THE LINE — never from wherever a + // half-finished retreat happened to leave it, which is how a rush could start on your toes + // with no run-up to read. It covers the whole distance in bossChargeT, so the approach looks + // the same every time whatever the design's speed. + if (zb.moveT <= 0 && Math.abs(zb.z - home) < 0.1) { zb.charging = true; zb.moveT = TUNE.bossChargeT; - zb.chargeV = Math.max(v, (P.z - zb.z) / TUNE.bossChargeT); + zb.lane = P.x; // the lane, locked. You have the run-up to leave it. + zb.chargeV = (P.z - TUNE.bossStrike - zb.z) / TUNE.bossChargeT; } } } else if (style === 'shadow') {