Files
splatter-tennis/state.js
T
wuwiandClaude Fable 5 c506a631c2 Split the monolith into ES modules; hoist scattered magic numbers
game.js (830 lines) -> 9 modules, native ES modules so still zero build step:
  dom      canvas handles (imports nothing; keeps cycles harmless)
  config   TUNE (live-tunable) + structural constants
  state    all mutable game state (G) + setupPoint/sendBoy
  audio    sfx synthesis, recorded umpire voice, reverb, speech fallback
  input    keyboard + touch, exports axes and takeFlickAim()
  gameplay ballistics, swings, serve, rules, AI, update loop
  render   camera, viewport, sprites, crowd, all drawing
  panel    T tuning UI
  main     boot, loop, intro, window.T test hooks

Deduped into named constants: bounce tempo (was in 3 places), contact
height band, intercept zone depth, home positions, player bounds, toss
params, shot depths, pose hold time, sprite scale, actor heights. Also
extracted drawShadow/drawBall/expirePose from repeated code.

One intentional import cycle (input <-> gameplay), safe: both sides only
call functions at runtime, never during module evaluation.

Regression-verified in browser sims: point cycle, CPU returns 11/12,
assist contract 8/8, corner-winner gradient intact (0/12 perfect corners
returned, 10/12 center balls returned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:38:15 +02:00

48 lines
2.2 KiB
JavaScript

// All mutable game state in one place, plus the point-reset that re-arms it.
import { HOME } from './config.js';
export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
export const ball = { x: 0, y: 1, z: 20, vx: 0, vy: 0, vz: 0, g: 9.2,
active: false, lastHit: 'P', bounces: 0, firstLand: null, collected: false };
export const score = { pts: [0, 0], games: [0, 0], server: 'P' }; // pts index 0=P 1=O
export const crowd = { cheer: 0, dots: [] }; // dots are (re)built by render.buildCrowd
// everything that used to be loose top-level lets
export const G = {
phase: 'pretoss', // pretoss | toss | rally | over
serveNum: 1,
overT: 0,
hitstop: 0,
flash: null, // {text, t}
idleT: 0,
outMarker: null, // {x, z, out, t} — where your last shot lands
boy: null, // ball boy sprinting across: {x, z, dir, runT}
aiServeT: 0,
aiSwingCool: 0,
aiReactT: 9, // time since the player's last hit; CPU reads the shot after TUNE.aiReaction
wasServe: false, // the ball in flight is a serve (first bounce must hit the box)
};
export const other = s => s === 'P' ? 'O' : 'P';
export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even points → deuce (right) side
export function setupPoint() {
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.firstLand = null;
const sv = score.server === 'P' ? P : O, rc = score.server === 'P' ? O : P;
const sx = deuceSide() ? 1.3 : -1.3;
sv.x = sv.side === 'P' ? sx : -sx; // deuce side mirrors across the net
sv.z = sv.side === 'P' ? 24.3 : -0.5;
rc.x = -sv.x; rc.z = rc.side === 'P' ? 22.5 : HOME.O.z - 1.3;
P.pose = O.pose = 'idle'; P.swingT = O.swingT = -1;
P.facing = O.facing = 1; // canonical righty stance — bounce/serve sprites must not render mirrored
G.aiServeT = score.server === 'O' ? 1.4 : 0;
}
export function sendBoy() {
if (ball.z < -3 || ball.z > 27) return; // ball left the field lengthwise, let it go
const dir = ball.x > 0 ? 1 : -1; // enter from the opposite side so the pickup is mid-run
G.boy = { x: dir > 0 ? -13 : 13, z: ball.z, dir, runT: 0 };
}