Files
splatter-tennis/config.js
T
wuwiandClaude Opus 5 32401366fe Drawn frames, not procedural fakes: brute lurch, boy stride, crowd applause
Every animation in this round gets real frames. The procedural squash-and-sway
on the wounded brute read as fake on sight, and that gait is where the comedy
lives.

- Each brute wound stage now has its own 2-frame lurch (z_gw/z_gwb, z_gw2/z_gw2b,
  ...). The generator gave every B frame a dragged pole that only stage 1
  actually carries, so it would have blinked in and out each step — stripped by
  keeping the largest blob, and where the pole touched the body, by cutting on
  column density (a torso is dense, a dragged pole is a thin diagonal).
- Frames step off DISTANCE COVERED, never elapsed time — boy.dist for ball boys,
  z/bruteStride for brutes. A time-driven cycle moonwalks the moment speed and
  frame rate disagree, which is exactly what "he slides faster than the animation
  suggests" was. Change a speed now and the legs keep up on their own.
- One boy, one slot. sendBoy no longer replaces a boy already on court: that was
  the appearing and disappearing, since every kill teleported him back to the
  wing mid-stride. New mess is appended to his round instead. He now walks for
  10.5s a crossing, and there is a lot of mopping to do.
- The applauding crowd is a real two-frame animation (seated / arms up), swapped
  per column on its own phase. The old "smooth wave" still shifted each column by
  a FRACTIONAL dy, and nearest-neighbour resampling at a fractional y re-snaps a
  column's pixel rows every frame — that boiling beside the HUD is what read as
  the scoreboard flickering. The board art is 99.9% opaque; nothing was ever
  showing through it. With the crowd calm the frame delta is now 0.0%.

Benchmarks: 136/150 serve returns (90.7% ±2.4%, the 11/12 bar is inside one
sigma), 30 human-tapped points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:20:43 +03:00

164 lines
12 KiB
JavaScript

// All tuning and constants. TUNE is live-editable in game via the T panel;
// the exported consts below are structural values that code shares but the panel doesn't show.
export const TUNE = {
camZ: 42, camY: 9, camF: 406, horizon: 40, // camF/horizon are per-orientation, set in fitViewport
gravity: 9.2, // slightly floaty vs 9.8 for readability
spinFactor: 0.9, // extra dive on quality shots (fake topspin)
bounceY: 0.58, bounceXZ: 0.78,
playerSpeed: 5.4, aiSpeed: 4.3,
reach: 1.3, aiReach: 1.6, // tight contact radius: the assist positions you, the tap must be honest
swingWindup: 0.06, // s before contact window opens
swingWindow: 0.16, // s contact window length
baseShotSpeed: 16, qualityShotSpeed: 12, // horizontal m/s = base + q*quality; perfect timing = real pace
// the slingshot's pace range. Weighted hard toward QUALITY so a full draw is a smash you can
// see: 21 m/s floppy at the minimum pull, 41 m/s at full. (baseShotSpeed above is the rally
// volley and stays put — it's shared with tennis mode and the CPU.)
serveSpeed: 19, serveQualitySpeed: 22,
serveSpin: 1.3, // extra gravity a full-power serve carries (was a fixed 0.35). Without it the
// net-clearance loop ate the whole smash: a flat fast ball can't get over the
// tape AND come down on a target, so it got throttled back to walking pace.
// Heavy topspin lets it leave flat and DIVE, which is what a smash actually is.
aimError: 2.4, // m of scatter at quality 0
perfectMs: 45, // |offset from window center| for PERFECT
aiQualityMin: 0.45, aiQualityMax: 0.85, aiFlub: 0.09,
aiReaction: 0.45, // s before the CPU reads your shot — well-aimed corners are winners
autoAssist: matchMedia('(pointer: coarse)').matches ? 1.15 : 0, // auto-run speed factor (mobile)
zSpeed: 0.7, // zombie shamble speed m/s
zSpawnEvery: 3.0, // s between spawns in game 1 — shrinks by zSpawnStep per finished game
zSpawnMin: 1.0, // spawn-gap floor late in a run
zSpawnStep: 0.22, // spawn-gap reduction per finished game
zMax: 3, // shamblers on court in game 1 (cap grows +1 per game to zMaxCap; 0 = spawns off, for tests)
zMaxCap: 14, // absolute shambler cap
changeoverT: 5.0, // s the changeover breather lasts (after odd games, as the rules demand)
boyMopSpeed: 2.2, // m/s a MOPPING ball boy crosses at (a fetching one still sprints at 7.5)
boyStride: 0.28, // m of travel per animation frame. Frames are driven by DISTANCE, not time, so
// the feet cannot slide: change a boy's speed and his legs keep up on their own.
gameBeatT: 1.6, // s the game-end beat lasts on even (non-changeover) games
eatenR: 0.9, // m a shambler must close to eat you — matches the visual sprite overlap
zSnap: 0.9, // m of lateral slack at zero range — the aim cone's base width (fat-thumb forgiveness)
killsPerPoint: 3, // splatter: zombie kills to score ONE tennis point (1 = every kill scores, old behavior)
zSplatQ: 0.5, // min shot quality to splatter; below = stagger pushback
zPushback: 1.4, // m a staggered zombie is knocked back
splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed
// the slingshot: the pull's DIRECTION picks a lane, its LENGTH is power. Deliberately DECOUPLED —
// reaching a far shambler must not cost a longer drag than the screen has room for. The old
// mapping (pull length = reach) is why you had to drag off the bottom of the phone.
pullMin: 18, // px of pull below which the release cancels — no shot, nothing spent
pullFull: 95, // px of pull for a full-power shot. Keep this well inside one thumb's travel.
pullQMin: 0.12, // shot quality at pullMin, scaling to 1.0 (= PERFECT) at pullFull. Below
// zSplatQ it only staggers, so a lazy flick can't kill: THE anti-spam knob.
aimCone: 0.10, // lane half-width added per meter of range. This is what makes the pull's ANGLE
// mean something: at 0.45 the cone was ±10 m at the far baseline — the whole
// court — so every downward pull picked the same body and the angle felt inert.
// 0.10 gives ±2.9 m out there: you have to actually point at a lane.
aimReach: 22, // m the aim ray runs when no shambler stands in the lane
// How the camera moves while you're drawing the slingshot. NEGATIVE (the default) drops it and
// pushes in — you crouch into the shot. POSITIVE lifts it and leans back for a steeper, more
// top-down look, which reads the depth behind the net better but feels less like aiming.
// Both 0 = the old fixed camera.
aimCamY: 0, // m of camera height change (negative drops you into the shot)
aimCamZ: 0, // m of camera dolly (negative = closer). Both 0 = the camera holds still,
// which is where Oliver landed: the aim graphics carry the read on their own.
backstopZ: -1.8, // z of the far backstop — must sit IN FRONT of the -2 retire line, or balls die before they reach it
backstopKeep: 0.55, // pace a ball keeps after the rebound — still lethal, slower
backstopPop: 1.8, // m/s of lift off the fence, so the rebound travels back instead of dying at the net posts
meleeCool: 1.0, // s between racket swishes — melee is the panic button, not the default
zPierce: 0.75, // pace a ball keeps after a splatter — hot balls plow through lined-up pairs
// The schedule calls the DUEL, not the tin: weapons are only ever won, but a duel comes round
// on its own so there is always a chance at one. Fires off the next hand-cleared wave.
duelFirst: 2, // game after which the first scheduled sudden-death duel is called
duelEvery: 3, // games between scheduled duels after the first
bombMax: 3, // bomb balls in a fresh tin
tinTriples: 3, // triple-serves in a fresh tin
tinFlames: 3, // flame balls in a fresh tin
tinGuns: 1, // machine-gun charges in a fresh tin
wCap: 9, // per-type cap on the courtside rack — tins accumulate, but only so many
rackH: 22, // px height of the bottom weapon rack (feel knob: how much court it covers)
bombRadius: 2.5, // m blast radius of a bomb ball
hordeLen: 12, // s a horde wave lasts
hordeRate: 2.5, // zombies spawned per second during a horde
mgRate: 6, // machine-gun pellets fired per second during a horde
mgPickupLen: 8, // s the machine-gun tin (4th delivery) auto-fires outside hordes
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
runnerSpeed: 3.2, // runner ('f') speed multiplier over the shamble
runnerVMax: 4.2, // hard runner speed cap — the late-game ramp must stay humanly reactable
shamblerHp: 2, // splat-quality hits a classic shambler ('a'/'b') soaks before it dies — only the runner ('f') is still one-shot glass
// fire: the flame ball no longer just kills what it touches, it starts an epidemic.
burnTime: 3.0, // s a body burns before the fire goes out
burnDps: 2.0, // hp lost per second while burning — burnTime*burnDps must exceed bruteHp or tanks walk out of the fire
burnSpread: 2.2, // m — a burning body ignites anything this close, every frame it burns
burnIgniteR: 4.5, // m — the flame ball's ignition radius around the body it kills
burnBallR: 2.4, // m — the flame ball also lights anything it FLIES PAST. Without this the ball's
// full pierce simply killed the whole lane outright and nothing was left to catch fire.
burnPanic: 1.0, // walk-speed multiplier while burning (1 = off; crank it for panicked sprinting)
setCardT: 2.0, // s the centered SET card holds (the 'kurze unterbrechung' before the horde)
bruteStride: 0.5, // m the brute covers per lurch frame — it is the slowest thing on court, so
// its cycle is driven by ground covered rather than by the clock
bruteHp: 5, // splat-quality hits (or pellets) a brute ('g') soaks before it dies — one wound skin per stage
shieldDown: 1.5, // s a shield zombie's ('s') chair stays dropped after a block — the kill window
meleeRange: 1.7, // m — a racket swish cleaves shamblers this close to you
racketHp: 3, // splat-quality hits a racket zombie (mini-boss) takes before it dies
racketSpeed: 2.8, // m/s the racket zombie shambles to chase a ball
racketQMax: 0.55, // its return quality is capped here (slower, sloppier than the tennis CPU)
racketFlub: 0.2, // chance the racket zombie whiffs a return
missHud: 0, // show the last miss reason on screen (full log: T.G.missLog) — off; dev-only
killPips: 1, // splatter: show the kills-to-next-point pips on the scoreboard
};
// the death animation's timeline, in seconds of G.eatenT. A: thrash · B: you burst into gore
// · C: the summary fades in. The restart tap is gated past the whole thing so a panic tap
// during the burst can't skip it.
export const EATEN_BURST = 0.5, EATEN_SUMMARY = 1.6, EATEN_RESTART = 2.2;
// UI accent colours, one place so the whole HUD can be re-tuned at once. Deliberately
// desaturated from the original arcade primaries: they were shouting over the artwork.
// (NOT the gore — blood and piles keep their loud reds; only text and interactive UI.)
export const UI = { hot: '#d9705f', warm: '#e0cc72', pick: '#cbd06a', cream: '#e8e8dc',
dim: '#8fa0b8', bad: '#a34a42', ball: '#dbe063' };
export const COURT = { L: 23.77, W: 8.23, dblW: 10.97, netZ: 11.885, netH: 0.914, netW: 10.06 };
COURT.svcFar = COURT.netZ - 6.4; // far service line
COURT.svcNear = COURT.netZ + 6.4; // near service line
// world sizes (meters)
export const PLAYER_H = 1.88;
export const BOY_H = 1.5; // was 1.2 — the mopping boy read as a distant speck at that scale
// how far off-court a boy starts. The mop boy comes in from just off-screen (he is slow, and
// waiting 8s for him to walk into shot is dead time); a fetching boy sprints in from further out.
export const BOY_ENTER = { mop: 9.5, run: 13 };
export const UMP_H = 3.0;
export const UMP_X = 5.6; // chair right beside the doubles line — x 7.1 projected off-screen in portrait
// contact rules: outside this height band the ball can't be hit (below = it's past you)
export const CONTACT = { minY: 0.24, maxY: 2.3 };
// volleys (pre-bounce contact) get extra forgiveness: the ball passes at full speed and
// there's no bounce cue to time against, so reach and swing window widen by these factors
export const VOLLEY = { reach: 1.4, window: 1.8 };
// how deep from the far/near baseline a player's interception zone extends
export const ZONE_DEPTH = 9.5;
// recovery positions between shots
export const HOME = { P: { x: 0, z: 22.8 }, O: { x: 0, z: 2.5 } };
// where the near player is allowed to move
export const P_BOUNDS = { x: 6.5, zMin: COURT.netZ + 1.2, zMax: 26 };
// serve toss: release height and upward speed (apex derives from these + gravity)
export const TOSS = { y0: 1.8, vy: 4.6 };
// shot target depth from the receiving baseline, by aim (up = deep, none = mid, down = short)
export const SHOT_DEPTH = { deep: 3.0, mid: 5.4, short: 8.6 };
// how long swing/serve poses hold before returning to idle (s)
export const POSE_TIME = 0.38;
// the server's idle ball bounce: sideways offset to the free hand, apex height, tempo.
// tempo drives the ball, the hand sprite AND the pock sound — one value, always in sync.
export const BOUNCE_IDLE = { dx: 0.5, apex: 0.55, tempo: 3.6 };
// generated sprite sheets: the idle pose is this many pixels tall; all scales derive from it
export const SPRITE_H = 112;