Make the pink weapons throw themselves, and let the big bodies soak
Oliver's playtest of the barrel weapons. Four notes, all four real defects. "The bigger guys were way too easy to kill at the beginning." A bug, and an old one. The ball-vs-body chain handled boss, g/m, s, f and a/b, and everything NOT named in it fell through to "splat-quality kills outright" -- so the STEWARD has died to one serve since the day he shipped with stewardHp 5 and a comment calling it a brute's worth of hp, and the three new bodies inherited that the moment they existed. Pellets and melee always read hp; only the ball skipped it. One generic hpMax > 1 branch now, and measured after: shambler 2, steward 5, brute 5, ball boy 3, bouncer 5, roller man 7 -- each exactly its hp. "I have seen all of them by round 10." The new three move 3/8/15 -> 5/12/20. The original four keep their gates: that curve was already tuned through game 12. "The laser fired only when I tapped, that was confusing." The whole tap-per-round kind is gone. A barrel weapon is now either the normal ball gesture made spectacular or something that fires itself -- nothing waits for a tap. - laser is a serve, and the RAY is the weapon: no ball anywhere in it. The preview IS the wedge, it leaves the RACKET rather than lying on the grass, the release erases the plane along its own path behind a bright front, and the edges are hairlines instead of the heaviest lines on the court. - beacon and rocket are LOBBED bursts -- `arc` turns a flat pellet into a thrown one, solved onto the nearest body ahead, and landing is itself a hit so a rocket that misses still goes off where it lands. The beacon changes colour every ball and drags that colour behind it. - flamer got "way more pronounced": 30 rounds/s, a ~10 m cone, embers. "I got the laser almost every time." A flat 1-in-4 over four items does not feel uniform. BARREL_WEAPONS carries weight and a `from` gate now, never repeats twice running, and the beacon is what you meet first. Measured: laser 16-28%, rocket unlocked at game 12 so it is genuinely a late find. And the barrel keeps its pink halo -- that was always procedural, painted by drawBarrel, and never went near the chroma key. Three rings and a pool on the lawn now, because one flat disc read as a smudge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25f915f236
commit
282f5145a9
8 files changed
+363
-98
No files matched your search
+38
-32
@@ -5,7 +5,7 @@ import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy, res
|
||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel } from './zombies.js';
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam } from './zombies.js';
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it arrives at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -264,6 +264,15 @@ function hitServe(sv, quality, aim = null) {
|
||||
// rack's call now. GUN (slot 4) never reaches here; it starts a burst before the serve.
|
||||
const wsel = G.mode === 'splatter' ? G.weapon : 0;
|
||||
const wid = wsel === 0 && !G.temp ? 'ball' : slotId(wsel), armed = slotStock(wsel) > 0;
|
||||
// THE LASER fires instead of the ball, down the lane this serve was aimed at. Everything else
|
||||
// about the gesture is untouched — the pull, the aim, the release — which is the whole point of it.
|
||||
if (wid === 'laser' && armed) {
|
||||
spendSlot(wsel);
|
||||
fireBeam(ball.vx, ball.vz);
|
||||
ball.active = false; G.phase = 'over'; G.overT = 0.5;
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
return;
|
||||
}
|
||||
ball.bomb = wid === 'bomb' && armed;
|
||||
if (ball.bomb) spendSlot(wsel);
|
||||
ball.flame = wid === 'fire' && armed;
|
||||
@@ -878,21 +887,18 @@ function racketAI(rz, dt) {
|
||||
}
|
||||
}
|
||||
|
||||
// A GUN-like weapon isn't a ball — committing to one braces you at the baseline for a window (the
|
||||
// same mode a horde uses) and the serve gesture spends it instead of serving. Generalised from
|
||||
// tryGunBurst: 'burst' weapons auto-fire at their own rate for `len` seconds, 'shot' weapons hold
|
||||
// the same stance but fire one per TAP until the magazine is out. G.mgT stays the mode timer for
|
||||
// BOTH, which is why the eighteen `!(G.mgT > 0)` guards across five files did not have to change.
|
||||
// A BURST weapon isn't a ball — committing to one braces you at the baseline for a window (the same
|
||||
// mode a horde uses) and it fires ITSELF for `len` seconds while your finger only steers. G.mgT is
|
||||
// the mode timer, which is why the eighteen `!(G.mgT > 0)` guards across five files never changed.
|
||||
// There was briefly a third kind that fired one round per tap; Oliver read it as the weapon being
|
||||
// broken, so a barrel weapon is now either this or a serve. Nothing waits for a tap.
|
||||
function tryArmGun() {
|
||||
if (G.mode !== 'splatter') return false;
|
||||
const id = slotId(G.weapon), w = WEAPONS[id];
|
||||
if (!w || (w.kind !== 'burst' && w.kind !== 'shot') || slotStock(G.weapon) <= 0) return false;
|
||||
const slot = G.weapon;
|
||||
if (!w || w.kind !== 'burst' || slotStock(G.weapon) <= 0) return false;
|
||||
G.gunId = id;
|
||||
if (w.kind === 'burst') { G.mgT = w.len ?? TUNE.mgPickupLen; G.gunShots = 0; spendSlot(slot); }
|
||||
// a 'shot' weapon spends per ROUND, not on commit, so the magazine IS the stock: hold the stance
|
||||
// for as long as it could take to empty it, and gunShots ends the window early when it runs out.
|
||||
else { G.gunShots = slotStock(slot); G.mgT = TUNE.shotHoldLen; }
|
||||
G.mgT = w.len ?? TUNE.mgPickupLen;
|
||||
spendSlot(G.weapon);
|
||||
G.mgCool = 0;
|
||||
ball.active = false; G.phase = 'rally';
|
||||
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
|
||||
@@ -900,22 +906,7 @@ function tryArmGun() {
|
||||
G.flash = { text: w.label, t: 1.0 };
|
||||
return true;
|
||||
}
|
||||
// a tap while a 'shot' weapon is braced fires ONE round. Returns true if it consumed the tap.
|
||||
export function tryGunShot() {
|
||||
const w = WEAPONS[G.gunId];
|
||||
if (!(G.mgT > 0) || !w || w.kind !== 'shot' || G.gunShots <= 0) return false;
|
||||
firePellet(w);
|
||||
G.gunShots--;
|
||||
// the magazine lives in the slot it came from, so a half-spent laser you never finish stays half
|
||||
// spent on the rack rather than silently refilling
|
||||
const slot = [0, 1, 2, 3, 4].find(i => slotId(i) === G.gunId);
|
||||
if (slot !== undefined) spendSlot(slot);
|
||||
if (G.gunShots <= 0) G.mgT = 0; // out of rounds: the window closes, updateZombies resumes serving
|
||||
return true;
|
||||
}
|
||||
|
||||
// slingshot serve, stage one: the pull-back starts the toss animation. The ball rises while
|
||||
// you aim (dotted line, render.js); the release (dragServe) fires it. Only from a fresh pretoss.
|
||||
export function serveToss() {
|
||||
// A NEW WIND-UP ENDS THE LAST SHOT'S FOLLOW-THROUGH. Without this he latched: hitServe sets
|
||||
// pose 'serve' with poseT 0, that only expires after POSE_TIME, and the pull frame is only drawn
|
||||
@@ -926,6 +917,22 @@ export function serveToss() {
|
||||
if (score.server === 'P' && G.phase === 'pretoss' && !ball.active) tossBall(P);
|
||||
}
|
||||
|
||||
// The laser fired from a TAP (the keyboard/two-stage path). The drag path solves its own direction
|
||||
// and goes through hitServe; this one has no arc to read a lane off, so it takes the held flick aim
|
||||
// if there is one and otherwise fires straight up-court.
|
||||
function tryLaser() {
|
||||
if (G.mode !== 'splatter' || G.interlude) return false;
|
||||
const w = WEAPONS[slotId(G.weapon)];
|
||||
if (!w || !w.beam || slotStock(G.weapon) <= 0) return false;
|
||||
const flick = takeFlickAim();
|
||||
const dx = flick ? flick.fx : 0, dz = flick ? -Math.abs(flick.fy) : -1;
|
||||
spendSlot(G.weapon);
|
||||
fireBeam(dx, dz || -1);
|
||||
ball.active = false; G.phase = 'over'; G.overT = 0.5;
|
||||
P.pose = 'serve'; P.poseT = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// release of the slingshot aim: an instant serve to the reticle (quality fixed — placement
|
||||
// is this serve's power; the toss ritual keeps the PERFECT-timing power ceiling)
|
||||
export function dragServe(aim) {
|
||||
@@ -975,10 +982,6 @@ export function keyPressed(code) {
|
||||
swish(0.3);
|
||||
return;
|
||||
}
|
||||
// A braced 'shot' weapon (laser, rocket) fires ONE round per tap — that is the whole difference
|
||||
// between it and a burst weapon, and it has to sit above the auto-fire guard below, which would
|
||||
// otherwise swallow the tap as a swish.
|
||||
if (tryGunShot()) return;
|
||||
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
|
||||
if (G.interlude && G.interlude.introT > 0) return; // let the umpire finish announcing the duel
|
||||
@@ -992,7 +995,10 @@ export function keyPressed(code) {
|
||||
if (flick) { dragServe(flick); return; } // dragServe handles GUN (burst) and the ball check
|
||||
}
|
||||
if (G.phase === 'pretoss' && score.server === 'P') {
|
||||
if (tryArmGun()) return; // a GUN-like weapon selected: the gesture arms it, no ball
|
||||
if (tryArmGun()) return; // a burst weapon selected: the gesture arms it, no ball
|
||||
// ...and a LASER never tosses one either. The ray IS the weapon, so a two-stage toss ritual was
|
||||
// throwing a ball into the air and then firing something else entirely.
|
||||
if (tryLaser()) return;
|
||||
tossBall(P); return; // plain tap: the two-stage toss ritual (tennis, and the duel)
|
||||
}
|
||||
if (G.phase === 'toss' && score.server === 'P') {
|
||||
|
||||
Reference in new issue
Block a user