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
+25
-4
@@ -1,6 +1,6 @@
|
||||
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
||||
|
||||
import { TUNE, UI, APPLAUSE_T, EATEN_BURST, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE } from './config.js';
|
||||
import { TUNE, UI, BOSS_ART, APPLAUSE_T, EATEN_BURST, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
|
||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX } from './input.js';
|
||||
@@ -240,6 +240,19 @@ export function dismissModal() {
|
||||
return true; // the caller must swallow the gesture: dismissing must never also serve
|
||||
}
|
||||
|
||||
// The umpire's asides. He has never once acknowledged that the match turned into a massacre, and
|
||||
// that is the joke — so he keeps officiating it. Fired on a jittered timer, and only when nothing
|
||||
// else is talking (a modal, a changeover, the duel announcement all own the audio).
|
||||
const ASIDES = ['ump_dress_code', 'ump_no_appeal', 'ump_hindrance', 'ump_new_balls_dark',
|
||||
'ump_still_a_match', 'ump_spectators', 'ump_let', 'ump_scoreline', 'ump_unsportsmanlike',
|
||||
'ump_out_of_order'];
|
||||
function umpireAside(dt) {
|
||||
if (G.mode !== 'splatter' || G.modal || G.changeover || G.interlude || G.card) return;
|
||||
if ((G.asideT -= dt) > 0) return;
|
||||
G.asideT = TUNE.asideEvery * (0.6 + Math.random() * 0.8);
|
||||
voice(ASIDES[Math.floor(Math.random() * ASIDES.length)], 0.2, 0.95);
|
||||
}
|
||||
|
||||
export function cheer(seconds, vol) {
|
||||
sfx('tennis-applaus', Math.min(1.5, vol * 2.2)); // recorded crowd; vol scales with the size of the moment (gain > 1 amplifies)
|
||||
// floored at the CLIP LENGTH: the sample always plays out in full, so the stand must stay on its
|
||||
@@ -444,8 +457,9 @@ export function startBossFight(chapter) {
|
||||
ball.active = false; setupPoint();
|
||||
spawnChapterBoss(chapter);
|
||||
zombieDuck(true); // the music drops out for it
|
||||
voice('quiet_please', 0, 0.8); voice('sudden_death', 1.6, 0.85);
|
||||
G.card = { title: 'CHAPTER ' + chapter, sub1: 'SOMETHING BIG IS COMING', sub2: 'IT THROWS. RETURN IT.',
|
||||
voice('ump_boss_arrives', 0, 0.9); voice('sudden_death', 2.6, 0.85);
|
||||
const art = BOSS_ART[(chapter - 1) % BOSS_ART.length];
|
||||
G.card = { title: 'CHAPTER ' + chapter, sub1: art.name, sub2: 'IT THROWS. RETURN IT.',
|
||||
color: UI.bad, t: TUNE.setCardT };
|
||||
G.flash = null;
|
||||
if (!G.seen.boss) { // the rules change: a wall, and a thing that shoots back
|
||||
@@ -480,8 +494,14 @@ export function resolveInterlude(side) {
|
||||
// winning the duel patches you up. THE reason to want a duel rather than merely tolerate it —
|
||||
// it is the only healing in the game, and blood is the only thing you cannot buy back otherwise.
|
||||
if (G.hp < G.hpMax) {
|
||||
const before = G.hp;
|
||||
G.hp = Math.min(G.hpMax, G.hp + TUNE.healPerDuel);
|
||||
G.flash = { text: 'PATCHED UP', t: 1.4 };
|
||||
// a real popup, not a flash: getting patched up is the only way blood comes back, so it deserves
|
||||
// to stop the world for a beat and say so plainly. The trainer came on. It's still tennis.
|
||||
voice('ump_trainer', 0.2, 0.95);
|
||||
showModal({ title: 'WOUNDS TREATED', color: UI.pick, icon: 'x_cross', iconH: 34,
|
||||
body: ['THE TRAINER PATCHED YOU UP',
|
||||
(G.hp - before) + ' OF ' + G.hpMax + ' BACK · NOW ' + G.hp + '/' + G.hpMax] });
|
||||
}
|
||||
deliverTin(); // win the point, win the next tin — on the spot
|
||||
} else {
|
||||
@@ -892,6 +912,7 @@ export function update(dt) {
|
||||
}
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||
G.idleT += dt;
|
||||
umpireAside(dt);
|
||||
// "Time." belongs to the changeover alone. It used to ride the generic 'over' expiry, so any
|
||||
// other beat that overwrote overT while the breather ran (a melee kill taking a game, say)
|
||||
// had the umpire calling time on a pause that was never a changeover.
|
||||
|
||||
Reference in new issue
Block a user