Zombies get faster over time; when one reaches you, you're eaten
splatT now ticks while in splatter mode and scales spawn speed by 1 + min(1.5, splatT/70). A zombie within 0.7m of the player ends the run: gore burst, 'game, set, massacre', dark EATEN. overlay with final games and a tap-to-restart (gated 0.7s so death-spam doesn't skip it). Zombies keep shambling on the end screen but no longer score. Also verified the two prior unverified items: quick-serve cadence (0.25s between balls, flick = instant aimed serve) and flick aiming across the net (far/near lane selection works as designed). HANDOFF updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5a6f550a3
commit
943b7da9fd
6 files changed
+58
-47
No files matched your search
+22
-43
@@ -38,51 +38,30 @@ Two runtime-only import cycles (documented in headers): input↔gameplay, zombie
|
||||
Ball boys mop gore they pass. HUD shows THEM instead of CPU.
|
||||
- Balance knobs in TUNE (T panel): zSpeed, zSpawnEvery, zMax, zSplatQ, zPushback.
|
||||
|
||||
## ACTIVE TASK — Oliver's last message (his exact words):
|
||||
> much better!!!
|
||||
> the time between each serve is too long in zombie mode.
|
||||
> right now it auto aims to the closest. i should also be able to aim anywhere includibg over the net.
|
||||
> the speed of zombies should increase over time
|
||||
> when they reach me i should be eaten
|
||||
## ACTIVE TASK — all four items from Oliver's feedback round are DONE and VERIFIED
|
||||
(serve cadence · flick-aim anywhere incl. over the net · zombie speed ramp · eaten):
|
||||
|
||||
Status per item (working tree runs clean, no console errors, but items 1–2 are UNVERIFIED):
|
||||
1. **Serve cadence — verified.** Dead splatter ball → 15 frames (0.25s) to next pretoss;
|
||||
setupPoint leaves P where he stands; FLICK in pretoss fires an instant quick serve
|
||||
(no toss, pow 0.62, aimed), plain TAP still does the toss ritual.
|
||||
2. **Aim anywhere — verified.** Flick up targeted a far-side zombie (marker z=5.5 for a
|
||||
zombie at z=5, over the net), flick down a near-side one (z=18.1). If the lane system
|
||||
still fights Oliver, next step is true free-aim (flick → landing point, splash radius).
|
||||
3. **Speed ramp — done.** G.splatT ticks in updateZombies (splatter only); spawnZombie
|
||||
multiplies speed by `1 + min(1.5, splatT/70)`. Verified 0.76 m/s fresh vs ~1.5 at splatT=140.
|
||||
Spawn interval / zMax do NOT ramp yet — add if Oliver wants more pressure.
|
||||
4. **Eaten — done.** updateZombies: zombie within 0.7m of P (splatter only) → gore + 30
|
||||
particles + splat(1) + voice('game_set_massacre') + mode 'eaten', ball dead, flash cleared.
|
||||
gameplay.update early-exits in 'eaten' (ticks idleT/eatenT, still updates zombies —
|
||||
they shamble on; baseline crossings no longer score). render: P not drawn, dark overlay
|
||||
EATEN. / games score / TAP TO PLAY AGAIN (restart prompt appears after G.eatenT > 0.7 so
|
||||
death-spam taps don't skip the screen). main.js: tap/key while eaten → location.reload().
|
||||
|
||||
1. **Serve cadence — code DONE, not verified.**
|
||||
- gameplay.js dead-ball branch: splatter overT 0.7 → 0.25, new_balls voice freq 0.2 → 0.12.
|
||||
- state.js setupPoint splatter branch no longer repositions P (serve from where you stand).
|
||||
- keyPressed pretoss+splatter: a FLICK now fires an instant quick serve (no toss, quality
|
||||
0.62, aimed by the flick via hitServe(P, 0.62, flick)); a plain TAP still does the toss
|
||||
ritual (perfect serves remain possible). hitServe gained an `aim` param.
|
||||
|
||||
2. **Aim anywhere incl. over the net — code DONE, not verified.**
|
||||
- `zombieTarget(aim)` in gameplay.js rewritten: no aim → most advanced zombie (max z);
|
||||
with aim → lane relative to the PLAYER (`P.x + ax*4`), az<0 prefers far zombies,
|
||||
az>0 prefers close ones on your own side. All z candidates included.
|
||||
- NOTE Oliver's complaint was that it "auto aims to the closest" — if the rewrite still
|
||||
feels wrong, the next step is true free-aim: flick direction → landing point directly
|
||||
(no zombie snap), with a generous splash/hit radius. Ask him which feel he wants if
|
||||
the lane system still fights him.
|
||||
|
||||
3. **Zombie speed ramps over time — NOT DONE.**
|
||||
- `G.splatT` field was added to state.js but is NEVER incremented — wire it up in
|
||||
updateZombies (only while `G.mode==='splatter'`).
|
||||
- Planned approach (my edit failed on a stale string, zombies.js current content is in
|
||||
this repo, read it): in `spawnZombie`, multiply speed by
|
||||
`1 + Math.min(1.5, G.splatT / 70)` (ramp at spawn time keeps the shot-lead math exact),
|
||||
and optionally shrink the spawn interval / grow zMax with splatT.
|
||||
|
||||
4. **Eaten when they reach you — NOT DONE.** Plan agreed with Oliver:
|
||||
- In updateZombies: `G.mode==='splatter' && Math.hypot(zb.x-P.x, zb.z-P.z) < 0.7` →
|
||||
eat: gore decal + big particle burst at P, splat(1), voice('game_set_massacre', 0.8),
|
||||
`G.mode='eaten'`, ball.active=false. Zombies keep shambling for ambience (guard the
|
||||
eat check to mode==='splatter' so it fires once).
|
||||
- gameplay.update: early-exit branch for mode 'eaten' (still tick idleT + updateZombies).
|
||||
- render.js: skip drawChar(P) when eaten; dark overlay with 'EATEN.', final games score,
|
||||
'TAP TO PLAY AGAIN'. Also gate the serve prompt on mode!=='eaten'.
|
||||
- main.js: in the cv pointerdown listener (and a keydown), if mode==='eaten' →
|
||||
location.reload() (laziest honest restart; fine for now).
|
||||
- Existing baseline-cross rule (splatPoint('O') at z >= L+0.7) stays: crossing without
|
||||
touching you is a point for THEM, touching you is death.
|
||||
ALSO: tennis volley fix (Oliver's follow-up "impossible to volley when the marker is behind
|
||||
me"): pre-bounce player contact widens reach x1.4 and swing window x1.8 (VOLLEY in config.js,
|
||||
applied in trySwing + the expiry in update). Root cause was no hard block, just a ~0.28s
|
||||
no-cue timing window at full ball pace; now ~0.5s. doHit also clears G.wasServe so volleying
|
||||
a serve back doesn't fault-check the return. CPU still returns 11/12 serves (benchmark holds).
|
||||
|
||||
## Verification recipe (the regression net — use it after the above)
|
||||
Drive the game deterministically via `window.T` with javascript_tool: the Browser-pane eval
|
||||
|
||||
@@ -329,6 +329,11 @@ function expirePose(ch, dt) {
|
||||
// ---------------------------------------------------------------- update ----
|
||||
export function update(dt) {
|
||||
if (G.hitstop > 0) { G.hitstop -= dt; return; }
|
||||
if (G.mode === 'eaten') { // the match is over in the truest sense
|
||||
G.idleT += dt; G.eatenT += dt;
|
||||
updateZombies(dt);
|
||||
return;
|
||||
}
|
||||
if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; }
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||
G.idleT += dt;
|
||||
|
||||
@@ -31,6 +31,11 @@ function dismissIntro() {
|
||||
intro.addEventListener('pointerdown', e => { e.preventDefault(); dismissIntro(); });
|
||||
addEventListener('keydown', () => dismissIntro(), { once: true });
|
||||
|
||||
// eaten: any tap or key (after a beat, so death-spam doesn't skip the screen) restarts
|
||||
const maybeRestart = () => { if (G.mode === 'eaten' && G.eatenT > 0.7) location.reload(); }; // ponytail: reload = honest full reset
|
||||
cv.addEventListener('pointerdown', maybeRestart);
|
||||
addEventListener('keydown', maybeRestart);
|
||||
|
||||
// hidden shortcut: tap the umpire three times to summon zombie mode early
|
||||
let umpTaps = [];
|
||||
cv.addEventListener('pointerdown', e => {
|
||||
|
||||
@@ -258,7 +258,7 @@ export function render() {
|
||||
ctx.fillStyle = 'rgba(170,22,22,0.9)';
|
||||
ctx.fillRect(pp.x | 0, pp.y | 0, 2, 2);
|
||||
}
|
||||
drawChar(P);
|
||||
if (G.mode !== 'eaten') drawChar(P); // there is no P anymore
|
||||
|
||||
// HUD — scoreboard
|
||||
ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30);
|
||||
@@ -272,7 +272,15 @@ export function render() {
|
||||
ctx.fillStyle = score.server === 'O' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText((G.mode === 'tennis' ? 'CPU ' : 'THEM ') + score.games[1] + ' ' + ptTxt(1), 12, 22);
|
||||
ctx.textAlign = 'center';
|
||||
if (G.phase === 'pretoss' && score.server === 'P') {
|
||||
if (G.mode === 'eaten') {
|
||||
ctx.fillStyle = 'rgba(10,4,6,0.72)'; ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#c02020'; ctx.font = 'bold 22px monospace';
|
||||
ctx.fillText('EATEN.', W / 2, H / 2 - 30);
|
||||
ctx.fillStyle = '#c8d0dc'; ctx.font = 'bold 10px monospace';
|
||||
ctx.fillText('GAMES YOU ' + score.games[0] + ' — THEM ' + score.games[1], W / 2, H / 2);
|
||||
if (G.eatenT > 0.7) { ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace'; ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 20); }
|
||||
}
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten') {
|
||||
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
|
||||
const touch = matchMedia('(pointer: coarse)').matches;
|
||||
ctx.fillText(G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
||||
|
||||
@@ -29,6 +29,7 @@ export const G = {
|
||||
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)
|
||||
splatT: 0, // time survived in splatter mode — drives the difficulty ramp
|
||||
eatenT: 0, // time since you were eaten — gates the restart tap
|
||||
};
|
||||
|
||||
export const other = s => s === 'P' ? 'O' : 'P';
|
||||
|
||||
+15
-2
@@ -15,7 +15,8 @@ export const parts = []; // splatter particles {x, y, z, vx, vy, vz, t}
|
||||
let spawnT = 1.5, moanT = 3;
|
||||
|
||||
export function spawnZombie(x, z, variant) {
|
||||
zombies.push({ x, z, v: TUNE.zSpeed * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, variant });
|
||||
const ramp = 1 + Math.min(1.5, G.splatT / 70); // they get faster the longer you survive
|
||||
zombies.push({ x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, variant });
|
||||
}
|
||||
|
||||
function splatter(i) {
|
||||
@@ -39,6 +40,7 @@ export function updateZombies(dt) {
|
||||
if (moanT <= 0) { moan(); moanT = 2.5 + Math.random() * 4; }
|
||||
|
||||
if (G.mode === 'splatter') { // transition spawns are scripted, not timed
|
||||
G.splatT += dt;
|
||||
spawnT -= dt;
|
||||
if (spawnT <= 0 && zombies.length < TUNE.zMax) {
|
||||
spawnZombie((Math.random() * 2 - 1) * 4.8, -1.5 - Math.random(), Math.random() < 0.5 ? 'a' : 'b');
|
||||
@@ -51,6 +53,17 @@ export function updateZombies(dt) {
|
||||
zb.walkT += dt;
|
||||
zb.z += zb.v * dt;
|
||||
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // shamblers don't walk straight lines
|
||||
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < 0.7) { // it got you
|
||||
gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 });
|
||||
for (let k = 0; k < 30; k++) parts.push({
|
||||
x: P.x, y: 1.1, z: P.z,
|
||||
vx: (Math.random() * 2 - 1) * 4.5, vy: 1 + Math.random() * 4, vz: (Math.random() * 2 - 1) * 4.5,
|
||||
t: 0.5 + Math.random() * 0.5,
|
||||
});
|
||||
splat(1);
|
||||
voice('game_set_massacre', 0.8);
|
||||
G.mode = 'eaten'; ball.active = false; G.flash = null; // zombies keep shambling for ambience
|
||||
}
|
||||
if (ball.active && ball.lastHit === 'P' && ball.y < 1.9) {
|
||||
const d = Math.hypot(ball.x - zb.x, ball.z - zb.z);
|
||||
if (d < 0.55) {
|
||||
@@ -60,7 +73,7 @@ export function updateZombies(dt) {
|
||||
ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; // spent — one stagger per ball
|
||||
}
|
||||
}
|
||||
if (zb.z >= COURT.L + 0.7) { zombies.splice(i, 1); splatPoint('O'); } // it reached your baseline
|
||||
if (zb.z >= COURT.L + 0.7) { zombies.splice(i, 1); if (G.mode !== 'eaten') splatPoint('O'); } // it reached your baseline
|
||||
}
|
||||
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
|
||||
Reference in new issue
Block a user