[orchestrator] round 5 (MEGA) review: integration fixes — the radio works in prod
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 <noreply@anthropic.com>
This commit is contained in:
parent
0cc80d3c82
commit
f35f159b72
@ -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) */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -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<typeof createMasterChain> | 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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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<GameData, 'seams'> & { seams?: SeamMap | SeamDef[] };
|
||||
|
||||
const item = (id: string): ItemDef => ({ id, name: id, codex: 'fixture', tier: 0, color: '#fff' });
|
||||
const mach = (m: Partial<MachineDef> & Pick<MachineDef, 'id' | 'kind'>): MachineDef => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user