Feedback round + Phase 13: the duel, free-aim, honest stakes
Oliver's six complaints, addressed:
1. Opponent never came — a racket zombie now spawns on EVERY game you win
(if none alive); endHorde no longer double-spawns
2. No wave-cleared feeling — sweepCourt: at game end the surviving wave
collapses, every game gets a real beat (gameBeatT 1.6 / changeover 4.5),
and 'GAME N' announces the next wave when play re-arms
3. Serve-posture lockup during machine gun — root cause: any 'over' beat
expiring into setupPoint() -> pretoss during auto-fire (no movement outside
rally, no serving during horde/GUN). The over-expiry now returns auto-fire
modes to 'rally', never the serve loop
4. Forced bomb spending — specials commit ONLY on deliberate serves: flick
(or arrow-aimed keyboard serve) fires the tin, plain tap keeps it; serve
prompt teaches it ('FLICK FIRES THE BOMB - TAP KEEPS IT')
5. Auto-aim — free-aim: the flick vector maps to a landing point (length =
depth, up to 22m), zombies within zSnap 1.4 magnet the shot, otherwise the
ball lands exactly where flicked; taps keep the old auto-target assist;
flicks now also work at the toss apex
6. Zombies walking through you — option (a): touch is honest. eatenR 0.9
(brute +0.2) matches the visual sprite overlap
Phase 13 — SUDDEN DEATH interlude: court clear + un-dueled racket zombie +
about to serve -> 'Quiet, please. Play will resume.' / 'Sudden death.' One
point under REAL tennis rules (line calls live, the boss plays every ball,
spawns paused). Win (line call, double bounce, or regicide by any weapon) =
the next tin delivered on the spot; lose (out, or its return passes you) =
a game on their scoreboard + an instant 5-shambler surge. One duel per
opponent. New lines: play_resume.m4a, sudden_death.m4a.
Verified in browser: horde 'over' beat -> rally (lockup dead); game win ->
sweep + boss + beat -> SUDDEN DEATH chains naturally; duel lose path (GAME.
THEM., surge 5, scoreboard), no re-duel, fresh-boss duel, bomb regicide ->
tin on the spot; tap serve keeps stock, real touch-flick consumes + lands
at the flicked point, magnet snaps when near; eaten at 0.85m; tennis 11/12;
no console errors. PLAN.md/HANDOFF updated: plan v2 complete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06bd24e6ea
commit
2f1419b1f2
10 files changed
+183
-285
No files matched your search
+21
-5
@@ -6,7 +6,7 @@
|
||||
import { TUNE, COURT } from './config.js';
|
||||
import { G, P, ball, sendBoy, setupPoint } from './state.js';
|
||||
import { splat, moan, pock, voice, boom, swish, shriek } from './audio.js';
|
||||
import { splatPoint, cheer, spentBall } from './gameplay.js';
|
||||
import { splatPoint, cheer, spentBall, resolveInterlude } from './gameplay.js';
|
||||
|
||||
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
|
||||
export const gore = []; // lawn decals {x, z, r, a} — the flat ground layer
|
||||
@@ -43,6 +43,7 @@ function killZombie(i, quiet) {
|
||||
const zb = zombies[i];
|
||||
const big = zb.variant === 'g'; // a brute goes down like a building — twice the gore
|
||||
zombies.splice(i, 1);
|
||||
if (G.interlude && zb.variant === 'r') resolveInterlude('P'); // regicide ends the duel your way — any weapon counts
|
||||
gore.push({ x: zb.x, z: zb.z, r: (big ? 0.8 : 0.45) + Math.random() * 0.35, a: 0.85 });
|
||||
if (gore.length > 40) gore.shift();
|
||||
const pl = piles.find(p => Math.hypot(p.x - zb.x, p.z - zb.z) < 0.6); // stack up where kills cluster
|
||||
@@ -60,6 +61,21 @@ function killZombie(i, quiet) {
|
||||
if (!G.horde) splatPoint('P', quiet); // a horde is survival — it doesn't score, so it can't chain into itself
|
||||
}
|
||||
|
||||
// game over: the surviving wave collapses where it stands — no points, no piles, just bodies.
|
||||
// This is what makes a game END visibly instead of bleeding into the next one.
|
||||
export function sweepCourt() {
|
||||
let n = 0;
|
||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||
const zb = zombies[i];
|
||||
if (zb.variant === 'r') continue; // the opponent keeps its residence
|
||||
zombies.splice(i, 1); n++;
|
||||
for (let k = 0; k < 6; k++) parts.push({
|
||||
x: zb.x, y: 0.5, z: zb.z, t: 0.4, dark: k % 2 === 0,
|
||||
vx: (Math.random() * 2 - 1) * 1.5, vy: 0.4 + Math.random(), vz: (Math.random() * 2 - 1) * 1.5 });
|
||||
}
|
||||
if (n) splat(0.4);
|
||||
}
|
||||
|
||||
// swishing the racket cleaves any shambler in reach (the racket-zombie boss shrugs melee off)
|
||||
export function meleeSwipe(x, z, range) {
|
||||
let hit = 0;
|
||||
@@ -91,13 +107,13 @@ function firePellet() {
|
||||
pellets.push({ x: P.x, y: 0.9, z: P.z, vx: Math.sin(a) * s, vz: -Math.cos(a) * s, t: 1.2 });
|
||||
pock(0.25, 1400); // rapid fire tick; if it clips on device, gate to every 2nd pellet
|
||||
}
|
||||
function spawnRacketZombie() { // one takes up residence on the far court each horde you survive
|
||||
export function spawnRacketZombie() { // one takes up residence on the far court — an opponent, of sorts
|
||||
zombies.push({ x: (Math.random() * 2 - 1) * 2.5, z: 3.5, v: 0, walkT: Math.random() * 9,
|
||||
variant: 'r', side: 'O', facing: 1, pose: 'idle', poseT: 0, swingT: -1, swingCool: 0, runT: 0, hp: TUNE.racketHp });
|
||||
}
|
||||
function endHorde() {
|
||||
G.horde = null; pellets.length = 0;
|
||||
spawnRacketZombie(); // survive the wave → a racket zombie stays to rally you
|
||||
if (!zombies.some(z => z.variant === 'r')) spawnRacketZombie(); // survive the wave → a racket zombie stays
|
||||
setupPoint(); // serve loop resumes; any zombies still standing are yours to mop up
|
||||
voice('quiet_dark', 0.5, 0.85);
|
||||
}
|
||||
@@ -141,7 +157,7 @@ export function updateZombies(dt) {
|
||||
if ((G.mgCool -= dt) <= 0) { firePellet(); G.mgCool = 1 / TUNE.mgRate; }
|
||||
if (G.mgT <= 0) setupPoint(); // ammo dry — the serve loop resumes
|
||||
}
|
||||
if (!G.horde && !G.changeover) { // changeovers are a real breather — no fresh spawns
|
||||
if (!G.horde && !G.changeover && !G.interlude) { // breathers and duels get a quiet court
|
||||
spawnT -= dt;
|
||||
// the spawn director: the match's age (games finished) sets both crowd size and tempo
|
||||
const cap = Math.min(TUNE.zMaxCap, TUNE.zMax + G.runGames);
|
||||
@@ -162,7 +178,7 @@ export function updateZombies(dt) {
|
||||
zb.x += Math.sign(P.x - zb.x) * Math.min(Math.abs(P.x - zb.x), zb.v * 0.7 * dt);
|
||||
else
|
||||
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // far half: aimless shamble
|
||||
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < (zb.variant === 'g' ? 0.9 : 0.7)) { // it got you
|
||||
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < TUNE.eatenR + (zb.variant === 'g' ? 0.2 : 0)) { // it got you — touch is HONEST now: sprite overlap = eaten
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user