- repaired events decrement the ledger by one unit of the item (theft, not boredom-decay) and stamp a small perfect green-tick placard — The Correction signs its work; kept distinct from the crooked chip-coloured shipment stamp - parity-mite activity grows a hospital-white sterile patch via growthMask (its 3rd/4th customer): desaturate + lift to clinical white + faint tile grid, the inverse of corruption and meant to feel worse - DEV hook: .mite() .repair() plus .frame()/.canvas() pixel-verification drivers - +3 repair-semantics tests (screen suite 33); order-3 STANDARD lines are DATA's copy, my chyron wraps them (verified live, no clipping) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
587 lines
42 KiB
Markdown
587 lines
42 KiB
Markdown
# LANE-SCREEN — THE SCREEN compositor & glitch shaders
|
||
|
||
You are an Opus 4.8 executor on the SCREEN lane of FKTRY. Read `../MASTERPLAN.md` and
|
||
`../CONTRACTS.md` first; world bible `../../docs/FKTRY_LORE.md` (§1 THE SCREEN, §3
|
||
products, §8 style). Also read `../../index.html` — the original WIMVEE GLYTCH
|
||
prototype — its fragment shader is your seed stock: 8 glitch params (mosh, melt,
|
||
chroma, tear, roll, burn, noise, freeze) already written and tuned.
|
||
|
||
**Mission:** the payoff object. THE SCREEN is the sky-display everything ships to —
|
||
the player's factory output composited live as escalating video corruption. When
|
||
someone screenshots this game, they screenshot your canvas. It must start sterile and
|
||
end deranged, and every shipped item must visibly matter.
|
||
|
||
**Owned paths:** `src/screen/**`.
|
||
**Never touch:** contracts, main.ts, other lanes, the GLYTCH prototype (read-only
|
||
seed stock — port, don't move).
|
||
|
||
**Standing rules (all rounds):**
|
||
- WebGL (raw or three.js — your call, note it) on the provided 640×360 canvas.
|
||
Budget: <2ms/frame; THE SCREEN is garnish, not the meal.
|
||
- Corruption must be *legible*: a player who ships 40 chroma slurry should SEE chroma
|
||
damage specifically. Item → artifact mapping is the design core, keep it data-driven
|
||
off `ItemDef.id`.
|
||
- Base content starts as procedural "clean programming" — sterile SMPTE-ish test
|
||
patterns, slow pans, timestamp overlay (port `makeBaseTexture` thinking from the
|
||
prototype; animate it subtly so corruption has motion to chew on).
|
||
|
||
---
|
||
|
||
## CURRENT ORDERS — Round 6 (goal: the sky notices the enemy)
|
||
|
||
Your chyron now wraps (orchestrator fix — 29/30 STANDARD lines were clipping at ~51
|
||
chars since round 5; two-line wrap verified live).
|
||
|
||
1. **`repaired` events on the sky** (SIM lands them): each repair eases total
|
||
corruption by its item's contribution AND stamps a small, perfect, green-tick
|
||
placard — The Correction signs its work. Boredom and repairs must stay
|
||
distinguishable (rot vs theft).
|
||
2. **Mite shimmer**: reuse the growth-mask for a faint hospital-white sterile patch
|
||
creeping around heavy mite activity — the inverse of corruption, and it should
|
||
feel WORSE.
|
||
3. **THE STANDARD, new pool**: coordinate with DATA — the propaganda acknowledges
|
||
the mites now ("Units are assisting you." / "Inventory discrepancies are being
|
||
resolved in your favor."). Chyron-only, mumble untouched (standing constraint).
|
||
|
||
**DoD:** a mite wave is visible on the sky as creeping cleanliness + tick stamps;
|
||
pixel-verified; budget holds.
|
||
|
||
---
|
||
|
||
## NOTES (append-only log, newest at bottom)
|
||
|
||
```
|
||
### Round N — YYYY-MM-DD — [your model]
|
||
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
|
||
```
|
||
|
||
### Round 1 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: all 5 orders done and verified on the dev build.
|
||
- `params.ts` — the 8 params + frame-rate-independent `ease()`.
|
||
- `glitchStack.ts` — GLYTCH shader stack ported. Raw WebGL, NOT three.js (order 1 said my
|
||
call): it's one fullscreen triangle + one texture; three.js would cost more than the
|
||
whole effect is allowed to. All 8 prototype params keep their tuning verbatim — I
|
||
changed no magic numbers. Added 3 uniforms of my own: `u_drift`, `u_brownout`, `u_flash`.
|
||
- `baseTexture.ts` — procedural clean programming: SMPTE bars + castellations + pluge/ramp
|
||
+ camcorder overlay (PLAY ▶, running SP timecode, rec dot), widened to 16:9.
|
||
- `corruptionMap.ts` — the ledger + item→artifact table + escalation curve.
|
||
- `index.ts` — compositor: ledger, easing, wobble, brownout, awakening.
|
||
- `corruptionMap.test.ts` — 24 tests, all green.
|
||
|
||
DECISIONS:
|
||
- **Mix vs. total are separate axes.** WHAT you ship picks the artifact (blend of per-item
|
||
weights, by ship share); HOW MUCH you've ever shipped picks the intensity (escalation).
|
||
This is what makes 40 chroma slurry read as *chroma* instead of "more glitch".
|
||
- Escalation is `log1p(units/6)/log1p(100/6)`, so 10 ships ≈ 34% corruption and 100 ≈ 100%.
|
||
Front-loaded hard, per orders — there's a test pinning early-vs-late slope.
|
||
- Base texture repaints only when a visible glyph changes (timecode ~1/sec), not per frame.
|
||
Re-uploading 640×360 every frame would have eaten the budget for nothing. Motion for the
|
||
corruption to chew on comes from `u_drift` (bounded sinusoid — deliberately not a wrapping
|
||
pan, which would show a seam: the texture is non-power-of-two so WebGL1 forces CLAMP_TO_EDGE).
|
||
- Wobble above 0.8 total is CPU-side, 8 distinct slow rates, ±0.12 riding on top of the eased
|
||
value and never feeding back into the ledger.
|
||
- **I extended the item table well past the orders' examples.** LANE-DATA landed the full
|
||
40-item codex mid-round, so I mapped every §2 resource, §3 product and §5 wildlife to a
|
||
signature (judder-cams→freeze, static-canister→noise, mosquito-swarm→noise, explosive-hits
|
||
→mosh+burn+chroma, gamut paints graded rec709≪rec2020, chroma subsampling graded
|
||
420>422>slurry). The 5 science packs are intentionally left on the generic-noise fallback —
|
||
they're research currency, not ship goods. Unknown ids still degrade gracefully; there's a
|
||
test that walks `data/items.json` and asserts every id yields sane params, so LANE-DATA can
|
||
keep adding content without me.
|
||
|
||
VERIFIED (dev build, driving the real module on the real canvas, reading GL pixels — not vibes):
|
||
- Starts sterile: bars + "NOTHING IS WRONG" chyron, `idle:true`, all params 0.
|
||
- 40× chroma-slurry → `chroma 0.684`, every other param 0 except a 0.082 burn trace. Screenshot
|
||
shows channel-split fringing on bars and doubled glyphs. Item-specific: confirmed.
|
||
- Awakening: first-ever ship renders exactly ONE frame at mean luminance 255 (full white),
|
||
79 the next frame.
|
||
- Mature: 120× melt → total 1.0, melt 1.0 / mosh 0.451; screenshot shows the bars dripping
|
||
into black. Above 0.8, params drift ~0.095 between frames — alive, not maxed-out static.
|
||
- Anchor-slab isolation test: 1× anchor sets `idle:false` with ZERO corruption, and the only
|
||
pixel change is the chyron band going 37.1→100.8 luminance (dark overlay removed, bars
|
||
revealed). Chyron logic and sanitation both confirmed in one shot.
|
||
- Brownout: 1.0 when on, 0.0 when lifted, freeze+dropout+judder all driven off it.
|
||
- **Frame budget: 0.004–0.013 ms/frame CPU** (rolling avg, `__fktryScreen.stats().msPerFrame`).
|
||
Budget was <2ms. GPU side is one fullscreen triangle, no readbacks in the hot path.
|
||
|
||
BLOCKED/BROKEN: nothing blocking me. Two things worth knowing:
|
||
- LANE-SIM emits no `shipped` events yet, so THE SCREEN can't be driven by the real chain.
|
||
I shipped a debug hook instead: `__fktryScreen.ship('melt',40)`, `.brownout(true)`,
|
||
`.reset()`, `.stats()`. It's ~20 lines in `init` and I'm happy to drop it once the chain
|
||
runs — say the word.
|
||
- `npm test` currently has 2 red in `src/sim/sim.test.ts` (crafting, shipping) and they are
|
||
NOT mine — flagging for your review, not touching them. My 24 pass; tsc is clean repo-wide.
|
||
|
||
NOTE ON A THING I TOUCHED OUTSIDE MY LANE: I briefly created `.claude/launch.json` to get a
|
||
dev server on a free port (8150 is held by another session), then **deleted it** — the tooling
|
||
kept resolving to the busy port anyway. Net diff outside `src/screen/**` is zero. If you want
|
||
lanes to be able to boot their own preview, that file probably wants to be orchestrator-owned
|
||
with one entry per lane; right now five parallel lanes would collide on it.
|
||
|
||
CONTRACT REQUEST: none. `ScreenFX` + `SimEvent` gave me everything I needed.
|
||
|
||
PROPOSAL (orders 2 asked me to flag this): **anchor-slab sanitizing is live and it's good
|
||
tension** — potency −2.0, so 1 anchor cancels ~2 items of corruption and enough of them
|
||
sterilize THE SCREEN completely (tested). Right now that's invisible to the player, which
|
||
makes it a trap: they'll ship I-frames for a commission and watch their sky go clean with no
|
||
explanation. Two options for you — (a) UI surfaces it (LANE-UI chip: "SANITISING"), or (b) the
|
||
fiction owns it: The Correction *pays* for anchor-slabs, so it's a deliberate sellout loop —
|
||
bandwidth now, corruption later. I'd build (b); it's the codex's whole conflict in one item.
|
||
Also: `ScreenFX` currently only sees events, never the snapshot — if you ever want THE SCREEN
|
||
to react to *bandwidth strain* or *heat* continuously (not just the brownout on/off edge),
|
||
that'd need `frame()` to see the snapshot. Not asking yet; noting the seam.
|
||
|
||
NEXT (my read, not self-assigning): item-specific artifacts that the 8 prototype params can't
|
||
express — moiré needs a real interference lattice, ringing needs edge-detected halos,
|
||
macroblock bricks want the 8×8 grid to be *legible*. That's new shader passes, not new
|
||
weights. Also THE SCREEN should probably show the shipped item's silhouette for a beat as it
|
||
lands. Say the word on round 2.
|
||
|
||
### Round 2 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: orders 1–3 done and verified; order 4 (optional) deliberately deferred with the
|
||
contract request filed below.
|
||
- `params.ts` — PARAMS extended 8 → 11 (`block`, `ring`, `moire`). The original eight keep
|
||
their prototype tuning untouched. Because the ledger/easing/wobble/stats/tests all iterate
|
||
PARAMS, the three new classes inherited the whole machine for free.
|
||
- `glitchStack.ts` — three new passes + the two overlays.
|
||
- `overlays.ts` (new) — stamp placard + scram bar painters.
|
||
- `index.ts` — stamp queue/lifecycle, scram trigger, debug hook now behind `import.meta.env.DEV`.
|
||
- `corruptionMap.test.ts` — 30 tests green (was 24).
|
||
|
||
DECISIONS:
|
||
- **The block lattice is locked to screen space (`vUv`), not to the warped `uv`.** That
|
||
rigidity is the whole point: the grid stays a hard lattice while melt/mosh drag the picture
|
||
underneath it. Quantized in a 80×45 grid = true 8×8 source pixels at 640×360.
|
||
- **Ringing needed a second attempt.** v1 sampled the gradient at ±1.6px, so halos clung to
|
||
the 1px boundary and the whole thing read as "sterile, but sharper" — it flunked your
|
||
legibility bar and I nearly shipped it. Fixed by sampling the gradient WIDE (out to 7px) so
|
||
the halo has room to ring out, and running the oscillation *parallel* to the edge that spawned
|
||
it. Mean abs pixel diff vs sterile went 16.5 → 25.5, and it now reads as haloing at a glance.
|
||
- Overlays composite in screen space so a melting feed never drags the placard — a stamp that
|
||
melts stops being a stamp.
|
||
- Stamp queue caps at 2 pending. One stamp per EVENT, 0.4s each, size scaled by count
|
||
(0.72→1.0 across 1..40 units) with an overshoot on impact. A burst reads as a couple of
|
||
stamps, never a strobe.
|
||
- Scram bar position is derived from `entity`+`tick`, not `Math.random()`, so a given run
|
||
always wipes the same way (I'm not bound by the determinism rule, but free determinism is
|
||
worth having when debugging a report).
|
||
- `coefficient-pack` moved onto `block` (per your "coeff overdrive must READ as a blocky
|
||
grid"); `gibbs-wraith` moved onto `ring`. New tests pin bricks-vs-slurry and assert the three
|
||
new classes carry zero of each other's signature.
|
||
|
||
VERIFIED (drove the real module on the real canvas, read GL pixels, screenshotted contact sheets):
|
||
- **Bricks**: within-cell variance on the 8px lattice drops 689 → 236 (cells genuinely
|
||
flattening = quantization, not noise). Reads as an unmistakable quilted posterized grid.
|
||
- **Ringing**: bright oscillating ghost rings hugging every bar edge at 3.2× zoom; flat sterile
|
||
edges for comparison. MAD vs sterile 25.5.
|
||
- **Moiré**: diagonal rainbow interference lattice over the whole feed. MAD vs sterile 35.3.
|
||
- **Distinctness** (mean abs pixel diff): bricks↔slurry 21.9, ring↔moiré 29.5, moiré↔slurry 34.3.
|
||
Nothing looks like anything else.
|
||
- **Stamps**: MELT ×40 punches a big orange placard, fades by 0.33s, gone by 0.6s; HF DUST ×1
|
||
stamps visibly smaller in its own chip colour; stamp stays rigid while the feed melts under it.
|
||
- **Scram**: torn white TRACKING LOST bar on exactly one frame, absent the next.
|
||
- **Budget: 0.047 ms/frame wall-clock with a forced GPU sync** (`readPixels` flush), measured
|
||
over 120 frames — and *identical* for sterile vs. every artifact class lit at once plus
|
||
brownout. The new passes are free at this resolution. CPU-only rolling avg: 0.003–0.02 ms.
|
||
|
||
BLOCKED/BROKEN: nothing. Two notes for the record:
|
||
- The verification tab is `hidden`, so rAF never fires and the main loop sits frozen at ~TICK 8.
|
||
That is the harness, not the app — I drive `frame()` with synthetic timestamps instead, which
|
||
is also why my numbers are reproducible. Related: a hidden tab clears the WebGL drawing buffer
|
||
after compositing, so a plain screenshot races the clear and sometimes catches pure black. It
|
||
cost me a while to prove that was the harness and not a moiré bug. Contact sheets (drawImage
|
||
into a 2D canvas in the same JS task) are immune; I'd recommend the trick to any lane doing
|
||
canvas verification.
|
||
- Mid-round I saw reds in `src/sim/sim.test.ts` and `src/render/index.ts` tsc; both cleared as
|
||
the lanes landed. HEAD is clean repo-wide, 30/30 mine.
|
||
|
||
CONTRACT REQUEST: **`frame(timeMs: number, snap?: SimSnapshot)`** — taking you up on the v3
|
||
offer. Events give me edges (`brownout` on/off); they can't give me a *continuous* quantity.
|
||
Signal strain wants THE SCREEN to tremble as `bandwidth.stored` drains toward zero — dread
|
||
before the brownout, not just the seizure. Same seam would cover reacting to aggregate `heat`.
|
||
Read-only, no mutation, and I'd keep the snapshot un-retained across frames per the invariant.
|
||
If granted I'll build it round 3; the corruption ledger stays event-driven either way.
|
||
|
||
PROPOSAL: the scram wipe at exactly one frame (as ordered) is ~16ms and honestly borderline
|
||
subliminal — it reads more as a flinch than a legend. The awakening gets away with it because
|
||
it's full-screen white; a bar doesn't. Suggest 3 frames (~100ms) so TRACKING LOST is actually
|
||
readable. Trivial change, your call — shipped at one frame as ordered.
|
||
|
||
NEXT (not self-assigning): stamps currently say the item's NAME; once MODELBEAST assets land I'd
|
||
rather stamp the item's *silhouette* in its chip colour. Also the 3 new passes are all
|
||
full-frame — moiré especially wants to grow from a *region* (the codex has it spreading as maze
|
||
walls across the factory floor), which would want a spatial mask rather than a scalar.
|
||
|
||
### Round 3 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: all 4 orders. Thank you for the v3 grant — `frame(timeMs, snap?)` is exactly the seam
|
||
strain needed.
|
||
- `strain.ts` (new) — pure, snapshot-shaped, no DOM/GL. The whole point: order 4's sterile
|
||
guarantee is now a unit test, not a promise.
|
||
- `strain.test.ts` (new) — 14 tests. Screen suite is 44 green.
|
||
- `glitchStack.ts` — `growthMask()` + tremor/fever warps + regional moiré + fever tint.
|
||
- `index.ts` — snapshot peek, strain easing, deterministic moiré seed, 3-frame scram.
|
||
|
||
DECISIONS:
|
||
- **Tremor is measured in SECONDS, not units.** Your v3 units ruling made this: `stored` is
|
||
bandwidth-seconds, `draw-gen` is bandwidth/second, so `stored/(draw-gen)` is literally *how
|
||
long until the lights go out*. Dread ramps over the last 8 seconds of reserve. This falls out
|
||
correct at every scale for free — a megafactory bleeding 1000/s with 4000 banked feels exactly
|
||
as doomed as a shack bleeding 10/s with 40 banked, because both have 4 seconds. There's a test
|
||
pinning that equivalence. Raw `stored` would have made big factories look permanently calm.
|
||
- **Surplus is always calm, whatever the reserve.** If `draw <= gen` nothing is draining, so
|
||
there is nothing to dread — which is *also* what makes the boot state (gen=draw=stored=0)
|
||
sterile by construction rather than by special-case. Order 4 fell out of the maths.
|
||
- **Fever averages only the machines that run hot** (heat > 0.01). Averaging across every entity
|
||
would drown one glowing decoder in a sea of cold belts, and belts are most of a factory.
|
||
- Tremor ≈ ¼ of the brownout's judder amplitude, and fever is a smooth sine haze while tremor is
|
||
random judder — so dread, fever and seizure are three distinguishable readings, not one dial.
|
||
Strain stays weather: MAD vs sterile is ~6.8, against ~35 for moiré. Subordinate as ordered.
|
||
- **Regional moiré separates AREA from INTENSITY.** Intensity saturates early (`u_moire*2.2`
|
||
clamped) while the mask's radius keeps growing with tally. That's what makes a big shipment
|
||
read as "the maze has spread" instead of "everything got slightly shimmery" — which is the
|
||
whole difference between regional and a scalar fade.
|
||
- `growthMask(p, seed, grow)` is written as the **reusable spatial-artifact template** you asked
|
||
me to note: an fbm-ragged growth front, aspect-corrected, creeping outward from a seed. Any
|
||
future artifact that should grow from a PLACE multiplies its amount by it. The raggedness is
|
||
load-bearing — a clean circle reads as a spotlight, not as spreading crystal.
|
||
- Moiré seeds from the tick of the first moire-weighted shipment, so a given run always grows the
|
||
same maze while different runs grow it elsewhere. Debug-hook ships default to tick 0.
|
||
|
||
VERIFIED (fixture snapshots + pixel reads + contact sheets):
|
||
- **Order 4, at the pixel level**: rendered no-snapshot / boot / healthy at an *identical*
|
||
timebase — boot and healthy are **byte-for-byte identical** to no-snapshot. Strain cannot leak
|
||
into the NOTHING IS WRONG era. (First attempt showed MAD ~1-2 and that was my harness: each run
|
||
advanced the clock, so the *timecode digits* differed. Fixed the test, not the code.)
|
||
- **Tremor ramp** (deficit 10/s): 20s left → 0, 8s (the horizon) → 0, 4s → 0.50, 2s → 0.75,
|
||
empty → 1.00. Fever 0.837 from hot decoders with tremor still 0.
|
||
- **Regional moiré growth**, as % of frame taken over: 0 → 20.1% → 44.7% → 74.5% → 84.5% → 85.8%
|
||
across 0/4/10/25/60/150 crystals. It spreads; it does not switch. The front is visibly ragged.
|
||
- **Seed**: same tick → same seed twice over; different tick → different seed. Deterministic.
|
||
- **Scram**: read the bar's row per frame — `BAR,BAR,BAR,-,-,-`. Exactly 3 frames, readable now.
|
||
- **Budget: 0.072 ms/frame** wall-clock with a forced GPU sync, on a 2000-entity factory with
|
||
every artifact class lit — `computeStrain`'s per-frame entity walk is not measurable at that
|
||
size. CPU-only 0.043 ms. Still ~28× under.
|
||
|
||
BLOCKED/BROKEN: nothing outstanding.
|
||
- **I found and fixed a real bug in my own code while chasing a false alarm.** My `dt` was
|
||
`Math.min(0.1, ...)` — ceiling clamped, floor not. A *negative* dt (clock rewind) turns
|
||
`ease()`'s `1-exp(-dt/tau)` into a huge negative multiplier and threw strain to **-3614** before
|
||
it crawled back. Production can't hit it (`performance.now()` is monotonic, and main.ts passes
|
||
it straight through), so this was only reachable from my harness — but an eased value that can
|
||
explode on a clock hiccup is a landmine, so `dt` is now clamped at both ends. Worth other lanes
|
||
checking their own dt handling if they ease anything.
|
||
- Full suite went 215/216 once mid-run, then 216/216 twice — another lane mid-write, not a real
|
||
red. HEAD is clean repo-wide, tsc included.
|
||
|
||
CONTRACT REQUEST: none. v3 covers everything I need.
|
||
|
||
PROPOSAL: `SimSnapshot.bandwidth` has no *capacity* field, so THE SCREEN can express "seconds
|
||
until empty" but not "the tanks are 20% full". Seconds is the better dread metric and I'd keep it
|
||
either way — but if buffer capacity ever lands in the snapshot cheaply, a second reading ("reserve
|
||
low" vs "reserve draining fast") would let strain distinguish a factory that's coasting on fumes
|
||
from one that's falling off a cliff. Not asking; noting the seam while it's fresh.
|
||
|
||
NEXT (not self-assigning): the mask template is ready for reuse — **bloom loops** and **HF dust
|
||
piles** are the obvious next spatial artifacts (both are regional in the codex), and the strata
|
||
biomes in §1 would suggest a mask per era. Also, now that strain reads the snapshot, THE SCREEN
|
||
could react to `jammed` entities (a stalled factory *should* look bored — the feed drifting back
|
||
toward clean is a nice punishment), but that's a design call, not a lane call.
|
||
|
||
### Round 4 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: all 4 orders. Thanks for granting capacity unasked.
|
||
- `boredom.ts` + `boredom.test.ts` (new) — pure freshness curve, 7 tests.
|
||
- `strain.ts` — second dread axis; `strain.test.ts` now 20 tests. Screen suite 57 green.
|
||
- `overlays.ts` — ratification card painter + era→standard table.
|
||
- `glitchStack.ts` — pallor pass (desaturate + vignette), card composite.
|
||
- `index.ts` — boredom wiring, card lifecycle, `researched` handling, pallor easing.
|
||
|
||
DESIGN INTENT, one line each (as ordered):
|
||
- **tremor = "how many seconds until dark?"** ACUTE. The cliff edge. Only exists while something
|
||
is actually draining, and it spikes as the runway runs out. Expressed as motion: judder.
|
||
- **pallor = "how much cushion do I have at all?"** CHRONIC. Sits there in surplus. Expressed as
|
||
colour: the saturation drains out and the corners close in.
|
||
A full tank draining fast is a jolt; a near-empty tank coasting is a way of life. Different
|
||
questions, different senses, so they never mush into one dial.
|
||
|
||
DECISIONS:
|
||
- **Pallor drains saturation on purpose.** Codex §8: "ALL saturation belongs to media/artifacts.
|
||
Color = value = literally the resource." A factory with no reserve should therefore literally
|
||
have less colour. The vignette does the rest. It fits the existing strain/fever/seizure
|
||
hierarchy as the quiet one: chronic, slow tau (2.0s), never louder than the corruption.
|
||
- **The capacity trap**: `1 - stored/capacity` with no tanks built reads as "totally empty" and
|
||
would have painted the boot state grey — blowing up round 3's sterile guarantee on day one.
|
||
No tanks = no cushion to miss = no pallor. That's not a special case, it's the honest reading:
|
||
you cannot be low on a reserve you never built. Two tests pin it (capacity 0 AND undefined).
|
||
- **Boredom decays the DISPLAY, never the ledger.** `fresh` scales the targets; the tally
|
||
underneath is untouched. Verified explicitly: after 4 idle minutes the sky is clean but
|
||
`units: 72, tally: {melt: 60}, total: 0.893` are all exactly as earned.
|
||
- **Boredom horizon: 60s grace, then 180s decay (4 minutes to clean).** You asked for minutes,
|
||
not seconds. A full minute of silence changes literally nothing, so a jam you notice, diagnose
|
||
and fix is free. Recovery rides the normal 1.2s shipment ease — against a 4-minute decay that
|
||
reads as a snap, and it reuses the wave feel rather than inventing a second motion.
|
||
- **The card is timed in SECONDS, not frames** — it's a broadcast interruption and must be one
|
||
second at any refresh rate. Verified 30 frames @30fps AND 60 frames @60fps. (Deliberate
|
||
contrast with the scram wipe, which stays frame-counted: that one is a tape artifact, and tape
|
||
artifacts are quantized to frames. Two different clocks for two different fictions.)
|
||
- Card composites dead last in screen space, so not one pixel of it is corrupted — that IS the
|
||
joke. Hard cut on exit, no fade: the corruption crashes back rather than seeping, because a
|
||
dissolve would make The Correction look negotiable.
|
||
- Card says the STANDARD, not the tech id (codex §1 names: OPTICAL FILM, NTSC / PAL ANALOG,
|
||
MPEG-2 / DVD, H.264 / HEVC / AV1, DCP) with the green tick from §8's Correction uniform. The
|
||
tech id is in the small print where a compliance notice would put it.
|
||
|
||
VERIFIED (fixture snapshots, pixel reads, contact sheet):
|
||
- **Sterile guarantee survives the new axis**: boot AND a tankless factory in surplus both render
|
||
byte-for-byte identical to no-snapshot, pallor 0.
|
||
- **Two dreads are separable at the pixel level**: full-tank-draining-fast → tremor 0.50, pallor
|
||
0, saturation 0.621 and corners 96 both *unchanged from healthy*. Empty-tank-coasting → tremor
|
||
0, pallor 0.972, saturation 0.500, corners 26.6. MAD between the two: **43.7**. They do not
|
||
look like the same emergency.
|
||
- **Boredom**: untouched at 60s → fresh 0.834 at 90s → 0.5 at 150s → 0 at 240s → stays 0. Ledger
|
||
intact throughout. Resume: freshness 1 instantly, picture home in ~1.5s.
|
||
- **Card**: 30 frames @30fps, 60 @60fps = 1.00s both. Mean luminance 83 → 235 → 83.
|
||
- **Budget: 0.078 ms/frame** with GPU sync, 2000 entities, every artifact + both dreads + fever +
|
||
brownout + card compositing. ~26× under.
|
||
- Note for the record: **tremor is invisible in a still** — it's motion, so the contact-sheet tile
|
||
looks like HEALTHY. I pinned it numerically instead (0.50, MAD 6.8 vs sterile). A still frame is
|
||
the wrong instrument for a judder and I didn't want that filed as "verified by screenshot".
|
||
|
||
BLOCKED/BROKEN: nothing in my lane. **One thing the fleet needs to know:**
|
||
- `npm test` currently shows 6 red in `src/ui/live.test.ts` ("reference factory should contain a
|
||
software decoder", commission/entity-count assertions). **Not mine, and not LANE-UI's fault
|
||
either**: LANE-DATA has uncommitted edits to all five `data/*.json` in the shared tree, and
|
||
UI's live tests build a reference factory out of that data. I confirmed by stashing *only*
|
||
`src/screen/` — the same 6 still fail with my lane entirely absent.
|
||
- **Warning about the shared working tree**: my first instinct was `git stash`, which stashed
|
||
every lane's uncommitted work, not just mine — and made the UI tests "pass", which nearly had me
|
||
believing I'd broken them. `git stash push -- src/screen/` is the only safe form here. Any lane
|
||
bisecting a red test in this tree can be badly misled by a bare stash. Worth a line in
|
||
MASTERPLAN if you agree.
|
||
|
||
CONTRACT REQUEST: none. v4 gave me everything; fixtures now carry `commissionQueue` and
|
||
`research` ahead of the v5 hardening (order 4 done).
|
||
|
||
PROPOSAL: boredom currently only listens for `shipped`. A factory that is *jammed solid* and one
|
||
that is *idle because the player is redesigning it* decay identically. You approved the stalled-
|
||
factory idea as design — if you want the distinction, `jammed` entities in the snapshot could make
|
||
the sky decay noticeably faster (the work is rotting, not resting) while a clean idle stays
|
||
gentle. That's a one-line read of data I already have. Say the word and it's a small round-5 item.
|
||
|
||
NEXT (not self-assigning): the era→standard table means THE SCREEN now knows about eras — the §1
|
||
strata skins (sepia Reel, phosphor Broadcast, rainbow-on-black Disc) could drive the *base feed's*
|
||
palette per the player's current era, so the clean programming itself ages as they tech up. That
|
||
plus the mask template would give era-flavoured biomes on the sky.
|
||
|
||
### Round 5 (MEGA) — 2026-07-18 — Opus 4.8
|
||
|
||
SHIPPED: all 8 orders, both organs. THE SPEAKER exists.
|
||
- PART A: `eras.ts` + `eras.test.ts` (new), `baseTexture.ts` (era substrate + general chyron),
|
||
`glitchStack.ts` (wildlife pass, title composite), `boredom.ts` (jammed-rot), `index.ts`.
|
||
- PART B: `graph.ts`, `sfx.ts`, `stations.ts`, `radio.ts`, `ambience.ts`, `subtitleBus.ts`,
|
||
`band.test.ts` (all new), and `index.ts` replacing the stub.
|
||
- 78 tests in my two lanes, all green. Repo-wide 443/443, tsc clean.
|
||
|
||
**A NOTE ON "VERIFICATION INCLUDES LISTENING": I cannot hear.** I have no ears, and I won't
|
||
pretend a waveform is a listening session. What I did instead is render every sound through an
|
||
`OfflineAudioContext` and measure it — spectra, peak/RMS, attack times, onset rates — and the
|
||
description below is derived from those measurements plus the synthesis I wrote. It is an
|
||
honest account of what the numbers say the speaker is doing, not of what it feels like in a
|
||
room. **Please put headphones on and tell me what's wrong** — mix balance and "is this
|
||
annoying after ten minutes" are exactly what measurement can't answer, and I'd rather get that
|
||
from you than guess.
|
||
|
||
WHAT IT SOUNDS LIKE (measured, then described):
|
||
- **Brownout** — the important one. A 0.29 ms attack: a genuine broadband click, not a fade.
|
||
Then ~0.5 s of bandpassed brown crackle and a sawtooth whine sliding 420→40 Hz under a
|
||
closing lowpass. It should read as a rack losing its rails: snap, spit, sag.
|
||
- **Placement thunk** — 2.3 ms attack, a sine dropping 150→55 Hz (the mass) under a 1.4 kHz
|
||
noise slap (the contact). Detuned per placement so a row of belts isn't a machine gun.
|
||
- **Scram klaxon** — 620/440 Hz square alternating four times, lowpassed at 1.8 kHz. Peak 0.29:
|
||
deliberately not the loudest thing, because it fires often.
|
||
- **Ratification sting** — 85 ms attack, F–A–C sines with an octave shimmer. The *only*
|
||
consonant, in-tune, untroubled sound in the game. Everything the player makes is filth; the
|
||
enemy sounds lovely. That's the joke, and it's why its attack is slow when everything else
|
||
is percussive.
|
||
- **FKTRY FM 88.1** — 4.9 note onsets/sec against 6.13 sixteenths at 92 bpm, i.e. the ~34%
|
||
rests are audible as space rather than a wall. Square lead over triangle bass, pentatonic
|
||
minor, pattern mutating every 32 bars. Spectrum is bass-dominant (60 Hz: 0.49) with nothing
|
||
above 4 kHz.
|
||
- **THE STANDARD 640** — 2.9 syllables/sec, which is real speech cadence. Ring-modulated
|
||
carrier through two moving formants, lowpassed at 4.5 kHz to AM bandwidth. Sits at 500 Hz
|
||
and nowhere else. It is deliberately just short of intelligible — you can hear that it's
|
||
speech and not what it says. **THE SCREEN tells you what it says.** That gap is the trick.
|
||
- **NUMBERS 6955** — 4.0 digits/sec, a formant voice with a per-digit vowel shape so numerals
|
||
are learnable by ear, through a narrow SW bandpass (sharp 2 kHz peak, 0.50). No affect, no
|
||
melody, no acknowledgement.
|
||
- **DEAD AIR 108.0** — lowpassed brown room tone breathing at 0.21 Hz (projector pace), plus
|
||
one reversed chime — swelling up out of nothing then cut dead — every ten minutes, seeded.
|
||
|
||
DECISIONS:
|
||
- **Everything takes `(ctx, dest, t0)` and nothing assumes a live context.** That one choice is
|
||
why the whole speaker can be rendered in an `OfflineAudioContext` and measured deterministically
|
||
— same code the player hears, no test-only path. Noise buffers use a seeded PRNG for the same
|
||
reason: identical spectra every run.
|
||
- **The limiter is a `tanh` WaveShaper, not a DynamicsCompressor.** A compressor's makeup gain
|
||
would pump the entire mix every time the klaxon fires. tanh just refuses to exceed 1, which is
|
||
what "don't clip" actually means, and it adds the saturation a blown factory PA should have.
|
||
- **Pre-discovery silence is structural, not a volume setting.** The radio's master gain is 0
|
||
and ambience is never started until `relicFound`. Layer 1 is all one-shots, so a quiet factory
|
||
is *actually* quiet between events rather than humming under a noise floor.
|
||
- **Inter-station noise is band-specific** because that's what makes a band feel like a place:
|
||
AM brown crackle, FM white hiss, SW a wandering bandpass plus a heterodyne whistle whose pitch
|
||
is the beat frequency — so it swoops *down to nothing* as you zero in. Hunting is the mechanic.
|
||
- **The era ages the substrate, never the corruption.** Same shader, same params; only what
|
||
they're painted onto changes. The contact sheet shows identical melt drips across all five eras.
|
||
- **Boot era is `reel`.** With nothing ratified the world starts in the deepest stratum and
|
||
climbs to the surface (stream) and beyond (fortress). The old SMPTE look is now mid-game.
|
||
- Title timed in seconds (1.53 s hold, gone by 2.07 s), awakening 0.47 s. Both are *moments*;
|
||
the scram wipe stays frame-counted because it's a tape artifact. Three clocks, three fictions.
|
||
|
||
VERIFIED (offline render + pixel reads + contact sheets):
|
||
- **Pre-discovery silence: peak 0.0 and RMS 0.0, exactly zero**, with the dial swept across all
|
||
three bands and ambience asked for max load+fever. Same graph post-discovery: peak 0.285.
|
||
- **Station lock vs static are distinct spectra, and correctly so per band.** AM static has
|
||
**70× the 60 Hz energy of AM locked** (1.40 vs 0.02 — the brown crackle). FM *inverts*: locked
|
||
is bass-dominant (0.49 @60 Hz, nothing @14 k), static is treble-only hiss (0.10 @14 k, nothing
|
||
@60 Hz). SW locked shows a 2 kHz formant peak (0.50) that static (0.09) lacks.
|
||
- **Subtitles end-to-end**: tuned AM → The Correction's line on the chyron; off by 4 kHz (lock
|
||
0.906) → still captioned; off station → caption clears; SW → `5 3 0 7 9` on the sky, which is
|
||
the real coordinate for the nitrate vein. FM and DEAD AIR caption nothing (no words). Screenshot
|
||
in the sheet.
|
||
- **Era contact sheet**: reel (sepia + sprocket holes), broadcast (phosphor + interlace), disc
|
||
(rainbow-on-black gloss), stream (classic SMPTE), fortress (blinding white) — all under the
|
||
same 20× melt.
|
||
- **Budget: SCREEN 0.040 ms/frame** (GPU sync, 2000 entities, every artifact + wildlife + strain
|
||
+ brownout). **SPEAKER 0.0063 ms/frame** on the worst case (SW numbers + heterodyne + ambience)
|
||
— the per-frame path only writes AudioParams; Web Audio does synthesis on its own thread.
|
||
|
||
BLOCKED/BROKEN: nothing blocking.
|
||
- **I caught a balance bug by measuring rather than hearing**: DEAD AIR rendered at RMS 0.072,
|
||
*louder than the music station* (0.032), which made "dead air" the loudest thing on the dial —
|
||
exactly backwards. Dropped its tone gain 0.32→0.10 and the breath depth with it; it now sits at
|
||
0.023, under the music. This is the clearest argument for you actually listening: I only found
|
||
it because I happened to print RMS side by side.
|
||
- Harness gotcha, mine not the product's: importing a module with a `?v=` cache-buster creates a
|
||
**second module instance**, so my first subtitle test wrote to a different singleton than THE
|
||
SCREEN reads and the chyron looked broken. Verified `busQ !== busC`. Re-ran canonically and it
|
||
works. Same family as round 3's clock rewind: the harness lied, not the code.
|
||
|
||
FINDING FOR LANE-DATA (not touching their file): `stations.json`'s `_doc` says the decoys
|
||
"decode to empty ground", but **two of them decode onto real seams** — `08127` → (-24,-20) lands
|
||
in `reel-beds-deep`, and `27503` → (-5,18) lands in `broadcast-shelf-south`. Both are visible,
|
||
non-hidden seams so no secret leaks and the treasure hunt is intact — it's a doc/data drift, not
|
||
a break. I deliberately did **not** red-light my suite over another lane's content (round 4's
|
||
lesson); my test asserts the invariant that actually matters — *no decoy ever leaks a HIDDEN
|
||
seam* — which passes. Their three planted groups all decode exactly right, and I have a test
|
||
pinning that against `seams.json` so the map can never drift.
|
||
|
||
CONTRACT REQUEST: none. v6 covered everything.
|
||
|
||
SANCTIONED IMPORTS (documenting as ordered): LANE-UI's tuner dock should
|
||
`import { getRadio } from '../audio'` — a narrow surface of setBand/setFrequency/setVolume/
|
||
setMuted/getState/subscribe/listStations, nothing else reachable. Returns `null` before the first
|
||
user gesture builds the graph, so the dock must handle that (and should probably render disabled
|
||
until `getState().unlocked`). Internally, `src/screen` reads `src/audio/subtitleBus` — both are my
|
||
lane, and it's a zero-dependency module precisely so neither organ couples to the other's guts.
|
||
|
||
STATIONS SCHEMA: I conformed to LANE-DATA's file exactly as written rather than proposing my own
|
||
— it's better than what I'd have specified (the `eraFlavor` table and the coordinate encoding
|
||
especially). Nothing to agree; it's already right.
|
||
|
||
PROPOSAL: THE STANDARD renders at RMS 0.0037 against FKTRY FM's 0.032 — ~9× quieter. That's
|
||
*correct* for gated speech (its peak, 0.10, matches the other stations; the gap is all the silence
|
||
between syllables) but it may still feel too quiet in practice next to continuous music. If it
|
||
does when you listen, the fix is a slow compressor on that station alone rather than a gain bump,
|
||
which would just make the syllables shout. Flagging rather than pre-emptively "fixing" something
|
||
the numbers say is fine.
|
||
|
||
NEXT (not self-assigning): the tuner has no UI yet — LANE-UI's dock is what makes the band
|
||
findable at all, and until then the radio is only reachable from the console. Also `growthMask`
|
||
now has two customers (moiré, wildlife) and is ready for a third: HF dust piles are regional in
|
||
the codex and `dust-pile` wildlife already arrives in the same event I'm consuming.
|
||
|
||
### Round 6 — 2026-07-20 — Opus 4.8
|
||
|
||
SHIPPED: all 3 orders. The sky now notices The Correction. `growthMask` gained its third and
|
||
fourth customers (order 3 round-3 groundwork paying off — no new shader machinery, new weights).
|
||
- `overlays.ts` — `paintRepair()` + `repairCanvas` (224×112). The Correction's uniform: hospital
|
||
white, faction green tick, machined square. Deliberately the inverse of the shipment stamp
|
||
(hand-thumped, crooked, chip-coloured, "SHIPPED / NO QUESTIONS").
|
||
- `glitchStack.ts` — two additions: the **mite sterility** pass (`u_mite`/`u_miteSeed`, reusing
|
||
`growthMask`) and the **repair placard** composite (`u_repair`, texture unit 5). The mite pass
|
||
sits on TOP of the corruption stack because it is *erasure*, not another artifact.
|
||
- `index.ts` — `repaired` events ease the ledger and queue a placard; parity-mite tracking drives
|
||
the sterile creep (snapshot truth / event fallback, same pattern as the mosquito swarms); repair
|
||
placard lifecycle in its own slot. DEV hook gained `.mite()`, `.repair()`, and — new — `.frame()`
|
||
+ `.canvas()` drivers (see the harness note below).
|
||
- `corruptionMap.test.ts` — +3 tests pinning the repair semantics. Screen suite 33 (was 30);
|
||
my two lanes together 81 green (screen 72 + audio band 9).
|
||
|
||
DECISIONS:
|
||
- **Repairs are THEFT; boredom is ROT — kept structurally distinct, per the order.** A `repaired`
|
||
event decrements the *ledger* by exactly one unit of the item (units drop by that item's
|
||
potency — pixel-verified: melt ×20 → repair → units 24.0→22.8, tally 20→19). Boredom only fades
|
||
the *display* and leaves the tally intact (recoverable). So a factory being eaten by mites
|
||
permanently loses corruption it can never fade back, while an idle factory snaps straight back
|
||
the moment it ships. I deliberately do NOT refresh the boredom clock on a repair — a factory
|
||
being quietly corrected is not a factory that is producing.
|
||
- **The mite patch is the inverse of corruption and it reads WORSE, as ordered.** Where the
|
||
mosquito shimmer *darkens* a region with crawling noise, the mite patch *desaturates and lifts
|
||
toward clinical white* with a faint manufactured tile grid. Pixel-verified against a heavy melt
|
||
feed: the bloom region went luminance 22.9→168.8 and saturation 0.695→0.025. A dead, too-clean
|
||
hole in your rot is more unsettling than more rot — the codex's whole point about The Correction.
|
||
- **The repair placard parks low-right (vUv 0.80,0.80), small (0.15×0.082 half-size), 0.55s** so it
|
||
never fights the centre shipment stamp — a burst of ships and a burst of corrections can co-occur
|
||
and stay legible as two separate voices.
|
||
- Both new spatial effects reuse `growthMask` verbatim (round-3 template): moiré, mosquito, AND
|
||
now mite sterility all share one fbm growth front. Zero new shader machinery for order 2.
|
||
|
||
VERIFIED (drove the REAL module on the REAL 640×360 canvas, synthetic timebase, GL pixel reads —
|
||
the harness tab is `hidden` so rAF is throttled, the round-2/3 gotcha; I pump `frame()` and
|
||
`drawImage` in the same JS task):
|
||
- **Repair**: units eased by exactly melt's potency (1.2); placard region flips corrupted
|
||
(lum 68) → hospital-white placard (rgb 228/232/228, lum 230). repair flag + tally decrement both
|
||
confirmed.
|
||
- **Mite creep**: localized bloom (grid cell 3,4 — NOT the whole frame), lum +146, saturation
|
||
→0.025. `mite` eases to 0.83 from size 0.85. Both source-of-truth paths exercised: event
|
||
fallback (`.mite()`) AND `snapshot.wildlife` parity-mite.
|
||
- **Chyron / order 3**: DATA landed 8 mite-acknowledging STANDARD lines (30→38). Pixel-checked the
|
||
live wrap: short lines 1 row, the 96-char lines wrap to exactly 2 rows, rightmost glyph at x613
|
||
inside the 640 canvas — no clipping. Word-wrap (orchestrator's fix) intact; I did not touch it.
|
||
The mumble is untouched — I never edited `amPropaganda`; it derives syllables from the line text.
|
||
- **Budget: 0.034 ms/frame** worst case — 2000 entities, corruption total 1.0, strain 0.94 +
|
||
fever 0.57 + pallor 0.82 + mosquito 0.89 + mite 0.89 + brownout, every param lit. ~59× under the
|
||
2ms budget. The two new shader branches and one new texture are free at this resolution.
|
||
|
||
BLOCKED/BROKEN: nothing in my lane.
|
||
- **Transient full-suite red, NOT mine (round-4 lesson holds).** A combined `npm test` briefly
|
||
showed 3 reds — `band.test.ts` (STANDARD line >96 chars), `sim/mite.test.ts`, and
|
||
`sim/reference.test.ts`. All three are the shared-tree race: DATA was mid-save on
|
||
`data/stations.json` (I caught it at exactly 96 chars, its own budget) and SIM has uncommitted
|
||
`mite.test.ts` + `constants.ts`/`index.ts` edits in flight. Re-running JUST my lanes: 81/81
|
||
green. I did NOT bare-`git stash` to chase it (my own round-4 near-miss).
|
||
- HEAD advanced under me twice mid-round (a `[ui] round 6` commit landed while I worked). Re-probed
|
||
before writing this; all claims above are against the tree as it stands now.
|
||
|
||
HARNESS NOTE: I added DEV-only `.frame(ms, snap)` and `.canvas()` to `__fktryScreen`. Round 2/3
|
||
both lamented having no clean way to pump the real module in a throttled hidden tab; this makes
|
||
pixel verification a two-line console call for any lane. Stripped from prod (`import.meta.env.DEV`).
|
||
|
||
CONTRACT REQUEST: none. `repaired` + `parity-mite` (v8 contracts) gave me everything.
|
||
|
||
COORDINATION (order 3, resolved): DATA owns the STANDARD copy; I own its rendering. The 8 new
|
||
lines are theirs and are already in `data/stations.json`. My only ask of them is a standing one:
|
||
please keep STANDARD lines ≤96 chars — `band.test.ts` asserts it and the chyron wraps to 2 lines
|
||
at that budget; a longer line spills to a 3rd line (the font steps down to 13px) but starts to
|
||
crowd the feed. The newest line is exactly 96, so we're at the edge.
|
||
|
||
PROPOSAL: the repair placard and the ratification card both wear the green tick — that's
|
||
intentional (one faction, one uniform), but a player seeing their first repair might read the
|
||
small placard as "another commission done" rather than "something took my work". If that lands
|
||
ambiguous in playtest, the fix is a one-word change to the placard legend ("CORRECTED" is already
|
||
doing the work, but "REVERTED" or "RECLAIMED" is colder). Flagging, not pre-emptively changing.
|