Runner is glass; battery pass (30fps cap, cached piles, GC fixes)

Runner counter (no new weapon needed — one you don't hold when it shrieks
can't save you): the sprinter is now a GLASS CANNON — any ball contact at
any quality bursts it (main ball and triple-serve side balls; melee and
pellets already killed it). The hold-to-aim auto-lock targets the most
advanced zombie = the charging runner, so touch-and-release is the panic
button. Plus runnerVMax 4.2: the late-game speed ramp is capped so runners
stay humanly reactable.

Battery (iPhone drain):
- TUNE.fps = 30 (T-panel tunable, 0/60 = uncapped): frame-skip cap in the
  RAF loop — halves paint work on a 810x1440 canvas, the dominant cost.
  dt clamp raised 0.033 -> 0.04 so 30fps doesn't run 1% slow-motion.
- Gore piles pre-rendered to per-pile cached canvases (rebuilt on growth /
  orientation change): 1 drawImage per pile per frame instead of ~26
  ellipse paths — late-game courts had ~800 path fills per frame.
- matchMedia('(pointer: coarse)') hoisted out of the render loop (was
  allocating a MediaQueryList every pretoss frame).

Verified: weak (pow .05) ball kills a runner; max-ramp runner v = 4.2
exactly; piles render from cache (screenshot, pl.cv set); frame-cap
arithmetic yields clean every-other-tick at 60Hz RAF (RAF itself is paused
in this pane — logic deterministic); tennis serve benchmark 30/36 across
runs (flub-band; diff is provably tennis-neutral); no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-12 18:04:51 +02:00
1 parent 1b97905217
commit 70882d2dd4
4 files changed
+25 -11

No files matched your search

+2 -1
View File
@@ -15,7 +15,8 @@ import './input.js';
let last = performance.now();
function frame(now) {
requestAnimationFrame(frame); // schedule FIRST: one bad frame must never kill the loop (a thrown error froze the game for good)
const dt = Math.min(0.033, (now - last) / 1000); last = now;
if (TUNE.fps && now - last < 1000 / TUNE.fps - 2) return; // frame cap (battery): skip RAF ticks until it's our beat
const dt = Math.min(0.04, (now - last) / 1000); last = now;
update(dt); render();
}