diff --git a/index.html b/index.html
index 68ed74a..c59b283 100644
--- a/index.html
+++ b/index.html
@@ -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') {
MATCH SETUP
01 · PLAYER
+
+
02 · KIT
diff --git a/src/config.js b/src/config.js
index 8b6e0e9..5341c20 100644
--- a/src/config.js
+++ b/src/config.js
@@ -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
diff --git a/src/main.js b/src/main.js
index cd62b1a..6f3d50e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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
diff --git a/src/net.js b/src/net.js
index 59a903e..7169bd3 100644
--- a/src/net.js
+++ b/src/net.js
@@ -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
diff --git a/src/render.js b/src/render.js
index 3070cf9..1555f87 100644
--- a/src/render.js
+++ b/src/render.js
@@ -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
diff --git a/test.js b/test.js
index 43a50b9..de46c2a 100644
--- a/test.js
+++ b/test.js
@@ -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', () => {