Compare commits
No commits in common. "e20ce1341bbb2c572cd98b9da370662a27f785ec" and "eafd31f263f63051cb450bebe6ecaef2818f6c66" have entirely different histories.
e20ce1341b
...
eafd31f263
@ -503,84 +503,3 @@ 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.
@ -145,35 +145,6 @@ 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({
|
||||
|
||||
@ -34,11 +34,8 @@ uniform float u_strain, u_fever, u_pallor;
|
||||
uniform vec2 u_moireSeed;
|
||||
uniform float u_wildlife;
|
||||
uniform vec2 u_wildlifeSeed;
|
||||
uniform float u_mite;
|
||||
uniform vec2 u_miteSeed;
|
||||
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex, u_repairTex;
|
||||
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex;
|
||||
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){
|
||||
@ -228,22 +225,6 @@ 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);
|
||||
|
||||
@ -257,17 +238,6 @@ 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;
|
||||
@ -298,9 +268,9 @@ void main(){
|
||||
|
||||
type UniformKey =
|
||||
| Param | 't' | 'drift' | 'brownout' | 'flash' | 'strain' | 'fever' | 'pallor' | 'moireSeed'
|
||||
| 'wildlife' | 'wildlifeSeed' | 'mite' | 'miteSeed'
|
||||
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex' | 'repairTex'
|
||||
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title' | 'repair' | 'repairScale';
|
||||
| 'wildlife' | 'wildlifeSeed'
|
||||
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex'
|
||||
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title';
|
||||
|
||||
export interface DrawState {
|
||||
params: ParamSet;
|
||||
@ -327,10 +297,6 @@ 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 */
|
||||
@ -349,12 +315,6 @@ 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 {
|
||||
@ -418,13 +378,11 @@ 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');
|
||||
@ -439,14 +397,10 @@ 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);
|
||||
@ -454,16 +408,13 @@ 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, mite, miteSeed,
|
||||
stampAmt, stampScale, scram, scramY, stampDirty,
|
||||
repair, repairScale, repairDirty,
|
||||
moireSeed, wildlife, wildlifeSeed, stampAmt, stampScale, scram, scramY, stampDirty,
|
||||
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 })) {
|
||||
@ -486,11 +437,6 @@ 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);
|
||||
@ -503,14 +449,10 @@ 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);
|
||||
},
|
||||
|
||||
@ -31,8 +31,6 @@ 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. */
|
||||
@ -55,7 +53,6 @@ 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 }
|
||||
|
||||
/**
|
||||
@ -123,9 +120,6 @@ 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;
|
||||
|
||||
@ -149,9 +143,6 @@ 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
|
||||
@ -192,11 +183,8 @@ export function createScreenFX(): ScreenFX {
|
||||
while (stampQueue.length > STAMP_QUEUE_MAX) stampQueue.shift();
|
||||
}
|
||||
|
||||
let liveCanvas: HTMLCanvasElement | null = null;
|
||||
|
||||
const api: ScreenFX = {
|
||||
return {
|
||||
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);
|
||||
@ -222,19 +210,6 @@ 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;
|
||||
@ -253,10 +228,6 @@ 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) => {
|
||||
@ -275,8 +246,6 @@ 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),
|
||||
@ -287,11 +256,6 @@ 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,
|
||||
};
|
||||
},
|
||||
|
||||
@ -314,25 +278,6 @@ 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();
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -394,20 +339,6 @@ 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 =
|
||||
@ -444,23 +375,6 @@ 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;
|
||||
@ -486,20 +400,16 @@ export function createScreenFX(): ScreenFX {
|
||||
|
||||
stack.draw({
|
||||
params: out, timeSec, drift: 1, brownout, flash, era, chyron,
|
||||
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed, mite, miteSeed,
|
||||
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed,
|
||||
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;
|
||||
}
|
||||
|
||||
@ -12,9 +12,6 @@ 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 —
|
||||
@ -37,19 +34,12 @@ 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) {
|
||||
@ -105,17 +95,11 @@ 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
|
||||
@ -236,50 +220,5 @@ 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user