Seven verdicts from the test group
- opening CPU beatable in a game or two (softer cap, more whiffs, rubber band) - modals are for calm courts only; live-court news is a card now - the transition howto is gone: a ghost hand teaches the pull, the toss, the apex and the swing, each on the exact struggle it answers - a shot barrel keeps your weapon and glows its slot instead - switching weapons clicks, the live tile is unmistakable - barrels come faster the deeper the run goes - the umpire triple-tap cheat is deleted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f13cbef28a
commit
a89e8ff419
10 files changed
+242
-79
No files matched your search
+37
-38
@@ -112,6 +112,7 @@ function zombieTarget(aim) {
|
||||
function doHit(ch, quality, aim) {
|
||||
// aim: {ax: -1|0|1, az: -1|0|1} from held keys / flick at contact (az -1 = deep, 1 = short)
|
||||
const toFar = ch.side === 'P';
|
||||
if (ch.side === 'P') G.passStreak = 0; // a landed swing ends the tap-to-hit lesson
|
||||
// snap the ball to the racket at the contact frame so the hit visually connects
|
||||
ball.x = ch.x + ch.facing * 0.5;
|
||||
ball.z = ch.z + (toFar ? -0.3 : 0.3);
|
||||
@@ -330,6 +331,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
if (sv.side === 'P' && G.mode === 'tennis') G.outMarker = { x: boxX, z: tzs, out: !inServeBox(boxX, tzs), t: 1.3 };
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -402,6 +404,7 @@ function pointTo(side, reason) {
|
||||
const n = G.rallyNear;
|
||||
logSwing({ result: 'PASSED', reason: `never swung in range — ball got ${r2(n.d)}m away at y=${r2(n.y)}m`,
|
||||
dMin: r2(n.d), y: r2(n.y), ballZ: r2(n.bz), px: r2(n.px), pz: r2(n.pz) });
|
||||
G.passStreak++; // two of these in a row and the ghost hand shows the tap (render.drawHitHint)
|
||||
}
|
||||
const gameOver = advancePoint(side);
|
||||
G.phase = 'over'; G.overT = gameOver ? 3.2 : 2.2;
|
||||
@@ -467,9 +470,10 @@ export function splatPoint(side, quiet = false) {
|
||||
if (side === 'P' && G.runGames >= TUNE.machEvery && G.runGames % TUNE.machEvery === 0
|
||||
&& zombies.filter(z => z.variant === 'm').length < TUNE.machMax && !G.bossFight)
|
||||
spawnMachine();
|
||||
// ...and a weapon barrel on its own schedule. One at a time: two glowing pink things on court at
|
||||
// once would out-shout everything the palette uses to say "this one is dangerous".
|
||||
if (side === 'P' && G.runGames % TUNE.barrelEvery === 0
|
||||
// ...and a weapon barrel on its own schedule — which TIGHTENS as the run goes on (barrelRamp),
|
||||
// because the late game is where the rack has to carry you. Still one at a time: two glowing
|
||||
// pink things on court would out-shout everything the palette uses to say "this one is dangerous".
|
||||
if (side === 'P' && G.runGames % Math.max(1, TUNE.barrelEvery - Math.floor(G.runGames / TUNE.barrelRamp)) === 0
|
||||
&& !zombies.some(z => z.variant === 'o') && !G.bossFight)
|
||||
spawnBarrel();
|
||||
// The wave does NOT auto-collapse anymore: the survivors stay and must be cleared BY HAND before
|
||||
@@ -544,7 +548,8 @@ const slotOf = id => { // -1 if you are not carrying it
|
||||
export function selectWeapon(i) {
|
||||
if (G.interlude) return; // the sudden-death duel is played under tennis rules — plain balls only
|
||||
if (slotStock(i) <= 0) return; // dud: nothing in that slot (slot 0 is only ever empty if a spent
|
||||
G.weapon = i; pock(0.3, 1100); // temp has just been cleared, and then it is BALL again)
|
||||
G.weapon = i; // temp has just been cleared, and then it is BALL again)
|
||||
sfx('weapon-click2.mp3', 0.45); // a real mechanical click, quiet — switching is audible, not loud
|
||||
}
|
||||
|
||||
// Grant a weapon and equip it. Tops up what you already carry, else takes a free slot, else rides
|
||||
@@ -607,30 +612,13 @@ export function deliverTin() {
|
||||
if (wasPlain) G.weapon = 0;
|
||||
sendBoy(); voice('new_balls', 0.6);
|
||||
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
||||
// THE PRIZE GETS ITS OWN CARD, with the tin's own icon on it. It used to borrow G.flash — the
|
||||
// same channel as PERFECT and DOUBLE KILL — so the one thing you played the duel FOR arrived as a
|
||||
// word in the praise slot and read as another compliment. The first time a tin is seen the modal
|
||||
// announces it (and teaches its rules), so the card would be the SAME news twice, back to back —
|
||||
// it only speaks when the modal doesn't. Every time after, the card is the announcement, and it
|
||||
// does not want a tap: you have just won a point, not been asked a question.
|
||||
if (!weaponModal(id))
|
||||
G.card = { title: WEAPONS[id].label, sub1: 'DELIVERED TO THE RACK',
|
||||
sub2: (WEAPONS[id].desc && WEAPONS[id].desc[0]) || '',
|
||||
icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT };
|
||||
}
|
||||
|
||||
// The one-per-run "what this thing does" popup, shared by the duel ladder and the barrels.
|
||||
// THE GATE LIVES HERE, not at the call sites. The barrels gated themselves and the tin ladder did
|
||||
// not, so the four delivered weapons re-explained themselves every three games for a whole run
|
||||
// while a barrel weapon explained itself once — the same popup behaving two different ways.
|
||||
// Returns whether it actually announced, so a caller with its own announcement (the tin card, the
|
||||
// barrel's flash) can stay quiet rather than say the same thing twice.
|
||||
export function weaponModal(id) {
|
||||
if (G.seen['w' + id]) return false;
|
||||
G.seen['w' + id] = true;
|
||||
const w = WEAPONS[id];
|
||||
showModal({ title: w.label, body: w.desc, icon: w.icon, iconH: 40, color: UI.warm });
|
||||
return true;
|
||||
// THE PRIZE GETS ITS OWN CARD, with the tin's own icon on it — the announcement AND the whole
|
||||
// explanation. The first-sight weapon MODAL is gone (with popBarrel's): the testteam never read
|
||||
// them, and a popup that freezes a live court mid-run costs runs. The card carries the one desc
|
||||
// line that matters and does not want a tap: you have just won a point, not been asked a question.
|
||||
G.card = { title: WEAPONS[id].label, sub1: 'DELIVERED TO THE RACK',
|
||||
sub2: (WEAPONS[id].desc && WEAPONS[id].desc[0]) || '',
|
||||
icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT };
|
||||
}
|
||||
|
||||
// ---- TEST MODE. Triple-tap the scoreboard. A sandbox for trying the new content without playing
|
||||
@@ -905,11 +893,11 @@ function transition(dt) {
|
||||
once('go', 8.2, () => {
|
||||
G.mode = 'splatter'; score.server = 'P'; setupPoint(); voice('play', 0.4, 0.9); zombieMusic();
|
||||
G.shake = { t: 0.7, mag: 9 }; // the floor goes out from under the match
|
||||
// the mechanic changed under you — the one popup that gets an illustration, because it has
|
||||
// to TEACH the gesture, not just announce it
|
||||
showModal({ title: 'THE MATCH HAS CHANGED', color: UI.hot, icon: 'x_howto', iconH: 120,
|
||||
body: ['PULL BACK TO AIM — THEN LET GO', 'THE FURTHER YOU PULL, THE HARDER IT HITS',
|
||||
'A WEAK SHOT ONLY STAGGERS THEM', 'SWIPE SIDEWAYS TO STEP OUT OF THE WAY'] });
|
||||
// NO modal here anymore — nobody read it (Oliver's testteam, verbatim). The ghost hand teaches
|
||||
// the pull instead: prime the idle clock so the apron demo starts miming IMMEDIATELY rather
|
||||
// than after the usual beat of inaction, and it keeps coming back whenever the player stalls.
|
||||
G.idleHintT = TUNE.pullHintAfter;
|
||||
G.flash = { text: 'THE MATCH HAS CHANGED', t: 2.0 };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1458,6 +1446,7 @@ export function update(dt) {
|
||||
if (G.mode !== 'tennis') updateZombies(dt);
|
||||
tickTestMode(dt); // keeps the sandbox stocked and rolls it a barrel now and then
|
||||
if (G.meleeT > 0) G.meleeT -= dt;
|
||||
if (G.rackGlow && (G.rackGlow.t -= dt) <= 0) G.rackGlow = null; // the fresh-pickup pulse fades out
|
||||
watchdog(dt);
|
||||
if (G.clearing && !zombies.some(blocksClear)) proceedAfterClear(); // the won-game wave is mopped up — advance
|
||||
if (G.mode === 'transition') transition(dt);
|
||||
@@ -1498,7 +1487,9 @@ export function update(dt) {
|
||||
const near = Math.hypot(ball.x - O.x, (ball.z - O.z) * 0.8) < TUNE.aiReach
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY && ball.vz < 0;
|
||||
if (near) {
|
||||
if (Math.random() < TUNE.aiFlub) G.aiSwingCool = 0.5; // whiff: hesitates and misses
|
||||
// rubber band: every game HE has taken makes him flub more — the opening match must not
|
||||
// be a wall a new player grinds against for rounds (see aiFlubLoss in config)
|
||||
if (Math.random() < TUNE.aiFlub + score.games[1] * TUNE.aiFlubLoss) G.aiSwingCool = 0.5; // whiff: hesitates and misses
|
||||
else {
|
||||
O.swingT = TUNE.swingWindup + TUNE.swingWindow / 2;
|
||||
trySwing(O, true); O.swingT = -1;
|
||||
@@ -1577,6 +1568,9 @@ export function update(dt) {
|
||||
if (G.phase === 'toss' && ball.y <= 1.4 && ball.vy < 0 && !G.aim) { // uncaught toss → retoss (a held slingshot aim keeps the toss up)
|
||||
ball.active = false; G.phase = 'pretoss';
|
||||
if (score.server === 'O') G.aiServeT = 0.8;
|
||||
// dropping the toss twice running means the second tap isn't landing — the ghost hand then
|
||||
// rides the next toss and presses at the apex (render.drawApexHint)
|
||||
else if (G.mode === 'tennis') G.tossDrops++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1593,10 +1587,15 @@ export function update(dt) {
|
||||
// card, because 30 is both a set and a boss. The wave then poured onto the boss stage. Leave the
|
||||
// latch alone and let the beat AFTER the fight cash it: the set still gets its horde, later.
|
||||
if (G.pendingHorde && !G.bossFight && !G.interlude) { // the set card's pause is over — but the horde is a different CONTROL MODE
|
||||
G.pendingHorde = false; // (racket auto-fires, the finger steers you absolutely),
|
||||
showModal({ title: 'THEY KEEP COMING', color: UI.hot, // so it gets a real block, not just a card
|
||||
body: ['THE RACKET FIRES ON ITS OWN', 'SLIDE TO AIM · HOLD THE BASELINE'],
|
||||
onDismiss: startHorde });
|
||||
G.pendingHorde = false; // (racket auto-fires, the finger steers you absolutely),
|
||||
if (!G.seen.horde) { // so its FIRST arrival gets a real block...
|
||||
G.seen.horde = true;
|
||||
showModal({ title: 'THEY KEEP COMING', color: UI.hot,
|
||||
body: ['THE RACKET FIRES ON ITS OWN', 'SLIDE TO AIM · HOLD THE BASELINE'],
|
||||
onDismiss: startHorde });
|
||||
} else startHorde(); // ...and every later set starts on the flash alone. Re-showing the
|
||||
// same popup at the hottest moment of every set was one of the
|
||||
// "popups get me killed" reports — you know the rules by set two.
|
||||
return;
|
||||
}
|
||||
// auto-fire modes must NEVER re-enter the serve loop: an 'over' beat expiring into
|
||||
|
||||
Reference in new issue
Block a user