diff --git a/PROJECT.md b/PROJECT.md index 91df6db..d1f49cb 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -2577,6 +2577,19 @@ of the `o_*` originals they were composed from. The one thing the first roll got wrong: it drew a TENNIS BALL into both bouncing frames. The game draws the ball; a drawn one is a blob stuck to his hand. The prompt says so now. +**THE TOUR RIDES THE WIRE TOO** (Oliver, same session: *"ja, das geschlecht sollte auch über die +leitung gehen"*). A tour is which BODY you play in, so it is part of the look and it goes in the same +message (`u`, alongside `k`/`s`/`n`). Before that, both phones drew the friend in whatever tour THIS +phone picked, so a men's/women's pairing showed each player a body their friend was not using. +`oTour()` in render.js is the one accessor — yours everywhere, theirs in netplay — and it feeds the +three places that must agree: the sprite suffix (`SK`), how the art takes a colour (`PS`: her whites +wash, his blue dyes) and the palm the idle ball bounces under (`BOUNCE_IDLE.oDx/oApex`, measured per +body). An older build sends no tour and falls back to ours, which is exactly what it used to do. + +Those palm knobs were re-checked against the new art rather than assumed: the ball marker still +lands under the drawn palm on `n_bounce` and `n_bounce_f` at the values measured off `o_bounce`, +because the poses were copied. No retune. + **And the SETUP SCREEN uses the front too** (Oliver, same session: *"das heisst aber auch, dass wir in der farb- und charakterauswahl den frontfacing nehmen können"*). Picking a face off the back of a head was the best the art allowed; `drawLook` draws `n_idle1` now — same kit, same palette, same diff --git a/index.html b/index.html index 06fbb60..021e9a1 100644 --- a/index.html +++ b/index.html @@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/net.js b/src/net.js index 8e1cc06..4d06ae1 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, KITS, SKINS } from './config.js'; -import { G, P, O, ball, score, setupPoint, selectCourt, court, kit, skin } from './state.js'; +import { COURT, UI, KITS, SKINS, TOURS } from './config.js'; +import { G, P, O, ball, score, setupPoint, selectCourt, court, kit, skin, tour } from './state.js'; import { pointTo, fault, 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'; @@ -129,7 +129,11 @@ function netStart(side, death, courtId) { // tell them how we look AND WHO WE ARE, 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, n: savedName() }); + // `u` is the TOUR — which body they play in, not just what colour it wears. It rides the same + // message because it is the same fact: what the friend across the net LOOKS like. Without it both + // phones drew the opponent in whatever tour THIS phone picked, so a men's/women's pairing showed + // each player a body their friend was not using. + send({ t: 'look', k: kit.id, s: skin.id, n: savedName(), u: tour.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 @@ -202,6 +206,9 @@ export function netMsg(m) { 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], + // a build that predates the tour field sends none, and the fallback is what it used to do: + // draw them in OUR tour. Same rule as the kit — an unknown id resolves to the drawn default. + tour: TOURS.find(t => t.id === m.u) || tour, name: tidyName(m.n || '').slice(0, 8) || 'FRIEND', }; } diff --git a/src/render.js b/src/render.js index fc1dc99..f34a8bd 100644 --- a/src/render.js +++ b/src/render.js @@ -234,7 +234,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 = 141; +export const SPRV = 142; // 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". @@ -1526,10 +1526,19 @@ function drawPiles(near) { // split at the net like zombies so height occludes c // one whose PNG has not loaded yet — falls straight through to the men's frame. That fallback is // deliberate: a cached 404 on one female frame degrades to a man for a frame, it does not blink the // body out of existence (the same rule drawZombie's missing-frame guard is built on). +// THE TOUR THE BODY ACROSS THE NET IS PLAYING. Yours everywhere except netplay, where it is the +// one their phone sent — a tour is which BODY you play in, and the friend picked that on their own +// setup screen. It reaches three things and they must not disagree: the sprite suffix (SK), how the +// art takes a colour (PS: her whites wash, his blue dyes) and the palm the idle ball bounces under +// (BOUNCE_IDLE.oDx/oApex, which are measured per body). +const oTour = () => (G.net && G.net.look && G.net.look.tour) || tour; + const SK = n => { // the bench's living slots pick the suffix directly (G.bench.char 'o'/'of'/'p'/'pf'), so all four // tennis bodies can be reviewed without touching the picked tour - const sfx = G.bench && G.bench.char != null ? (G.bench.char.endsWith('f') ? '_f' : '') : tour.sfx; + const sfx = G.bench && G.bench.char != null ? (G.bench.char.endsWith('f') ? '_f' : '') + : n[0] === 'n' && n[1] === '_' ? (oTour().sfx || '') // the friend, in THEIR tour + : tour.sfx; return (sfx && SPR[n + sfx]) ? n + sfx : n; }; @@ -1636,7 +1645,7 @@ function PS(name) { // women's it sits at the same hue as her skin and her hair, so a dark pick painted her arms, her // legs and her ponytail. Blue-over-white is hue-isolated by construction — see TINT in config. if (n[0] === 'n' && n[1] === '_' && G.net && G.net.look) - return tinted(n, img, G.net.look.kit, G.net.look.skin, tour.kit, TINT.kit); + return tinted(n, img, G.net.look.kit, G.net.look.skin, oTour().kit, TINT.kit); return img; } @@ -2445,8 +2454,8 @@ export function render() { // the racket side. One `* sv.facing` fixes it for both players and both tours; it was wrong in // the men's world too, it just took a second body to notice. const mine = sv.side === 'P'; - const bx = sv.x + sv.facing * (mine ? -bounceDx() : (tour.oDx ?? BOUNCE_IDLE.oDx)); - const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : (tour.oApex ?? BOUNCE_IDLE.oApex)); + const bx = sv.x + sv.facing * (mine ? -bounceDx() : (oTour().oDx ?? BOUNCE_IDLE.oDx)); + const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : (oTour().oApex ?? BOUNCE_IDLE.oApex)); drawBall(bx, by, sv.z); } // hot-ball ghost streak: a fast rally ball drags three fading ghosts behind it. It gated on diff --git a/test.js b/test.js index 79b10bf..5fbacb6 100644 --- a/test.js +++ b/test.js @@ -1840,13 +1840,17 @@ test('netplay: your look goes over the wire on pairing, and theirs comes back', 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(look.u, state.tour.id, "...and WHICH BODY it plays in — the tour is part of the look"); + + netMsg({ t: 'look', k: 'red', s: 'pale', u: 'women' }); 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'); + assert.equal(G.net.look.tour.id, 'women', 'and their tour, whatever THIS phone picked'); // 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 }); + netMsg({ t: 'look', k: 'chartreuse', s: undefined, u: 'mixed doubles' }); 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); + assert.equal(G.net.look.tour.id, state.tour.id, 'and an unknown tour falls back to OUR tour'); state.selectKit(cfg.KITS[0].id); state.selectSkin(cfg.SKINS[0].id); // leave the table as found });