Tighten the returns, give each chapter its own boss, and let the umpire talk
Five reports from Oliver's second playtest. The parry was too generous — at 6 m of arm with a 0.34 s window the shot flew into reach on its own, so any tap once it was vaguely close worked. 3.4 m / 0.2 s / 1.7 m reach now: a tap at 4.5 m is rejected and costs blood, 3.2 m and in returns. The boss telegraphs in 0.5 s against the machine's 0.9 and throws harder, because a boss should be worse than furniture. His three balls were decoration: fanned ±1.9 m across space, so only the middle one could ever reach you. They are a timed salvo now — three shots 0.42 s apart, each aimed at where you are at that moment. hurtGrace still caps a whole volley at one point of damage, which is what it is for. Every chapter was the same boss. Three designs cycling now, each with its own sheet, stride, throw pose, wrecked skin and wrecked-throw skin: the GROUNDSKEEPER, the LINE JUDGE, the CHAIR UMPIRE with his chair strapped on. The chapter card names them. Designs are data in BOSS_ART, so a fourth is a sheet plus a row. Healing announces itself with a first-aid tin and a red cross instead of a 1.4 s flash. And 14 new umpire lines: ten sarcastic asides on a jittered timer, four hooked to specific beats. He still has not acknowledged that the match became a massacre, so he keeps officiating it. Two traps found and written into the pipeline notes. Sprite loads now carry ?v=SPRV, because python http.server sends no cache headers and the browser caches 404s — a name added before its PNG exists stays permanently missing for that browser. And a scripted replace that silently didn't match left drawZombie on the old hardcoded boss names while everything else moved to BOSS_ART; the built-in fallback hid it perfectly, all four designs rendering as design 1 with no error. Assert your rewiring edits matched. Tennis untouched: 30 tapped points, 50 hits / 0 whiffs. 45-game soak clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
295470b73c
commit
8bc57e9358
34 files changed
+180
-40
No files matched your search
+18
-10
@@ -3,7 +3,7 @@
|
||||
// your baseline — one crossing it is a point for THEM.
|
||||
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
|
||||
|
||||
import { TUNE, UI, COURT, VOLLEY, CONTACT } from './config.js';
|
||||
import { TUNE, UI, COURT, VOLLEY, CONTACT, BOSS_ART } from './config.js';
|
||||
import { G, P, ball, sendBoy, setupPoint } from './state.js';
|
||||
import { splat, moan, pock, voice, boom, swish, shriek, aww, sfx } from './audio.js';
|
||||
import { splatPoint, cheer, spentBall, resolveInterlude, showModal, endBossFight } from './gameplay.js';
|
||||
@@ -44,6 +44,7 @@ export function spawnZombie(x, z, variant) {
|
||||
// shoots at you until you deal with it.
|
||||
export function spawnMachine() {
|
||||
spawnZombie((Math.random() * 2 - 1) * 3.6, 2.2 + Math.random() * 3, 'm');
|
||||
voice('ump_machine', 0.4, 0.95);
|
||||
if (!G.seen.mach) {
|
||||
G.seen.mach = true;
|
||||
showModal({ title: 'A BALL MACHINE', color: UI.warm,
|
||||
@@ -59,7 +60,8 @@ export function spawnChapterBoss(chapter) {
|
||||
const hp = TUNE.bossHp + (chapter - 1) * TUNE.bossHpPerChapter;
|
||||
zombies.push({ x: 0, z: -1.2, v: TUNE.bossSpeed, walkT: 0, homeOff: 0, variant: 'B',
|
||||
chapter, hp, hpMax: hp, scale: TUNE.bossScale + (chapter - 1) * TUNE.bossScalePer,
|
||||
fireT: TUNE.bossCycle, windT: 0 });
|
||||
fireT: TUNE.bossCycle, windT: 0, salvo: 0, salvoT: 0,
|
||||
art: BOSS_ART[(chapter - 1) % BOSS_ART.length] }); // which of the boss designs this chapter is
|
||||
}
|
||||
|
||||
// ---- the player's second health channel. Only these two functions touch it.
|
||||
@@ -74,7 +76,7 @@ export function hurtPlayer(n) {
|
||||
vx: (Math.random() * 2 - 1) * 2.2, vy: 0.8 + Math.random() * 2, vz: (Math.random() * 2 - 1) * 2.2,
|
||||
t: 0.35 + Math.random() * 0.35, dark: Math.random() < 0.5 });
|
||||
if (G.hp <= 0) killPlayer();
|
||||
else voice('order', 0.3); // the umpire objects on your behalf. He is still watching.
|
||||
else voice('ump_quiet_bleeding', 0.3, 0.95); // the umpire objects on your behalf. He is still watching.
|
||||
}
|
||||
|
||||
// Death, from EITHER channel: a shambler reaching you, or your hp running out. Extracted so the two
|
||||
@@ -104,10 +106,10 @@ export function killPlayer() {
|
||||
// One hostile ball, lobbed at where the player is standing. `spread` fans a volley sideways.
|
||||
// It is a real ARC (solved like a serve, not fired flat) so it is readable coming in and so the
|
||||
// swing window has something to meet — a flat bolt at head height is not a tennis shot.
|
||||
function fireAtPlayer(x, z, spread = 0) {
|
||||
function fireAtPlayer(x, z, spread = 0, speed = TUNE.machSpeed) {
|
||||
const y0 = 1.1;
|
||||
const tx = P.x + spread, tz = P.z;
|
||||
const d = Math.hypot(tx - x, tz - z), Tf = Math.max(0.35, d / TUNE.machSpeed);
|
||||
const d = Math.hypot(tx - x, tz - z), Tf = Math.max(0.35, d / speed);
|
||||
const g = TUNE.gravity;
|
||||
shots.push({ x, y: y0, z,
|
||||
vx: (tx - x) / Tf, vz: (tz - z) / Tf,
|
||||
@@ -471,15 +473,21 @@ export function updateZombies(dt) {
|
||||
// ---- the two RANGED attackers. Both wind up visibly before firing (TUNE.machWind / drawZombie's
|
||||
// charge glow) because a shot you cannot see coming is not a fairness contract, it is a gotcha.
|
||||
if (!held && !G.interlude && (zb.variant === 'm' || zb.variant === 'B') && G.mode === 'splatter') {
|
||||
if (zb.windT > 0) {
|
||||
const boss = zb.variant === 'B';
|
||||
if (zb.salvo > 0) { // mid-volley: the rest of the balls are still coming
|
||||
if ((zb.salvoT -= dt) <= 0) {
|
||||
zb.salvo--; zb.salvoT = TUNE.bossSalvoGap;
|
||||
fireAtPlayer(zb.x, zb.z, (Math.random() * 2 - 1) * 0.35, TUNE.bossShotSpeed); // aimed at where
|
||||
} // you are NOW, with only a hand's width of scatter
|
||||
} else if (zb.windT > 0) {
|
||||
zb.windT -= dt;
|
||||
if (zb.windT <= 0) {
|
||||
const n = zb.variant === 'B' ? TUNE.bossVolley : 1;
|
||||
for (let k = 0; k < n; k++) fireAtPlayer(zb.x, zb.z, n === 1 ? 0 : (k - (n - 1) / 2) * 1.9);
|
||||
if (boss) { zb.salvo = TUNE.bossVolley; zb.salvoT = 0; } // the volley starts next frame
|
||||
else fireAtPlayer(zb.x, zb.z, 0, TUNE.machSpeed);
|
||||
}
|
||||
} else if ((zb.fireT -= dt) <= 0) {
|
||||
zb.fireT = zb.variant === 'B' ? TUNE.bossCycle : TUNE.machCycle;
|
||||
zb.windT = TUNE.machWind;
|
||||
zb.fireT = boss ? TUNE.bossCycle : TUNE.machCycle;
|
||||
zb.windT = boss ? TUNE.bossWind : TUNE.machWind;
|
||||
pock(0.3, 1500); // the tell: a mechanical click as it loads
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user