The reaction lands where the ball does
- praise was firing at RELEASE, half a second before impact (measured: 32-frame flight, word on frame 0) — it now arms on the ball and pops on the first body it meets, or its first bounce - DOUBLE KILL / MASSACRE / RETURNED rise off the thing that earned them, as world-anchored pop texts — the announcement band is for milestones - the band got rows: a card can no longer print over the flash line - GAME N flash deleted (the scoreboard bar already says it), CHANGEOVER folded into the countdown panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9476f354c2
commit
1337048a85
7 files changed
+91
-30
No files matched your search
+33
-18
@@ -5,7 +5,7 @@ import { P, O, ball, score, crowd, G, court, other, deuceSide, setupPoint, sendB
|
||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam } from './zombies.js';
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam , pops, popText } from './zombies.js';
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it arrives at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -46,10 +46,14 @@ let praiseN = 0;
|
||||
// ...and he does not say it every time. Same cooldown model as the asides (aside()): once you have
|
||||
// learned the band you hit it constantly, and a HUD that talks on every other serve is noise
|
||||
// whatever the word is. The bright click in the pull is the reliable feedback; this is the garnish.
|
||||
function praise() {
|
||||
if (G.praiseT > 0) return;
|
||||
// Returns the word (or null, on cooldown) — the CALLER decides where and when it appears, because
|
||||
// the word must land at the IMPACT, not at the release (Oliver: "the reactions are playing after
|
||||
// shooting the ball, not when the ball hits"). A serve arms it on ball.praise and the contact/
|
||||
// bounce pops it; a rally hit pops it at the racket, where contact and praise are the same moment.
|
||||
function praiseWord() {
|
||||
if (G.praiseT > 0) return null;
|
||||
G.praiseT = TUNE.praiseEvery;
|
||||
G.flash = { text: PRAISE[praiseN++ % PRAISE.length], t: 0.7 };
|
||||
return PRAISE[praiseN++ % PRAISE.length];
|
||||
}
|
||||
|
||||
// Court dust: a kicked-up puff at ground level, tinted per surface — chalk on clay, flecks on the
|
||||
@@ -157,6 +161,7 @@ function doHit(ch, quality, aim) {
|
||||
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
ball.bomb = false; ball.flame = false; ball.wid = 'ball'; // only a consuming serve arms these; any other hit clears them
|
||||
ball.praise = null; // a re-hit is a new shot: a stale armed word must not pop on ITS impact
|
||||
if (ch.side === 'P') { // shot marker: where this will land — red means it's going out
|
||||
// TENNIS ONLY. It answers "did that land in?", and splatter does not ask: `out` could never be
|
||||
// true there, so it was a permanent yellow ring on the grass carrying no information — and
|
||||
@@ -169,7 +174,11 @@ function doHit(ch, quality, aim) {
|
||||
G.aiReactT = 0; // CPU hasn't read the shot yet
|
||||
}
|
||||
sfx(ch.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded racket strike, per side
|
||||
if (quality > 0.999) { G.hitstop = 0.05; praise(); pock(0.5, 1600); } // the rally hit: a real 45 ms window (perfectMs)
|
||||
if (quality > 0.999 && ch.side === 'P') { // the rally hit: a real 45 ms window (perfectMs)
|
||||
G.hitstop = 0.05; pock(0.5, 1600);
|
||||
const w = praiseWord(); // a rally hit IS the impact — the word pops right on the contact
|
||||
if (w) popText(ball.x, ball.y, ball.z, w);
|
||||
}
|
||||
ch.pose = 'swing'; ch.poseT = 0;
|
||||
}
|
||||
|
||||
@@ -292,6 +301,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
ball.lastHit = sv.side; ball.bounces = 0;
|
||||
ball.pow = quality;
|
||||
ball.kills = 0; // fresh serve: multi-kill streak restarts
|
||||
ball.praise = null; // ...and any word a spent ball never got to pop dies with it
|
||||
// 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.
|
||||
@@ -332,7 +342,12 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
if (sv.side === 'P') G.tossDrops = 0; // a served toss ends the apex lesson
|
||||
if (perfect) { praise(); G.hitstop = 0.05; }
|
||||
if (perfect && sv.side === 'P') {
|
||||
G.hitstop = 0.05;
|
||||
// the word is ARMED here and pops at the IMPACT — first body the ball touches, or its first
|
||||
// bounce. Praising at release meant the HUD spoke while the ball was still mid-air.
|
||||
ball.praise = praiseWord();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- scoring ----
|
||||
@@ -511,10 +526,12 @@ export function proceedAfterClear() {
|
||||
G.pendingHorde = true; // the horde begins when the card's pause ends
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.setCardT;
|
||||
} else if (mode === 'changeover') {
|
||||
G.pendingGame = true; startChangeover(); // 'GAME N' after the breather
|
||||
} else { // 'game': even-game beat, then GAME N announces the next wave
|
||||
G.pendingGame = true; ball.active = false; G.phase = 'over'; G.overT = TUNE.gameBeatT;
|
||||
startChangeover();
|
||||
} else { // 'game': even-game beat, then the next wave arms
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.gameBeatT;
|
||||
}
|
||||
// (no 'GAME N' flash on either path anymore — the bar under the scoreboard prints the game
|
||||
// number permanently, and the flash was the same news shouted a second time)
|
||||
}
|
||||
|
||||
// changeover (after odd games, as the rules demand): a forced breather — no spawns,
|
||||
@@ -526,7 +543,8 @@ function startChangeover() {
|
||||
// parked off the field (sendBoy's z guard) meant no boy came out at all, and the breather is
|
||||
// now long enough that an empty court reads as the game having stalled.
|
||||
sendBoy(0, COURT.L * 0.75, true); // near enough the camera to actually watch him work
|
||||
G.flash = { text: 'CHANGEOVER', t: 1.4 };
|
||||
// no CHANGEOVER flash — the countdown panel says 'CHANGEOVER · RESUMING IN n' for the whole
|
||||
// breather (render), so the flash was the same word with a shorter lifespan
|
||||
aside('ump_still_a_match', 2.6); // the breather is when he reminds you this is a match
|
||||
// no tins here anymore — weapons are won in the duel, never handed out on a schedule
|
||||
}
|
||||
@@ -824,7 +842,6 @@ const xLimit = () => Math.max(xVisible(), COURT.W / 2);
|
||||
function startHorde() {
|
||||
if (G.horde) return;
|
||||
G.changeover = false; // a cheat-forced horde mid-changeover must not leave spawns locked
|
||||
G.pendingGame = false; // the horde IS the announcement
|
||||
const grow = 1 + (G.sets[0] + G.sets[1]) * TUNE.hordeGrow; // each set banked makes the next wave worse
|
||||
// the gun grows WITH the wave: spawn rate and length both scale by grow, so a flat gun fell
|
||||
// further behind on every set banked (the second horde was where every run ended)
|
||||
@@ -917,6 +934,8 @@ export function inServeBox(x, z) {
|
||||
function onBounce() {
|
||||
ball.bounces++;
|
||||
pock(0.35, 500);
|
||||
// a deferred praise word that met no body lands with the ball — the DRAW was still perfect
|
||||
if (ball.praise) { popText(ball.x, 0.3, ball.z, ball.praise); ball.praise = null; }
|
||||
if (G.mode === 'splatter' && ball.bomb && ball.bounces === 1) { // a bomb that missed everyone still blows up
|
||||
explode(ball.x, ball.z); ball.bomb = false;
|
||||
spentBall(); return;
|
||||
@@ -1303,6 +1322,7 @@ export function update(dt) {
|
||||
if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // settles rather than freezing
|
||||
if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null;
|
||||
if (G.boomFlash > 0) G.boomFlash -= dt;
|
||||
for (let i = pops.length - 1; i >= 0; i--) if ((pops[i].t -= dt) <= 0) pops.splice(i, 1); // no DOUBLE KILL frozen over the feast
|
||||
updateZombies(dt);
|
||||
return;
|
||||
}
|
||||
@@ -1331,6 +1351,7 @@ export function update(dt) {
|
||||
// late in a run — the weapon modal is first-sight-only, so once you have seen a tin the label
|
||||
// flash IS the announcement, and the trainer's popup was eating it every time you were wounded.
|
||||
if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; }
|
||||
for (let i = pops.length - 1; i >= 0; i--) if ((pops[i].t -= dt) <= 0) pops.splice(i, 1); // reaction pops decay
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||
G.idleT += dt;
|
||||
if (G.asideT > 0) G.asideT -= dt; // the asides' shared cooldown (see aside())
|
||||
@@ -1604,13 +1625,7 @@ export function update(dt) {
|
||||
if (G.horde || G.mgT > 0) {
|
||||
G.phase = 'rally'; ball.active = false;
|
||||
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
|
||||
} else {
|
||||
setupPoint();
|
||||
if (G.pendingGame && G.mode === 'splatter') { // the next wave is announced as it arms
|
||||
G.pendingGame = false;
|
||||
G.flash = { text: 'GAME ' + (G.runGames + 1), t: 1.1 };
|
||||
}
|
||||
}
|
||||
} else setupPoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user