Encode the splatter bed too, and give the opponent his racket back

zombie-mode: 5.9 MB of 24-bit WAV down to 287 KB. Its master is 982597 frames
at 48 kHz, so loopEnd is the un-round 20.47077 — the decoder hands back
20.4787, and those 7.9 ms of AAC padding would tick once a loop. Nothing in
assets/audio/ is a multi-megabyte WAV any more.

o_run1/o_run2 had no racket in either hand. The cause was the batch prompt:
opponent_run never said "racket", and its reference sheet was built from the
racket-less frames it was replacing, so there was nothing to copy one from. It
is a ref_only batch off o_idle1 now — same man, front-facing, racket in his
right hand — with the subject spelling out that he still carries it mid-stride.
The stride reads as well as the old one did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 10:55:18 +02:00
1 parent cf3498c049
commit cd1c7a0cd8
9 files changed
+23 -8

No files matched your search

+10 -2
View File
@@ -463,8 +463,16 @@ included, or a duck silently no-ops.
48.000 s master to a 48.0174 s buffer — 17 ms of silence spliced in at every seam, once a loop, on
the title screen. `menuMusic` passes `48`. Re-encode the file and re-measure:
`fetch(f).then(r=>r.arrayBuffer()).then(a=>new AudioContext().decodeAudioData(a)).then(b=>b.duration)`.
`zombie-mode.wav` (5.9 MB) is the other candidate for this treatment, and it loads mid-match rather
than up front, so it matters less.
`zombie-mode` got the same treatment: 5.9 MB of 24-bit WAV to 287 KB, `loopEnd` 20.47077 (982597
frames at 48 kHz — an un-round master, and the decoder returns 20.4787, so 7.9 ms of padding is
trimmed). Both beds are AAC now; nothing in `assets/audio/` is still a multi-megabyte WAV.
**The opponent was running with empty hands.** `o_run1`/`o_run2` had no racket in either fist, and
the cause was the batch prompt: `opponent_run` never said the word "racket" and its reference sheet
was built from the racket-less frames it was replacing, so the generator had nothing to copy one
from. Fixed by making it a `ref_only` batch off `o_idle1` — same man, front-facing, racket
unmistakably in his right hand — with the subject spelling out that he still carries it mid-stride.
**When a prop goes missing from a sprite, check the batch's `subject` before blaming the roll.**
Measuring which audio files actually loaded: `performance.getEntriesByType('resource')` is USELESS
here — the ~200 sprite PNGs blow past the 250-entry resource-timing buffer and every later fetch is
+6 -2
View File
@@ -145,8 +145,12 @@
"o_run1",
"o_run2"
],
"subject": "Two frames of the same tennis opponent in a RED shirt and dark shorts running across the court towards the camera, two stages of one stride, drawn as coarse low-resolution pixel art.",
"anchor": 0
"ref_only": [
"o_idle1"
],
"anchor": 0,
"check": "same",
"subject": "The reference is ONE tennis opponent in a RED polo shirt, dark grey shorts and a white headband, seen from the FRONT, gripping a tennis racket in his RIGHT hand. Draw TWO frames of that SAME man, same size, same clothes, same face, same front-facing view, RUNNING across the court towards the camera as two stages of one stride: the first with his left leg forward and his right leg trailing, the second with the legs reversed. In BOTH frames he is STILL HOLDING THE TENNIS RACKET in his right hand, the same racket as the reference \u2014 head, strings, throat and grip all drawn, carried out to his side at about waist height as a runner carries it. His other arm swings free. The racket must be clearly visible and fully inside the canvas in both frames; neither hand is ever empty."
},
"ump_boy": {
"sprites": [
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+5 -2
View File
@@ -107,7 +107,10 @@ function loopBed(file, lowcut, vol, loopEnd = 0) {
.catch(() => beds.delete(file)); // failed start (decode/network) can retry next call
}
export const ambient = () => loopBed('tennis-ambient-loop.wav', 500, 0.035); // whole-scene atmosphere, from first tap
export const zombieMusic = () => loopBed('zombie-mode.wav', 800, 0.2); // background music once splatter kicks in
// ...and the splatter bed, same treatment: 5.9 MB of 24-bit WAV down to 287 KB. Its master is
// 982597 frames at 48 kHz, which is the un-round 20.47077 s below — an AAC loop needs the SOURCE
// length as loopEnd, not the decoder's padded one. See the note on menuMusic.
export const zombieMusic = () => loopBed('zombie-mode.m4a', 800, 0.2, 20.47077);
// Menu theme: the splash, the court picker, the death summary. Barely highpassed — this one is
// MUSIC, not an atmosphere bed, so the low end is the point. Started at boot so it is already
// running by the time the title is on screen; on a phone the autoplay policy holds the context
@@ -129,7 +132,7 @@ function fadeBed(file, out, secs = 0.7) {
b.g.gain.setValueAtTime(b.g.gain.value, t); // anchor at current level so a mid-fade toggle stays smooth
b.g.gain.linearRampToValueAtTime(out ? 0 : b.vol, t + secs);
}
export const zombieDuck = out => fadeBed('zombie-mode.wav', out); // key must match loopBed's, filename and all
export const zombieDuck = out => fadeBed('zombie-mode.m4a', out); // key must match loopBed's, filename and all
// The menu theme is never stopped, only faded — the buffer is already decoded, so fading it back up
// on the death screen costs nothing and can't miss its cue waiting on a re-fetch.
export const menuDuck = out => fadeBed('background-music.m4a', out);
+1 -1
View File
@@ -117,6 +117,6 @@
<div id="courts"><h2>CHOOSE YOUR COURT</h2></div>
</div>
</div>
<script type="module" src="main.js?v=86"></script>
<script type="module" src="main.js?v=87"></script>
</body>
</html>
+1 -1
View File
@@ -151,7 +151,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 74; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPRV = 75; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
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_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