b9d9d816ad
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b9d9d816ad |
[lane B+E] Stop the game lying: the death sting at living players, the stuck hiss, and a crew naming the wrong organ
Four audio/comms cues wired to the wrong condition. All four are written content that has been
in the repo for rounds, firing at the wrong moment or never firing at all.
THE DEATH STING PLAYED AT LIVING PLAYERS. finish() mapped every non-win outcome to `death` — and
`death` is not a stinger, it is a ~12 s noise table (synth.js). So surviving L1's Mast Cell
("degranulated"), outrunning the Tapeworm ("escaped") and clearing the Sovereign's Toll all told
an alive player they had died, over the top of the next twelve seconds of play. Outcomes are
classified now: won/spared/communion triumph, alive-but-it-went-badly gets a downbeat, actually
dead gets the sting, and housekeeping (reset/dispose) is silent because nothing happened to the
player at all. Verified: surviving the Mast Cell no longer emits `death`; dying still does.
THE PHASE TELL WAS A SUSTAINING HISS. boss.js used `overheat` as its phase-change cue, but that
layer sustains until the engine sends `overheat_clear` on a combat:state.overheated falling edge
— which a boss never produces. Every phase change leaked a ~12 s steam hiss and ducked the mix
under it for the rest of the fight. Three sites, now short cues.
THE CREW HAS NEVER MENTIONED HULL DAMAGE. comms.js gates its two hull lines on
`kind === 'hull_hit'`, but `kind` is the damage SOURCE ('dart'|'acid'|'gas'…), so that branch has
never once been true in the game's life and every hit read as a coat graze. damage() already
computes `leak > 0` for its own audio cue — the bus event now carries it as `hull`.
AND IT NAMED THE WRONG ORGAN. There was exactly ONE boss line — "that is the pylorus. it does not
open for you." — fired for all five bosses, so the tutorial's Mast Cell and the campaign's final
boss were both announced as a stomach valve. Per-boss lines now, off boss:start (which is the only
place the id is known, and for the Sovereign whether there is a fight at all). The Mast Cell's is
doing real work: "wait — wait. that is a mast cell. it is not hunting you." / "do NOT shoot it."
The crew is the game's only tutorialisation channel, and that is the lesson L1 exists to teach.
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>
|
||
|
|
4ee6ae49a1 |
[lane B+E+F] Arena mode: in a room, you own the forward axis
The 6DOF unlock the player header has promised since round 1 ("Arena/6DOF mode is round 2;
modeAt() is guarded below"). It unblocks L1 and L3, which are both arena levels in the GDD and
have been skeletons waiting on this.
The insight that made it small: spline space ALREADY spans a room — s is the axial coordinate,
x/y are the cross-section — and world.collide() already resolves against arena spheres
(arenaSpatial, Lane A round 2). So this is not a new controller. The only real difference
between a tube and a room is WHO OWNS s: in a tube the flow carries you and you may only trim
it +/-40%; in a room `intent.throttle` becomes signed thrust with its own damping, so you can
stop dead and you can reverse. That is the whole feature, and it is what "slower, spatial,
exploratory" actually means in play.
- tuning.js: arena{thrust 44, maxSpeed 11, damping 2.2}. maxSpeed sits UNDER the esophagus's
12-20 flow on purpose — the GDD asks for slower.
- player.js: st.vs (axial velocity), the mode branch, surf disabled in rooms (no travelling
wave to ride, and a hanging surfBlend would speed-lock you to a crest that isn't there),
respawn() clears vs so a death in a room can't leak momentum into a tube.
- player:state gains `mode`; `flow` reads 0 in a room and `speed` goes signed.
- hud.js prints "6dof · free" instead of "flow 0 · thr 100%" — two lies on one instrument.
Safe by construction: both arena triggers are opt-in (level.arenas, or a segment marked
mode:"open"). L1/L2/L4 declare neither and are byte-for-byte unaffected.
Verified in L3's stomach: tube hands-off carries you at 4.53 u/s, acid-sea hands-off coasts to
a dead stop (0.0), and a room lets you travel BACKWARDS (-3.8u). Bus reports
{mode:arena,flow:0,speed:0} vs {mode:tube,flow:3,speed:3}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
4bcb942312 |
[lane E] The biome meter: the reputation you're building, made visible
A teal BIOME STANDING bar joins coat/hull in the HUD (matches the thin-cyan line-work; scale- only per the 60fps law). Hidden until the first `standing` event, so a flora-less level never shows a bar you can't fill. Brightens near full to telegraph an incoming phage ally. combat/index.js: rescaled standing to a clean 0..100 (tend +5, harm -35) — a FULL bar buds an ally and drops to a 50 recharge floor, so the meter reads as "charge it to summon help." First- pass numbers; tune to taste. Verified in-engine: tending flora reveals the bar and fills it proportionally (0.20 -> 0.45), teal, below coat/hull. 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>
|
||
|
|
83c30bf344 |
[lane E] The instrument exists: a HUD, and a surgical team who talk to you
#ui had been an empty div since round 0 — E never ran. It has an owner now. HUD (charter #1): coat/hull, heat, torpedo pips, speed/flow/throttle, boost, score, combo, biome + segment name, and a progress rail with C's 10 checkpoints ticked. Bus-only — zero imports from B, exactly as the charter asks. player:state and combat:state arrive every frame and ARE the clock, so the HUD cannot tick while the game isn't running. 60fps law: DOM is built once; the hot path is transform + textContent-on-change only. Bars scale (scaleX), they never resize — a width:% write per bar per frame is a layout pass. Torpedo pips rebuild only when ammoMax changes, not when ammo does. COMMS — on nobody's list. Argued for, then built. The GDD says "Star Fox arcade flow" and there was nobody in the game but the player. Three-hander: voss (attending, terse), park (resident, over-excited), adeyemi (immunology, apologetic — and amber, the colour of the cells attacking you; his fault). Deterministic round-robin per trigger key, no RNG. Priority ladder + per-key cooldowns so the crew shuts up during a firefight. It turned out to be more than flavour: it is the tutorialisation channel the GDD asks for and L1 has no other vehicle for. The hazard lines teach what C's HAZARDS registry note says the hazard IS ("ring gate. it opens on a beat — match the beat, do not ram it"). Keys are the real HAZARDS ids, checked against levels/enemies.js rather than guessed — all three L2 kinds get bespoke lines, zero generic fallbacks. A level's worth of dialogue is now a data edit in LINES. Three defects, all mine, all found by the stepped sim, none visible to the eye: - The progress rail was dead. I read `level.length` — a field that does not exist. The level carries segments, not a total. It now uses the runtime world.length that B already divides into p.progress. - My clock was fiction. Comms counted `+= 0.1` per timer tick and called it seconds; a backgrounded tab throttles setTimeout to ~1s, so lines held ~10x too long. It now reads performance.now(). - Every hazard warning had the LOWEST priority in the game. PRI has a key `warn`, but say() is called with `warn_reflux_surge`, so PRI[key] was undefined and fell through to the default of 1 — the surge screaming "GO. GO." could be talked over by park admiring a specimen. Keys now resolve exact-then-family. The evidence shot IS the fix: same seed, same frame, the warning now sits where the checkpoint line used to be. ### -> Lane B — player:state stops emitting the moment you die player.js:103 early-returns before the emit when !st.alive. So `alive` is unobservable (E can only ever read true — the field is currently decorative), and E cannot use player:state as a clock, because it stops exactly when the death UI needs to run and through boot's whole 2.0s respawn window. Comms works around it with its own wall-clock. The death/respawn feed-drop (charter #4) will not be able to. Ask is in NOTES; not patching your file. ### -> Lane F — two contract items I edited boot.js (~10 lines mirroring your own dynamic-import/try-catch/ console.info fallback) because #ui needed an owner and two modules nothing constructs aren't worth much. Ratify or move it. Second: shot_sink.py cannot photograph Lane E — it POSTs renderer.domElement.toDataURL(), and the HUD is a DOM overlay, so the house evidence tool renders it invisible. This shot is a composite (canvas -> 2D canvas, then #ui via an SVG foreignObject). Offer to lift it into pipeline/ as DBG.shotUI() stands. qa GREEN. Evidence: docs/shots/laneE/round2_hud_aortic.webp (s=1111, seed 7, live hazard warn + x6 chain). Dev server guts-e on 8146. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |