[ui] dev-only fake sim behind ?uifixture
The sim stub reported an empty world, so there was nothing on screen to verify the HUD against: an empty inspector and a 0/0 meter prove nothing. This synthesises a plausible snapshot -- entities mid-craft, a jam, heat, shipments, a recurring brownout and commission -- so every surface can be watched doing its job in a browser. Runs on a wall clock rather than the sim tick: rAF stops in a backgrounded tab, and a tick-driven fixture crawls too slowly to ever reach the brownout window. It is a fake sim for eyeballing chrome; nothing here is or needs to be deterministic. Inert without the query param, dispatches no Commands, touches no other lane. Delete once the real sim can be driven to a brownout by hand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c18309f464
commit
df1eee7249
135
fktry/src/ui/fixture.ts
Normal file
135
fktry/src/ui/fixture.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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<string, number> = {};
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user