Ten small flourishes, all borrowed machinery
Kills stutter the world a touch more per streak; a massacre borrows the boss's slow-mo; a boss death gets the transition's zoom punch. PERFECT contact flashes a spark ring. The sidestep and every real bounce kick up court-tinted dust. The net cord ripples where the ball clips it. The landing ring contracts on time-to-touchdown. Hot rally balls drag three fading ghosts. And every 8-15 seconds the floodlights stutter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d847c2e4a8
commit
8ab7ac0c25
4 files changed
+98
-10
No files matched your search
+25
-4
@@ -1,7 +1,7 @@
|
||||
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
||||
|
||||
import { TUNE, UI, BOSS_ART, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||
import { P, O, ball, score, crowd, G, court, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||
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
|
||||
@@ -21,7 +21,7 @@ export function predictLanding() { // first-bounce point, closed form — exact,
|
||||
const disc = ball.vy * ball.vy + 2 * g * ball.y;
|
||||
if (disc < 0) return null;
|
||||
const t = (ball.vy + Math.sqrt(disc)) / g; // time until y = 0
|
||||
return { x: ball.x + ball.vx * t, z: ball.z + ball.vz * t };
|
||||
return { x: ball.x + ball.vx * t, z: ball.z + ball.vz * t, t }; // t rides along: the landing ring contracts on it
|
||||
}
|
||||
|
||||
// splatter FREE-AIM (slingshot). The pull is a RAY, not a landing point: its DIRECTION picks the
|
||||
@@ -52,6 +52,18 @@ function praise() {
|
||||
G.flash = { text: PRAISE[praiseN++ % PRAISE.length], t: 0.7 };
|
||||
}
|
||||
|
||||
// Court dust: a kicked-up puff at ground level, tinted per surface — chalk on clay, flecks on the
|
||||
// grass, grit on the slab. Squarely inside the "blood, fire, smoke" procedural exemption; the ball's
|
||||
// bounce and the sidestep both kick it, which is what pins fast feet and fast balls to the ground.
|
||||
const DUST_COL = { grass: '#8fa06a', clay: '#c9895a', hard: '#8494a4' };
|
||||
function dustPuff(x, z, n = 5, sp = 1) {
|
||||
for (let k = 0; k < n; k++) parts.push({
|
||||
x: x + (Math.random() * 2 - 1) * 0.15, y: 0.05, z: z + (Math.random() * 2 - 1) * 0.1,
|
||||
vx: (Math.random() * 2 - 1) * 1.2 * sp, vy: 0.5 + Math.random() * 0.9 * sp,
|
||||
vz: (Math.random() * 2 - 1) * 1.2 * sp,
|
||||
t: 0.25 + Math.random() * 0.2, spark: DUST_COL[court.id] || '#98948a' });
|
||||
}
|
||||
|
||||
export function flickTarget(ch, aim) { // exported: render draws the live reticle with it
|
||||
const q = pullQuality(aim.mag), f = pullFrac(aim.mag);
|
||||
if (aim.mag < TUNE.pullMin) return { q, f }; // barely drawn: no lane yet, so no target
|
||||
@@ -156,7 +168,8 @@ 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) { G.hitstop = 0.05; praise(); pock(0.5, 1600); // the rally hit: a real 45 ms window (perfectMs)
|
||||
if (ch.side === 'P') G.spark = { x: ball.x, y: ball.y, z: ball.z, t: 0.3 }; } // ...and the contact flashes a ring
|
||||
ch.pose = 'swing'; ch.poseT = 0;
|
||||
}
|
||||
|
||||
@@ -318,7 +331,8 @@ 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 (perfect) { praise(); G.hitstop = 0.05; }
|
||||
if (perfect) { praise(); G.hitstop = 0.05;
|
||||
if (sv.side === 'P') G.spark = { x: ball.x, y: ball.y, z: ball.z, t: 0.3 }; } // the PERFECT ring, at the strike
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- scoring ----
|
||||
@@ -803,6 +817,7 @@ export function spentBall() {
|
||||
export function dodge(dir) {
|
||||
if (G.mode !== 'splatter' || G.modal || G.dodgeCd > 0 || G.dodgeT > 0) return;
|
||||
G.dodgeDir = dir; G.dodgeT = TUNE.dodgeT; G.dodgeCd = TUNE.dodgeCool; G.dodgeI = TUNE.dodgeIFrame;
|
||||
dustPuff(P.x, P.z, 6); // the committed step kicks up the court — it also marks where the i-frame began
|
||||
G.idleHintT = 0; // ...and the apron demo goes away, like any other gesture. The touch path nulls
|
||||
// G.aim to fire this, and the demo's own guard only watches G.aim.
|
||||
P.facing = dir;
|
||||
@@ -1299,6 +1314,8 @@ export function update(dt) {
|
||||
if (was < EATEN_SUMMARY && G.eatenT >= EATEN_SUMMARY) { zombieDuck(true); menuDuck(false); }
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt); // the stand sits back down over the death too —
|
||||
// a cheer live at the kill used to clap forever
|
||||
if (G.spark && (G.spark.t -= dt) <= 0) G.spark = null; // one-shot effects finish
|
||||
if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // rather than freezing mid-air
|
||||
if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null;
|
||||
if (G.boomFlash > 0) G.boomFlash -= dt;
|
||||
updateZombies(dt);
|
||||
@@ -1311,6 +1328,8 @@ export function update(dt) {
|
||||
else { G.card.t -= dt; if (G.card.t <= 0) G.card = null; }
|
||||
}
|
||||
if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null;
|
||||
if (G.spark && (G.spark.t -= dt) <= 0) G.spark = null; // the PERFECT ring burns out
|
||||
if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // the tape settles
|
||||
if (G.zoom && (G.zoom.t += dt) >= G.zoom.T) G.zoom = null; // scripted punch-in (render applies it)
|
||||
if (G.bossBurst && (G.bossBurst.t -= dt) <= 0) G.bossBurst = null;
|
||||
if (G.boomFlash > 0) G.boomFlash -= dt;
|
||||
@@ -1514,11 +1533,13 @@ export function update(dt) {
|
||||
ball.z = COURT.netZ + (oldZ < COURT.netZ ? -0.05 : 0.05);
|
||||
ball.vz *= -0.08; ball.vx *= 0.3; ball.vy = Math.min(ball.vy, 0);
|
||||
pock(0.4, 300);
|
||||
G.netRipple = { x: ball.x, t: 0.4 }; // the tape carries the hit: a damped wobble where it clipped
|
||||
}
|
||||
}
|
||||
if (ball.y <= 0 && ball.vy < 0) {
|
||||
ball.y = 0; ball.vy = -ball.vy * TUNE.bounceY;
|
||||
ball.vx *= TUNE.bounceXZ; ball.vz *= TUNE.bounceXZ;
|
||||
if (Math.abs(ball.vy) > 1.0) dustPuff(ball.x, ball.z, 3, 0.7); // a real bounce kicks up the court
|
||||
onBounce();
|
||||
if (ball.active && Math.abs(ball.vy) < 0.8) { // rolled out — dead ball
|
||||
ball.active = false;
|
||||
|
||||
Reference in new issue
Block a user