Commit Graph

11 Commits

Author SHA1 Message Date
type-two
628ee150ca [lane F+E] The run gets a lifecycle, and failure gets a face
Audit items 4 and 5. Both are shell behaviour a finished game is expected to have and this one
never did.

THE RUN NOW STARTS WHEN YOU START IT. cards.js has emitted `ui:start` on title dismissal since
round 1 and its own comment says nothing consumes it — so the simulation ran behind the title
card. Measured: 1.2 s of sitting on the title moved the ship and burned 1.2 s off the par clock
before the player had touched anything. L1's first checkpoint (s=20) and the opening comms line
both fired while the title was still up, and a hazard could hurt you before you started, which is
why cards.js already carries a player:damage auto-dismiss as a safety valve. Now `started` gates
step(); verified s stays at 2 across a second of real frames.

PAUSE ON BLUR. The ui:pause consumer has worked since round 2 and nothing but the pause key ever
produced for it. Alt-tab mid-fight and you came back to a dead ship. blur + visibilitychange now
produce it.

AUDIO HEARS THE PAUSE. engine.js handled eleven bus events and not that one, so the bed, drone,
grain scheduler and heartbeat ran at full level behind the pause card, and the heart slewed to
its resting 50 bpm while you read and snapped back on resume. Ducks the master rather than
suspending the context — the heartbeat's pump schedules against ctx.currentTime and would wake
up in the past.

FAILURE HAS A FACE. Every failure mode in this game presented as the same black rectangle, so a
player could not tell "loading" from "crashed" and had nothing to report:
  - loadLevel did `return mod.getLevel(id)` without awaiting, so a rejection escaped its own
    try/catch and killed module evaluation. A mistyped ?lvl= now falls back to the stub tube
    (which names itself STUB ESOPHAGUS) instead of a dead page.
  - frame() re-armed its rAF at the BOTTOM, so one throw anywhere stopped the loop forever while
    the canvas kept showing the last good frame. Body is guarded; the loop always re-arms.
  - bus.emit ran listeners bare, so a throw in one subscriber aborted the rest of the emit — and
    player:state is emitted every frame from inside step(), so one bad frame in the HUD took
    combat and audio down with it. Listeners are isolated now.
  - webglcontextlost had no handler at all: a laptop sleeping left a white page with a floating
    HUD.
All four now land on a FEED LOST card naming the cause, in the game's own voice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 21:46:41 +10:00
type-two
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>
2026-07-26 19:04:25 +10:00
type-two
5a891107e2 [lane B+F] The enemies were never wearing their meshes: boot forgot to pass assets to combat
Chasing "more detailed looking enemies" turned up the reason detail never landed, and it was
not the art.

boot.js built combat as `createCombat({ scene, world, bus, rng, flags, player })` — no `assets`.
createCombat defaults it to null, so createEnemies got null, so `assets?.get?.('models', type)`
returned null for EVERY archetype and every enemy in the game has been quietly drawing its
procedural primitive. Lane D's GLBs loaded correctly, verified correctly, and were never once
rendered. spore_pod has been an icosahedron since the day it shipped; the Giardia and H. pylori
meshes from an hour ago were cones.

This is the nastiest shape a bug can have: the fallback path is INDISTINGUISHABLE from success.
`assets.get('models','spore_pod').geometry` was there and correct every time I checked it — I
was checking the loader, never the pool that consumes it. Measured after the one-word fix: pool
geometry goes 42 verts -> 6056 (spore_pod), and the new pathogens render at 2234 (Giardia) and
2500 (H. pylori).

Also in: the entity matcap the same investigation was written to prototype. emissiveMat takes an
optional matcap and applies it as LUMINANCE ONLY — sampled, reduced to luma, multiplied into the
core — so an amber foe stays exactly amber and a cyan commensal stays exactly cyan. The
ART_BIBLE readability law that the whole friend/foe system rests on is untouched by construction,
while the silhouettes stop being flat glows and start reading as lit, wet, three-dimensional
organisms. Switchable with ?matcap=0 for A/B, and absent-safe if D's matcap never shipped.
Weapons and pickups deliberately keep the flat look — a tracer is not a creature.

Net effect for playtest: this is the first build where the pathogens are actually visible as
pathogens. The GLB pipeline was correct for weeks; the wiring was one word short.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:31:14 +10:00
type-two
87a4803bde [lane A+B+C] L3 Stomach: the acid sea, and pH as a resource you can see
The last skeleton. L3 has been 4 events since round 1, waiting (in its own status field) on
"the pH field" and a boss framework; both exist now, so the level is authored: 46 events,
2500 units, the sea, and the Guardian at the end of it.

THE PH MECHANIC, finally load-bearing. Lane A built an acid plane in round 2 and left two
hooks on it — depthAt() labelled "B:" and set() labelled "C's event pump drives this" — and
nothing had ever called either. Now:
  - player.js scales the biome's ambient coat drain by SUBMERSION. Stomach 2.2/s x5 = 11/s
    under the surface (measured: 8/s net of regen, and regen is off for 2.5s after any hit,
    so it is brutal mid-fight); x0.15 above it, which is effectively safe air.
  - a new `acid` level event names a target height and boot eases the sea toward it in step(),
    so a RISING TIDE is a beat C can author. L3 uses two: -14 -> -6 mid-sea, then -6 -> +2 in
    the antrum, where the surface goes ABOVE the centreline and the safe lane simply stops
    existing. Measured: the identical spot (y -5) goes from 0/s to 9.67/s across that tide.
This is the GDD's "mucus shallows are safe lanes" expressed as DEPTH rather than wall-hugging,
which is better on two counts: it reads instantly (you can SEE the surface) and it makes the
vertical axis matter in the one level that is a room. Every collectible worth having is under it.

Gated on the sea's s-span, not on depthAt alone — depthAt returns a bare -1 outside the span
and that is indistinguishable from "1 unit above the surface", so reading it naively would have
quietly scaled ambient drain on every level in the game. Caught before it shipped.

Level: cardia (still teal, so the fundus can REVEAL the sea) -> fundus (the teaching room:
huge, low tide, nothing kills you fast) -> body (the sea proper, tide rises while you are in
it) -> antrum (no safe air left, only speed) -> pyloric antrum (the Guardian). H. pylori is
taught alone in the dome and tested in a rising sea; pepsin swarms scale 3 -> 6; bezoar_chunk
is a new catalogue row for the stomach's traffic.

L3 has NO commensals and no biofilm gate, which is anatomically exact — acid sterilises, which
is why H. pylori is famous for surviving it. So BIOME STANDING goes quiet for nine minutes and
comes back in L4, which makes L4's colonies read as relief rather than furniture.

C's sim: par 540s vs par-pace 555s ("requires pushing above par-pace"), pressure
3.75/1.50/2.44/2.16, min clearance 7.15 vs the 2.5 law, all 21 checkpoint gaps under 30s.
par.score corrected 9000 -> 5500, because 9000 was above everything the level can actually pay.

Deferred, and said out loud rather than quietly dropped: churn cyclones. A vortex that flings
the player is a FORCE and no hazard in the tree applies one; it wants the same A/B work as the
Guardian's arena churn and should land with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:26:31 +10:00
type-two
a318cf09f7 [lane B+C+F] The Appendix Sovereign: the secret boss, and a reputation you cannot fake
Two boss specs written to C's format (LANE_C_NOTES §Boss specs, round 3 additions) and the
first of them built. Both extend the roster along C's own law — one boss, one pressure — onto
the two axes the game had systems for and no boss pointed at: reputation (§4 Sovereign) and
restraint (§5 Mast Cell, specced only).

THE SOVEREIGN. The appendix is not vestigial: it is a biofilm reservoir where the gut's flora
shelter and re-seed the colon after a purge, and the whole fight is that fact. It is the only
boss whose existence is decided before you arrive — read once on entry:
  karma >= 70  THE COMMUNION — no fight at all. Full coat, a permanent phage escort. The
               reward for five levels of restraint is being spared a boss.
  karma >= 30  THE TOLL — P1 only.
  below        THE SOVEREIGN — three phases. P1's matrix is immune to the cannon and only the
               antacid torpedo disperses it (a mature biofilm's whole defining trait); P2 is a
               quorum bloom that re-armours when the room gets crowded; P3 is a 20s reseed run.
And the mechanic that makes it this boss: the lair is seeded with YOUR OWN FLORA, and it feeds
on dysbiosis — every commensal your stray fire kills re-armours a plate, live, mid-fight.

KARMA vs STANDING — a real bug found in testing, and worth the extra number. `standing` is a
SPENDABLE meter: a full bar is consumed to bud an ally, dropping to a 50 floor. That meant a
perfect player could never HOLD >= 70, so the Communion was almost unreachable — verified: a
symbiotic pilot showed standing 50 and was demoted to "toll". `karma` is the same inputs with
nothing spending it. The Sovereign's tier and gates' `secretTo` branches read karma; E's bar
still reads standing.

Also: boss.js generalised to a registry, which was the claim its header made — the Sovereign
needed zero changes to enemies.js. LS_appendix (3 segments, 800 units, 16 events), reached from
L4's ileocecal valve via `secretTo`/`secretNeed` — anatomically exact, the appendix hangs off
the cecum right past that valve. The secret is hidden behind conduct, not behind a wall.

C's selfcheck caught par.time as a free medal and the fix improved the design: par 160 sits
under the timid traversal, so only a Communion run (which has no fight) can beat it. The time
medal rewards the same conduct the level is about.

Verified: all three tiers branch correctly (communion = 0 nodes + 4 phages; toll = 1 phase;
full = 3 phases, 4 armoured); killing a commensal mid-fight re-armours a plate (reason
"dysbiosis"); the valve routes karma 100 -> LS_appendix and karma 0 -> L5. Levels 5/5 valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:25:36 +10:00
type-two
fb08d7895b [lane F] Chain the campaign: CONTINUE now loads the next segment
cards.js has been raising `ui:continue {from,to}` since Lane E round 1, with the comment
"INTEGRATOR: nothing consumes ui:continue yet." Boot consumes it now, so crossing a gate and
pressing CONTINUE loads C's `next` level instead of ending the run. Three levels in a folder
become a campaign.

Deliberately a full page load: the alternative is rebuilding world + player + combat + UI in
place and re-wiring every bus listener to save ~1s on a transition that happens 4 times a run,
and the reload guarantees no leaked pools or stale event pumps. Flags ride along (?dbg=1,
?seed= survive the hop). A `to` that isn't in CAMPAIGN — or is null — ends the campaign on the
medal card rather than dumping the player into the stub tube.

Verified: L2 -> CONTINUE -> ?lvl=L3_stomach with flags intact; unknown "L4_small_intestine"
and null both correctly no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:19:14 +10:00
type-two
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>
2026-07-19 13:57:38 +10:00
type-two
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>
2026-07-17 23:01:40 +10:00
jing
29cca53675 [lane F] Mid-round-2 recovery: the glue that makes L2 a game
All round-2 lane sessions were cut off 15:08-15:16 before NOTES/commits; the three prior
commits preserve their work verbatim. This commit is the wiring B was cut off inside of,
plus the run-state boot owes per ruling #4:

- combat/index.js: fiction-id -> archetype resolve via C's getEnemy() (the round's declared
  ONE blocker), theta-array + spread fan-out for group spawns, hazards + pickups constructed
  and updated (order: enemies -> weapons -> hazards -> pickups, so a torpedo can neutralize
  a surge the frame it detonates), pickup score/samples onto the combat:state scoreboard,
  combat.reset(fromS) for respawn.
- flight/player.js: kill/shove/refill — the API B's new modules call; kill ignores iframes
  by design (boost-dodging through C's lethal acid wall is not a read). Deleted the
  crestSpeed fallback guard per its own comment — A's law landed.
- boot.js: run state. checkpoint crossed -> remembered; death -> 2s -> respawn at it +
  combat.reset() re-arms hazard timelines (pump deliberately does NOT rewind: no
  double-spawns, collected stays collected); gate -> level:complete {stats, par} for E's
  medal card. Exported step(dt) so the stepped-sim harness drives the REAL loop.
- ROUND2_INSTRUCTIONS: MID-ROUND STATUS box for the resuming lanes; f-progress entry.

Verified (deterministic stepped sim via boot.step, :8140): full L2 run fires all 47 events
with ZERO console warns (was: 11 spawn no-ops), all 5 hazard telegraphs, 2400 proximity
ticks, 14 pickups collected, finish 157s vs par 180 while surfing 94s of it. Death path:
throttle 0.7 into the finale -> killed by acid s=3386 -> respawn at Cardia Approach 2980 ->
surge re-arms -> second attempt escapes -> level:complete {deaths: 1}. Survivable AND
losable. Evidence: docs/shots/laneF/round2_L2_integrated_full_run.png. qa.sh GREEN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:39:44 +10:00
jing
4ccf5edcc0 [lane F] Round 1 integration + ROUND2 instructions: the game is a game
Wired B's player/combat + event pump, D's assets, C's level pick into boot.
Fixed qa ESM gate (node --check no-op, found by B) + added spline selfcheck.
TECH: world contract FROZEN v1.1, bus events ratified, colorspace + crest-speed
laws (CREST_FACTOR 1.6 — B proved surfing lost to throttle at 1.0). Stub complies.
Verified integrated build via 60s stepped sim: 9 draws, 0 errors, 0 asset misses;
one known gap (fiction-id spawn resolve) confirmed and assigned as B's top item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:39:02 +10:00
jing
34f84ab162 [lane F] Round 0 scaffold: docs, contracts, lane charters, bootable stub world
GDD v1 (flow-locked hybrid movement, 5 biomes + secret, boss roster),
TECH contracts (world API, level schema v0, bus events, manifest law),
ART_BIBLE (synthetic scanner look), PIPELINE (MODELBEAST-first, fal gated),
PROCESS (PROCITY lane/round law), charters A-F + ROUND1 instructions.
Seed code: shell + boot + core (rng/bus/flags) + stub world (peristalsis
shader tube, 1 draw / 102k tris, contract-complete) + qa.sh (GREEN).
Verified in-browser; evidence docs/shots/laneF/round0_stub_tube.png.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:28:35 +10:00