diff --git a/assets/_fine.py b/assets/_fine.py index df9ce17..472dd71 100644 --- a/assets/_fine.py +++ b/assets/_fine.py @@ -17,7 +17,7 @@ KEY_D2 = 60 * 60 MIN_GAP = 12 MIN_BLOB = 60 -def key(im, defringe=2): +def key(im, defringe=2, bed=False): a = np.array(im.convert('RGBA')) bg = a[0, 0, :3].astype(int) d = ((a[..., :3].astype(int) - bg) ** 2).sum(2) @@ -39,7 +39,14 @@ def key(im, defringe=2): # ...and what that leaves — magenta INSIDE a body — is repainted as pale grey rather than kept, # because bright pink strings are as wrong as no strings. Luminance is preserved, so the drawn # crosshatch survives as crosshatch; only the hue goes. + # UNLESS the sheet MEANS its magenta beds (`bed=True` — the famroll convention, adopted by the + # opponent refresh and _runroll): then enclosed magenta is the string bed and CUTS TO + # TRANSPARENCY, exactly as _zsheet already treats it. The grey repaint is what shipped the + # opponents holding solid white paddles. inner = magenta & ~outside + if bed: + a[inner] = 0 + inner = np.zeros_like(inner) if inner.any(): lum = a[..., :3].mean(2) a[inner, 0] = a[inner, 1] = a[inner, 2] = np.clip(lum[inner] * 0.5 + 115, 0, 255).astype('uint8') @@ -152,8 +159,8 @@ def frames(a, cuts, want=0): out = list(zip(edges, edges[1:])) return out -def main(sheet, outs, target_h, cuts, anchor, defringe=2): - a = key(Image.open(sheet), defringe) +def main(sheet, outs, target_h, cuts, anchor, defringe=2, bed=False): + a = key(Image.open(sheet), defringe, bed) b = block_size(a) crops = [] spans = frames(a, cuts, len(outs)) diff --git a/assets/_pose_ref_run_o.png b/assets/_pose_ref_run_o.png index 1fd8b0f..d68f57f 100644 Binary files a/assets/_pose_ref_run_o.png and b/assets/_pose_ref_run_o.png differ diff --git a/assets/_pose_ref_run_of.png b/assets/_pose_ref_run_of.png index 7ca844f..759d027 100644 Binary files a/assets/_pose_ref_run_of.png and b/assets/_pose_ref_run_of.png differ diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index 1a2ee91..a11ab9d 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -408,7 +408,8 @@ "anchor": 0, "check": "loose", "subject": "Three frames of the same tennis opponent, drawn as coarse low-resolution pixel art, side by side. FRAME 1: a plain standing frame for scale, exactly like the reference. FRAME 2 \u2014 THE BACKSWING: he has turned his shoulders and taken the racket BACK BEHIND HIS RIGHT HIP, low and well behind his body, arm coiled, racket head pointing backwards away from the net, weight on his back foot. He has NOT hit the ball yet. FRAME 3 \u2014 THE FOLLOW-THROUGH: the swing is FINISHED. His arm has swept all the way ACROSS HIS CHEST to the OTHER SIDE of his body and the racket head is now UP AND OVER HIS LEFT SHOULDER, near his left ear, his chest turned to face the net, weight on his front foot. FRAMES 2 AND 3 MUST LOOK COMPLETELY DIFFERENT FROM EACH OTHER \u2014 in frame 2 the racket is BEHIND him on his RIGHT, in frame 3 it is HIGH on his LEFT. This contrast is the whole point of the pair. Every figure is the SAME man as the reference and the SAME size: RED polo shirt, DARK CHARCOAL GREY shorts, RED cloth headband, blond hair, white shoes, seen from the FRONT. Same face, same colours, same proportions. His headband is RED, never white or cream. In both swing frames he stays UPRIGHT at his full standing height \u2014 head at the same level as the standing frame, back straight, knees only slightly bent, never crouching or lunging low. The whole racket is drawn and fully inside the canvas. His shoes are WHITE with RED socks and his shirt is a BRIGHT PURE RED, exactly like the reference \u2014 not brick, not brown, not orange.", - "palette_ref": "o_idle1" + "palette_ref": "o_idle1", + "pose": true }, "opponent_idle": { "sprites": [ diff --git a/assets/_runroll.py b/assets/_runroll.py index 2d047a1..7408767 100644 --- a/assets/_runroll.py +++ b/assets/_runroll.py @@ -103,7 +103,9 @@ def prompt(k): 'behind the back to the left, never changes sides between neighbouring figures. The right ' 'arm swings a LITTLE; the racket head stays visibly right of the body\'s midline throughout. ' 'A single figure with the racket on the left side makes the whole animation flicker and is ' - 'WRONG.\n\n' + 'WRONG. THE INSIDE OF EVERY RACKET HEAD IS EXACTLY THE BACKGROUND MAGENTA #FF00FF — never ' + 'white, never grey, never solid: it cuts to transparency, the same rule the family sheets ' + 'follow (a white-filled bed shipped as a solid paddle once — that is what this line prevents).\n\n' f'THE {N} FIGURES ARE ONE CONTINUOUS RUN CYCLE, {c["view"]}. Evenly spaced, every figure on ' 'one shared ground line, every figure at the same scale as its neighbours. THE FIGURES ' 'NEVER TOUCH OR OVERLAP: leave a clear band of empty magenta between every pair of ' @@ -184,7 +186,7 @@ def cut(k, install=False, n=0): outdir = HERE if install else os.path.join(SCRATCH, f'runcut_{k}') os.makedirs(outdir, exist_ok=True) outs = [os.path.join(outdir, f'{pre}{i}{suf}.png') for i in range(1, got + 1)] - _fine.main(sheet, outs, TARGET_H, [], -1) + _fine.main(sheet, outs, TARGET_H, [], -1, bed=True) # ANCHOR EVERY FRAME ON ITS FEET. drawChar centres a frame by its total width, and the racket's # reach varies wildly across a cycle (the cut female frames measured -28..+15 px of feet drift # from frame centre) — so the BODY teleported sideways every frame, which is exactly Oliver's diff --git a/assets/o_bounce.png b/assets/o_bounce.png index 38c2f54..046a98e 100644 Binary files a/assets/o_bounce.png and b/assets/o_bounce.png differ diff --git a/assets/o_bounce2.png b/assets/o_bounce2.png index 875bd7e..13e27b5 100644 Binary files a/assets/o_bounce2.png and b/assets/o_bounce2.png differ diff --git a/assets/o_bounce2_f.png b/assets/o_bounce2_f.png index ad4888c..70971e4 100644 Binary files a/assets/o_bounce2_f.png and b/assets/o_bounce2_f.png differ diff --git a/assets/o_bounce_f.png b/assets/o_bounce_f.png index 5eb1c1c..f1eb9f6 100644 Binary files a/assets/o_bounce_f.png and b/assets/o_bounce_f.png differ diff --git a/assets/o_idle1.png b/assets/o_idle1.png index f6bc819..eb25e84 100644 Binary files a/assets/o_idle1.png and b/assets/o_idle1.png differ diff --git a/assets/o_idle1_f.png b/assets/o_idle1_f.png index 6922d8e..37f2069 100644 Binary files a/assets/o_idle1_f.png and b/assets/o_idle1_f.png differ diff --git a/assets/o_idle2.png b/assets/o_idle2.png index aa750ec..5136aea 100644 Binary files a/assets/o_idle2.png and b/assets/o_idle2.png differ diff --git a/assets/o_idle2_f.png b/assets/o_idle2_f.png index 70cd731..73ab938 100644 Binary files a/assets/o_idle2_f.png and b/assets/o_idle2_f.png differ diff --git a/assets/o_run1.png b/assets/o_run1.png index d2806af..f10f821 100644 Binary files a/assets/o_run1.png and b/assets/o_run1.png differ diff --git a/assets/o_run1_f.png b/assets/o_run1_f.png index 2c53800..a4dff92 100644 Binary files a/assets/o_run1_f.png and b/assets/o_run1_f.png differ diff --git a/assets/o_run2.png b/assets/o_run2.png index 592042b..b016a19 100644 Binary files a/assets/o_run2.png and b/assets/o_run2.png differ diff --git a/assets/o_run2_f.png b/assets/o_run2_f.png index 4cde3cc..263dd10 100644 Binary files a/assets/o_run2_f.png and b/assets/o_run2_f.png differ diff --git a/assets/o_run3.png b/assets/o_run3.png index 26e3ace..8b9a767 100644 Binary files a/assets/o_run3.png and b/assets/o_run3.png differ diff --git a/assets/o_run3_f.png b/assets/o_run3_f.png index 96710a2..e057c75 100644 Binary files a/assets/o_run3_f.png and b/assets/o_run3_f.png differ diff --git a/assets/o_run4.png b/assets/o_run4.png index d04366a..10f5282 100644 Binary files a/assets/o_run4.png and b/assets/o_run4.png differ diff --git a/assets/o_run4_f.png b/assets/o_run4_f.png index 42e72e7..ef92d7a 100644 Binary files a/assets/o_run4_f.png and b/assets/o_run4_f.png differ diff --git a/assets/o_run5.png b/assets/o_run5.png index 7af05ad..5baa1d4 100644 Binary files a/assets/o_run5.png and b/assets/o_run5.png differ diff --git a/assets/o_run5_f.png b/assets/o_run5_f.png index 85f0019..0d154a2 100644 Binary files a/assets/o_run5_f.png and b/assets/o_run5_f.png differ diff --git a/assets/o_run6.png b/assets/o_run6.png index 24ee77f..9180fbc 100644 Binary files a/assets/o_run6.png and b/assets/o_run6.png differ diff --git a/assets/o_run6_f.png b/assets/o_run6_f.png index d1e1ba4..11327eb 100644 Binary files a/assets/o_run6_f.png and b/assets/o_run6_f.png differ diff --git a/assets/o_run7.png b/assets/o_run7.png new file mode 100644 index 0000000..87f6678 Binary files /dev/null and b/assets/o_run7.png differ diff --git a/assets/o_run7_f.png b/assets/o_run7_f.png index ef78f7d..63d2a94 100644 Binary files a/assets/o_run7_f.png and b/assets/o_run7_f.png differ diff --git a/assets/o_run8.png b/assets/o_run8.png new file mode 100644 index 0000000..2672f61 Binary files /dev/null and b/assets/o_run8.png differ diff --git a/assets/o_run8_f.png b/assets/o_run8_f.png index f56ce61..49a3eb1 100644 Binary files a/assets/o_run8_f.png and b/assets/o_run8_f.png differ diff --git a/assets/o_smash.png b/assets/o_smash.png index 59d981e..b16ecb4 100644 Binary files a/assets/o_smash.png and b/assets/o_smash.png differ diff --git a/assets/o_smash_f.png b/assets/o_smash_f.png index dbdb607..c04afb6 100644 Binary files a/assets/o_smash_f.png and b/assets/o_smash_f.png differ diff --git a/assets/o_swing1.png b/assets/o_swing1.png index 30fc1a2..bb5028b 100644 Binary files a/assets/o_swing1.png and b/assets/o_swing1.png differ diff --git a/assets/o_swing1_f.png b/assets/o_swing1_f.png index e1f0de3..2a2939f 100644 Binary files a/assets/o_swing1_f.png and b/assets/o_swing1_f.png differ diff --git a/assets/o_swing2.png b/assets/o_swing2.png index cb197b4..54fbbc5 100644 Binary files a/assets/o_swing2.png and b/assets/o_swing2.png differ diff --git a/assets/o_swing2_f.png b/assets/o_swing2_f.png index 6da7532..085300b 100644 Binary files a/assets/o_swing2_f.png and b/assets/o_swing2_f.png differ diff --git a/assets/o_toss.png b/assets/o_toss.png index 5be580c..529694a 100644 Binary files a/assets/o_toss.png and b/assets/o_toss.png differ diff --git a/assets/o_toss_f.png b/assets/o_toss_f.png index 0d4bba3..62e9c42 100644 Binary files a/assets/o_toss_f.png and b/assets/o_toss_f.png differ diff --git a/index.html b/index.html index 9301b33..abc894f 100644 --- a/index.html +++ b/index.html @@ -494,6 +494,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index 983564a..bfbed97 100644 --- a/src/config.js +++ b/src/config.js @@ -689,7 +689,8 @@ export const CUP_ROUNDS = [ export const TOURS = [ { id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null }, - { id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', dx: 0.46, apex: 0.78, tossDx: -0.59 }, + { id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', dx: 0.46, apex: 0.78, tossDx: -0.59, + oDx: 0.48, oApex: 0.73 }, ]; // netW was the singles-stick width (10.06) while the court is drawn to the DOUBLES lines, so the @@ -1137,7 +1138,11 @@ export const POSE_TIME = 0.38; // opponent's ball out by his racket instead of under his palm. Both are FEEL KNOBS measured off the // drawn palm by eye — a skin-detector guess at the hand lands on a knee, so tune these by looking. export const BOUNCE_IDLE = { dx: 0.36, apex: 0.68, tempo: 3.6, // the player, drawn from behind - oDx: 0.45, oApex: 0.55 }; // the opponent, drawn from the front + // the opponent, drawn from the front. Remeasured 2026-08-14 for the + // friendlier anchor art: HIS new palm-out bounce holds the ball close + // (palm +0.24 m, 0.83 m up); HER reach stays wide — her pair rides + // TOURS like the player's dx/apex already does. + oDx: 0.24, oApex: 0.72 }; // generated sprite sheets: the idle pose is this many pixels tall; all scales derive from it export const SPRITE_H = 224; // 2x remaster (was 112). RES is 3, so the player at the baseline diff --git a/src/gameplay.js b/src/gameplay.js index 77293ca..565a6f0 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1732,7 +1732,15 @@ export function update(dt) { G.aiReactT += dt; const incoming = ball.active && ball.lastHit === 'P' && G.aiReactT >= (G.diff ? G.diff.reaction : TUNE.aiReaction); - const tgt = incoming ? intercept('O') : HOME.O; + // THE INTERCEPT GETS A DEADBAND (Oliver: "switching between a left and right movement + // increasingly faster the nearer the ball comes"). intercept() re-solves every frame, and as + // the flight shortens the earliest-reachable point jumps around its solution — each jump + // flips his run direction, faster and faster. He commits to the stored point unless the + // fresh solve moves it meaningfully (or the chase resets between shots). + const fresh = incoming ? intercept('O') : HOME.O; + if (!incoming || !G.aiTgt || Math.hypot(fresh.x - G.aiTgt.x, fresh.z - G.aiTgt.z) > 0.35) + G.aiTgt = fresh; + const tgt = G.aiTgt; const spd = TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) * (incoming ? 1 : (G.diff ? G.diff.recover : 0.5)); // slow recovery: wrong-footing wins points diff --git a/src/render.js b/src/render.js index 1f46d0b..863fb20 100644 --- a/src/render.js +++ b/src/render.js @@ -221,7 +221,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 = 119; +export const SPRV = 121; // 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". @@ -230,7 +230,7 @@ const SPR = {}; for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_run5','p_run6','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', - 'o_run1','o_run2','o_run3','o_run4','o_run5','o_run6', // the rolled 6-frame cycle (_runroll.py; the model drew 6, and a clean 6 beats a fought-for 8) + 'o_run1','o_run2','o_run3','o_run4','o_run5','o_run6','o_run7','o_run8', // the rolled 8-frame cycle (_runroll.py; the friendly refresh landed a full 8 again) 'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4', 'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set 'x_crowd_c','x_crowd_c2','x_ads_c', // TERRE ROUGE (clay) @@ -2129,8 +2129,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() : BOUNCE_IDLE.oDx); - const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : BOUNCE_IDLE.oApex); + 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)); drawBall(bx, by, sv.z); } // hot-ball ghost streak: a fast rally ball drags three fading ghosts behind it. It gated on