The whites take a wash, the friend brings their own colours, and the menu shows the man
Three things off the back of the picker. THE LIVE PLAYER on the setup screen is the real sprite through the real tint, not a mock-up of the pick — so the menu cannot promise a colour the court will not keep. It redraws on every tap of a tour, a kit or a skin. THE WOMEN'S TOUR IS NO LONGER SAT OUT. Her set is drawn head to toe in whites, so there was no hue to rotate — the answer is not a dye but a WASH: the pick laid over her whites at a low saturation with the lightness kept, which is how you dye a white kit in life and gives rose, powder blue, mint, lilac. Her hair and skin fall out of it on lightness alone, which is exactly what a dye could not manage. THE FRIEND ACROSS THE NET now wears what they picked on their own phone. Their look crosses on pairing, and since the opponent art is red rather than blue the kit rides its own hue arc — safe there for the one reason it would not be on the player's own sheets: the opponent sprites carry no drawn blood. An older build simply never sends it and the opponent draws as before; the match still plays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1daa3e9d6a
commit
b5838b2b24
6 files changed
+194
-64
No files matched your search
+9
-3
@@ -230,9 +230,12 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
.swrow button { flex: 1; aspect-ratio: 1; min-width: 0; padding: 0; border: 2px solid var(--line);
|
||||
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
||||
.swrow button.on { border-color: var(--acid); box-shadow: inset 0 0 0 2px var(--ink); }
|
||||
/* a tour played in whites has nothing to dye — the row stays visible so the screen does not jump,
|
||||
but it reads as unavailable and the header says why */
|
||||
.swrow.dead { opacity: 0.32; pointer-events: none; filter: saturate(0.25); }
|
||||
/* The live player, sitting between the tour tiles and the colours it answers to. `position: static`
|
||||
is not decoration: the bare `canvas` rule above is the COURT — fixed, full-bleed, 100dvh — and it
|
||||
catches any canvas in the document, so this one flew to the top-left corner of the screen until
|
||||
it was told otherwise. Every geometry property that rule sets has to be undone here. */
|
||||
#look { position: static; display: block; margin: 12px auto 0;
|
||||
width: auto; height: 150px; touch-action: auto; image-rendering: pixelated; }
|
||||
/* THE BOSS PICKER (back room). Two columns of portraits, because you pick a boss by looking at
|
||||
him — the tile is mostly art. Same plate as the tour tiles above; the grid is what makes ten
|
||||
of them fit on a phone. */
|
||||
@@ -466,6 +469,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
<div class="hdr"><button class="chev" id="sBack">‹</button><h2>MATCH SETUP</h2></div>
|
||||
<h3>01 · PLAYER</h3>
|
||||
<div id="tours"></div>
|
||||
<!-- THE LIVE PLAYER. Not an illustration of the pick — the actual sprite the court will draw,
|
||||
through the actual tint, so the menu can never promise a colour the match does not keep. -->
|
||||
<canvas id="look" width="112" height="150"></canvas>
|
||||
<!-- KIT and SKIN. Both rows are built in main.js from config.KITS/SKINS, so a colour added
|
||||
there needs no markup — the same rule the tour tiles and the court rows follow. -->
|
||||
<h3>02 · KIT<span id="kitName"></span></h3>
|
||||
|
||||
+31
-10
@@ -747,15 +747,21 @@ export const CUP_ROUNDS = [
|
||||
];
|
||||
|
||||
export const TOURS = [
|
||||
// `kit` — whether this tour's art HAS a kit to dye. The men's set is drawn in blue shorts, so it
|
||||
// does. The women's set is drawn in WHITES: her skirt is (240,248,248)/(208,216,224), a cool-cast
|
||||
// white, and the only genuinely saturated blue anywhere on her sheets is HER HAIR — same hue as
|
||||
// his shorts (210-240) and overlapping in lightness, so no hue or lightness rule can tell the two
|
||||
// apart. A kit tint on her set therefore dyes her hair and nothing else, which is why she is
|
||||
// flagged out of it rather than fenced around. THE FIX IS ART, NOT CODE: give her set a coloured
|
||||
// kit through the sprite pipeline and this becomes `kit: true` with no other change.
|
||||
{ id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null, kit: true },
|
||||
{ id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', kit: false, dx: 0.46, apex: 0.78, tossDx: -0.59,
|
||||
// `kit` — HOW this tour's art takes a colour, because the two sets are dressed differently and the
|
||||
// same operation cannot serve both.
|
||||
//
|
||||
// 'dye' the men. Drawn in blue shorts, so the kit pick rotates that hue and keeps the shading.
|
||||
//
|
||||
// 'wash' the women. Drawn head to toe in WHITES — her skirt is (240,248,248)/(208,216,224), a
|
||||
// cool-cast white — so there is no hue to rotate: rotating a near-neutral does nothing,
|
||||
// and the only genuinely saturated blue on her sheets is HER HAIR, at the same hue AND
|
||||
// lightness as his shorts. So her whites are TINTED INSTEAD: the pick's hue laid over
|
||||
// them at a low saturation, lightness kept, which is how you dye a white kit in real
|
||||
// life and gives rose / powder blue / mint / lilac rather than a solid block of colour.
|
||||
// Her hair and skin fall out of it on lightness alone, which is why this is safe where
|
||||
// a dye was not.
|
||||
{ id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null, kit: 'dye' },
|
||||
{ id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', kit: 'wash', dx: 0.46, apex: 0.78, tossDx: -0.59,
|
||||
oDx: 0.48, oApex: 0.73 },
|
||||
];
|
||||
|
||||
@@ -814,7 +820,22 @@ export const SKINS = [
|
||||
// them cleanly and only just — blood is a spike at 0 deg, skin peaks at 16-24 — so the arc starts
|
||||
// at 10. What that leaves untinted on the death frames is the reddest skin, which is the
|
||||
// blood-soaked skin, and it reads as gore rather than as a miss.
|
||||
export const TINT = { kit: [150, 300], skin: [10, 45], satKit: 0.15, satSkin: 0.28, lumSkin: 0.80 };
|
||||
//
|
||||
// THE OPPONENT wears red, not blue — (184,8,8) for him, (184,48,48) for her — so the friend across
|
||||
// the net in a netplay match needs his own kit arc, `oKit`, sitting around 0 deg. It is safe there
|
||||
// for the one reason it would not be on the player's sheets: the opponent art carries no DRAWN
|
||||
// blood (his damage is all live particles), so nothing in that arc is blood.
|
||||
//
|
||||
// THE WHITES, for a 'wash' tour. Bright and barely saturated, which is exactly what her skin is
|
||||
// NOT — hers reaches lum 0.77 at saturation 0.79, so the pair of limits keeps them apart with room
|
||||
// to spare — and what her hair is not either, at lum 0.25. washS/washL are the dye strength: the
|
||||
// pick's saturation scaled down to a pastel, and a slight darkening without which a tint laid on a
|
||||
// 0.96-lightness white is invisible.
|
||||
export const TINT = {
|
||||
kit: [150, 300], oKit: [345, 15], skin: [10, 45],
|
||||
satKit: 0.15, satSkin: 0.28, lumSkin: 0.80,
|
||||
whiteLum: 0.72, whiteSat: 0.60, washS: 0.30, washL: 0.93,
|
||||
};
|
||||
|
||||
// netW was the singles-stick width (10.06) while the court is drawn to the DOUBLES lines, so the
|
||||
// net stopped 0.46 m short of each sideline and visibly failed to meet the court edge. The posts
|
||||
|
||||
+17
-12
@@ -8,7 +8,7 @@ import { P, O, ball, score, G, setupPoint, selectCourt, selectTour, selectKit, s
|
||||
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';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR } from './render.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook } from './render.js';
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
|
||||
import { cv } from './dom.js';
|
||||
@@ -186,20 +186,24 @@ 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);
|
||||
refreshLook();
|
||||
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;
|
||||
// THE LIVE PLAYER, redrawn on every tap of a tour, a kit or a skin. It draws the REAL sprite through
|
||||
// the REAL tint (drawLook), so there is no second recolour to keep in step with the first — the only
|
||||
// way this can show a wrong colour is if the court would show it too.
|
||||
// The sprite sheets load asynchronously and the menu deliberately parks the game loop, so the first
|
||||
// draw can land before the art does: retry on a slow poll that stops itself the moment it succeeds.
|
||||
const lookCv = document.getElementById('look');
|
||||
let lookPoll = 0;
|
||||
function refreshLook() {
|
||||
const t = TOURS.find(x => x.id === pickedTour) || TOURS[0];
|
||||
const ok = drawLook(lookCv, t, KITS.find(e => e.id === pickedKit) || KITS[0],
|
||||
SKINS.find(e => e.id === pickedSkin) || SKINS[0]);
|
||||
clearTimeout(lookPoll);
|
||||
if (!ok) lookPoll = setTimeout(refreshLook, 200); // sprites still coming; ask again shortly
|
||||
}
|
||||
|
||||
// --- KIT and SKIN swatches, built from the tables. Same rule as the tour tiles: tapping selects and
|
||||
@@ -219,6 +223,7 @@ const swatches = (rowId, nameId, table, keyName, get, set) => {
|
||||
try { localStorage.setItem(keyName, e.id); } catch {}
|
||||
for (const el of row.children) el.classList.toggle('on', el === b);
|
||||
label.textContent = e.name;
|
||||
refreshLook();
|
||||
});
|
||||
row.appendChild(b);
|
||||
}
|
||||
@@ -228,7 +233,7 @@ 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
|
||||
refreshLook();
|
||||
|
||||
// --- 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
|
||||
|
||||
+13
-2
@@ -17,8 +17,8 @@
|
||||
//
|
||||
// ponytail: no reconnect-and-resume. A dropped radio ends the match with a card; the recorded
|
||||
// plan calls reconnection "the bulk of the real work" and a prototype must not start there.
|
||||
import { COURT, UI } from './config.js';
|
||||
import { G, P, O, ball, score, setupPoint, selectCourt, court } from './state.js';
|
||||
import { COURT, UI, KITS, SKINS } from './config.js';
|
||||
import { G, P, O, ball, score, setupPoint, selectCourt, court, kit, skin } from './state.js';
|
||||
import { pointTo, fault, showModal, tossBall } from './gameplay.js';
|
||||
import { startMatch, matchReady, resolveInterlude, netMatchWon } from './beats.js'; // runtime-only cycle: beats sends verdicts back through this module
|
||||
import { zombies } from './zombies.js';
|
||||
@@ -116,6 +116,9 @@ function netSync(m) {
|
||||
|
||||
function netStart(side, death, courtId) {
|
||||
G.net.ready = true; G.net.side = side;
|
||||
// tell them how we look, the moment there is somebody to tell. Sent once, on pairing: neither
|
||||
// phone can change its pick mid-match, because the setup screen is only reachable between runs.
|
||||
send({ t: 'look', k: kit.id, s: skin.id });
|
||||
G.modal = null; G.modalQ.length = 0; // the waiting card has done its job
|
||||
// THE HOST'S COURT RIDES THE ROOM, exactly as the death-match pick does — two phones playing the
|
||||
// same rally on two different surfaces disagree about the bounce as well as the picture, and the
|
||||
@@ -176,6 +179,14 @@ export function netMsg(m) {
|
||||
else if (m.t === 'fault') netApplyFault();
|
||||
else if (m.t === 'dead') netMatchWon(); // their blood ran out: this side's win ending
|
||||
else if (m.t === 'pos') { G.net.opp = m; if (m.srv) netSync(m); }
|
||||
// WHAT THEY LOOK LIKE. Their phone's kit and skin, resolved against our own tables so a build
|
||||
// that has never heard of a colour falls back to the drawn one rather than drawing nothing.
|
||||
// Unknown message types are ignored above, so an older build simply never sends this and their
|
||||
// friend sees the opponent as drawn — the match still plays.
|
||||
else if (m.t === 'look') G.net.look = {
|
||||
kit: KITS.find(k => k.id === m.k) || KITS[0],
|
||||
skin: SKINS.find(s => s.id === m.s) || SKINS[0],
|
||||
};
|
||||
}
|
||||
|
||||
// ticked from update() while G.net: the position stream out, the O sprite in
|
||||
|
||||
+60
-28
@@ -1538,18 +1538,28 @@ const inArc = (h, [a, b]) => a <= b ? h >= a && h < b : h >= a || h < b;
|
||||
// interesting claim in this feature is "the kit can move without the skin moving", and that claim
|
||||
// lives entirely here; test.js checks it against the colours actually measured off p_idle1.png,
|
||||
// with no DOM in the way. Returns null for "leave this pixel exactly as it is".
|
||||
export function tintPx(r, g, b, k, sk) {
|
||||
export function tintPx(r, g, b, k, sk, mode = 'dye', arc = TINT.kit) {
|
||||
const [h, s, l] = rgb2hsl(r, g, b);
|
||||
// each bucket carries its own fence — see TINT. The kit only needs a low saturation floor (nothing
|
||||
// else on the sheet is blue, and a low floor catches its soft edges); the skin needs a floor AND a
|
||||
// lightness ceiling, because the shirt's warm highlights sit above every real skin tone.
|
||||
const spec = inArc(h, TINT.kit) && s >= TINT.satKit ? k
|
||||
: inArc(h, TINT.skin) && s >= TINT.satSkin && l <= TINT.lumSkin ? sk : null;
|
||||
if (!spec || spec.h == null) return null; // "as drawn" entries cost one branch and exit
|
||||
return hsl2rgb(spec.h, Math.min(1, s * (spec.s ?? 1)), Math.min(1, l * (spec.l ?? 1)));
|
||||
// THE KIT, two ways. 'dye' rotates a coloured kit's hue and needs only a low saturation floor
|
||||
// (nothing else on those sheets is blue, and a low floor catches the soft edges). 'wash' lays the
|
||||
// pick over a WHITE kit — bright, barely saturated — which is a different test entirely and one
|
||||
// that would swallow the men's shirt, which is why it belongs to the tour and not to the pixel.
|
||||
const isKit = mode === 'wash' ? (l >= TINT.whiteLum && s <= TINT.whiteSat)
|
||||
: (inArc(h, arc) && s >= TINT.satKit);
|
||||
if (isKit) {
|
||||
if (!k || k.h == null) return null;
|
||||
return mode === 'wash'
|
||||
// lightness kept (times any darkening the pick asks for), the hue laid on at a pastel strength
|
||||
? hsl2rgb(k.h, Math.min(1, TINT.washS * (k.s ?? 1)), Math.min(1, l * TINT.washL * (k.l ?? 1)))
|
||||
: hsl2rgb(k.h, Math.min(1, s * (k.s ?? 1)), Math.min(1, l * (k.l ?? 1)));
|
||||
}
|
||||
// the skin needs a floor AND a ceiling: below the floor is grey, above the ceiling is shirt
|
||||
if (!(inArc(h, TINT.skin) && s >= TINT.satSkin && l <= TINT.lumSkin)) return null;
|
||||
if (!sk || sk.h == null) return null; // "as drawn" entries cost one branch and exit
|
||||
return hsl2rgb(sk.h, Math.min(1, s * (sk.s ?? 1)), Math.min(1, l * (sk.l ?? 1)));
|
||||
}
|
||||
|
||||
function tint(img, k, sk) {
|
||||
function tint(img, k, sk, mode, arc) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width; c.height = img.height;
|
||||
const g = c.getContext('2d');
|
||||
@@ -1557,34 +1567,56 @@ function tint(img, k, sk) {
|
||||
const d = g.getImageData(0, 0, c.width, c.height), px = d.data;
|
||||
for (let i = 0; i < px.length; i += 4) {
|
||||
if (px[i + 3] < 8) continue; // fully transparent: nothing to recolour
|
||||
const out = tintPx(px[i], px[i + 1], px[i + 2], k, sk);
|
||||
const out = tintPx(px[i], px[i + 1], px[i + 2], k, sk, mode, arc);
|
||||
if (out) { px[i] = out[0]; px[i + 1] = out[1]; px[i + 2] = out[2]; }
|
||||
}
|
||||
g.putImageData(d, 0, 0);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Recoloured sheets are cached PER FRAME NAME and built on first draw, not on the pick: a swatch tap
|
||||
// costs nothing, and each frame pays its ~1 ms once, the first time it is actually on screen. The
|
||||
// cache is keyed on the pick, so changing it mid-run (the setup screen is reachable between runs,
|
||||
// and the bench can drive it live) drops the old bodies instead of showing stale ones.
|
||||
// Recoloured sheets are built on FIRST DRAW, not on the pick: a swatch tap costs nothing, and each
|
||||
// frame pays its ~1 ms once, the first time it is actually on screen. The cache key carries the
|
||||
// whole look, so a changed pick simply misses and rebuilds — there is no invalidation to forget.
|
||||
// The cap is a runaway guard for the sprite bench, which can cycle looks all afternoon.
|
||||
const TINTC = new Map();
|
||||
let tintKey = '';
|
||||
export function tinted(n, img, k, sk, mode, arc) {
|
||||
if ((!k || k.h == null) && (!sk || sk.h == null)) return img; // "as drawn" both ways: no pass
|
||||
const key = n + '|' + (k ? k.id : '-') + '|' + (sk ? sk.id : '-') + '|' + mode;
|
||||
let out = TINTC.get(key);
|
||||
if (!out) {
|
||||
if (TINTC.size > 96) TINTC.clear();
|
||||
TINTC.set(key, out = tint(img, k, sk, mode, arc));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function PS(name) {
|
||||
const n = SK(name), img = SPR[n];
|
||||
// YOUR body only. drawChar draws both sides through this one lookup, and the man across the net
|
||||
// wears the same blue out of the same sheets — recolouring him too would mean picking the
|
||||
// opponent's kit as a side effect of picking your own.
|
||||
if (n[0] !== 'p' || n[1] !== '_') return img;
|
||||
// a tour whose art has no kit to dye (see TOURS) sits the kit pick out entirely — on her sheets
|
||||
// the only thing in the kit's hue arc is her hair
|
||||
const k = tour.kit === false ? null : kit;
|
||||
const key = (k ? k.id : '-') + '/' + skin.id;
|
||||
if (key !== tintKey) { TINTC.clear(); tintKey = key; }
|
||||
if (!img || ((k == null || k.h == null) && skin.h == null)) return img; // nothing to do: no pass
|
||||
let out = TINTC.get(n);
|
||||
if (!out) TINTC.set(n, out = tint(img, k, skin));
|
||||
return out;
|
||||
if (!img) return img;
|
||||
// YOUR body, dyed or washed depending on what this tour is dressed in (see TOURS.kit)
|
||||
if (n[0] === 'p' && n[1] === '_') return tinted(n, img, kit, skin, tour.kit, TINT.kit);
|
||||
// ...AND THE FRIEND ACROSS THE NET, wearing what THEY picked on their own phone (netplay only —
|
||||
// the roster CPU is not a person and stays exactly as drawn). His sheets are red, not blue, so
|
||||
// the kit rides its own arc; his shirt is always a dye, whatever tour the art came from.
|
||||
if (n[0] === 'o' && n[1] === '_' && G.net && G.net.look)
|
||||
return tinted(n, img, G.net.look.kit, G.net.look.skin, 'dye', TINT.oKit);
|
||||
return img;
|
||||
}
|
||||
|
||||
// THE SETUP SCREEN'S LIVE PLAYER. Not a mock-up of the pick — the pick, run through the same tint
|
||||
// and the same sheet the court will draw, so what the menu shows cannot drift from what walks on.
|
||||
// Returns false while the sprites are still loading, so the caller knows to come back.
|
||||
export function drawLook(cv2, t, k, sk) {
|
||||
const img = SPR['p_idle1' + (t.sfx || '')] || SPR.p_idle1;
|
||||
if (!img) return false;
|
||||
const out = tinted('look' + (t.sfx || ''), img, k, sk, t.kit, TINT.kit);
|
||||
const g = cv2.getContext('2d');
|
||||
g.clearRect(0, 0, cv2.width, cv2.height);
|
||||
g.imageSmoothingEnabled = false; // it is pixel art; let it be pixel art
|
||||
const s = Math.min(cv2.width / out.width, cv2.height / out.height);
|
||||
const w = Math.round(out.width * s), h = Math.round(out.height * s);
|
||||
g.drawImage(out, Math.round((cv2.width - w) / 2), cv2.height - h, w, h);
|
||||
return true;
|
||||
}
|
||||
|
||||
// BOUNCE_IDLE.dx/apex are measured off the DRAWN PALM (see config), so a tour whose player reaches
|
||||
|
||||
@@ -1579,15 +1579,70 @@ test('BLOOD STAYS BLOOD — no skin pick may turn the death frames to mud', () =
|
||||
assert.equal(tintPx(...POOL, k, cfg.SKINS[0]), null, `${k.id} leaves the pool red`);
|
||||
});
|
||||
|
||||
// A tour whose art has no kit sits the kit pick out — otherwise the only thing on her sheets inside
|
||||
// the kit's hue arc is HER HAIR, and picking ACID dyed it green.
|
||||
test("a tour played in whites carries no kit, so nothing of hers is dyed", () => {
|
||||
const women = cfg.TOURS.find(t => t.id === 'women');
|
||||
assert.equal(women.kit, false, 'the women\'s set is flagged as having no kit');
|
||||
assert.equal(cfg.TOURS.find(t => t.id === 'men').kit, true, 'the men\'s set has one');
|
||||
const HER_HAIR = [40, 44, 72]; // dark blue-black, hue ~220 — inside the kit arc
|
||||
assert.ok(tintPx(...HER_HAIR, cfg.KITS[1], cfg.SKINS[0]), 'her hair IS in the kit arc (the trap)');
|
||||
assert.equal(tintPx(...HER_HAIR, null, cfg.SKINS[0]), null, 'and a null kit is what spares it');
|
||||
// A WHITE KIT IS WASHED, NOT DYED. Rotating a near-neutral does nothing, and the only saturated
|
||||
// blue on her sheets is HER HAIR — same hue as his shorts, so a dye turned it green. Colours
|
||||
// measured off p_idle1_f.
|
||||
const HER_SKIRT = [208, 216, 224], HER_BRIGHT = [240, 248, 248]; // cool-cast whites
|
||||
const HER_HAIR = [40, 44, 72]; // dark blue-black, hue ~220
|
||||
test('a white kit takes a wash: her whites colour, her hair and skin do not', () => {
|
||||
assert.equal(cfg.TOURS.find(t => t.id === 'women').kit, 'wash');
|
||||
assert.equal(cfg.TOURS.find(t => t.id === 'men').kit, 'dye');
|
||||
const red = cfg.KITS.find(k => k.id === 'red');
|
||||
|
||||
for (const c of [HER_SKIRT, HER_BRIGHT]) {
|
||||
const out = tintPx(...c, red, cfg.SKINS[0], 'wash');
|
||||
assert.ok(out, 'her whites take the colour');
|
||||
assert.equal(dom(out), 'r', 'CLARET reads as rose on her');
|
||||
// a WASH, not a dye: it must stay pale, or she is wearing a block of poster paint
|
||||
assert.ok(Math.min(...out) > 150, `${c} stays a pastel, not a solid (${out})`);
|
||||
}
|
||||
// the trap this whole mode exists to avoid, pinned from both sides
|
||||
assert.ok(tintPx(...HER_HAIR, red, cfg.SKINS[0], 'dye'), 'her hair IS in the dye arc — the trap');
|
||||
assert.equal(tintPx(...HER_HAIR, red, cfg.SKINS[0], 'wash'), null, 'a wash cannot reach it');
|
||||
assert.equal(tintPx(...SKINPX, red, cfg.SKINS[0], 'wash'), null, 'nor her skin');
|
||||
});
|
||||
|
||||
test('the wash leaves the shading: her folds stay in order', () => {
|
||||
const red = cfg.KITS.find(k => k.id === 'red');
|
||||
const lum = c => c[0] * 0.3 + c[1] * 0.59 + c[2] * 0.11;
|
||||
const lit = tintPx(...HER_BRIGHT, red, cfg.SKINS[0], 'wash');
|
||||
const shade = tintPx(...HER_SKIRT, red, cfg.SKINS[0], 'wash');
|
||||
assert.ok(lum(lit) > lum(shade), 'the lit fold stays lighter than the shaded one');
|
||||
});
|
||||
|
||||
// NETPLAY: the friend across the net wears their own pick. Their sprites are RED, not blue, so the
|
||||
// kit rides its own arc — safe there only because the opponent art carries no drawn blood.
|
||||
test("the opponent's red kit takes a dye on its own arc, and his skin still reads as skin", () => {
|
||||
const HIS_SHIRT = [184, 8, 8], HER_TOP = [184, 48, 48], HIS_SKIN = [248, 176, 128];
|
||||
const green = cfg.KITS.find(k => k.id === 'green');
|
||||
for (const [nm, c] of [['his', HIS_SHIRT], ['hers', HER_TOP]]) {
|
||||
const out = tintPx(...c, green, cfg.SKINS[0], 'dye', cfg.TINT.oKit);
|
||||
assert.ok(out && dom(out) === 'g', `${nm} red shirt takes the pick`);
|
||||
}
|
||||
// his skin must not go with it, and the player's own blue arc must not claim his shirt
|
||||
assert.equal(tintPx(...HIS_SKIN, green, cfg.SKINS[0], 'dye', cfg.TINT.oKit), null,
|
||||
'the opponent kit arc leaves his skin alone');
|
||||
assert.equal(tintPx(...HIS_SHIRT, green, cfg.SKINS[0], 'dye', cfg.TINT.kit), null,
|
||||
"and the PLAYER's blue arc never claims the opponent's red");
|
||||
});
|
||||
|
||||
// The look crosses the wire ONCE, on pairing — neither phone can change its pick mid-match, because
|
||||
// the setup screen is only reachable between runs.
|
||||
test('netplay: your look goes over the wire on pairing, and theirs comes back', () => {
|
||||
state.selectKit('green'); state.selectSkin('deep');
|
||||
freshNet('A');
|
||||
const look = netTest.sent.find(m => m.t === 'look');
|
||||
assert.ok(look, 'pairing announces this phone\'s look');
|
||||
assert.equal(look.k, 'green'); assert.equal(look.s, 'deep');
|
||||
|
||||
netMsg({ t: 'look', k: 'red', s: 'pale' });
|
||||
assert.equal(G.net.look.kit.id, 'red', 'their kit is what the opponent is drawn in');
|
||||
assert.equal(G.net.look.skin.id, 'pale');
|
||||
// an older build, or a colour this one has never heard of, must not blank the opponent out
|
||||
netMsg({ t: 'look', k: 'chartreuse', s: undefined });
|
||||
assert.equal(G.net.look.kit.id, cfg.KITS[0].id, 'an unknown colour falls back to as-drawn');
|
||||
assert.equal(G.net.look.skin.id, cfg.SKINS[0].id);
|
||||
state.selectKit(cfg.KITS[0].id); state.selectSkin(cfg.SKINS[0].id); // leave the table as found
|
||||
});
|
||||
|
||||
test('every kit and skin in the tables is a live, distinct pick', () => {
|
||||
|
||||
Reference in new issue
Block a user