From f35f159b72746d58dbc911e9eecd1d13edbd96c9 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 14:32:56 +1000 Subject: [PATCH] =?UTF-8?q?[orchestrator]=20round=205=20(MEGA)=20review:?= =?UTF-8?q?=20integration=20fixes=20=E2=80=94=20the=20radio=20works=20in?= =?UTF-8?q?=20prod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: 5/5 lanes pass, 446/446, verified live end-to-end: fax ladder opens the game, floor + seam glints render (RENDER's colorspace find), fossil excavates at the seeded position, tuner dock slides in, radio locks 88.1 FKTRY FM and AM 640 THE STANDARD. The pipe is real. Integration fixes (the cross-lane seams no lane owned): - audio: radio handle exposed outside the DEV gate — the dock was silent in PROD (UI probed __fktrySpeaker, SCREEN's getRadio read a DEV-only object; both halves correct, handshake unowned). main.ts now publishes __fktrySpeaker on first gesture. - contracts v7: GameData.seams (SeamDef/SeamMap lifted from SIM) + main.ts imports seams.json — seams were inert in the real game (SIM's request). - index.html RULING: #screen stays interactive (UI's click-to-enlarge landed after RENDER's dead-zone probe — overtaken by events); cursor:pointer added. - main.ts: ghost refreshes every frame (stale demolition tint on Esc — UI finding); __fktryDemo uses the grantResearch command (testkit retired). - relic.test.ts Seamed type vs the new contract field (my breakage, my fix). Co-Authored-By: Claude Fable 5 --- fktry/index.html | 6 +++++- fktry/src/audio/index.ts | 8 +++++++- fktry/src/contracts.ts | 16 ++++++++++++++++ fktry/src/main.ts | 27 ++++++++++++++++++++------- fktry/src/sim/relic.test.ts | 4 +++- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/fktry/index.html b/fktry/index.html index 2901f7b..f9ed2fd 100644 --- a/fktry/index.html +++ b/fktry/index.html @@ -9,7 +9,11 @@ font-family:"Courier New", ui-monospace, monospace; } #game { position:fixed; inset:0; } /* LANE-RENDER mounts here */ #screen { position:fixed; top:10px; left:50%; transform:translateX(-50%); - width:320px; height:180px; border:1px solid #2a2440; z-index:5; } /* LANE-SCREEN canvas */ + width:320px; height:180px; border:1px solid #2a2440; z-index:5; + cursor:pointer; } /* LANE-SCREEN canvas. RULING (round 5 review): as of UI's + click-to-enlarge this is a legitimate interactive surface, not a dead zone — + RENDER's pointer-events request is overtaken by events. World tiles under it + are occluded like any panel; don't build under the sky. */ #ui { position:fixed; inset:0; pointer-events:none; z-index:10; } /* LANE-UI root (children re-enable pointer-events) */ diff --git a/fktry/src/audio/index.ts b/fktry/src/audio/index.ts index b15cf7d..e85f8c4 100644 --- a/fktry/src/audio/index.ts +++ b/fktry/src/audio/index.ts @@ -45,6 +45,11 @@ function isRealSeam(x: number, y: number): boolean { /** Sounds that must not machine-gun when the sim emits a burst in one tick. */ const MIN_GAP = { thunk: 0.03, chunk: 0.05 }; +/** The live radio handle for getRadio(). Module-level — set by build(), which runs on the + * first user gesture — so the control surface exists in PRODUCTION, not just behind the + * DEV debug object (orchestrator round-5 integration fix: the dock was silent in prod). */ +let liveRadio: Radio | null = null; + export function createAudioFX(): AudioFX { let ctx: BaseAudioContext | null = null; let master: ReturnType | null = null; @@ -122,6 +127,7 @@ export function createAudioFX(): AudioFX { ctx = new AC(); master = createMasterChain(ctx); radio = createRadio(ctx, STATIONS, isRealSeam); + liveRadio = radio as Radio; ambience = createAmbience(ctx); radio.out.connect(master.input); ambience.out.connect(master.input); @@ -207,5 +213,5 @@ export function createAudioFX(): AudioFX { * the audio graph. */ export function getRadio(): Radio | null { - return (window as any).__fktryAudio?.radio ?? null; + return liveRadio; } diff --git a/fktry/src/contracts.ts b/fktry/src/contracts.ts index e12fbcc..fa950c8 100644 --- a/fktry/src/contracts.ts +++ b/fktry/src/contracts.ts @@ -83,12 +83,28 @@ export interface CommissionDef { repeat?: boolean; // v2: standing order — re-activates after completion } +// v7: lifted verbatim from SIM's round-5 mirror — data/seams.json's shape. +export interface SeamDef { + id: string; + rect: { x: number; y: number; w: number; h: number }; + era?: string; + resource?: string; + richness?: number; + hidden?: boolean; // treasure — only the numbers station may reveal it +} +export interface SeamMap { + /** DATA keeps this corner seam-free so the seed-derived relic always lands somewhere. */ + relicReserve?: { x: number; y: number; w: number; h: number }; + seams: SeamDef[]; +} + export interface GameData { items: ItemDef[]; machines: MachineDef[]; recipes: RecipeDef[]; tech: TechDef[]; commissions: CommissionDef[]; + seams?: SeamMap; // v7: wired by main.ts from data/seams.json; extractors need ore } // ---------------------------------------------------------------- live sim state diff --git a/fktry/src/main.ts b/fktry/src/main.ts index 43df33a..0db4806 100644 --- a/fktry/src/main.ts +++ b/fktry/src/main.ts @@ -7,15 +7,16 @@ import { createSim } from './sim'; import { createRenderer } from './render'; import { createUI } from './ui'; import { createScreenFX } from './screen'; -import { createAudioFX } from './audio'; +import { createAudioFX, getRadio } from './audio'; import items from '../data/items.json'; import machines from '../data/machines.json'; import recipes from '../data/recipes.json'; import tech from '../data/tech.json'; import commissions from '../data/commissions.json'; +import seams from '../data/seams.json'; -const data = { items, machines, recipes, tech, commissions } as unknown as GameData; +const data = { items, machines, recipes, tech, commissions, seams } as unknown as GameData; const sim = createSim(); const renderer = createRenderer(); @@ -56,13 +57,21 @@ async function boot() { screen.init(document.getElementById('screen') as HTMLCanvasElement, data); audio.init(data); // browsers gate AudioContext on a user gesture — unlock on the first one - addEventListener('pointerdown', () => audio.unlock(), { once: true }); + addEventListener('pointerdown', () => { + audio.unlock(); + // UI's tuner dock probes window.__fktrySpeaker.radio (its documented handshake). + (window as any).__fktrySpeaker = { radio: getRadio() }; + }, { once: true }); // click-to-place / hover ghost glue const game = document.getElementById('game')!; - game.addEventListener('pointermove', (e) => { + // Ghost refresh runs on move AND every frame (a keyboard disarm — Esc — used to leave + // a stale demolition tint on screen until the mouse moved; round-5 UI finding). + let lastPointer: { x: number; y: number } | null = null; + function refreshGhost() { + if (!lastPointer) return; const sel = bus.selection!(); - const tile = renderer.pickTile(e.clientX, e.clientY); + const tile = renderer.pickTile(lastPointer.x, lastPointer.y); if (sel?.mode === 'remove') { renderer.setGhost(null, tile, 0, 'remove'); } else if (sel?.mode === 'build') { @@ -70,6 +79,10 @@ async function boot() { } else { renderer.setGhost(null, tile, 0, 'build'); // inspect / no selection: no ghost } + } + game.addEventListener('pointermove', (e) => { + lastPointer = { x: e.clientX, y: e.clientY }; + refreshGhost(); }); game.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; // pan/context buttons never place @@ -87,6 +100,7 @@ async function boot() { while (acc >= MS_PER_TICK) { sim.tick(); acc -= MS_PER_TICK; } const snap = sim.snapshot(); const events = sim.drainEvents(); + refreshGhost(); renderer.render(snap, acc / MS_PER_TICK); ui.update(snap, events); screen.onEvents(events); @@ -103,9 +117,8 @@ async function boot() { if (import.meta.env.DEV) { (window as any).__fktryDemo = async () => { const { referenceFactory, referenceFactoryTech } = await import('./sim/reference'); - const { grantResearch } = await import('./sim/testkit'); const techs = referenceFactoryTech(data); - grantResearch(sim, techs); + sim.enqueue({ kind: 'grantResearch', tech: techs }); // v5 command; queued first const cmds = referenceFactory(data); cmds.forEach((c) => sim.enqueue(c)); return `${techs.length} techs granted, ${cmds.length} commands dispatched`; diff --git a/fktry/src/sim/relic.test.ts b/fktry/src/sim/relic.test.ts index 831f7cf..dd82aef 100644 --- a/fktry/src/sim/relic.test.ts +++ b/fktry/src/sim/relic.test.ts @@ -25,7 +25,9 @@ const DATA = { items, machines, recipes, tech, commissions } as unknown as GameD const N: Dir = 0, E: Dir = 1; /** GameData plus the seam map — the shape once the orchestrator wires GameData.seams. */ -type Seamed = GameData & { seams?: SeamMap | SeamDef[] }; +// Omit first: contracts v7 types GameData.seams as SeamMap, but the sim deliberately +// also accepts a bare rect list (defensive path) — this test covers both forms. +type Seamed = Omit & { seams?: SeamMap | SeamDef[] }; const item = (id: string): ItemDef => ({ id, name: id, codex: 'fixture', tier: 0, color: '#fff' }); const mach = (m: Partial & Pick): MachineDef => ({