diff --git a/assets/mode_classic.png b/assets/mode_classic.png new file mode 100644 index 0000000..a6f7e2c Binary files /dev/null and b/assets/mode_classic.png differ diff --git a/assets/mode_cup.png b/assets/mode_cup.png new file mode 100644 index 0000000..4ab2300 Binary files /dev/null and b/assets/mode_cup.png differ diff --git a/assets/mode_gore.png b/assets/mode_gore.png new file mode 100644 index 0000000..7d6e4c6 Binary files /dev/null and b/assets/mode_gore.png differ diff --git a/assets/mode_zombie.png b/assets/mode_zombie.png new file mode 100644 index 0000000..4b8f71c Binary files /dev/null and b/assets/mode_zombie.png differ diff --git a/index.html b/index.html index 467e677..f6450aa 100644 --- a/index.html +++ b/index.html @@ -219,16 +219,25 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { letter-spacing: 1px; } /* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */ - /* MODE tiles reuse the court row wholesale — same box, same selected state. Only the picture - differs: a court tile is a photo of a court and crops fine, a mode tile is a SPRITE and must be - shown whole, so it gets `contain` and a dark plate to stand on. */ - #modeList img { object-fit: contain !important; background: rgba(0,0,0,0.35); } - #courtList button.court, #modeList button.court { display: flex; align-items: center; gap: 11px; width: 100%; + /* MODE buttons are the BANNER row: the whole button is a bright scene of its mode (main.js sets + the art + a scrim gradient as the background stack) with the caption written over the bottom. + Unpicked buttons sit dimmed and desaturated so the chosen mode is the one that glows — the + same "one bright thing means selected" rule the menu palette already follows. */ + #modeList button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 78px; + margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center 30%; + image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit; + text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; + filter: saturate(0.45) brightness(0.62); transition: filter 0.15s; } + #modeList button.mode.on { border-color: var(--acid); filter: none; } + #modeList b { text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); } + #modeList i { color: var(--cream); opacity: 0.8; margin-bottom: 0; + text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); } + #courtList button.court { display: flex; align-items: center; gap: 11px; width: 100%; margin: 0 0 8px; padding: 7px; background: var(--panel); border: 2px solid var(--line); color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; } - #courtList button.court.on, #modeList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } - #courtList img, #modeList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none; + #courtList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } + #courtList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none; object-fit: cover; } #courtList .cb, #modeList .cb { flex: 1; min-width: 0; } #courtList b, #modeList b { display: block; font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } @@ -431,6 +440,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index c082b6d..6f16fea 100644 --- a/src/config.js +++ b/src/config.js @@ -629,13 +629,15 @@ export const COURTS = [ // classic — real tennis, best of TUNE.classicSets sets, nothing ever turns // zombie — the exhibition match (beats.startMatch): one game per opponent, blood only // gore — the original: the opening match TURNS and you survive what walks on -// `tile` is an existing sprite rather than new art on purpose — each one is the thing you will be -// looking at for the whole mode, which is a better advert than a drawn icon of it. +// `art` is a full-bleed banner scene behind the whole button (Oliver's call over the sprite tiles, +// 2026-08-14: "bright backgrounds symbolising the individual game mode with the descriptions +// written on top"). Generated through the _famroll OpenRouter route, one scene per mode, no text +// baked in — the button's own name/sub render over a scrim, so the caption stays crisp. export const MODES = [ - { id: 'classic', name: 'CLASSIC TENNIS', sub: 'REAL TENNIS · BEST OF THREE SETS', tile: 'o_swing1' }, - { id: 'zombie', name: 'ZOMBIE TENNIS', sub: 'ONE GAME PER OPPONENT · BLOOD ONLY', tile: 'z_r1' }, - { id: 'cup', name: 'THE TOURNAMENT', sub: 'FOUR ROUNDS · THE CHAMPION WAITS', tile: 'z_champ1' }, - { id: 'gore', name: 'SPLATTER GORE', sub: 'THE MATCH TURNS · SURVIVE IT', tile: 'z_boss1' }, + { id: 'classic', name: 'CLASSIC TENNIS', sub: 'REAL TENNIS · BEST OF THREE SETS', art: 'mode_classic' }, + { id: 'zombie', name: 'ZOMBIE TENNIS', sub: 'ONE GAME PER OPPONENT · BLOOD ONLY', art: 'mode_zombie' }, + { id: 'cup', name: 'THE TOURNAMENT', sub: 'FOUR ROUNDS · THE CHAMPION WAITS', art: 'mode_cup' }, + { id: 'gore', name: 'SPLATTER GORE', sub: 'THE MATCH TURNS · SURVIVE IT', art: 'mode_gore' }, ]; // THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's diff --git a/src/main.js b/src/main.js index 344074d..6461c63 100644 --- a/src/main.js +++ b/src/main.js @@ -80,9 +80,11 @@ let pickedMode = remembered('mode') || MODES[0].id; const modeList = document.getElementById('modeList'); for (const m of MODES) { const b = document.createElement('button'); - b.className = 'court' + (m.id === pickedMode ? ' on' : ''); - b.innerHTML = `` - + `${m.name}${m.sub}`; + b.className = 'mode' + (m.id === pickedMode ? ' on' : ''); + // the scrim rides IN the background stack (gradient over art), so the caption needs no extra box + b.style.backgroundImage = + `linear-gradient(180deg, rgba(0,0,0,0) 34%, rgba(0,0,0,0.78)), url(assets/${m.art}.png?v=${SPRV})`; + b.innerHTML = `${m.name}${m.sub}`; b.addEventListener('click', () => { pickedMode = m.id; try { localStorage.setItem('mode', m.id); } catch {} // come back to what you were playing diff --git a/src/render.js b/src/render.js index 827c860..cea9867 100644 --- a/src/render.js +++ b/src/render.js @@ -221,7 +221,7 @@ function farB(z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang 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 = 114; +export const SPRV = 115; // 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".