You pick your own kit and your own skin, and the blood stays red

Testers asked for some control over how their player looks. From behind — the
only way you ever see him — there are two things worth controlling, and neither
needs a single new asset: the kit and the skin are one hue-bucket pixel pass over
the sheets already loaded, cached per frame and built on first draw.

The buckets are measured, not guessed, and three neighbours had to be fenced off
one at a time: the shirt's warm HIGHLIGHTS (which spattered the man with mud on
every dark pick), the drawn BLOOD on the death frames (which turned the pool
brown — the worst bug this file could ship), and the women's HAIR, which is the
same hue as his shorts and cannot be told apart from it by any rule. Her set is
played in whites and has no kit to dye, so it is flagged out and the row says so
rather than silently doing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-15 15:00:57 +02:00
1 parent 2f0c04fad4
commit 3b639e743b
6 files changed
+321 -11

No files matched your search

+42 -2
View File
@@ -3,8 +3,8 @@
// 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, COURTS, TOURS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, CUP_ROUNDS, matchFoe } from './config.js';
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js';
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, CUP_ROUNDS, matchFoe } from './config.js';
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour, selectKit, selectSkin } from './state.js';
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, startGore, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs } from './beats.js';
import { netConnect, netHooks } from './net.js';
@@ -186,10 +186,49 @@ for (const t of TOURS) {
b.addEventListener('click', () => {
pickedTour = t.id;
for (const el of tourList.children) el.classList.toggle('on', el === b);
kitRowFor(t);
summary();
});
tourList.appendChild(b);
}
// A TOUR PLAYED IN WHITES HAS NO KIT TO DYE (see TOURS.kit), and a row of swatches that silently
// does nothing is worse than no row — you tap all six and conclude the feature is broken. The row
// goes dead and the header says why, in the same slot that names the colour when it is live.
function kitRowFor(t) {
const off = t.kit === false;
document.getElementById('kitRow').classList.toggle('dead', off);
if (off) document.getElementById('kitName').textContent = 'PLAYED IN WHITES';
else document.getElementById('kitName').textContent =
(KITS.find(e => e.id === pickedKit) || KITS[0]).name;
}
// --- KIT and SKIN swatches, built from the tables. Same rule as the tour tiles: tapping selects and
// nothing more, SERVE UP is what commits. Both rows are one builder — they differ only in which
// table they read and which localStorage key they remember, and a second copy of this loop to say
// "skin" instead of "kit" would be the same eleven lines twice.
const swatches = (rowId, nameId, table, keyName, get, set) => {
const row = document.getElementById(rowId), label = document.getElementById(nameId);
for (const e of table) {
const b = document.createElement('button');
b.className = e.id === get() ? 'on' : '';
b.style.background = e.sw;
b.title = e.name; // desktop hover; the h3 label is what a phone reads
b.setAttribute('aria-label', e.name);
b.addEventListener('click', () => {
set(e.id);
try { localStorage.setItem(keyName, e.id); } catch {}
for (const el of row.children) el.classList.toggle('on', el === b);
label.textContent = e.name;
});
row.appendChild(b);
}
label.textContent = (table.find(e => e.id === get()) || table[0]).name;
};
let pickedKit = remembered('kit') || KITS[0].id;
let pickedSkin = remembered('skin') || SKINS[0].id;
swatches('kitRow', 'kitName', KITS, 'kit', () => pickedKit, id => pickedKit = id);
swatches('skinRow', 'skinName', SKINS, 'skin', () => pickedSkin, id => pickedSkin = id);
kitRowFor(TOURS.find(t => t.id === pickedTour) || TOURS[0]); // a remembered women's tour comes up dead too
// --- court rows, built from the table.
// The two meters come out of the court's OWN bounce numbers rather than a pair of hand-written
@@ -323,6 +362,7 @@ function refreshSplash() {
function start(modeId = pickedMode) {
if (!menuUp()) return;
selectTour(pickedTour);
selectKit(pickedKit); selectSkin(pickedSkin);
selectCourt(picked);
menuDuck(true); // the theme belongs to the menus — hand the court over to the ambience
intro.hidden = true; ambient(); voice('quiet_please'); // low atmosphere bed starts with the first gesture