diff --git a/fktry/src/ui/fixture.ts b/fktry/src/ui/fixture.ts deleted file mode 100644 index 389c699..0000000 --- a/fktry/src/ui/fixture.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * LANE-UI — dev-only fake sim, for verifying the HUD before LANE-SIM lands. - * - * LANE-SIM's stub reports zero entities, zero bandwidth and no events forever, so with - * the real snapshot there is literally nothing on screen to check: an empty inspector - * and a 0/0 meter prove nothing. This module synthesises a plausible snapshot from the - * real tick so every surface can be seen doing its job in a browser. - * - * INERT unless the URL says `?uifixture`. It never dispatches Commands and never touches - * another lane; when the real sim arrives, drop the query param (or this file). - * - * See NOTES — this is scaffolding, not a feature. - */ -import type { GameData, EntityState, SimEvent, SimSnapshot } from '../contracts'; - -export function fixtureEnabled(): boolean { - return typeof location !== 'undefined' && new URLSearchParams(location.search).has('uifixture'); -} - -export interface Fixture { - snapshot(paused: boolean): SimSnapshot; - /** Events for ticks that have elapsed since the last call. */ - drain(): SimEvent[]; -} - -const SHIP_EVERY = 90; // one melt every 3s at 30tps -const MS_PER_TICK = 1000 / 30; -/** Never replay more than this many ticks of events in one drain (backgrounded tab). */ -const MAX_CATCHUP = 300; - -export function createFixture(data: GameData): Fixture { - const entities: EntityState[] = data.machines.map((m, i) => ({ - id: i + 1, - def: m.id, - pos: { x: i * 3 - 9, y: i % 2 === 0 ? -2 : 2 }, - dir: (i % 4) as 0 | 1 | 2 | 3, - recipe: m.recipes[0] ?? null, - progress: 0, - inputBuf: {}, - outputBuf: {}, - jammed: null, - heat: 0, - })); - - const recipes = new Map(data.recipes.map((r) => [r.id, r])); - const jamVictim = entities.find((e) => e.def === 'mosh-reactor') ?? entities[0]; - let lastTick = -1; - let brownoutOn = false; - - // Wall clock, not the passed-in sim tick. rAF stops in a backgrounded tab, so a - // tick-driven fixture crawls and you can never actually watch the brownout arrive. - // This is a fake sim for eyeballing the HUD; nothing here has to be deterministic. - const t0 = performance.now(); - const nowTick = () => Math.floor((performance.now() - t0) / MS_PER_TICK); - - function brownoutAt(tick: number) { - return tick % 600 > 480; // ~4s of pain every 20s - } - - return { - snapshot(paused) { - const tick = nowTick(); - for (let i = 0; i < entities.length; i++) { - const e = entities[i]; - const r = e.recipe ? recipes.get(e.recipe) : null; - e.progress = r ? ((tick + i * 13) % r.ticks) / r.ticks : 0; - // The mosh reactor gets to be the one that's unhappy, for the amber readout. - e.jammed = e.def === 'mosh-reactor' && tick % 300 > 240 ? 'output buffer full' : null; - e.heat = e.def === 'quantizer' ? Math.min(1, (tick % 900) / 900) : 0; - if (r) { - e.inputBuf = {}; - for (const [item, need] of Object.entries(r.inputs)) { - e.inputBuf[item] = (tick + i) % (need + 2); - } - e.outputBuf = {}; - for (const item of Object.keys(r.outputs)) { - e.outputBuf[item] = Math.floor(((tick + i * 7) % 200) / 40); - } - } - } - - const shipped = Math.floor(tick / SHIP_EVERY); - const out = brownoutAt(tick); - const draw = 6 + (out ? 22 : Math.abs(((tick / 60) % 4) - 2) * 7); - - const shippedTotal: Record = {}; - if (shipped > 0) { - shippedTotal.melt = shipped; - shippedTotal['anchor-slab'] = shipped; - } - - return { - tick, - paused, - entities, - beltItems: [], - bandwidth: { gen: 20, draw, stored: out ? 0 : 40 + Math.sin(tick / 40) * 20, brownout: out }, - shippedTotal, - activeCommission: data.commissions[0]?.id ?? null, - commissionProgress: { melt: shipped }, - }; - }, - - drain() { - const tick = nowTick(); - const events: SimEvent[] = []; - if (lastTick < 0) { - lastTick = tick; - return events; - } - if (tick - lastTick > MAX_CATCHUP) lastTick = tick - MAX_CATCHUP; - for (let t = lastTick + 1; t <= tick; t++) { - if (t % SHIP_EVERY === 0 && t > 0) { - events.push({ kind: 'shipped', item: 'melt', count: 1, tick: t }); - // Recurring rather than one-shot: the stamp is a 1.8s animation, and a - // fixture that fires it once at tick 90 is unobservable in practice. - if (t % (SHIP_EVERY * 3) === 0) { - const c = data.commissions[0]; - if (c) events.push({ kind: 'commissionDone', commission: c.id, tick: t }); - } - } - if (t % 300 === 241 && jamVictim) { - events.push({ kind: 'jammed', entity: jamVictim.id, reason: 'output buffer full', tick: t }); - } - const out = brownoutAt(t); - if (out !== brownoutOn) { - brownoutOn = out; - events.push({ kind: 'brownout', on: out, tick: t }); - } - } - lastTick = tick; - return events; - }, - }; -} diff --git a/fktry/src/ui/index.ts b/fktry/src/ui/index.ts index 7cd4147..a34a92b 100644 --- a/fktry/src/ui/index.ts +++ b/fktry/src/ui/index.ts @@ -10,10 +10,9 @@ import type { GameData, MachineDef, SimEvent, SimSnapshot, UI, UIBus } from '../contracts'; import { createBuildBar } from './buildbar'; import { createFax } from './fax'; -import { createFixture, fixtureEnabled, type Fixture } from './fixture'; import { createHotkeys } from './hotkeys'; import { createInspector } from './inspector'; -import { createRoster, entityAt, resolvePickTile, syncRoster, type PickTile } from './pick'; +import { createRoster, entityAt, syncRoster } from './pick'; import { createSelection } from './selection'; import { injectStyle } from './style'; import { createTopStrip } from './topstrip'; @@ -27,35 +26,16 @@ export function createUI(): UI { let fax: ReturnType; let toasts: ReturnType; - let pickTile: PickTile | null = null; - let fixture: Fixture | null = null; - /** Our own copy of the entity list; safe to read between frames. See pick.ts. */ const roster = createRoster(); let paused = false; - let manifestIndex = -1; - - function rosterIndexOf(id: number): number { - for (let i = 0; i < roster.length; i++) if (roster.entries[i].id === id) return i; - return -1; - } function machineName(entityId: number): string { - const i = rosterIndexOf(entityId); - if (i < 0) return `UNIT ${entityId}`; - return machines.get(roster.entries[i].def)?.name ?? `UNIT ${entityId}`; - } - - /** - * TEMPORARY: nothing reachable from the UI can turn a click into a tile (see pick.ts), - * so TAB walks the unit manifest instead. That keeps the inspector reachable with no - * console, which is the round's definition of done. Delete once the CONTRACT REQUEST - * for `UIBus.pickTile` lands and clicking a machine works. - */ - function cycleManifest() { - if (roster.length === 0) return; - manifestIndex = (manifestIndex + 1) % roster.length; - inspector.open(roster.entries[manifestIndex].id); + for (let i = 0; i < roster.length; i++) { + const r = roster.entries[i]; + if (r.id === entityId) return machines.get(r.def)?.name ?? `UNIT ${entityId}`; + } + return `UNIT ${entityId}`; } return { @@ -66,20 +46,17 @@ export function createUI(): UI { const sel = createSelection(bus); top = createTopStrip(data); build = createBuildBar(data, sel); - inspector = createInspector(data); + inspector = createInspector(data, bus); fax = createFax(data); toasts = createToasts(); root.append(top.el, build.el, inspector.el, fax.el, toasts.el); - if (fixtureEnabled()) { - fixture = createFixture(data); - console.info('[ui] fixture mode: the HUD is being driven by a fake sim (?uifixture)'); - } - // ------------------------------------------------------------------ keyboard const keys = createHotkeys(); for (let n = 1; n <= 9; n++) keys.bind(String(n), () => build.selectSlot(n)); + keys.bind('[', () => build.pageBy(-1)); + keys.bind(']', () => build.pageBy(1)); keys.bind('r', () => sel.rotate()); keys.bind('escape', () => { // One key, two jobs, in the order the player expects: drop the tool first. @@ -87,7 +64,6 @@ export function createUI(): UI { else inspector.close(); }); keys.bind(' ', () => bus.dispatch({ kind: 'setPaused', paused: !paused })); - keys.bind('tab', cycleManifest); keys.attach(); // ------------------------------------------------------------------- pointer @@ -107,26 +83,16 @@ export function createUI(): UI { if (e.button !== 0 || !inWorld(e.target)) return; if (sel.get()) return; // placing, not inspecting — that click is main.ts's - pickTile ??= resolvePickTile(); // renderer may register late - const tile = pickTile?.(e.clientX, e.clientY); + const tile = bus.pickTile(e.clientX, e.clientY); if (!tile) return; const hit = entityAt(roster, machines, tile); - if (hit === null) { - inspector.close(); - } else { - inspector.open(hit); - manifestIndex = rosterIndexOf(hit); - } + if (hit === null) inspector.close(); + else inspector.open(hit); }); }, update(snap: SimSnapshot, events: SimEvent[]) { - if (fixture) { - snap = fixture.snapshot(snap.paused); - events = fixture.drain(); - } - paused = snap.paused; syncRoster(roster, snap); diff --git a/fktry/src/ui/pick.ts b/fktry/src/ui/pick.ts index d4cd0e7..10c5fa0 100644 --- a/fktry/src/ui/pick.ts +++ b/fktry/src/ui/pick.ts @@ -1,27 +1,11 @@ /** * LANE-UI — figuring out which unit the player clicked. * - * PROBLEM: `UIBus` gives us `dispatch` + `selectedBuild`, and `ui.init(root, data, bus)` - * never sees the Renderer — so the UI has no access to `renderer.pickTile`, which is the - * only thing that knows the camera. CONTRACT REQUEST filed in NOTES (add `pickTile` to - * UIBus, or pass the renderer to `ui.init`). - * - * Until then: we opportunistically use `window.__fktryRenderer.pickTile` if LANE-RENDER - * happens to expose one, and the inspector otherwise falls back to TAB-cycling the unit - * manifest (see index.ts). Tile→entity resolution below is ours either way. + * `bus.pickTile` (contracts v2) turns a client point into a tile; tile→entity resolution + * below is ours. Together they are click-to-inspect. */ import type { Dir, MachineDef, SimSnapshot, Vec2 } from '../contracts'; -export type PickTile = (clientX: number, clientY: number) => Vec2 | null; - -export function resolvePickTile(): PickTile | null { - const r = (globalThis as any).__fktryRenderer; - if (r && typeof r.pickTile === 'function') { - return (x, y) => r.pickTile(x, y) ?? null; - } - return null; -} - /** * Flattened copy of the entities in a snapshot: exactly the fields the UI needs between * frames (hit-testing a click, naming a jammed unit in a toast).