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:
1 parent
2f0c04fad4
commit
3b639e743b
6 files changed
+321
-11
No files matched your search
@@ -1515,5 +1515,94 @@ test('death match (solo): the game-winning body bleeds out on the ground before
|
||||
// normalisation that has to agree with server.py. It was exported and never called by anything.
|
||||
test("stats.js's own check", () => { assert.equal(statsCheck(), 'ok'); });
|
||||
|
||||
// ---- THE PLAYER'S COLOURS. The feature's one real claim is that the kit can move without dragging
|
||||
// the skin with it, and the whole claim lives in tintPx. These are the colours MEASURED off
|
||||
// p_idle1.png (the four biggest opaque populations), not invented ones — if the art is ever redrawn
|
||||
// into a palette these buckets do not separate, this is the test that says so.
|
||||
const { tintPx } = await import('./src/render.js');
|
||||
const SHORTS = [22, 120, 186], SHORTS_DK = [0, 91, 159]; // kit blue, lit and in shadow
|
||||
const SKINPX = [214, 149, 111], SHIRT = [214, 213, 200]; // forearm, and the shirt white
|
||||
const dom = ([r, g, b]) => r > g && r > b ? 'r' : g > b ? 'g' : 'b';
|
||||
|
||||
test('kit and skin are separate buckets: recolouring one leaves the other alone', () => {
|
||||
const red = cfg.KITS.find(k => k.id === 'red'), asDrawn = cfg.SKINS[0];
|
||||
assert.equal(asDrawn.h, undefined, 'SKINS[0] is the "as drawn" entry');
|
||||
|
||||
// the kit moves, in the light AND in the shadow — a bucket that only catches the lit ramp step
|
||||
// leaves the folds behind in the old colour, which is the failure that would actually be visible
|
||||
assert.equal(dom(tintPx(...SHORTS, red, asDrawn)), 'r', 'lit shorts turn red');
|
||||
assert.equal(dom(tintPx(...SHORTS_DK, red, asDrawn)), 'r', 'shadowed shorts turn red too');
|
||||
|
||||
// ...and the skin does not, which is the question the whole design turns on
|
||||
assert.equal(tintPx(...SKINPX, red, asDrawn), null, 'skin is untouched by a kit change');
|
||||
assert.equal(tintPx(...SHIRT, red, asDrawn), null, 'the shirt white is in neither bucket');
|
||||
});
|
||||
|
||||
test('skin recolours by lightness, and stays a skin tone rather than rotating to green', () => {
|
||||
const asDrawn = cfg.KITS[0], deep = cfg.SKINS.find(s => s.id === 'deep');
|
||||
const out = tintPx(...SKINPX, asDrawn, deep);
|
||||
assert.equal(dom(out), 'r', 'a darker tone is still red-dominant, not hue-rotated off the band');
|
||||
assert.ok(out[0] < SKINPX[0], 'DEEP is genuinely darker than the drawn tan');
|
||||
assert.equal(tintPx(...SHORTS, asDrawn, deep), null, 'the kit is untouched by a skin change');
|
||||
});
|
||||
|
||||
test('the shading survives the swap: the ramp keeps its order and its spread', () => {
|
||||
const red = cfg.KITS.find(k => k.id === 'red'), asDrawn = cfg.SKINS[0];
|
||||
const lit = tintPx(...SHORTS, red, asDrawn), dk = tintPx(...SHORTS_DK, red, asDrawn);
|
||||
const lum = c => c[0] * 0.3 + c[1] * 0.59 + c[2] * 0.11;
|
||||
assert.ok(lum(lit) > lum(dk), 'the lit step stays lighter than the shadowed one');
|
||||
// hue rotation keeps lightness, so the gap between the two ramp steps must survive within rounding
|
||||
assert.ok(Math.abs((lum(lit) - lum(dk)) - (lum(SHORTS) - lum(SHORTS_DK))) < 12,
|
||||
'the ramp keeps its spread — a flattened one would read as a painted-on patch');
|
||||
});
|
||||
|
||||
// THE TWO NEIGHBOURS THE SKIN BUCKET MUST NOT EAT. Both of these shipped broken once — the shirt
|
||||
// came out mud-spattered, and then the death frames' blood came out brown, which on this game is
|
||||
// the worst bug in the file. Colours measured off p_run1/p_smash (the shirt) and p_die4 (the pool).
|
||||
const SHIRT_WARM = [243, 236, 224], SHIRT_W2 = [240, 232, 224]; // warm HIGHLIGHTS, not neutral
|
||||
const BLOOD = [150, 20, 25], POOL = [170, 15, 20], BLOOD_DK = [92, 10, 14];
|
||||
|
||||
test('the shirt stays white — its warm highlights are not skin', () => {
|
||||
for (const sk of cfg.SKINS.slice(1)) {
|
||||
assert.equal(tintPx(...SHIRT_WARM, cfg.KITS[0], sk), null, `${sk.id} leaves the shirt alone`);
|
||||
assert.equal(tintPx(...SHIRT_W2, cfg.KITS[0], sk), null, `${sk.id} leaves the shirt alone`);
|
||||
}
|
||||
});
|
||||
|
||||
test('BLOOD STAYS BLOOD — no skin pick may turn the death frames to mud', () => {
|
||||
for (const sk of cfg.SKINS.slice(1)) {
|
||||
for (const [nm, c] of [['spray', BLOOD], ['pool', POOL], ['dried', BLOOD_DK]])
|
||||
assert.equal(tintPx(...c, cfg.KITS[0], sk), null, `${sk.id} leaves the ${nm} red`);
|
||||
}
|
||||
// ...and it survives a kit change too, which is the other half of the same claim
|
||||
for (const k of cfg.KITS.slice(1))
|
||||
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');
|
||||
});
|
||||
|
||||
test('every kit and skin in the tables is a live, distinct pick', () => {
|
||||
for (const table of [cfg.KITS, cfg.SKINS]) {
|
||||
assert.equal(new Set(table.map(e => e.id)).size, table.length, 'ids are unique');
|
||||
for (const e of table) {
|
||||
assert.ok(/^#[0-9a-f]{6}$/i.test(e.sw), `${e.id} has a swatch colour for the menu`);
|
||||
assert.ok(e.name, `${e.id} is labelled`);
|
||||
if (e.h != null) assert.ok(e.h >= 0 && e.h < 360, `${e.id}'s hue is a degree`);
|
||||
}
|
||||
}
|
||||
// and the "as drawn" head of each table must actually be a no-op, or the default player is tinted
|
||||
assert.equal(tintPx(...SHORTS, cfg.KITS[0], cfg.SKINS[0]), null, 'defaults change nothing');
|
||||
assert.equal(tintPx(...SKINPX, cfg.KITS[0], cfg.SKINS[0]), null, 'defaults change nothing');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report
|
||||
Reference in new issue
Block a user