426bf3f33d
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9482b8a2bf |
[lane E+B] The pause card stops fighting the player, and a refused pointer lock stops
faking a crash Audit item 8, plus two bugs the last commit introduced and one it exposed. REQUESTPOINTERLOCK RETURNS A PROMISE. Both new call sites wrapped it in try/catch, which catches nothing — the rejection went to window.onunhandledrejection, which the previous commit had just taught to paint a full-screen FEED LOST. So a browser declining a lock for any of its ordinary reasons (document not focused; Chrome's ~1s lockout after the player leaves a lock with Escape) presented to the player as the game crashing. Caught by clicking: "the root document of this element is not valid for pointer lock" in 15px type over a game running perfectly well underneath. All three sites — title dismissal, resume, and input.js's canvas click — now swallow the rejection. A refused lock costs one click, which is what input.js has always been for. PAUSE ON BLUR HAD NO CARD AND TWO OWNERS. boot.js emitted ui:pause directly, so the loop halted while cards.js's own `paused` stayed false: the game stopped with nothing on screen to say why, and the next P press raised a card that then took a SECOND press to leave. Moved to cards.js, which owns the pause state — setPaused raises the card and emits. It already refuses while the title or debrief is up, so alt-tabbing away from a card correctly does nothing. Verified: blur raises PAUSED, one P resumes, exactly one true and one false on the bus. THE PAUSE CARD WAS NOT MODAL. #ui is pointer-events:none and it inherits, so a click anywhere but the RESUME button fell through to the canvas — whose mousedown handler re-requests pointer lock whenever it is not held. Clicking beside the button captured the pointer, hid the cursor, and left the player paused with nothing to press RESUME with. The dimmer takes pointer-events now (our own handlers are capture-phase on window, so they still see every click; only the canvas stops seeing them). The title keeps no dimmer and stays click-through by design. Pausing also now releases the lock and resuming takes it back. Pausing with P used to keep the pointer captured — a RESUME button that could not be pointed at — while Escape looked fine only because the browser drops the lock for you. Same key, two behaviours, no stated reason. And losing the lock now drops the queued EDGES, not just held keys: the Space you pressed to reach the RESUME button was still in the queue when the card went, so the run restarted by spending a boost you did not ask for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4e4311b568 |
[lane E+F] The game remembers you: a save file, a level select and an ending
Audit items 9, 10 and 11 — three features whose backends were finished and whose front ends did not exist. DIFFICULTY WAS UNREACHABLE. levels/difficulty.js has carried three fully specified tiers since round 2 — Endoscopy, Clinical, Terminal, with per-tier assists and a note explaining each — and every single call site in the codebase took the default. `easy` and `hard` were a table nobody could select. getLevel now takes the saved difficulty (?diff= still wins, so a test URL stays absolute), and the title has the switch. Verified live: the world boots at 11 spawns on Terminal against 8 on Clinical. THE CAMPAIGN WAS SIX LEVELS WITH ONE WAY IN. Reaching anything but L1 meant hand-editing ?lvl= in the address bar. The title now lists the campaign, unlocking each segment as the one before it is filed, showing the medal you hold on each. `LS_` is C's secret prefix, so the appendix stays a row of question marks until you have earned your way in — naming it in the menu would have given away the one thing in this game you have to discover. THE CAMPAIGN HAD NO ENDING. C's anal_verge gate carries `to: null` — the only gate in the game that does, its own note calling it "the end of the campaign" — and boot's ui:continue handler opened with `if (!e.to) return`. Finishing GUTS parked you on a medal card forever with no way out but the address bar, and the card said "any key", the same words it uses when it has nothing to tell you. Now the clear is filed and counted, the card says you are out, and the title carries CANAL CLEARED. New core/save.js is the whole persistence layer: one localStorage key, one flat object, best time / best score / best medal kept per column (they need not come from the same run) plus an attempt count. Every entry point is wrapped — Safari private mode throws on setItem, and a save file must never be able to stop the game booting. Its selfcheck (`node web/js/core/save.js --selfcheck`, now in qa.sh) covers the best-of comparisons and feeds it corrupt, future-versioned and malformed saves. cards.js records rather than boot: this file computes the grade, so routing it through the bus for boot to re-derive would be two copies of the medal table waiting to disagree. One bug found by clicking rather than reading: the first cut of the chips had no pointer-events:auto. #ui is pointer-events:none and it inherits — the file's own header says so — so the buttons rendered, highlighted nothing, and passed the click through to the canvas, which on the title starts the run. It looked like a menu and behaved like wallpaper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7455c03ae3 |
[lane B+C+E+F] Playtest blockers: the game never taught its controls, deleted bosses on death, and shipped an unwinnable tutorial
A five-way audit of the shipped build found the first thirty seconds are broken in three
independent ways. All three are verified, not inferred.
1. THE CONTROLS WERE NEVER STATED. The title said "press any key", and aim + fire are both gated
on pointer lock (input.js:45 onMouseMove early-returns unlocked; input.js:50 the first
mousedown only REQUESTS the lock) — so a player who dismissed with a key got no mouse look and
a cannon that silently did nothing. Worse: in an ARENA the forward axis belongs solely to
intent.throttle (player.js), so an untaught player coasts to a dead stop in front of L1's Mast
Cell and reasonably concludes the game is broken. Six arenas across L1/L3/L5/LS are the same
trap. The card now says CLICK TO FLY, takes the pointer lock on any dismissal, and prints the
legend that has existed — written and styled — in flight/dev.html since round 1, visible only
to whoever was running Lane B's harness.
2. DYING DELETED THE BOSS. boot's pump advanced `_ei` monotonically and never rewound. That
reasoning is right for `spawn` and `pickup` (re-firing double-spawns survivors and resurrects
banked pickups) but it silently applied to `boss` as well: die to any of the five, respawn,
fly through an empty lair, cross the gate, and collect a graded medal for a fight that never
happened. Dying was the fastest route past every boss in the game. Now a REARM set ({boss,
acid}) re-fires on respawn while everything else is retired into `_spent`. Verified by event
trace: start:mast_cell -> end:wipe -> start:mast_cell. Also despawns the sentinel's body on a
wipe, which the fix exposed as a duplicate (1 -> 2 Mast Cells per death).
3. L1'S TIDE WAS UNWINNABLE, AND MINE. I authored it this morning at speed 5 in a flow-2 segment,
reasoning "5 against 2 catches a dawdler". The player ceiling there is 2.8 u/s sustained
(throttleMax 1.4 x flow 2); boost peaks 5.2 for 0.55s on a 2.4s cooldown. C's own --sim prints
the verdict in plain text — "required 2.50x UNWINNABLE" — and I never ran it. Because
hazards.js grazes on gap<=catchRadius and the gap stays negative once passed, it was a ~74 dps
loop back onto the s=475 checkpoint: a brand-new player, in the default level, hard-stopped
three minutes in. Retuned to 2.4 (required 1.20x — the same shape as L2's teaching burp).
The validator was exempting it: `if (!c.lethal) continue` in levels/index.js meant qa.sh went
green over a death loop. An unwinnable chase is a design failure whatever its lethality — a
non-lethal one you cannot outrun is a continuous-damage trap. That exemption is gone, and all
six levels still validate.
Also: DEFAULT_LEVEL was still 'L2_esophagus' from when L2 was the only finished level, so every
new player skipped the tutorial entirely and started mid-campaign. LANE_C_NOTES §→ Lane F #2
said "change it if you would rather it be L1". It is L1 now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
464f5c8a18 |
[lane E] The game finds its voice: procedural synth bank, the WebAudio graph, the damage feed, and the medal/title/pause cards
Round-1 Lane E audio + the two remaining UI surfaces, consolidated from the JING5
clone onto main and made determinism-clean:
- audio/synth.js — procedural voice bank (primary path; audible with an empty manifest)
- audio/engine.js — WebAudio graph, cue router, bed, heartbeat; now the listener for the
11 gameplay bus events that were previously firing into the void
- ui/feedback.js — feed-corruption damage overlay (an ART_BIBLE law previously unmet)
- ui/cards.js — title / medal / pause cards; boot now honours ui:pause
- boot.js — mounts all five modules with per-module failure isolation and a
frame() tick (never step(), so stepped sims stay deterministic)
Determinism gate: threaded engine's seeded rnd (mulberry32 off ?seed=) into createSynth,
replacing every Math.random in synth.js. Audio texture is now reproducible per-seed and
qa.sh is green.
Verified: QA green; runtime smoke on ?seed=7 scheduled 182 osc + 91 buffer voices + 268
envelope ramps during play, zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|