The match gets its own board, and it names what is across the net
A second plate, drawn rather than tinted (assets/_board.py — _regen.py cannot do this job: it keys magenta, crops to ink and rescales figures to a common height, and a scoreboard is a full-bleed rectangle with no figure in it). The club's bottle-green board is the club's; the match is played on one nobody has cleaned in a long time, and the ink changes with it — a green 'dim' over dried blood reads as a printing fault. Two column changes go with it: the name slot says KEEPER / JUDGE / CHAMPION instead of THEM (eight characters, `short` on the ladder rows), and the SETS column is gone in the match, where it was a permanent 0 — the bar under the board already carries that mode's progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c629ae1108
commit
d5942e8aad
5 files changed
+111
-18
No files matched your search
+12
-10
@@ -677,17 +677,19 @@ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
|
||||
// pace scale on its returns' speed wide scale on how far off centre they may land
|
||||
// flub scale on its whiff chance speed scale on how fast it covers the ground
|
||||
// line the personality, said once, on the name plate as it walks on
|
||||
// short 8 characters, because that is the board's name slot — the match board names
|
||||
// the thing you are playing instead of calling it THEM
|
||||
const LADDER_TRAITS = [
|
||||
{ line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85 },
|
||||
{ line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 },
|
||||
{ line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 },
|
||||
{ line: 'HE HAS SEEN YOUR BACKHAND.', pace: 1.15, wide: 1.1 },
|
||||
{ line: 'HE IS HERE TO TREAT YOU.', speed: 1.2, pace: 1.1 },
|
||||
{ line: 'THE CROWD LOVES HIM.', flub: 1.2, wide: 1.3 },
|
||||
{ line: 'HE RESTRUNG THIS RACKET HIMSELF.', pace: 1.2, flub: 0.75 },
|
||||
{ line: 'HE CAN HAVE YOU REMOVED.', speed: 1.15, wide: 1.2 },
|
||||
{ line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 },
|
||||
{ line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 },
|
||||
{ short: 'KEEPER', line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85 },
|
||||
{ short: 'JUDGE', line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 },
|
||||
{ short: 'UMPIRE', line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 },
|
||||
{ short: 'COACH', line: 'HE HAS SEEN YOUR BACKHAND.', pace: 1.15, wide: 1.1 },
|
||||
{ short: 'PHYSIO', line: 'HE IS HERE TO TREAT YOU.', speed: 1.2, pace: 1.1 },
|
||||
{ short: 'MASCOT', line: 'THE CROWD LOVES HIM.', flub: 1.2, wide: 1.3 },
|
||||
{ short: 'STRINGER', line: 'HE RESTRUNG THIS RACKET HIMSELF.', pace: 1.2, flub: 0.75 },
|
||||
{ short: 'DIRECTOR', line: 'HE CAN HAVE YOU REMOVED.', speed: 1.15, wide: 1.2 },
|
||||
{ short: 'SELLER', line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 },
|
||||
{ short: 'CHAMPION', line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 },
|
||||
];
|
||||
// Rung N's identity. The chapter roster's own walk is reused wholesale (bossArt), so the champion
|
||||
// lands on rung 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
|
||||
|
||||
+22
-7
@@ -195,7 +195,7 @@ export function proj(x, y, z) {
|
||||
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
|
||||
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
|
||||
// many: the one that does not get bumped is the one that serves a cached 404.
|
||||
export const SPRV = 93;
|
||||
export const SPRV = 94;
|
||||
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
|
||||
// already requested at import — it just makes the wait visible and holds the menu back until the
|
||||
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
|
||||
@@ -263,7 +263,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
'z_c1_f','z_c2_f','z_c3_f','z_c4_f','z_cw1_f','z_cw2_f','z_cw1b_f','z_cw2b_f',
|
||||
'z_r1_f','z_r2_f','z_r3_f','z_r4_f',
|
||||
'z_rw1_f','z_rw1b_f','z_rw1s_f','z_rw2_f','z_rw2b_f','z_rw2s_f',
|
||||
'scoreboard']) {
|
||||
'scoreboard','scoreboard_z']) { // the club's board, and the one the match is played on
|
||||
const img = new Image();
|
||||
// ?v=SPRV, for the same reason index.html versions main.js. It is also what buys the year-long
|
||||
// immutable cache in server.py: a versioned URL can never go stale, because changing the file
|
||||
@@ -2057,15 +2057,24 @@ export function render() {
|
||||
// sunk into a recessed slot — purple for the server, gold digits. Splatter adds a recessed
|
||||
// status well (kills-to-next-point pips + weapon stock). Hard edges, no radius = retro.
|
||||
const splat = G.mode !== 'tennis';
|
||||
const bd = { face: '#1e5b3a', hi: '#3f9560', lo: '#0e3624',
|
||||
spine: '#472a70', spineHi: '#5f3d94', cream: '#f2ead6', dim: '#89a08f', gold: UI.pick };
|
||||
// ZOMBIE TENNIS HAS ITS OWN BOARD. The club's bottle-green plate is the club's; the match is
|
||||
// played on a board nobody has cleaned in a long time (assets/_board.py), and its ink has to
|
||||
// change with it — a green 'dim' over dried blood reads as a printing fault. Falls back to the
|
||||
// club board and its palette until the plate has loaded.
|
||||
const zBoard = G.match && SPR.scoreboard_z;
|
||||
const bd = zBoard
|
||||
? { face: '#4a1c1c', hi: '#7a4a2a', lo: '#2a0e0e',
|
||||
spine: '#d9d2c4', spineHi: '#f2ead6', cream: '#f2ead6', dim: '#a8887c', gold: UI.warm }
|
||||
: { face: '#1e5b3a', hi: '#3f9560', lo: '#0e3624',
|
||||
spine: '#472a70', spineHi: '#5f3d94', cream: '#f2ead6', dim: '#89a08f', gold: UI.pick };
|
||||
const R = (x, y, w, h, c) => { ctx.fillStyle = c; ctx.fillRect(x, y, w, h); };
|
||||
const ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3
|
||||
? (score.pts[i] === score.pts[1 - i] ? '40' : score.pts[i] > score.pts[1 - i] ? 'AD' : '40')
|
||||
: ['0', '15', '30', '40'][Math.min(score.pts[i], 3)];
|
||||
ctx.save(); ctx.translate(6, 6);
|
||||
const bW = 150, bH = 42; // fixed: keeps the ornate PNG undistorted (art is 300x84 = 150x42)
|
||||
if (SPR.scoreboard) ctx.drawImage(SPR.scoreboard, 0, 0, bW, bH); // generated ornate board backdrop
|
||||
const plate = zBoard || SPR.scoreboard;
|
||||
if (plate) ctx.drawImage(plate, 0, 0, bW, bH); // generated ornate board backdrop
|
||||
else { // code-drawn fallback until the PNG loads
|
||||
R(0, 0, bW, bH, bd.face);
|
||||
R(0, 0, bW, 1, bd.hi); R(0, 0, 1, bH, bd.hi);
|
||||
@@ -2075,7 +2084,11 @@ export function render() {
|
||||
// live text laid straight onto the art's own regions (spine ~x14, two rows, right slot ~x105-147),
|
||||
// Silkscreen pixel font — no code-drawn slots or bevels painted on top of the art.
|
||||
ctx.textBaseline = 'middle';
|
||||
const names = splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||
// ...and the match names the thing across the net rather than calling it THEM. Eight characters,
|
||||
// which is the name slot — see `short` on the ladder rows.
|
||||
const rung = G.match && zombies.find(z => z.variant === 'r' && z.rung);
|
||||
const names = G.match ? ['YOU', (rung && rung.rung.short) || 'THEM']
|
||||
: splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cy = 12 + i * 18, svr = (score.server === 'P') === (i === 0); // rows aligned to the art's two bands
|
||||
if (svr) { ctx.fillStyle = bd.gold; ctx.beginPath(); ctx.arc(11, cy, 1.8, 0, 7); ctx.fill(); } // server pip on the purple spine
|
||||
@@ -2084,7 +2097,9 @@ export function render() {
|
||||
ctx.textAlign = 'center';
|
||||
// ...and CLASSIC TENNIS shows them too: it is the one mode where a set is the thing you are
|
||||
// actually playing for, and hiding the column left a match-scoring mode reading 0-0 all match.
|
||||
if (splat || G.classic) { ctx.font = F(8, 0); ctx.fillStyle = bd.dim; ctx.fillText(String(G.sets[i]), 74, cy); } // sets
|
||||
// the SETS column, and NOT in the match: it banks no sets, so the column was a permanent 0 —
|
||||
// the bar under the board carries that mode's progress instead (OPPONENT n · m FELLED)
|
||||
if ((splat || G.classic) && !G.match) { ctx.font = F(8, 0); ctx.fillStyle = bd.dim; ctx.fillText(String(G.sets[i]), 74, cy); }
|
||||
ctx.font = F(8); ctx.fillStyle = bd.cream; ctx.fillText(String(score.games[i]), 90, cy); // games
|
||||
ctx.font = F(8); ctx.fillStyle = svr ? bd.gold : bd.cream; ctx.fillText(ptTxt(i), 126, cy); // points, in the art's slot
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user