Stadium pass: real crowd, sponsor boards, umpire visible in portrait
- Umpire chair moved courtside (UMP_X 5.6, was 7.1 — which projected to x=272 on a 270px-wide portrait canvas: literally one pixel off-screen). Tap-cheat coords follow the constant. - Generated pixel-art night crowd (x_crowd.png) replaces the tier stripes + random dots; drawn in 16 columns that bounce independently while the crowd cheers. Old procedural crowd kept as the loading fallback. - Generated courtside sponsor boards (x_ads.png): ACEADE, TOPSPIN & CO, VOLLEX, NETTO GRIP, SLAMWICH — tiled at native aspect so lettering is never half-cut at any viewport width. Replaces the 'SPLATTER OPEN' text. - One rot tint over crowd + boards driven by the shared paletteT() (new helper), so the stadium dims into splatter and brightens back for duels. Verified: fresh origin; tennis + splatter screenshots (boards tile cleanly, crowd reads, tint dims, umpire on-screen beside the court); umpire triple-tap works at the new chair position; tennis serve benchmark 33/36 across two runs (first 12 was flub noise); no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e948e8b36e
commit
1b97905217
5 files changed
+37
-23
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 570 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 806 KiB |
@@ -58,6 +58,7 @@ COURT.svcNear = COURT.netZ + 6.4; // near service line
|
||||
export const PLAYER_H = 1.88;
|
||||
export const BOY_H = 1.2;
|
||||
export const UMP_H = 3.0;
|
||||
export const UMP_X = 5.6; // chair right beside the doubles line — x 7.1 projected off-screen in portrait
|
||||
|
||||
// contact rules: outside this height band the ball can't be hit (below = it's past you)
|
||||
export const CONTACT = { minY: 0.24, maxY: 2.3 };
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
|
||||
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
|
||||
|
||||
import { TUNE, COURT, UMP_H } from './config.js';
|
||||
import { TUNE, COURT, UMP_H, UMP_X } from './config.js';
|
||||
import { P, O, ball, score, G, setupPoint } from './state.js';
|
||||
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint } from './gameplay.js';
|
||||
import { fitViewport, render, proj, RES } from './render.js';
|
||||
@@ -43,7 +43,7 @@ cv.addEventListener('pointerdown', e => {
|
||||
const x = (e.clientX - r.left) / r.width * (cv.width / RES); // logical coords
|
||||
const y = (e.clientY - r.top) / r.height * (cv.height / RES);
|
||||
if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { G.logShow = !G.logShow; return; } // scoreboard tap = miss-log overlay
|
||||
const u = proj(7.1, 0, COURT.netZ);
|
||||
const u = proj(UMP_X, 0, COURT.netZ);
|
||||
if (Math.abs(x - u.x) > 16 || y > u.y + 6 || y < u.y - UMP_H * u.s - 8) return;
|
||||
const now = performance.now();
|
||||
umpTaps = umpTaps.filter(t => now - t < 1600);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.js';
|
||||
import { TUNE, COURT, PLAYER_H, BOY_H, UMP_H, BOUNCE_IDLE, SPRITE_H } from './config.js';
|
||||
import { TUNE, COURT, PLAYER_H, BOY_H, UMP_H, UMP_X, BOUNCE_IDLE, SPRITE_H } from './config.js';
|
||||
import { P, O, ball, score, crowd, G } from './state.js';
|
||||
import { predictLanding, flickTarget } from './gameplay.js';
|
||||
import { zombies, gore, piles, parts, pellets, extras } from './zombies.js';
|
||||
@@ -19,9 +19,12 @@ const hexLerp = (a, b, t) => {
|
||||
const [ar, ag, ab] = c(a), [br, bg, bb] = c(b);
|
||||
return `rgb(${Math.round(ar + (br - ar) * t)},${Math.round(ag + (bg - ag) * t)},${Math.round(ab + (bb - ab) * t)})`;
|
||||
};
|
||||
// how far into the rot we are, 0 (tennis) .. 1 (splatter) — duels lerp it back home
|
||||
function paletteT() {
|
||||
return G.mode === 'tennis' ? 0 : G.mode === 'splatter' ? 1 - G.duelLerp : Math.min(1, G.transT / 6);
|
||||
}
|
||||
function palette() {
|
||||
// splatter lerps back toward the tennis palette while a duel runs — order, restored
|
||||
const t = G.mode === 'tennis' ? 0 : G.mode === 'splatter' ? 1 - G.duelLerp : Math.min(1, G.transT / 6);
|
||||
const t = paletteT();
|
||||
if (t === 0) return PAL.tennis;
|
||||
if (t === 1) return PAL.splatter;
|
||||
const m = {};
|
||||
@@ -61,7 +64,7 @@ export function proj(x, y, z) {
|
||||
const SPR = {};
|
||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
|
||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
||||
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4',
|
||||
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4','x_crowd','x_ads',
|
||||
'z_a1','z_a2','z_a3','z_a4','z_b1','z_b2','z_b3','z_b4','z_c1','z_c2',
|
||||
'z_f1','z_f2','z_f3','z_f4','z_g1','z_g2','z_s1','z_s2','z_s3']) {
|
||||
const img = new Image();
|
||||
@@ -254,24 +257,34 @@ export function render() {
|
||||
ctx.translate((Math.random() * 2 - 1) * m, (Math.random() * 2 - 1) * m);
|
||||
}
|
||||
|
||||
const pal = palette();
|
||||
// stands: tiered crowd from top of screen down to just above the far court
|
||||
const pal = palette(), pt = paletteT();
|
||||
// stands: the generated crowd, drawn in columns so it can BOUNCE when it cheers
|
||||
const standsBottom = proj(0, 0, 0).y - 12;
|
||||
ctx.fillStyle = pal.stands; ctx.fillRect(0, 0, W, standsBottom);
|
||||
for (let r = 0; r < 16; r++) {
|
||||
ctx.fillStyle = r % 2 ? pal.tierA : pal.tierB;
|
||||
ctx.fillRect(0, 6 + r * 5.5, W, 5.5);
|
||||
const adH = 14, crowdH = standsBottom - adH + 2;
|
||||
const crowdImg = SPR['x_crowd'];
|
||||
if (crowdImg) {
|
||||
const cols = 16, sw = crowdImg.width / cols, dw = W / cols;
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const dy = crowd.cheer > 0 ? -Math.random() * 2 : 0;
|
||||
ctx.drawImage(crowdImg, c * sw, 0, sw, crowdImg.height, c * dw, dy, dw + 0.5, crowdH + 2);
|
||||
}
|
||||
} else { // fallback until the image loads: the old tier stripes + dots
|
||||
ctx.fillStyle = pal.stands; ctx.fillRect(0, 0, W, standsBottom);
|
||||
for (let r = 0; r < 16; r++) { ctx.fillStyle = r % 2 ? pal.tierA : pal.tierB; ctx.fillRect(0, 6 + r * 5.5, W, 5.5); }
|
||||
for (const d of crowd.dots) {
|
||||
const jx = crowd.cheer > 0 ? (Math.random() * 2 - 1) : 0;
|
||||
const jy = crowd.cheer > 0 ? -Math.random() * 1.5 : 0;
|
||||
ctx.fillStyle = d.c; ctx.fillRect(d.x + jx | 0, d.y + jy | 0, 2, 2);
|
||||
}
|
||||
}
|
||||
for (const d of crowd.dots) {
|
||||
const jx = crowd.cheer > 0 ? (Math.random() * 2 - 1) : 0;
|
||||
const jy = crowd.cheer > 0 ? -Math.random() * 1.5 : 0;
|
||||
ctx.fillStyle = d.c; ctx.fillRect(d.x + jx | 0, d.y + jy | 0, 2, 2);
|
||||
}
|
||||
// courtside billboard wall
|
||||
ctx.fillStyle = pal.board; ctx.fillRect(0, standsBottom - 10, W, 10);
|
||||
ctx.fillStyle = '#dfe8d0'; ctx.font = 'bold 7px monospace'; ctx.textBaseline = 'top'; ctx.textAlign = 'center';
|
||||
ctx.fillText(W > 400 ? '· SPLATTER OPEN · NEW BALLS PLEASE · SPLATTER OPEN ·' : '· SPLATTER OPEN ·', W / 2, standsBottom - 9);
|
||||
ctx.textAlign = 'left';
|
||||
// courtside advertising: tiled sponsor panels at native aspect (never half-cut text)
|
||||
const ads = SPR['x_ads'];
|
||||
if (ads) {
|
||||
const tw = ads.width * (adH / ads.height);
|
||||
for (let x = 0; x < W; x += tw) ctx.drawImage(ads, x, standsBottom - adH, tw, adH);
|
||||
} else { ctx.fillStyle = pal.board; ctx.fillRect(0, standsBottom - adH, W, adH); }
|
||||
// the rot dims the stadium: one tint over crowd + boards, fading home during duels
|
||||
if (pt > 0) { ctx.fillStyle = `rgba(12,8,20,${0.5 * pt})`; ctx.fillRect(0, 0, W, standsBottom); }
|
||||
|
||||
// ground + court (Super Tennis lawn); everything outside the doubles court is OUT
|
||||
const dw = COURT.dblW / 2;
|
||||
@@ -314,7 +327,7 @@ export function render() {
|
||||
|
||||
// far-side drawables in z order (far → near), then the net, then the near side
|
||||
const farSide = [
|
||||
{ z: COURT.netZ - 0.01, f: () => drawStatic('x_ump', 7.1, COURT.netZ, UMP_H) },
|
||||
{ z: COURT.netZ - 0.01, f: () => drawStatic('x_ump', UMP_X, COURT.netZ, UMP_H) },
|
||||
];
|
||||
if (!G.hideO) farSide.push({ z: O.z, f: () => drawChar(O) });
|
||||
if (G.boy && G.boy.z <= COURT.netZ) farSide.push({ z: G.boy.z, f: () => drawBoy(G.boy) });
|
||||
|
||||
Reference in new issue
Block a user