Phase 12: the weapon ladder — every tin opens something nastier
Deliveries cycle BOMB -> TRIPLE -> FIRE -> GUN (G.deliveries indexes the
ladder); one tin at a time, a fresh delivery replaces unspent stock. All
weapons stay one-thumb: the serve auto-consumes, zero new inputs.
- Triple serve (x3): one real serve fans two ballistic clones +-6deg (extras[]
in zombies.js — NOT the reverted rapid-fire: each fan costs a proper serve).
Side balls respect variant rules (wound brutes, clank off chairs, pass the boss)
- Flame ball (x3): kills on ANY contact regardless of pow, chair ignored,
full pierce (splatter pace tax undone), ember trail, white-hot render
- Machine gun (8s): horde auto-fire as a pickup — no serving, drag steers,
no assist, spawner keeps running, ammo bar drains on the HUD, then the
serve loop resumes
- HUD pips per type (black/yellow/orange) at the bomb slot; flash names the
tin ('NEW BALLS: FIRE')
Verified: ladder cycles across 5 deliveries; triple spawns 2 extras and the
fan kills flanked targets; pow-0.1 flame ball clears shambler+shield+brute in
one pass with TRIPLE KILL chaining; GUN tin arms at changeover, blocks taps,
streams pellets (kill in 5 frames), expires back to pretoss; tennis 11/12;
no weapon-flag leaks into tennis; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bc26aec1c
commit
06bd24e6ea
6 files changed
+114
-22
No files matched your search
+37
-7
@@ -4,7 +4,7 @@ import { TUNE, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DE
|
||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
|
||||
import { audio, pock, swish, applause, voice } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim } from './input.js';
|
||||
import { updateZombies, spawnZombie, zombies, explode, meleeSwipe } from './zombies.js';
|
||||
import { updateZombies, spawnZombie, zombies, explode, meleeSwipe, extras, parts } from './zombies.js';
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -79,7 +79,7 @@ function doHit(ch, quality, aim) {
|
||||
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
|
||||
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
ball.bomb = false; // only a bomb-consuming serve is a bomb; any other hit clears it
|
||||
ball.bomb = false; ball.flame = false; // only a consuming serve arms these; any other hit clears them
|
||||
if (ch.side === 'P') { // shot marker: where this will land — red means it's going out
|
||||
const ok = G.mode !== 'tennis' || (Math.abs(tx) <= COURT.W / 2 + 0.02 && tz >= -0.02 && tz <= COURT.netZ);
|
||||
G.outMarker = { x: tx, z: tz, out: !ok, t: 1.3 };
|
||||
@@ -121,8 +121,19 @@ function hitServe(sv, quality, aim = null) {
|
||||
ball.lastHit = sv.side; ball.bounces = 0;
|
||||
ball.pow = quality;
|
||||
ball.kills = 0; // fresh serve: multi-kill streak restarts
|
||||
ball.bomb = G.mode === 'splatter' && G.bombs > 0; // auto-consume a stocked bomb ball
|
||||
// the armed tin auto-loads the serve — one-thumb rule, no weapon-select input
|
||||
ball.bomb = G.mode === 'splatter' && G.bombs > 0;
|
||||
if (ball.bomb) G.bombs--;
|
||||
ball.flame = G.mode === 'splatter' && !ball.bomb && G.flames > 0;
|
||||
if (ball.flame) G.flames--;
|
||||
if (G.mode === 'splatter' && G.triples > 0) { // one serve, three balls: clones fanned ±6°
|
||||
G.triples--;
|
||||
for (const deg of [-6, 6]) {
|
||||
const r = deg * Math.PI / 180, c = Math.cos(r), sn = Math.sin(r);
|
||||
extras.push({ x: ball.x, y: ball.y, z: ball.z, vy: ball.vy, pow: quality, t: 2.5,
|
||||
vx: ball.vx * c - ball.vz * sn, vz: ball.vx * sn + ball.vz * c });
|
||||
}
|
||||
}
|
||||
if (sv.side === 'P') G.outMarker = { x: boxX, z: tzs, out: G.mode === 'tennis' && !inServeBox(boxX, tzs), t: 1.3 };
|
||||
G.phase = 'rally'; pock(1, 1100);
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
@@ -213,11 +224,24 @@ function startChangeover() {
|
||||
if (G.tinDue) { // the boys arm you — unless they took a game since the last delivery
|
||||
G.tinDue = false;
|
||||
if (G.shameGame) G.flash = { text: 'NO NEW BALLS', t: 1.6 }; // the umpire withholds
|
||||
else { G.bombs = TUNE.bombMax; voice('new_balls', 0.6); G.flash = { text: 'NEW BALLS', t: 1.6 }; }
|
||||
else deliverTin();
|
||||
G.shameGame = false; // the slate is clean either way
|
||||
} else G.flash = { text: 'CHANGEOVER', t: 1.4 };
|
||||
}
|
||||
|
||||
// the weapon ladder: each delivery of the run opens a nastier tin. One tin at a time —
|
||||
// a fresh delivery replaces any unspent stock (simpler than stacking, and it keeps you spending).
|
||||
function deliverTin() {
|
||||
const w = G.deliveries++ % 4;
|
||||
G.bombs = G.triples = G.flames = 0; G.mgT = 0;
|
||||
if (w === 0) G.bombs = TUNE.bombMax;
|
||||
else if (w === 1) G.triples = 3;
|
||||
else if (w === 2) G.flames = 3;
|
||||
else G.mgT = TUNE.mgPickupLen; // starts firing when play resumes
|
||||
sendBoy(); voice('new_balls', 0.6);
|
||||
G.flash = { text: 'NEW BALLS: ' + ['BOMB', 'TRIPLE', 'FIRE', 'GUN'][w], t: 1.6 };
|
||||
}
|
||||
|
||||
// retire the splatter ball; a running changeover's long beat must not be clobbered short
|
||||
export function spentBall() {
|
||||
ball.active = false;
|
||||
@@ -434,7 +458,7 @@ export function keyPressed(code) {
|
||||
if (code !== 'Space') return;
|
||||
audio(); // unlock on first gesture
|
||||
if (G.mode === 'splatter') meleeSwipe(P.x, P.z, TUNE.meleeRange); // a swish cleaves adjacent shamblers
|
||||
if (G.horde) return; // horde: the racket auto-fires; a tap only swishes, never serves
|
||||
if (G.horde || G.mgT > 0) return; // auto-fire modes: a tap only swishes, never serves
|
||||
if (G.changeover) return; // the breather is mandatory — the melee swish above still works
|
||||
// splatter: a tap/flick during the brief 'over' beat re-arms the next serve immediately
|
||||
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
|
||||
@@ -495,7 +519,7 @@ export function update(dt) {
|
||||
P.x += mx * TUNE.playerSpeed * slow * dt;
|
||||
P.z += mz * TUNE.playerSpeed * slow * dt;
|
||||
let assist = 0;
|
||||
if (!mx && !mz && TUNE.autoAssist > 0 && ball.active && !G.horde) { // horde: you aim by hand, no assist
|
||||
if (!mx && !mz && TUNE.autoAssist > 0 && ball.active && !G.horde && !(G.mgT > 0)) { // auto-fire: you aim by hand, no assist
|
||||
const chasing = ball.lastHit === 'O';
|
||||
// no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it
|
||||
const rate = TUNE.playerSpeed * TUNE.autoAssist * dt * (chasing ? 1 : 0.55);
|
||||
@@ -575,6 +599,9 @@ export function update(dt) {
|
||||
|
||||
// --- ball physics
|
||||
if (ball.active) {
|
||||
if (ball.flame) parts.push({ // fire trail: one ember per frame, brief and rising
|
||||
x: ball.x, y: ball.y, z: ball.z, t: 0.22, fire: true,
|
||||
vx: (Math.random() * 2 - 1) * 0.6, vy: 0.8, vz: (Math.random() * 2 - 1) * 0.6 });
|
||||
ball.vy -= (ball.g || TUNE.gravity) * dt;
|
||||
const oldZ = ball.z;
|
||||
ball.x += ball.vx * dt; ball.y += ball.vy * dt; ball.z += ball.vz * dt;
|
||||
@@ -637,7 +664,10 @@ export function update(dt) {
|
||||
G.overT -= dt;
|
||||
if (G.overT <= 0) {
|
||||
if (G.changeover) { G.changeover = false; voice('time', 0, 0.95); } // "Time." — play resumes
|
||||
setupPoint();
|
||||
if (G.mgT > 0) { // a GUN tin was delivered: no serving, the racket fires itself, drag steers
|
||||
G.phase = 'rally'; ball.active = false;
|
||||
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
|
||||
} else setupPoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user