The friend across the net wears your kit, drawn from the front

His colour landed on the shirt while yours lands on the shorts, because he was
drawn on the roster opponent's red sheets and dyed on an arc of his own. Red is
the one hue that art cannot hang a kit on: the men's shorts are neutral grey at
saturation 0.05, so no rotation ever reached them, and the women's red top sits
at her skin's and her hair's hue — a dark pick was painting her arms and her
ponytail, live, on the shipped build.

So he gets his own sheets. `assets/_frontroll.py` is `_famroll`'s trick pointed
at the tennis poses — image 1 is the ten `o_*` frames composed from disk, image 2
is the player from behind — and two calls drew all twenty. He rides the PLAYER's
arcs and the tour's own mode now: his shorts dye, her whites wash. `TINT.oKit` is
gone. The roster CPU keeps the red art; it is not a person.

The setup screen shows that front too — picking a face off the back of a head was
only ever what the art allowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-20 16:04:10 +02:00
1 parent 4052ab1404
commit 5f9170b7d7
27 files changed
+324 -31

No files matched your search

+31 -9
View File
@@ -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 <img> 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 = 140;
export const SPRV = 141;
// 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".
@@ -249,6 +249,12 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
'p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
'p_pull', // the serve wind-up: the racket dropping behind him while the pull is drawn
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_swing1','o_swing2',
// THE FRIEND ACROSS THE NET, in YOUR kit seen from the front (assets/_frontroll.py).
// Netplay only: the roster CPU keeps the red o_ sheets, because it is a club
// opponent and not a person. The poses are the o_ family's, redrawn on the player's
// white-over-blue body, so the friend's pick dyes his SHORTS exactly as yours does.
'n_idle1','n_idle2','n_bounce','n_bounce2','n_toss','n_smash','n_swing1','n_swing2',
'n_run1','n_run2',
'o_run1','o_run2', // TWO frames, a true leg-swap pair — the whole 2026-08-14 tuning session's verdict: fewer, better-chosen frames at a calm rate beat every 6/8-frame roll ('this looks better than anything before')
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4',
'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set
@@ -311,6 +317,8 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
'p_die1_f','p_die2_f','p_die3_f','p_die4_f','p_die5_f',
'o_idle1_f','o_idle2_f','o_bounce_f','o_bounce2_f','o_toss_f','o_smash_f',
'o_run1_f','o_run2_f','o_swing1_f','o_swing2_f', // her pair likewise (frames 7+8 of the face-transplant cycle)
'n_idle1_f','n_idle2_f','n_bounce_f','n_bounce2_f','n_toss_f','n_smash_f',
'n_run1_f','n_run2_f','n_swing1_f','n_swing2_f', // ...and the friend in tennis whites
'z_c1_f','z_c2_f','z_c3_f','z_c4_f','z_cw1_f','z_cw2_f','z_cw1b_f','z_cw2b_f',
'z_r1_f','z_r2_f','z_r3_f','z_r4_f',
'z_rw1_f','z_rw1b_f','z_rw1s_f','z_rw2_f','z_rw2b_f','z_rw2s_f',
@@ -1618,19 +1626,30 @@ function PS(name) {
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);
// ...AND THE FRIEND ACROSS THE NET, wearing what THEY picked on their own phone. He is drawn on
// his OWN sheets now (n_*, the player's kit from the front), so he goes through the player's arc
// and the player's tour mode: his shorts dye and her whites wash, exactly as yours do.
//
// He used to be the roster opponent's red art on TINT.oKit, and that arc is why this changed. Red
// is the one hue the kit CANNOT own on these sheets: the only saturated red on the men's art is
// the SHIRT (his shorts are neutral grey, so a hue rotation never touched them) and on the
// 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 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.
// the court will draw, so what the menu shows cannot drift from what walks on.
//
// It shows the FRONT of him (`n_idle1`, the netplay body), not the back he plays in. Picking a face
// off the back of a head was the best the art allowed while every player frame was a back view; the
// moment the front existed, Oliver asked for it here. It is the same kit, the same palette and the
// same tint pass, so the promise above still holds — only the side you are standing on changes.
// 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;
const img = SPR['n_idle1' + (t.sfx || '')] || 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');
@@ -1657,7 +1676,10 @@ function drawChar(ch) {
// clamps at his deepest stance), so P draws within a hair of his old size everywhere he stands.
const hpx = PLAYER_H * p.s * farB(ch.z);
// pick sprite
const pre = ch.side === 'P' ? 'p_' : 'o_';
// THE BODY ACROSS THE NET is a PERSON in netplay and a club opponent everywhere else, and they
// are two different sets of art: n_* is the player's own kit drawn from the front (so the friend's
// colour lands where yours does), o_* is the roster's red. See PS() for why that split exists.
const pre = ch.side === 'P' ? 'p_' : (G.net ? 'n_' : 'o_');
let name = pre + (Math.floor(G.idleT * 1.6) % 2 ? 'idle1' : 'idle2'); // idle weight shift
if (G.phase === 'pretoss' && ch.side === score.server && ch.pose === 'idle') {
// The two bounce frames are the SAME drawing from one sheet — identical stance, identical legs,