diff --git a/assets/p_pull_f.png b/assets/p_pull_f.png index 8d34b2a..e410363 100644 Binary files a/assets/p_pull_f.png and b/assets/p_pull_f.png differ diff --git a/assets/p_smash_f.png b/assets/p_smash_f.png index 0d80c7b..5f4478d 100644 Binary files a/assets/p_smash_f.png and b/assets/p_smash_f.png differ diff --git a/assets/p_toss_f.png b/assets/p_toss_f.png index 6f205db..85aa007 100644 Binary files a/assets/p_toss_f.png and b/assets/p_toss_f.png differ diff --git a/config.js b/config.js index 4740748..0683bdb 100644 --- a/config.js +++ b/config.js @@ -412,9 +412,12 @@ export const COURTS = [ // (the racket zombie — a zombified opponent) have a second drawing; the horde, the officials and // the ball boys are shared, because they are staff, not the draw. `sfx` is the sprite-name suffix // render.js appends: null means "the frames as drawn", which is the men's set. +// dx/apex are optional and override BOUNCE_IDLE's pair: she reaches further out and higher than he +// does, so the pre-serve ball has to meet a different palm. Same rule as BOUNCE_IDLE — measured off +// the drawn hand by eye, not calculated. export const TOURS = [ { id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null }, - { id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f' }, + { id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', dx: 0.46, apex: 0.78 }, ]; // netW was the singles-stick width (10.06) while the court is drawn to the DOUBLES lines, so the diff --git a/index.html b/index.html index f6b9b4f..b4dd4e5 100644 --- a/index.html +++ b/index.html @@ -86,9 +86,11 @@ /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the display:grid above would otherwise win over the hidden attribute */ #intro[hidden] { display: none; } - /* headline: taller than the body text, so a screen reads as a screen and not as a list */ - #intro h2 { margin: 0 0 20px; font: 400 15px Silkscreen, monospace; color: #e0cc72; - letter-spacing: 3px; line-height: 1.5; } + /* headline: the ANNOUNCEMENT face, the same register the canvas uses for SET and CHAPTER — a + screen title is a thing that shouts, not a line of HUD. Bangers carries its own weight, so it + wants size and air rather than Silkscreen's letter-spacing. */ + #intro h2 { margin: 0 0 26px; font: 400 30px Bangers, cursive; color: #e0cc72; + letter-spacing: 2px; line-height: 1.2; padding-bottom: 6px; } /* one button look for every menu screen */ .btn { display: block; width: min(340px, 84vw); margin: 0 auto 10px; padding: 12px 10px; background: rgba(255,255,255,0.04); border: 2px solid #3b4354; color: #dfe6f2; @@ -198,6 +200,6 @@ - + diff --git a/main.js b/main.js index a19de92..d888d55 100644 --- a/main.js +++ b/main.js @@ -62,7 +62,7 @@ const tourList = document.getElementById('tours'); for (const t of TOURS) { const b = document.createElement('button'); b.className = t.id === pickedTour ? 'on' : ''; - b.innerHTML = `${t.name}`; + b.innerHTML = `${t.name}`; b.addEventListener('click', () => { pickedTour = t.id; for (const el of tourList.children) el.classList.toggle('on', el === b); @@ -75,7 +75,7 @@ const courtList = document.getElementById('courtList'); for (const c of COURTS) { const b = document.createElement('button'); b.className = 'court' + (c.id === picked ? ' on' : ''); // your last court comes up pre-picked - b.innerHTML = `${c.name}${c.sub}`; + b.innerHTML = `${c.name}${c.sub}`; // Tapping a tile SELECTS it and nothing more. It used to start the match on the spot, which gave // you no way to look at the three of them and no way back out of the screen. b.addEventListener('click', () => { diff --git a/render.js b/render.js index d40f51b..c75afa9 100644 --- a/render.js +++ b/render.js @@ -198,7 +198,7 @@ function drawRunOff() { // ---------------------------------------------------------------- sprites ---- // assets/.png; game falls back to code-drawn figures until sprites load. -const SPRV = 83; // asset cache-buster — bump with main.js?v=N (see the img.src note below) +const SPRV = 84; // asset cache-buster — bump with main.js?v=N (see the img.src note below) const SPR = {}; for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5', 'p_pull', // the serve wind-up: the racket dropping behind him while the pull is drawn @@ -877,6 +877,11 @@ function drawPiles(near) { // split at the net like zombies so height occludes c // body out of existence (the same rule drawZombie's missing-frame guard is built on). const SK = n => (tour.sfx && SPR[n + tour.sfx]) ? n + tour.sfx : n; +// BOUNCE_IDLE.dx/apex are measured off the DRAWN PALM (see config), so a tour whose player reaches +// differently needs its own pair. Only the women's row carries them; ?? falls back to the men's. +const bounceDx = () => tour.dx ?? BOUNCE_IDLE.dx; +const bounceApex = () => tour.apex ?? BOUNCE_IDLE.apex; + function drawChar(ch) { // No split-step. The receiver used to be slid 0.12 m side to side while waiting on a serve, on top // of a swap to the run frames; both are gone. Sliding a sprite sideways is not footwork — it reads @@ -1366,9 +1371,13 @@ export function render() { // drawn from BEHIND (his left hand is on the viewer's left) and the opponent from the FRONT // (his left hand is on the viewer's right). One sign for both put the opponent's ball out by // his racket instead of under his palm. + // ...and the offset FOLLOWS THE FACING, because drawChar mirrors the whole sprite when a server + // faces left — dodging during the bounce swapped his hands over and left the ball hanging off + // the racket side. One `* sv.facing` fixes it for both players and both tours; it was wrong in + // the men's world too, it just took a second body to notice. const mine = sv.side === 'P'; - const bx = sv.x + (mine ? -BOUNCE_IDLE.dx : BOUNCE_IDLE.oDx); - const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? BOUNCE_IDLE.apex : BOUNCE_IDLE.oApex); + const bx = sv.x + sv.facing * (mine ? -bounceDx() : BOUNCE_IDLE.oDx); + const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : BOUNCE_IDLE.oApex); drawBall(bx, by, sv.z); } // the match ball (and any triple-serve side balls in flight)