Both players run from a video, and the middle chip finally plays you
Six sessions of asking an image model for a row of running figures, and it
never once drew a gait: three phases repeated with the same leg leading,
"ropeskipping, not running". A frame-by-frame LEFT/RIGHT skeleton spec, a
closing checklist, a loop clause, six figures instead of eight — all lost.
One 5-second image-to-video clip has no such problem, because it moves a
body instead of drawing poses. ~$0.42 a character through the key already in
.env, pose pinned at BOTH ends (pinned only at the front, she rotated into
profile halfway), stride period MEASURED off the frames rather than sampled
by eye — hers repeats in 6 clip frames, his in 23 — and the phases cut and
palette-snapped through _stride.cut. assets/_vidroll.py films and cuts it;
her first frame came from one still roll that took his approved pose as a
picture instead of a paragraph.
Wired: runN probes the sheet through SK() so the women's tour counts HER
frames, the player is no longer clamped to a two-frame pair, runHz 2.4 ->
1.9 ("this looks like roadrunner"), and the bench grew two PLAYER stops —
a cycle only ever seen mid-rally is a cycle nobody has watched.
Also in: the difficulty retune (medium is a real step above the shipped CPU
and is where `smart` first appears; hard stepped back off superhuman; easy
closed its gap) with MEDIUM's dial-for-dial pin to TUNE removed from the
test, and Oliver's own transparency fixes on the male frames.
SPRV 138 — the frames were right on disk and wrong on screen for an hour
because assets are cached for a year unless the ?v= moves. main.js v269.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b7eb04e19
commit
1b7fc2cdcf
23 files changed
+352
-71
No files matched your search
+18
-11
@@ -170,6 +170,7 @@ export function endDrill() {
|
||||
// ONE NAMED FRAME, so frames the game cannot reach yet — the serve, the fall — can still be looked
|
||||
// at the day they are generated rather than the day they are wired up.
|
||||
const BENCH_HP = 12; // a deep pool so a wound stage is set by FRACTION, cleanly
|
||||
const BENCH_CHARS = ['o', 'of', 'p', 'pf']; // the living slots past ZFAMS, in walk order
|
||||
|
||||
export function startBench() {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
|
||||
@@ -200,8 +201,10 @@ export function endBench() {
|
||||
// wound stage — the bench must ask the same question the game asks or it is testing something else.
|
||||
function benchApply() {
|
||||
const zb = zombies.find(z => z.variant === 'r'); if (!zb) return;
|
||||
// the two slots past ZFAMS are the LIVING opponents; render's SK() reads b.char for the suffix
|
||||
G.bench.char = G.bench.fam >= ZFAMS.length ? (G.bench.fam - ZFAMS.length ? 'of' : 'o') : null;
|
||||
// the four slots past ZFAMS are the LIVING players: the two opponents, then the two PLAYERS
|
||||
// (the run cycles are re-rolled from the same script and judged the same way — a player cycle
|
||||
// that was only ever seen in a rally is a cycle nobody has actually watched)
|
||||
G.bench.char = G.bench.fam >= ZFAMS.length ? BENCH_CHARS[G.bench.fam - ZFAMS.length] : null;
|
||||
zb.hpMax = BENCH_HP;
|
||||
zb.hp = Math.max(1, Math.round(BENCH_HP * [1, 0.5, 0.2][G.bench.stage]));
|
||||
// ...and the named states take the wound stage with them: z_r<suffix> at full health,
|
||||
@@ -233,8 +236,8 @@ export function benchKey(code) {
|
||||
else if (code === 'ArrowRight' || code === 'KeyD') b.stage = (b.stage + 1) % 3;
|
||||
// up/down walks the OPPONENT FAMILIES — plus two LIVING slots past the end (the tennis
|
||||
// opponents m/f, Oliver's run-cycle finetune bench): the zombie steps aside and the real O runs
|
||||
else if (code === 'ArrowUp' || code === 'KeyW') b.fam = (b.fam + ZFAMS.length + 1) % (ZFAMS.length + 2);
|
||||
else if (code === 'ArrowDown' || code === 'KeyS') b.fam = (b.fam + 1) % (ZFAMS.length + 2);
|
||||
else if (code === 'ArrowUp' || code === 'KeyW') b.fam = (b.fam + ZFAMS.length + BENCH_CHARS.length - 1) % (ZFAMS.length + BENCH_CHARS.length);
|
||||
else if (code === 'ArrowDown' || code === 'KeyS') b.fam = (b.fam + 1) % (ZFAMS.length + BENCH_CHARS.length);
|
||||
// F plays the fall. Before the fallback below, which treats ANY other key as "next state" — a new
|
||||
// control that forgets to land above that line silently steps the stop instead.
|
||||
else if (code === 'KeyF') return benchFall();
|
||||
@@ -257,14 +260,18 @@ export function updateBench(dt) {
|
||||
// slot exists to tune.
|
||||
if (b.char) {
|
||||
zb.x = 40; zb.vx = 0; zb.pose = 'idle';
|
||||
G.hideO = false; // splatter hid him when the match turned; the bench un-hides
|
||||
O.z = 5.5;
|
||||
// 'p'/'pf' pace the PLAYER on the near court instead — same back-and-forth, his own end, and
|
||||
// the sprite is twice the size there, which is the whole reason his cycle needs its own stop.
|
||||
const ch = b.char[0] === 'p' ? P : O;
|
||||
G.hideO = b.char[0] === 'p'; // splatter hid him when the match turned; the opponent
|
||||
ch.z = ch === P ? HOME.P.z : 5.5; // stops un-hide, the player stops leave him out
|
||||
const step = TUNE.aiSpeed * 0.7 * dt;
|
||||
O.x += b.dir * step;
|
||||
O.runT += dt;
|
||||
if (Math.abs(O.x) > 4.0) { b.dir *= -1; O.x = Math.sign(O.x) * 4.0; }
|
||||
O.facing = b.dir;
|
||||
O.pose = 'run';
|
||||
ch.x += b.dir * step;
|
||||
ch.runT += dt;
|
||||
if (Math.abs(ch.x) > 4.0) { b.dir *= -1; ch.x = Math.sign(ch.x) * 4.0; }
|
||||
ch.facing = b.dir;
|
||||
ch.pose = 'run';
|
||||
if (ch === P) { O.pose = 'idle'; O.x = HOME.O.x; O.z = HOME.O.z; }
|
||||
return;
|
||||
}
|
||||
O.pose = 'idle'; O.x = HOME.O.x; O.z = HOME.O.z; G.hideO = true; // fam stops: the living man steps back out
|
||||
|
||||
+15
-10
@@ -46,8 +46,9 @@ export const TUNE = {
|
||||
// with Oliver 2026-08-14: 4.5 on six frames was strobe, 1.7 on two read as jogging,
|
||||
// and 3.4 with the stride-synced bob is the one he called "way better". (6.8 was
|
||||
// tried and reverted on sight — the ceiling is real.)
|
||||
runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was
|
||||
// tuned on 2-4 near-duplicate frames; on eight real phases it strobed
|
||||
runHz: 1.9, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was
|
||||
// tuned on 2-4 near-duplicate frames; on eight real phases it strobed — and so
|
||||
// did 2.4 on the back-view re-roll (Oliver: "this looks like roadrunner")
|
||||
faceFlip: 0.13, // s an auto-steered direction must persist before the body mirrors (earnFlip).
|
||||
// The rolled cycles hold a real racket, and an un-earned flip teleports it
|
||||
// across the body — the "racket flashing left and right" report
|
||||
@@ -710,12 +711,16 @@ export const MENU = [
|
||||
// CPU pre-plans its intercept, and the dials that actually read as skill were untouched. What a
|
||||
// player FEELS is: how fast the CPU reads a shot (reaction — "well-aimed corners are winners"),
|
||||
// how long its racket is (reach), the FLOOR of its shot quality (qMin — a raised floor means every
|
||||
// ball comes back with pace), how often it just misses (flub), and its serve. MEDIUM repeats the
|
||||
// shipped TUNE values dial for dial (pinned by test), so the middle chip IS the old game.
|
||||
// ball comes back with pace), how often it just misses (flub), and its serve.
|
||||
// `smart` is the probability the CPU aims at the OPEN court instead of a random lane (trySwing) —
|
||||
// the dial that turns "returns everything" into "plays you". HARD got a second pass after
|
||||
// Oliver's "still too easy": it now reads your shot in 0.22 s, covers court at 1.3x, floors its
|
||||
// quality at 0.70, and hunts the corner you are not standing in four times out of five.
|
||||
// the dial that turns "returns everything" into "plays you".
|
||||
// THE 2026-08-15 RETUNE (Oliver: "medium is too easy and hard is a tad too hard, easy could also be
|
||||
// a little harder"): the three chips were bunched at the bottom and then jumped. MEDIUM no longer
|
||||
// repeats the shipped TUNE values — it is a real step ABOVE them, and it is where `smart` first
|
||||
// appears (0.35), so the middle chip plays the court instead of just returning. HARD stepped back
|
||||
// on the dials that made it feel superhuman rather than good — speed 1.5 -> 1.38, floor 0.70 ->
|
||||
// 0.66, smart 0.8 -> 0.7 — and EASY closed most of its gap to the old MEDIUM. The order across all
|
||||
// three still climbs on every dial (pinned by test); only MEDIUM's old pin to TUNE is gone.
|
||||
// `recover` scales the between-shots jog (the rally block's 0.5 "slow recovery: wrong-footing wins
|
||||
// points") — a HARD CPU that ambles back to centre is free points, a recovering one is a wall.
|
||||
// Speed calibration: the zombie RUNNER covers 2.8 x 1.8 x 1.22 = 6.15 m/s with no reaction gate,
|
||||
@@ -723,9 +728,9 @@ export const MENU = [
|
||||
export const DIFFS = [
|
||||
// EASY's first cut (flub 0.30, quality cap 0.62, reaction 0.62) was "way too easy" once HARD
|
||||
// taught us which dials bite — it is a mild step below MEDIUM now, not a training dummy
|
||||
{ id: 'easy', name: 'EASY', flub: 0.18, quality: 0.76, qMin: 0.45, speed: 0.90, reaction: 0.54, reach: 1.52, serve: 0.9, smart: 0, recover: 0.45 },
|
||||
{ id: 'medium', name: 'MEDIUM', flub: 0.10, quality: 0.86, qMin: 0.45, speed: 1.0, reaction: 0.45, reach: 1.6, serve: 1.0, smart: 0, recover: 0.5 },
|
||||
{ id: 'hard', name: 'HARD', flub: 0.01, quality: 1.0, qMin: 0.70, speed: 1.50, reaction: 0.22, reach: 1.85, serve: 1.15, smart: 0.8, recover: 0.85 },
|
||||
{ id: 'easy', name: 'EASY', flub: 0.15, quality: 0.80, qMin: 0.45, speed: 0.95, reaction: 0.50, reach: 1.55, serve: 0.95, smart: 0, recover: 0.48 },
|
||||
{ id: 'medium', name: 'MEDIUM', flub: 0.06, quality: 0.92, qMin: 0.56, speed: 1.18, reaction: 0.34, reach: 1.70, serve: 1.05, smart: 0.35, recover: 0.66 },
|
||||
{ id: 'hard', name: 'HARD', flub: 0.02, quality: 1.0, qMin: 0.66, speed: 1.38, reaction: 0.27, reach: 1.80, serve: 1.15, smart: 0.7, recover: 0.80 },
|
||||
];
|
||||
|
||||
// THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's
|
||||
|
||||
+21
-14
@@ -221,13 +221,19 @@ 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 = 132;
|
||||
export const SPRV = 138;
|
||||
// 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".
|
||||
export const LOAD = { done: 0, total: 0 };
|
||||
export 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',
|
||||
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', // SIX phases lifted out of a five-second
|
||||
// image-to-video clip (2026-08-15): a video model animates a body through time,
|
||||
// so the legs alternate on their own — every still roll drew three phases and
|
||||
// repeated them ("ropeskipping, not running"). WHOLE frames, cut and
|
||||
// palette-snapped to his settled art; the phases are spaced across one
|
||||
// measured stride (his loped: 23 clip frames against her 6).
|
||||
'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', // 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')
|
||||
@@ -1002,7 +1008,7 @@ function drawBenchHud() {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.font = B(20);
|
||||
shadowText(b.char
|
||||
? 'PLAYER ' + (b.char === 'of' ? 'FEMALE' : 'MALE') + ' · RUN CYCLE'
|
||||
? (b.char[0] === 'p' ? 'PLAYER ' : 'OPPONENT ') + (b.char.endsWith('f') ? 'FEMALE' : 'MALE') + ' · RUN CYCLE'
|
||||
: (ZFAMS.length > 1 ? ZFAMS[b.fam].toUpperCase() + ' · ' : '') + BENCH[b.i].name,
|
||||
W / 2, y, UI.cream);
|
||||
if (b.char) { // the finetune readout: the dials a live tuning session actually turns
|
||||
@@ -1496,9 +1502,9 @@ function drawPiles(near) { // split at the net like zombies so height occludes c
|
||||
// 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).
|
||||
const SK = n => {
|
||||
// the bench's living slots pick the suffix directly (G.bench.char 'o'/'of'), so both tennis
|
||||
// opponents can be reviewed without touching the picked tour
|
||||
const sfx = G.bench && G.bench.char != null ? (G.bench.char === 'of' ? '_f' : '') : tour.sfx;
|
||||
// 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;
|
||||
return (sfx && SPR[n + sfx]) ? n + sfx : n;
|
||||
};
|
||||
|
||||
@@ -1653,23 +1659,24 @@ function drawChar(ch) {
|
||||
// (one call, one row, one character — see assets/_runroll.py for why the old per-frame art
|
||||
// flickered); un-rolled characters fall back to 4 or 2.
|
||||
// TUNE.runN: live override for the bench's tuning sessions (0 = probe the sheet as always)
|
||||
const runN = TUNE.runN || (pre === 'p_' ? 2 : SPR[pre + 'run8'] ? 8 : SPR[pre + 'run6'] ? 6 : SPR[pre + 'run4'] ? 4 : 2);
|
||||
// ...probed through SK, so the WOMEN'S sheet is counted when the women's tour is on. Her clip
|
||||
// gave a six-frame stride and his five; a probe blind to the suffix would have played five of
|
||||
// her six and cut the cycle mid-stride.
|
||||
const runN = TUNE.runN || [8, 6, 5, 4].find(n => SPR[SK(pre + 'run' + n)]) || 2;
|
||||
// The rolled 6/8-frame cycles run at their own STRIDE RATE (TUNE.runHz, cycles/s). The old 4.5
|
||||
// was tuned on 2-4 near-duplicate frames, where it read as vigour — on eight real phases it was
|
||||
// a strobe ("very flashy and unnatural"), most of a full stride every display frame.
|
||||
// The PLAYER runs a two-frame pair like the opponents now, cut from his rolled sheet by eye:
|
||||
// the man's true leg-swap is frames 5+6, the woman's is 1+2. The offset only applies when the
|
||||
// pair is running — TUNE.runN = 6 on the bench still reviews the full roll from frame 1.
|
||||
const runOfs = pre === 'p_' && runN === 2
|
||||
&& !(G.bench && G.bench.char != null ? G.bench.char === 'of' : tour.sfx === '_f') ? 4 : 0;
|
||||
if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * (runN >= 6 ? TUNE.runHz : TUNE.runHz2) * runN) % runN + 1 + runOfs);
|
||||
// The PLAYER runs a two-frame pair like the opponents. It used to be picked out of a six-frame
|
||||
// roll by eye (his swap was frames 5+6, hers 1+2); the man's pair is now DRAWN as a pair —
|
||||
// one back-view figure plus its _stride.py twin — so the pair is always frames 1+2.
|
||||
if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * (runN > 2 ? TUNE.runHz : TUNE.runHz2) * runN) % runN + 1);
|
||||
// (the receiver used to flip to run1/run2 here as a "split-step" — it read as a man sprinting on
|
||||
// the spot, because the run frames are a full leaning stride, not a ready-hop. He idles now.)
|
||||
if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');
|
||||
if (ch.pose === 'serve') name = pre + (G.phase === 'toss' ? 'toss' : 'smash');
|
||||
// the run bob rides the STRIDE, not a wall clock: one dip per step, in phase with the frame
|
||||
// flip — the old fixed 18 Hz toggle read as independent hopping over the calm 2-frame cycle
|
||||
if (ch.pose === 'run') p.y -= Math.floor(ch.runT * (runN >= 6 ? TUNE.runHz : TUNE.runHz2) * runN) % 2;
|
||||
if (ch.pose === 'run') p.y -= Math.floor(ch.runT * (runN > 2 ? TUNE.runHz : TUNE.runHz2) * runN) % 2;
|
||||
const img = PS(name);
|
||||
drawShadow(p.x, p.y, hpx * 0.22, hpx * 0.06);
|
||||
if (img) {
|
||||
|
||||
Reference in new issue
Block a user