THE TOURNAMENT, the pace-lit trail, and brighter opponents
The fourth mode: four rounds up the dead hierarchy (CUP_ROUNDS), each round drawing a rung from its band, the champion always the final. It rides the match machinery whole — cupDraw feeds the drawn rung through d.opp so every ladder dial scales untouched; the body's pool spans the round (need-scaled pool and divisor, so wounds carry between games); cupGameWon holds the same body mid-round, kills it on the round, and past the final cupWon banks a NOT-dead match run under CHAMPION OF THE DEAD CLUB. Blood carries; round one is a single game; deathmatch stays as the arcade run. Full-run state test. Sidequests: the ghost trail is pace-qualified (TUNE.trailSpeed) so both tennis modes streak — pow>0.8 was only ever reached by the gore slingshot; opponent sprites draw ~18% brighter via a load-time baked copy (render.brighten, TUNE.oppBright) with no PNG touched; the cup scoreboard line names the round instead of lying with deathmatch arithmetic; the rack and the apron demo no longer draw under a trophy screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
098aefeb63
commit
f9b26e5380
8 files changed
+202
-22
No files matched your search
+41
-10
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.js';
|
||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP } from './config.js';
|
||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, CUP_ROUNDS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY } from './gameplay.js';
|
||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||
@@ -309,11 +309,35 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
// second failure, or one genuinely missing file hangs the boot bar forever.
|
||||
let retried = false;
|
||||
LOAD.total++;
|
||||
img.onload = () => { SPR[n] = img; LOAD.done++; };
|
||||
img.onload = () => { SPR[n] = brighten(n, img); LOAD.done++; };
|
||||
img.onerror = () => { if (retried) LOAD.done++; else { retried = true; img.src = url + '&r=1'; } };
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
// OPPONENT BODIES DRAW BRIGHTER, without touching a single PNG (Oliver: "make the opponent
|
||||
// sprites brighter in game — filter or something — without changing the spritesheet files").
|
||||
// Baked ONCE at load into an offscreen copy: an additive self-blend at alpha (b-1) is an exact
|
||||
// brightness multiply with no per-pixel loop and no per-frame ctx.filter (which iOS Safari's
|
||||
// canvas doesn't reliably support). Covers every far-court body — the zombie families and the
|
||||
// tennis CPU — and leaves the player, the boys, the furniture and the HUD art untouched.
|
||||
// TUNE.oppBright is read at LOAD, so retuning it needs a reload, not just the panel.
|
||||
function brighten(n, img) {
|
||||
if (!(n[0] === 'z' && n[1] === '_') && !(n[0] === 'o' && n[1] === '_')) return img;
|
||||
const b = TUNE.oppBright || 1;
|
||||
if (b === 1) return img;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width; c.height = img.height;
|
||||
const g = c.getContext('2d');
|
||||
g.drawImage(img, 0, 0);
|
||||
g.globalCompositeOperation = 'lighter';
|
||||
g.globalAlpha = b - 1;
|
||||
g.drawImage(img, 0, 0);
|
||||
g.globalCompositeOperation = 'destination-in'; // re-clip: additive blending fattened the alpha
|
||||
g.globalAlpha = 1;
|
||||
g.drawImage(img, 0, 0);
|
||||
return c;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ crowd ----
|
||||
function buildCrowd() {
|
||||
crowd.dots.length = 0;
|
||||
@@ -2109,10 +2133,12 @@ export function render() {
|
||||
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : BOUNCE_IDLE.oApex);
|
||||
drawBall(bx, by, sv.z);
|
||||
}
|
||||
// hot-ball ghost streak: a rally ball above 0.8 pow drags three fading ghosts behind it. Serves
|
||||
// already have the erasing arc band; this gives the volley its own read. Render-owned visual
|
||||
// state (like the crowd dots) — gameplay never sees it.
|
||||
const hot = ball.active && (ball.pow || 0) > 0.8 && G.phase === 'rally' && !ball.collected;
|
||||
// hot-ball ghost streak: a fast rally ball drags three fading ghosts behind it. It gated on
|
||||
// pow > 0.8 alone, which in practice only the gore slingshot ever reached — tennis tap swings
|
||||
// live below it, so both tennis modes played without the streak (Oliver's sidequest). PACE now
|
||||
// qualifies a ball too: a flat serve or a clean winner streaks in every mode, a lob stays clean.
|
||||
const hot = ball.active && G.phase === 'rally' && !ball.collected
|
||||
&& ((ball.pow || 0) > 0.8 || Math.hypot(ball.vx, ball.vz) > TUNE.trailSpeed);
|
||||
if (hot) { ballHist.push({ x: ball.x, y: ball.y, z: ball.z }); if (ballHist.length > 7) ballHist.shift(); }
|
||||
else if (ballHist.length) ballHist.length = 0;
|
||||
if (hot) {
|
||||
@@ -2337,7 +2363,10 @@ export function render() {
|
||||
// prints those instead. Same slot, same size — the mode changes the words, not the furniture.
|
||||
// ...and in CLASSIC it says how far through the MATCH you are, which is the one thing the
|
||||
// board itself cannot: which set this is, and how many it takes to win.
|
||||
ctx.fillText(G.match ? `OPPONENT ${G.match.opp} · ${G.match.opp - 1} FELLED`
|
||||
// the cup names the ROUND — its opp is a drawn rung, so the deathmatch arithmetic lied here
|
||||
ctx.fillText(G.match && G.match.cup
|
||||
? `${CUP_ROUNDS[G.match.cup.round].name} · ${G.match.cup.round} FELLED`
|
||||
: G.match ? `OPPONENT ${G.match.opp} · ${G.match.opp - 1} FELLED`
|
||||
: G.classic ? `SET ${G.sets[0] + G.sets[1] + 1} · FIRST TO ${TUNE.classicSets}`
|
||||
: `GAME ${G.runGames + 1} · ${G.sets[0]} SET${G.sets[0] === 1 ? '' : 'S'}`, bW - 3, bH + 8);
|
||||
}
|
||||
@@ -2354,7 +2383,7 @@ export function render() {
|
||||
ctx.restore();
|
||||
ctx.textBaseline = 'top'; ctx.textAlign = 'center';
|
||||
if (!G.bench) drawBlood(); // how many more hits you have left, beside the board — the vignette says "worse", not "how many"
|
||||
if (G.mode === 'splatter' && !G.interlude && !G.drill && !G.bench) drawRack(); // courtside weapon rack (hidden during the tennis-rules duel AND the drill, which is played with plain balls)
|
||||
if (G.mode === 'splatter' && !G.interlude && !G.drill && !G.bench && !G.done) drawRack(); // ...and never under a trophy screen // courtside weapon rack (hidden during the tennis-rules duel AND the drill, which is played with plain balls)
|
||||
drawDrillHud(); // the triage readout — no-op unless a drill is running
|
||||
drawBenchHud(); // ...and the sprite bench's, same rule
|
||||
drawApproachRing(); drawApexBand(); // the match's timing, drawn — no-ops unless a match is running
|
||||
@@ -2376,10 +2405,12 @@ export function render() {
|
||||
ctx.fillStyle = won ? 'rgba(6,10,8,0.72)' : 'rgba(10,4,6,0.72)'; ctx.fillRect(0, 0, W, H);
|
||||
// The announcement face, at 26 rather than the card's 30: this is the longest string in the
|
||||
// game and at 30 it ran edge to edge on a 270px-wide portrait canvas.
|
||||
const title = dead ? (m ? 'BLED OUT.' : 'GAME, SET, MASSACRE.') : won ? 'GAME, SET, MATCH.' : 'BEATEN.';
|
||||
const title = dead ? (m ? 'BLED OUT.' : 'GAME, SET, MASSACRE.')
|
||||
: G.done.cup ? 'CHAMPION OF THE DEAD CLUB.' : won ? 'GAME, SET, MATCH.' : 'BEATEN.';
|
||||
const line = dead
|
||||
? (m ? 'FELLED ' + m.felled + ' OPPONENT' + (m.felled === 1 ? '' : 'S') + ' · ' + m.games + ' GAMES'
|
||||
: 'SURVIVED ' + G.runGames + ' GAMES · ' + G.sets[0] + ' SETS')
|
||||
: G.done.cup ? 'THE CHAMPION FELL · ' + ((G.match && G.match.games) || G.runGames) + ' GAMES'
|
||||
: G.sets[0] + '–' + G.sets[1] + ' IN SETS · ' + G.runGames + ' GAMES';
|
||||
ctx.font = B(26); shadowText(title, W / 2, H / 2 - 44, won ? UI.pick : UI.bad);
|
||||
ctx.font = F(9); shadowText(line, W / 2, H / 2 - 14, UI.cream);
|
||||
@@ -2422,7 +2453,7 @@ export function render() {
|
||||
// to aim, which is the one thing that does nothing there.
|
||||
// after a beat of inaction the apron demo takes over, and it says PULL DOWN in its own words —
|
||||
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
|
||||
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||
const demo = G.mode === 'splatter' && !G.interlude && !G.done && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||
// NO GHOST HAND ON THE TENNIS SERVE — not the idle tap demo, not the apex rider. Oliver: "this
|
||||
// ghost finger over the running serve animation is distracting. this must be removed. it is
|
||||
// not in zombie tennis mode" — and the duel serving fine without a teacher is the argument.
|
||||
|
||||
Reference in new issue
Block a user