Compare commits

...

2 Commits

Author SHA1 Message Date
type-two
e20ce1341b [orchestrator] MODELBEAST wave 2: 17 more machines — every machine has a body now
flux_local -> sf3d on the m3ultra, same pipeline as wave 1. demuxer, demosaicer,
dct-press, subsampler, mv-extractor, p-caster, gop-assembler, bloom-duplicator,
field-loom, telecine-puller, hex-splicer, shipper, asic-cooler, roto-bench,
matte-bath, optical-printer, neural-keyer. 27 of 30 machines now carry real GLBs
(belt + lane-splitter + buffer-tank stay procedural by design - the shader belts
carry the flow-field chevrons a baked mesh would lose). ~31MB total, served as
static runtime fetches - none of it in the JS bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:34:35 +10:00
type-two
32ff942fd5 [screen] round 6: repairs ease the ledger + green-tick placards; hospital-white mite sterility creep
- 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>
2026-07-20 18:34:32 +10:00
22 changed files with 326 additions and 7 deletions

View File

@ -503,3 +503,84 @@ NEXT (not self-assigning): the tuner has no UI yet — LANE-UI's dock is what ma
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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -145,6 +145,35 @@ describe('anchor slabs sanitize', () => {
});
});
describe('repairs ease the ledger by one unit of the item (The Correction, v6)', () => {
// A `repaired` event decrements the tally by one unit in index.ts; this pins the CONTRACT
// that decrement is built on — "eases total corruption by its item's contribution".
it("removing one unit eases net units by exactly that item's per-unit contribution", () => {
const before = resolveLedger(tally({ melt: 20 }));
const after = resolveLedger(tally({ melt: 19 }));
expect(before.units - after.units).toBeCloseTo(corruptionFor('melt').potency, 5);
expect(after.total).toBeLessThan(before.total);
});
it('correcting a hard-hitting item eases more than correcting a weak one', () => {
const heavy =
resolveLedger(tally({ 'explosive-hits': 20 })).units -
resolveLedger(tally({ 'explosive-hits': 19 })).units;
const light =
resolveLedger(tally({ 'gamut-paint-rec709': 20 })).units -
resolveLedger(tally({ 'gamut-paint-rec709': 19 })).units;
expect(heavy).toBeGreaterThan(light);
});
it('a repair never drives corruption below sterile (theft floors at clean)', () => {
// ship one, correct one → back to the NOTHING IS WRONG floor, never past it.
const { units, total, targets } = resolveLedger(tally({ melt: 0 }));
expect(units).toBe(0);
expect(total).toBe(0);
for (const p of PARAMS) expect(targets[p]).toBe(0);
});
});
describe('param hygiene', () => {
it('never emits a param outside 0..1, even shipping everything at once', () => {
const everything = tally({

View File

@ -34,8 +34,11 @@ uniform float u_strain, u_fever, u_pallor;
uniform vec2 u_moireSeed;
uniform float u_wildlife;
uniform vec2 u_wildlifeSeed;
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex;
uniform float u_mite;
uniform vec2 u_miteSeed;
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex, u_repairTex;
uniform float u_stampAmt, u_stampScale, u_scram, u_scramY, u_card, u_title;
uniform float u_repair, u_repairScale;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float vnoise(vec2 p){
@ -225,6 +228,22 @@ void main(){
col *= 1.0 - vig*u_pallor*0.75;
}
// ---- mite sterility: The Correction's hospital-white cleanliness creeping outward from heavy
// parity-mite activity. Same growth-mask template as moire/wildlife, but the INVERSE of
// corruption — it desaturates and washes the region toward clinical white with a faint
// manufactured tile grid. It sits on TOP of the corruption stack because it is erasure: your
// mess is yours, this is your work being quietly deleted. Meant to feel WORSE than rot.
if(u_mite > 0.001){
float mmask = growthMask(vUv, u_miteSeed, u_mite*0.7) * clamp(u_mite*1.7, 0.0, 1.0);
if(mmask > 0.001){
vec3 sterile = mix(vec3(lu(col)), vec3(0.93,0.96,0.95), 0.72); // desaturate → cool white
sterile = mix(sterile, vec3(0.98), 0.35); // lift toward clinical bright
float tile = max(step(0.93, fract(vUv.x*64.0)), step(0.93, fract(vUv.y*36.0)));
sterile = mix(sterile, sterile*0.9, tile*0.6); // faint manufactured grid
col = mix(col, sterile, mmask*0.9);
}
}
// faint scanlines for vibe (constant, not scored)
col *= 0.92 + 0.08 * sin(vUv.y * 360.0 * 3.1415);
@ -238,6 +257,17 @@ void main(){
}
}
// ---- repair placard: The Correction signs one unit off the ledger. Small, perfect, screen
// space (uncorrupted), parked low-right so it never fights the shipment stamp for the centre.
if(u_repair > 0.001){
vec2 halfSize = vec2(0.150, 0.082)*u_repairScale;
vec2 ruv = (vUv - vec2(0.80, 0.80))/(2.0*halfSize) + 0.5;
if(ruv.x > 0.0 && ruv.x < 1.0 && ruv.y > 0.0 && ruv.y < 1.0){
vec4 rp = texture2D(u_repairTex, ruv);
col = mix(col, rp.rgb, rp.a*u_repair);
}
}
// ---- scram: the tape loses tracking for exactly one frame
if(u_scram > 0.5){
float halfH = 0.085;
@ -268,9 +298,9 @@ void main(){
type UniformKey =
| Param | 't' | 'drift' | 'brownout' | 'flash' | 'strain' | 'fever' | 'pallor' | 'moireSeed'
| 'wildlife' | 'wildlifeSeed'
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex'
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title';
| 'wildlife' | 'wildlifeSeed' | 'mite' | 'miteSeed'
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex' | 'repairTex'
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title' | 'repair' | 'repairScale';
export interface DrawState {
params: ParamSet;
@ -297,6 +327,10 @@ export interface DrawState {
wildlife: number;
/** where the swarm shimmer grows from, in screen uv */
wildlifeSeed: [number, number];
/** 0..1 hospital-white sterile creep around heavy parity-mite activity (the inverse of rot) */
mite: number;
/** where the sterile patch grows from, in screen uv */
miteSeed: [number, number];
/** 0..1 opacity of the ratification card */
card: number;
/** set when the card canvas has been repainted and needs re-upload */
@ -315,6 +349,12 @@ export interface DrawState {
scramY: number;
/** set when the stamp canvas has been repainted and needs re-upload */
stampDirty: boolean;
/** 0..1 opacity of the repair placard */
repair: number;
/** repair placard size multiplier (slight overshoot on impact, like the stamp) */
repairScale: number;
/** set when the repair canvas has been repainted and needs re-upload */
repairDirty: boolean;
}
export interface GlitchStack {
@ -378,11 +418,13 @@ export function createGlitchStack(
overlays.paintStamp('', 0, '#000'); // allocate the stamp texture at full size up front
overlays.paintCard('reel', '');
overlays.paintTitle();
overlays.paintRepair(''); // allocate the repair placard texture up front
const baseTex = makeTex(0, base.canvas);
const stampTex = makeTex(1, overlays.stampCanvas);
makeTex(2, overlays.scramCanvas);
const cardTex = makeTex(3, overlays.cardCanvas);
const titleTex = makeTex(4, overlays.titleCanvas);
const repairTex = makeTex(5, overlays.repairCanvas);
const uni = {} as Record<UniformKey, WebGLUniformLocation | null>;
uni.t = gl.getUniformLocation(prog, 't');
@ -397,10 +439,14 @@ export function createGlitchStack(
uni.moireSeed = gl.getUniformLocation(prog, 'u_moireSeed');
uni.wildlife = gl.getUniformLocation(prog, 'u_wildlife');
uni.wildlifeSeed = gl.getUniformLocation(prog, 'u_wildlifeSeed');
uni.mite = gl.getUniformLocation(prog, 'u_mite');
uni.miteSeed = gl.getUniformLocation(prog, 'u_miteSeed');
uni.stampAmt = gl.getUniformLocation(prog, 'u_stampAmt');
uni.stampScale = gl.getUniformLocation(prog, 'u_stampScale');
uni.scram = gl.getUniformLocation(prog, 'u_scram');
uni.scramY = gl.getUniformLocation(prog, 'u_scramY');
uni.repair = gl.getUniformLocation(prog, 'u_repair');
uni.repairScale = gl.getUniformLocation(prog, 'u_repairScale');
for (const p of PARAMS) uni[p] = gl.getUniformLocation(prog, `u_${p}`);
gl.uniform1i(gl.getUniformLocation(prog, 'tex'), 0);
@ -408,13 +454,16 @@ export function createGlitchStack(
gl.uniform1i(gl.getUniformLocation(prog, 'u_scramTex'), 2);
gl.uniform1i(gl.getUniformLocation(prog, 'u_cardTex'), 3);
gl.uniform1i(gl.getUniformLocation(prog, 'u_titleTex'), 4);
gl.uniform1i(gl.getUniformLocation(prog, 'u_repairTex'), 5);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform1f(uni.drift, 1);
return {
draw({ params, timeSec, drift, brownout, flash, era, chyron, strain, fever, pallor,
moireSeed, wildlife, wildlifeSeed, stampAmt, stampScale, scram, scramY, stampDirty,
moireSeed, wildlife, wildlifeSeed, mite, miteSeed,
stampAmt, stampScale, scram, scramY, stampDirty,
repair, repairScale, repairDirty,
card, cardDirty, title, titleDirty }) {
// Re-upload the clean feed only when it actually changed (timecode tick / era / chyron).
if (base.update({ timeMs: timeSec * 1000, era, chyron })) {
@ -437,6 +486,11 @@ export function createGlitchStack(
gl.bindTexture(gl.TEXTURE_2D, titleTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.titleCanvas);
}
if (repairDirty) {
gl.activeTexture(gl.TEXTURE0 + 5);
gl.bindTexture(gl.TEXTURE_2D, repairTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.repairCanvas);
}
gl.uniform1f(uni.t, timeSec);
gl.uniform1f(uni.drift, drift);
gl.uniform1f(uni.brownout, brownout);
@ -449,10 +503,14 @@ export function createGlitchStack(
gl.uniform2f(uni.moireSeed, moireSeed[0], moireSeed[1]);
gl.uniform1f(uni.wildlife, wildlife);
gl.uniform2f(uni.wildlifeSeed, wildlifeSeed[0], wildlifeSeed[1]);
gl.uniform1f(uni.mite, mite);
gl.uniform2f(uni.miteSeed, miteSeed[0], miteSeed[1]);
gl.uniform1f(uni.stampAmt, stampAmt);
gl.uniform1f(uni.stampScale, stampScale);
gl.uniform1f(uni.scram, scram ? 1 : 0);
gl.uniform1f(uni.scramY, scramY);
gl.uniform1f(uni.repair, repair);
gl.uniform1f(uni.repairScale, repairScale);
for (const p of PARAMS) gl.uniform1f(uni[p], params[p]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
},

View File

@ -31,6 +31,8 @@ const WOBBLE_DEPTH = 0.12;
const WOBBLE_RATE = [0.13, 0.19, 0.11, 0.23, 0.17, 0.29, 0.31, 0.07, 0.09, 0.37, 0.05];
/** Seconds a shipment stamp stays on the feed. */
const STAMP_LIFE = 0.4;
/** The repair placard lingers a touch longer than a shipment stamp — a signature, read at leisure. */
const REPAIR_LIFE = 0.55;
/** A burst of shipments must not become a strobe: at most this many stamps wait their turn. */
const STAMP_QUEUE_MAX = 2;
/** Round 3: one frame was a flinch, not a legend. Three frames is readable. */
@ -53,6 +55,7 @@ const TITLE_FADE = 0.6;
const FLASH_SECONDS = 0.5;
interface Stamp { name: string; count: number; color: string; age: number }
interface Repair { name: string; age: number }
interface Swarm { pos: { x: number; y: number }; size: number }
/**
@ -120,6 +123,9 @@ export function createScreenFX(): ScreenFX {
const stampQueue: Stamp[] = [];
let stamp: Stamp | null = null;
let stampDirty = false;
const repairQueue: Repair[] = [];
let repair: Repair | null = null;
let repairDirty = false;
let scramFrames = 0;
let scramY = 0.5;
@ -143,6 +149,9 @@ export function createScreenFX(): ScreenFX {
let wildlife = 0; // eased regional-shimmer amount
let wildlifeSeed: [number, number] = [0.5, 0.5];
const swarms = new Map<number, Swarm>(); // event-tracked fallback when no snapshot
let mite = 0; // eased hospital-white sterile creep amount
let miteSeed: [number, number] = [0.5, 0.5];
const mites = new Map<number, Swarm>(); // event-tracked parity-mite fallback
let titleAge = 0; // boot title sequence clock
let titleDirty = true; // painted once at stack creation; upload on the first frame
let flashAge = -1; // <0 = not flashing
@ -183,8 +192,11 @@ export function createScreenFX(): ScreenFX {
while (stampQueue.length > STAMP_QUEUE_MAX) stampQueue.shift();
}
return {
let liveCanvas: HTMLCanvasElement | null = null;
const api: ScreenFX = {
init(canvas: HTMLCanvasElement, data: GameData) {
liveCanvas = canvas;
const base = createBaseFeed();
stack = createGlitchStack(canvas, base, overlays);
for (const it of data.items) items.set(it.id, it);
@ -210,6 +222,19 @@ export function createScreenFX(): ScreenFX {
if (on) swarms.set(id, { pos, size: 0.7 });
else swarms.delete(id);
},
mite: (on: boolean, id = 1, pos = { x: -4, y: 5 }, size = 0.7) => {
if (on) mites.set(id, { pos, size });
else mites.delete(id);
},
repair: (item: string, count = 1) => {
for (let i = 0; i < count; i++) {
const cur = tally.get(item) ?? 0;
if (cur > 0) { tally.set(item, cur - 1); ledgerDirty = true; }
const def = items.get(item);
repairQueue.push({ name: def?.name ?? item.toUpperCase(), age: 0 });
while (repairQueue.length > STAMP_QUEUE_MAX) repairQueue.shift();
}
},
reset: () => {
tally.clear();
ledgerDirty = true;
@ -228,6 +253,10 @@ export function createScreenFX(): ScreenFX {
forcedEra = null;
swarms.clear();
wildlife = 0;
mites.clear();
mite = 0;
repair = null;
repairQueue.length = 0;
for (const p of PARAMS) eased[p] = 0;
},
research: (tech: string) => {
@ -246,6 +275,8 @@ export function createScreenFX(): ScreenFX {
fever: +fever.toFixed(3),
pallor: +pallor.toFixed(3),
wildlife: +wildlife.toFixed(3),
mite: +mite.toFixed(3),
repair: repair ? 1 : 0,
card: cardAge >= 0 ? 1 : 0,
title: +titleOpacity(titleAge).toFixed(3),
flash: +flashEnvelope(flashAge).toFixed(3),
@ -256,6 +287,11 @@ export function createScreenFX(): ScreenFX {
params: Object.fromEntries(PARAMS.map((p) => [p, +out[p].toFixed(3)])),
}),
escalationItems: ESCALATION_ITEMS,
// Verification driver: a hidden harness tab throttles rAF, so pump the REAL module with
// a synthetic timebase and read pixels in the same JS task (prior-round contact-sheet
// trick — a hidden tab clears the WebGL buffer after compositing otherwise).
frame: (ms: number, snap?: SimSnapshot) => api.frame(ms, snap),
canvas: () => liveCanvas,
};
},
@ -278,6 +314,25 @@ export function createScreenFX(): ScreenFX {
// (debug hook). When a snapshot IS present, frame() reads it as the source of truth.
if (e.on === 'spawned') swarms.set(e.id, { pos: e.pos, size: 0.6 });
else swarms.delete(e.id);
} else if (e.kind === 'wildlife' && e.wildlife === 'parity-mite') {
// Parity-mites drive the sterile creep. Same fallback contract as the swarms above:
// snapshot.wildlife is truth when present, these events cover the no-snapshot case.
if (e.on === 'spawned') mites.set(e.id, { pos: e.pos, size: 0.6 });
else mites.delete(e.id);
} else if (e.kind === 'repaired') {
// The Correction corrected one unit of your work. This is THEFT, not decay: ease the
// LEDGER down by exactly that item's per-unit contribution (a real, permanent removal —
// unlike boredom, which only fades the display and leaves the tally intact). Then let
// it sign the job with a small green-tick placard. We deliberately do NOT refresh the
// boredom clock: a factory being quietly eaten is not a factory that is producing.
const cur = tally.get(e.item) ?? 0;
if (cur > 0) {
tally.set(e.item, cur - 1);
ledgerDirty = true;
}
const def = items.get(e.item);
repairQueue.push({ name: def?.name ?? e.item.toUpperCase(), age: 0 });
while (repairQueue.length > STAMP_QUEUE_MAX) repairQueue.shift();
}
}
},
@ -339,6 +394,20 @@ export function createScreenFX(): ScreenFX {
if (worst) wildlifeSeed = seedFromPos(worst.pos);
wildlife = ease(wildlife, worst ? clamp01(worst.size) : 0, dt, 1.0);
// Mite sterility: same worst-of pattern, but for parity-mites. The sterile creep grows
// around the heaviest infestation; snapshot is truth when present, events are the fallback.
let worstMite: Swarm | null = null;
if (live) {
for (const w of live) {
if (w.kind !== 'parity-mite') continue;
if (!worstMite || w.size > worstMite.size) worstMite = { pos: w.pos, size: w.size };
}
} else {
for (const m of mites.values()) if (!worstMite || m.size > worstMite.size) worstMite = m;
}
if (worstMite) miteSeed = seedFromPos(worstMite.pos);
mite = ease(mite, worstMite ? clamp01(worstMite.size) : 0, dt, 1.0);
const timeSec = timeMs / 1000;
const displayTotal = total * fresh;
const wobble =
@ -375,6 +444,23 @@ export function createScreenFX(): ScreenFX {
if (stamp.age >= STAMP_LIFE) stamp = null;
}
// Repair placard lifecycle — its own slot so it never fights the shipment stamp. Same
// punch-in-hold-fade shape, held a touch longer because it is a signature to be read.
if (!repair && repairQueue.length) {
repair = repairQueue.shift()!;
overlays.paintRepair(repair.name);
repairDirty = true;
}
let repairAmt = 0;
let repairScale = 1;
if (repair) {
const a = repair.age / REPAIR_LIFE;
repairAmt = Math.min(1, a / 0.1) * (1 - smoothstep(0.6, 1, a));
repairScale = 1.12 - 0.12 * Math.min(1, a / 0.1); // small, perfect, slight impact overshoot
repair.age += dt;
if (repair.age >= REPAIR_LIFE) repair = null;
}
// The card holds at full opacity and then cuts — no fade. The corruption crashes back in
// rather than seeping back; a dissolve would make it look negotiable.
let card = 0;
@ -400,16 +486,20 @@ export function createScreenFX(): ScreenFX {
stack.draw({
params: out, timeSec, drift: 1, brownout, flash, era, chyron,
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed,
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed, mite, miteSeed,
stampAmt, stampScale, scram: scramFrames > 0, scramY, stampDirty,
repair: repairAmt, repairScale, repairDirty,
card, cardDirty, title, titleDirty,
});
if (scramFrames > 0) scramFrames--;
stampDirty = false;
repairDirty = false;
cardDirty = false;
titleDirty = false;
frameCost = frameCost * 0.95 + (performance.now() - t0) * 0.05;
},
};
return api;
}

View File

@ -12,6 +12,9 @@ export const SCRAM_W = 512;
export const SCRAM_H = 64;
export const CARD_W = 640;
export const CARD_H = 360;
/** The repair placard is small on purpose — it's a signature, not a broadcast. */
export const REPAIR_W = 224;
export const REPAIR_H = 112;
/**
* The eras as the codex names them (§1). The card announces the STANDARD, not the tech id
@ -34,12 +37,19 @@ export interface Overlays {
scramCanvas: HTMLCanvasElement;
cardCanvas: HTMLCanvasElement;
titleCanvas: HTMLCanvasElement;
repairCanvas: HTMLCanvasElement;
/** Repaint the stamp for a landed shipment. OSHA-placard deadpan, in the item's chip colour. */
paintStamp(name: string, count: number, color: string): void;
/** Repaint the ratification card. The Correction's uniform: hospital white, green tick. */
paintCard(era: string, tech: string): void;
/** Paint the boot title card. Alpha channel matters — it dissolves over the feed. */
paintTitle(): void;
/**
* Repaint the repair placard The Correction signing one unit of your work off the ledger.
* Small, perfect (no crooked stamp), hospital white with the faction green tick: deliberately
* the opposite of the hand-thumped, chip-coloured shipment stamp. Rot vs theft, made legible.
*/
paintRepair(name: string): void;
}
function roundRect(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
@ -95,11 +105,17 @@ export function createOverlays(): Overlays {
titleCanvas.height = CARD_H;
const tg = titleCanvas.getContext('2d')!;
const repairCanvas = document.createElement('canvas');
repairCanvas.width = REPAIR_W;
repairCanvas.height = REPAIR_H;
const rg = repairCanvas.getContext('2d')!;
return {
stampCanvas,
scramCanvas,
cardCanvas,
titleCanvas,
repairCanvas,
paintTitle() {
// The game finally says its own name. A CRT burn-in: chunky phosphor letters with a
@ -220,5 +236,50 @@ export function createOverlays(): Overlays {
sg.fillText('SHIPPED / NO QUESTIONS', 22, 34);
sg.restore();
},
paintRepair(name: string) {
// The Correction's uniform (codex §8): hospital white, green tick, and the placid
// confidence of a compliance stamp. NOT rotated — where the shipment stamp is thumped
// on by a tired hand, this is machined on square. That contrast is the whole read.
rg.clearRect(0, 0, REPAIR_W, REPAIR_H);
// clean white placard with a thin green rule
rg.fillStyle = '#f2f5f2';
roundRect(rg, 6, 6, REPAIR_W - 12, REPAIR_H - 12, 5);
rg.fill();
rg.strokeStyle = '#2f7d3f';
rg.lineWidth = 3;
roundRect(rg, 6, 6, REPAIR_W - 12, REPAIR_H - 12, 5);
rg.stroke();
// the green tick — the faction signature
rg.strokeStyle = '#2f7d3f';
rg.lineWidth = 5;
rg.lineCap = 'round';
rg.lineJoin = 'round';
rg.beginPath();
rg.moveTo(26, 40);
rg.lineTo(38, 52);
rg.lineTo(60, 26);
rg.stroke();
rg.textBaseline = 'middle';
rg.fillStyle = '#12160f';
rg.font = "bold 20px 'Courier New', monospace";
rg.fillText('CORRECTED', 74, 40);
// the item that was quietly reverted — shrink long codex names to fit the placard
rg.fillStyle = '#5c6b5c';
let size = 15;
rg.font = `${size}px 'Courier New', monospace`;
while (rg.measureText(name).width > REPAIR_W - 40 && size > 8) {
size -= 1;
rg.font = `${size}px 'Courier New', monospace`;
}
rg.fillText(name, 26, 74);
rg.fillStyle = '#8a998a';
rg.font = "9px 'Courier New', monospace";
rg.fillText('THE CORRECTION · NO ACTION REQUIRED', 26, 94);
},
};
}