carlok — zsh — 88×30

cat _posts/2026-11-03-red-ink-sprint-27-a-crash-card-you-can-copy-from-and-a-save-that-survives-a-bad-value-and-a-second-tab.md

red-ink sprint 27: a crash card you can copy from, and a save that survives a bad value and a second tab

The sprint 27 branch of red-ink catches what used to freeze the page in silence: the first uncaught error or rejected promise pauses a fight and, 300 ms later, puts up the full-screen card with “Something broke” and a report — Babylon version, user agent, state, bout, language, quality level, viewport, the last five playtest-log entries, the error and its first stack lines — plus Reload and Copy details, with “Keep playing” offered only while frames still arrive; resource load errors, ResizeObserver noise, extension scripts and cross-origin “Script error.” are ignored, and nothing is sent anywhere (c904b72).

Stored values are validated on load, because a volume of 'abc' in localStorage went through clamp() as NaN and into an AudioParam, which throws and left the page frozen on Play: num() now returns the current value for anything not finite, migrateProgress sanitises wins, pass2, dock, arena and outfit while leaving unknown fields alone so a future save round-trips, and the difficulty lookup uses hasOwnProperty so 'constructor' no longer passes (bb4789c). Two tabs can no longer erase each other’s progress: saveProgressData() first takes what the stored copy has beyond its own — larger wins, pass2, dock, and the outfit and arena of whichever side has won more — unless the caller means to overwrite, and a storage event reloads progress so an idle tab does not show an old copy (77b83cb). A queued voice line is cancelled on pause, mute, a hidden tab and Quit, where suspending the Web Audio graph had never reached an <audio> clip or the browser’s speech engine (58d4fbe), and held Esc or Enter now ignore key repeat, a pause clears buffered input so a key pressed while paused no longer fires on Resume, and the dodge statistic counts punches actually evaded rather than presses (6f92fdb).

The regression harness was hardened alongside it: a page error, a console error or a 404 on an asset now fails the test it happened in unless the test names the message as expected (4659714), and unbounded while state !== fight waits were replaced with a bounded helper, every test raced against 120 s, with one result line per test and a regression-result.json uploaded when a job fails (ab193cf). The run stands at 60 of 60 in about 150 s locally, with both bout tables, 90.5 / 67.5 / 44.0 / 36.0 / 30.0 / 22.0 and 71.5 / 52.0 / 16.5 / 16.0 / 12.0 / 11.5, unchanged and the step probe identical over 900 ticks from 20 to 144 fps (9e951dc).