THE GHOST: '▶ watch him do it' — every day driver replays as a live demo
The dev harness ships in production now, because it turned out to be the
best teacher in the building. Any ticket whose day has a registered driver
grows a '▶ watch him do it' button: the same deterministic script that
verifies the day in CI performs it live on the real scene, real input,
real judge — then the judge waves it off ('A demonstration. None of it
counts. Now you.'), the save restores from a snapshot, and the day deals
again for the player to play for real.
The hard part was time. A demo must ride the app's own rAF loop to be
watchable — but hidden tabs pause rAF AND throttle setTimeout down to one
fire per MINUTE, which froze the show mid-egg. F(n) now has three gears:
sync (verification), rAF-riding (visible demo), and fast-forward when
document.hidden — no audience, no theatre; he finishes the dish while
you're away. Also fixed a straggler from the async conversion: the
egg-day float loop fired six un-awaited drags concurrently, so nothing
floated, the liar egg passed as safe, and the demo cracked it into the
bowl (5.6/10 with a fart). Awaited, the glass does its work.
Verified live, hidden-tab and visible: demos score 15:9.4 16:8.0 17:10.0
19:9.0 20:10.0 21:9.9 22:9.9, localStorage untouched during playback,
NEXT restores the snapshot and replays the day for real.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
56389dabab
commit
322aeec16d
172
src/dev.ts
172
src/dev.ts
@ -32,6 +32,45 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
const run = (frames: number, dt = 1 / 60) => {
|
||||
for (let i = 0; i < frames; i++) app.step(dt);
|
||||
};
|
||||
// DEMO PLAYBACK: when on, `F(n)` does not step at all — it waits for the
|
||||
// app's own rAF loop to advance n frames, so the same driver that verifies a
|
||||
// day headless becomes a watchable, real-time performance of it.
|
||||
let demoPlayback = false;
|
||||
let tick = 0;
|
||||
app.onTick(() => tick++);
|
||||
const F = (n: number, dt = 1 / 60): Promise<void> => {
|
||||
// No audience, no theatre: hidden tabs throttle both rAF and timers (down
|
||||
// to 1 fire/min), so a "real-time" demo would hang. Fast-forward instead —
|
||||
// he finishes the dish while you're away.
|
||||
if (!demoPlayback || document.hidden) {
|
||||
run(n, dt);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const until = tick + n;
|
||||
return new Promise((res) => {
|
||||
// Ride the app's own rAF loop; if it stalls (tab just hidden, or the
|
||||
// loop is throttled), step what the wall clock owes — or finish the
|
||||
// remainder synchronously once the tab reports hidden.
|
||||
const pump = () => {
|
||||
if (tick >= until) return res();
|
||||
if (document.hidden) {
|
||||
run(until - tick, dt);
|
||||
return res();
|
||||
}
|
||||
const before = tick;
|
||||
const t0 = performance.now();
|
||||
window.setTimeout(() => {
|
||||
if (tick === before) {
|
||||
const owed = Math.round((performance.now() - t0) / (1000 / 60));
|
||||
const steps = Math.min(Math.max(1, owed), 90, until - tick);
|
||||
for (let i = 0; i < steps; i++) app.step(dt);
|
||||
}
|
||||
pump();
|
||||
}, 16);
|
||||
};
|
||||
pump();
|
||||
});
|
||||
};
|
||||
const tap = (code: string) => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code }));
|
||||
app.step(1 / 60);
|
||||
@ -550,9 +589,9 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the whole day-17 fridge order: route in, put the delivery away
|
||||
* (well or badly), shut the door (3 days), serve, read the scorecard. */
|
||||
fridgeDay(good = true) {
|
||||
async fridgeDay(good = true) {
|
||||
game.setDay(17);
|
||||
run(5);
|
||||
await F(5);
|
||||
const fv = game.fridgeView as unknown as {
|
||||
items: { it: import('./sim/coldchain').StoredItem; mesh: unknown }[];
|
||||
storeState: import('./sim/coldchain').ColdStore;
|
||||
@ -578,9 +617,9 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
}
|
||||
}
|
||||
tap('Enter'); // shut the door — 3 days pass
|
||||
run(10);
|
||||
await F(10);
|
||||
tap('Enter'); // the inspector opens it → serve
|
||||
run(5);
|
||||
await F(5);
|
||||
const groups = [...document.querySelectorAll('.crit-group')].map((e) => e.textContent);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
const gradeEl = document.querySelector('.judge-grade, .grade');
|
||||
@ -589,80 +628,82 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the day-20 egg order with REAL input. good=true floats everything
|
||||
* first and only cracks the safe ones; false cracks blind (the beat). */
|
||||
eggDay(good = true) {
|
||||
async eggDay(good = true) {
|
||||
game.setDay(20);
|
||||
run(6);
|
||||
await F(6);
|
||||
const ev = game.eggBenchView as unknown as { session: import('./sim/eggs').EggSession };
|
||||
const s = ev.session;
|
||||
if (!s) return { error: 'not at eggs' };
|
||||
const GLASS = { x: 2.1, z: -0.4 };
|
||||
const BOWL = { x: 0, z: 0.55 };
|
||||
const slot = (i: number) => ({ x: -2.25 + (i % 3) * 0.55, z: -1.2 + Math.floor(i / 3) * 0.55 });
|
||||
const drag = (from: { x: number; z: number }, to: { x: number; z: number }) => {
|
||||
point(from.x, 0.4, from.z, false); run(2);
|
||||
point(from.x, 0.4, from.z, true); run(2);
|
||||
point((from.x + to.x) / 2, 0.4, (from.z + to.z) / 2, true); run(2);
|
||||
point(to.x, 0.4, to.z, true); run(2);
|
||||
point(to.x, 0.4, to.z, false); run(2);
|
||||
const drag = async (from: { x: number; z: number }, to: { x: number; z: number }) => {
|
||||
point(from.x, 0.4, from.z, false); await F(2);
|
||||
point(from.x, 0.4, from.z, true); await F(2);
|
||||
point((from.x + to.x) / 2, 0.4, (from.z + to.z) / 2, true); await F(2);
|
||||
point(to.x, 0.4, to.z, true); await F(2);
|
||||
point(to.x, 0.4, to.z, false); await F(2);
|
||||
};
|
||||
const holdCrack = (frames: number) => {
|
||||
const holdCrack = async (frames: number) => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }));
|
||||
run(frames);
|
||||
await F(frames);
|
||||
window.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }));
|
||||
run(3);
|
||||
await F(3);
|
||||
};
|
||||
if (good) {
|
||||
for (let i = 0; i < 6; i++) drag(slot(i), GLASS); // float the lot
|
||||
for (let i = 0; i < 6; i++) await drag(slot(i), GLASS); // float the lot
|
||||
const safe = s.eggs.filter((e) => e.floatWord !== 'FLOATS').map((e) => e.index);
|
||||
for (const i of safe.slice(0, s.need)) {
|
||||
drag(slot(i), BOWL);
|
||||
holdCrack(30); // ~0.55 — the clean window
|
||||
// separate with patient alternation (~0.9s gaps)
|
||||
for (let p = 0; p < 6 && s.eggs[i].state === 'cracked' && !s.eggs[i].yolkBroken; p++) {
|
||||
run(54);
|
||||
await drag(slot(i), BOWL);
|
||||
await holdCrack(30); // ~0.55 — the clean window
|
||||
// separate with patient alternation (~0.9s gaps). Generous budget:
|
||||
// under live demo timing, frames slip between taps — keep passing
|
||||
// until the egg is done, not until an arbitrary count runs out.
|
||||
for (let p = 0; p < 14 && s.eggs[i].state === 'cracked' && !s.eggs[i].yolkBroken; p++) {
|
||||
await F(54);
|
||||
tap(p % 2 === 0 ? 'ArrowRight' : 'ArrowLeft');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Blind: crack the first ROTTEN egg straight into the bowl. The beat.
|
||||
const rotten = s.eggs.find((e) => e.freshness < 0.25) ?? s.eggs[0];
|
||||
drag(slot(rotten.index), BOWL);
|
||||
holdCrack(30);
|
||||
await drag(slot(rotten.index), BOWL);
|
||||
await holdCrack(30);
|
||||
tap('KeyD'); // dump the crime
|
||||
run(4);
|
||||
await F(4);
|
||||
}
|
||||
tap('Enter');
|
||||
run(6);
|
||||
await F(6);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
return { floats: s.eggs.map((e) => e.floatWord), rottenIn: s.rottenIn, dumped: s.bowlsDumped, rows };
|
||||
},
|
||||
|
||||
/** Play the day-22 benedict: toast the bread, then poach the egg for it. */
|
||||
benedictDay() {
|
||||
async benedictDay() {
|
||||
game.setDay(22);
|
||||
run(6);
|
||||
await F(6);
|
||||
harness.toast(10, 6); // golden-ish; pops -> routes to the poach
|
||||
run(10);
|
||||
await F(10);
|
||||
const pv = game.poachView as unknown as { session: import('./sim/poach').PoachSession };
|
||||
const s = pv.session;
|
||||
if (!s) return { error: 'not at poach after toast', view: app.currentView === game.poachView ? 'poach' : 'other' };
|
||||
s.src.target = 7;
|
||||
let g = 0;
|
||||
while (s.src.output < 6 && g++ < 1200) run(1);
|
||||
while (s.src.output < 6 && g++ < 1200) await F(1);
|
||||
for (let k = 0; k < 3 * 24; k++) {
|
||||
const a = (k / 24) * Math.PI * 2;
|
||||
point(Math.cos(a) * 0.7, 0.7, Math.sin(a) * 0.7, true);
|
||||
run(1);
|
||||
await F(1);
|
||||
}
|
||||
point(1.6, 0.7, 1.6, false);
|
||||
run(2);
|
||||
await F(2);
|
||||
tap('Space');
|
||||
g = 0;
|
||||
while ((s.whiteSet < 0.86 || s.yolkSet > 0.35) && s.yolkSet <= 0.3 && g++ < 60 * 40) run(1);
|
||||
while ((s.whiteSet < 0.86 || s.yolkSet > 0.35) && s.yolkSet <= 0.3 && g++ < 60 * 40) await F(1);
|
||||
tap('KeyL');
|
||||
for (let i = 0; i < 60 * 4; i++) run(1);
|
||||
for (let i = 0; i < 60 * 4; i++) await F(1);
|
||||
tap('Enter');
|
||||
run(6);
|
||||
await F(6);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
const lines = [...document.querySelectorAll('.judge-lines p')].map((e) => e.textContent);
|
||||
return { rows, lines };
|
||||
@ -670,43 +711,43 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the day-21 poach with REAL input. good=true: shiver + vortex + drop
|
||||
* + lift-at-set + drain. false: still water, rolling boil, overstay, no drain. */
|
||||
poachDay(good = true) {
|
||||
async poachDay(good = true) {
|
||||
game.setDay(21);
|
||||
run(6);
|
||||
await F(6);
|
||||
const pv = game.poachView as unknown as { session: import('./sim/poach').PoachSession };
|
||||
const s = pv.session;
|
||||
if (!s) return { error: 'not at poach' };
|
||||
const swirlCircles = (turns: number) => {
|
||||
const swirlCircles = async (turns: number) => {
|
||||
// real circular drags over the pot
|
||||
for (let k = 0; k < turns * 24; k++) {
|
||||
const a = (k / 24) * Math.PI * 2;
|
||||
point(Math.cos(a) * 0.7, 0.7, Math.sin(a) * 0.7, true);
|
||||
run(1);
|
||||
await F(1);
|
||||
}
|
||||
point(1.6, 0.7, 1.6, false);
|
||||
run(2);
|
||||
await F(2);
|
||||
};
|
||||
if (good) {
|
||||
s.src.target = 7; // the shiver band
|
||||
let g = 0;
|
||||
while (s.src.output < 6 && g++ < 1200) run(1);
|
||||
swirlCircles(3);
|
||||
while (s.src.output < 6 && g++ < 1200) await F(1);
|
||||
await swirlCircles(3);
|
||||
tap('Space'); // the drop, into the spin
|
||||
g = 0;
|
||||
while ((s.whiteSet < 0.86 || s.yolkSet > 0.35) && s.yolkSet <= 0.3 && g++ < 60 * 40) run(1);
|
||||
while ((s.whiteSet < 0.86 || s.yolkSet > 0.35) && s.yolkSet <= 0.3 && g++ < 60 * 40) await F(1);
|
||||
tap('KeyL');
|
||||
for (let i = 0; i < 60 * 4; i++) run(1); // drain
|
||||
for (let i = 0; i < 60 * 4; i++) await F(1); // drain
|
||||
} else {
|
||||
s.src.target = 9; // straight to a rolling boil
|
||||
let g = 0;
|
||||
while (s.src.output < 8.5 && g++ < 1200) run(1);
|
||||
while (s.src.output < 8.5 && g++ < 1200) await F(1);
|
||||
tap('Space'); // no vortex, into the boil
|
||||
for (let i = 0; i < 60 * 30; i++) run(1); // overstay — yolk hardens, rags tear
|
||||
for (let i = 0; i < 60 * 30; i++) await F(1); // overstay — yolk hardens, rags tear
|
||||
tap('KeyL');
|
||||
// no drain
|
||||
}
|
||||
tap('Enter');
|
||||
run(6);
|
||||
await F(6);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
const q = (n: number) => +n.toFixed(3);
|
||||
return { white: q(s.whiteSet), yolk: q(s.yolkSet), rags: q(s.rags), vortexAtDrop: q(s.vortexAtDrop), rows };
|
||||
@ -801,7 +842,7 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the whole day-19 rib-eye order: sear on the pan, then rest (or don't)
|
||||
* and cut across (or along) the grain on the board, serve. */
|
||||
steakDay(opts: { rest?: boolean; alongGrain?: boolean } = {}) {
|
||||
async steakDay(opts: { rest?: boolean; alongGrain?: boolean } = {}) {
|
||||
const { rest = true, alongGrain = false } = opts;
|
||||
game.setDay(19);
|
||||
run(6);
|
||||
@ -1001,26 +1042,26 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the whole day-16 pan order: route in, butter → foam → food →
|
||||
* flip → serve, and read the scorecard. A clean cook should grade well. */
|
||||
panDay() {
|
||||
async panDay() {
|
||||
game.setDay(16);
|
||||
run(5);
|
||||
await F(5);
|
||||
const pv = game.panView as unknown as { session: import('./sim/pan').PanSession; result(): unknown };
|
||||
const s = pv.session;
|
||||
if (!s) return { error: 'not at pan' };
|
||||
// Only run() advances time (the scene's update() steps the pan). Set state
|
||||
// Only await F() advances time (the scene's update() steps the pan). Set state
|
||||
// through the sim, but never step it directly here.
|
||||
setPanKnob(s, 7);
|
||||
addButter(s);
|
||||
let guard = 0;
|
||||
while (butterState(s) !== 'foaming' && guard++ < 60 * 30) run(1); // wait for the foam
|
||||
while (butterState(s) !== 'foaming' && guard++ < 60 * 30) await F(1); // wait for the foam
|
||||
addFood(s, 'button_mushroom', 'A');
|
||||
// Flip a touch before the halfway mark: a gas flame gives the first side a
|
||||
// flare lead, so a good cook turns it early (induction wouldn't need to).
|
||||
for (let i = 0; i < 60 * 8; i++) run(1); // sear side one
|
||||
for (let i = 0; i < 60 * 8; i++) await F(1); // sear side one
|
||||
flip(s);
|
||||
for (let i = 0; i < 60 * 9; i++) run(1); // sear side two
|
||||
for (let i = 0; i < 60 * 9; i++) await F(1); // sear side two
|
||||
tap('Enter');
|
||||
run(5);
|
||||
await F(5);
|
||||
const groups = [...document.querySelectorAll('.crit-group')].map((e) => e.textContent);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
const gradeEl = document.querySelector('.judge-grade, .grade');
|
||||
@ -1154,9 +1195,9 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
|
||||
/** Play the whole day-15 grill order: route in, bank a two-zone bed, sear
|
||||
* each piece then move it to the lee, serve, and read the scorecard. */
|
||||
grillDay() {
|
||||
async grillDay() {
|
||||
game.setDay(15);
|
||||
run(5);
|
||||
await F(5);
|
||||
const g = game.grillView as unknown as { session: import('./sim/charcoal').GrillSession; result(): unknown };
|
||||
const s = g.session;
|
||||
if (!s) return { error: 'not at grill', view: 'other' };
|
||||
@ -1169,10 +1210,10 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
// pull each piece to the lee the moment it hits a good sear — before its
|
||||
// fat renders and flares (the whole technique).
|
||||
for (const f of s.foods) if (f.sear >= 0.56 && f.u < 0.6) f.u = 0.82;
|
||||
run(30); // half a second — tighter cadence so we catch the window
|
||||
await F(30); // half a second — tighter cadence so we catch the window
|
||||
}
|
||||
tap('Enter');
|
||||
run(5);
|
||||
await F(5);
|
||||
const groups = [...document.querySelectorAll('.crit-group')].map((e) => e.textContent);
|
||||
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||
const gradeEl = document.querySelector('.judge-grade, .grade');
|
||||
@ -1359,5 +1400,20 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
},
|
||||
};
|
||||
|
||||
// THE DEMONSTRATIONS: day -> a driver that performs that authored day live.
|
||||
// The game's "watch him do it" button runs these under demo playback.
|
||||
const demos: Record<number, () => Promise<unknown>> = {
|
||||
15: () => harness.grillDay(),
|
||||
16: () => harness.panDay(),
|
||||
17: () => harness.fridgeDay(true),
|
||||
19: () => harness.steakDay({ rest: true, alongGrain: false }),
|
||||
20: () => harness.eggDay(true),
|
||||
21: () => harness.poachDay(true),
|
||||
22: () => harness.benedictDay(),
|
||||
};
|
||||
(harness as unknown as { demos: typeof demos }).demos = demos;
|
||||
(harness as unknown as { setDemoPlayback: (on: boolean) => void }).setDemoPlayback = (on: boolean) => {
|
||||
demoPlayback = on;
|
||||
};
|
||||
(window as unknown as { __t: unknown }).__t = harness;
|
||||
}
|
||||
|
||||
@ -87,6 +87,9 @@ export class Game {
|
||||
private knifeTrip = false;
|
||||
/** The Daily Deal: everyone on Earth gets the same order today. */
|
||||
private dailyDate: string | null = null;
|
||||
/** A demonstration is playing — the judge performs, nothing counts. */
|
||||
private demoMode = false;
|
||||
private demoSnapshot: string | null = null;
|
||||
private rng = new Rng(20260716);
|
||||
private order!: Order;
|
||||
/** Sim-time service clock — wall clock lies whenever the tab throttles. */
|
||||
@ -170,6 +173,15 @@ export class Game {
|
||||
this.kitchen.flash(cut.knife === 'the_best_thing' ? 'cut with The Best Thing' : 'now toast it');
|
||||
};
|
||||
this.judgeView.onNext = () => {
|
||||
if (this.demoMode) {
|
||||
// The demonstration is over. Put everything back and deal the day for real.
|
||||
if (this.demoSnapshot) this.save = JSON.parse(this.demoSnapshot) as Save;
|
||||
this.demoSnapshot = null;
|
||||
this.demoMode = false;
|
||||
demoWriteGuard = false;
|
||||
this.beginDay();
|
||||
return;
|
||||
}
|
||||
// Commit any fridge consumption atomically with the day advance: a reload
|
||||
// before this point re-derives the same fetch, so the cap can't be dodged.
|
||||
if (this.pendingFridge) {
|
||||
@ -536,6 +548,22 @@ export class Game {
|
||||
* day counter and streaks are left alone. Deterministic: replaying today
|
||||
* re-deals today.
|
||||
*/
|
||||
/**
|
||||
* "Watch him do it": replay this authored day as a live demonstration, driven
|
||||
* by the same harness script that verifies it. The save is snapshotted and
|
||||
* write-guarded — a demo can neither farm best scores nor advance anything.
|
||||
*/
|
||||
startDemo(day: number): void {
|
||||
const t = (window as unknown as { __t?: { demos?: Record<number, () => Promise<unknown>>; setDemoPlayback?: (on: boolean) => void } }).__t;
|
||||
const demo = t?.demos?.[day];
|
||||
if (!demo || !t?.setDemoPlayback || this.demoMode) return;
|
||||
this.demoSnapshot = JSON.stringify(this.save);
|
||||
this.demoMode = true;
|
||||
demoWriteGuard = true;
|
||||
t.setDemoPlayback(true);
|
||||
void demo().finally(() => t.setDemoPlayback!(false));
|
||||
}
|
||||
|
||||
startDaily(): void {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
this.dailyDate = date;
|
||||
@ -949,6 +977,11 @@ export class Game {
|
||||
* ride the judge screen via extraLines.
|
||||
*/
|
||||
private recordVerdict(v: Verdict): void {
|
||||
if (this.demoMode) {
|
||||
this.judgeView.extraLines = ['A demonstration. None of it counts. Now you.'];
|
||||
this.judgeView.shareText = '';
|
||||
return;
|
||||
}
|
||||
const extras: string[] = [];
|
||||
// The streak. He notices. He hates that he notices.
|
||||
const prev = this.save.streak ?? 0;
|
||||
@ -987,6 +1020,11 @@ export class Game {
|
||||
this.ticketEl.innerHTML = '';
|
||||
el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`);
|
||||
el('div', 'ticket-who', this.ticketEl, o.who);
|
||||
const demos = (window as unknown as { __t?: { demos?: Record<number, unknown> } }).__t?.demos;
|
||||
if (!this.demoMode && demos && demos[o.day]) {
|
||||
const watch = el('button', 'btn ghost watch-btn', this.ticketEl, '▶ watch him do it');
|
||||
watch.addEventListener('click', () => this.startDemo(o.day));
|
||||
}
|
||||
const knownAs = this.save.customers?.[o.who];
|
||||
if (knownAs && knownAs.visits > 0) {
|
||||
el('div', 'ticket-hint', this.ticketEl, `visit №${knownAs.visits + 1} — last time: ${knownAs.last}${knownAs.last === 'F' ? '. they came BACK.' : ''}`);
|
||||
@ -1122,7 +1160,11 @@ function loadSave(): Save {
|
||||
return { day: 1, maxDay: 1, total: 0, best: {}, streak: 0, customers: {}, daily: {} };
|
||||
}
|
||||
|
||||
/** While a demonstration runs, nothing it does may touch the disk. */
|
||||
let demoWriteGuard = false;
|
||||
|
||||
function writeSave(s: Save): void {
|
||||
if (demoWriteGuard) return;
|
||||
try {
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(s));
|
||||
} catch {
|
||||
|
||||
@ -14,8 +14,10 @@ const game = new Game(app);
|
||||
// works without it — so boot the game now and let physics catch up.
|
||||
void game.init();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// The main harness ships in production too: it powers the in-game
|
||||
// demonstrations ("watch him do it"), and console cooks are welcome guests.
|
||||
void import('./dev').then((m) => m.installDevHarness(app, game));
|
||||
if (import.meta.env.DEV) {
|
||||
void import('./dev-juice').then((m) => m.installJuiceHarness(app));
|
||||
void import('./dev-grate').then((m) => m.installGrateHarness(app));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user