diff --git a/src/game/game.ts b/src/game/game.ts index 7e9768b..cead9f2 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -659,12 +659,36 @@ export class Game { this.beginDay(); } - async init(): Promise { - await this.drawer.init(); + private drawerInit?: Promise; + private drawerReady = false; + + /** + * Rapier's wasm compile. Memoised: the drawer gates on this promise when the + * toast lands before physics is up (main.ts boots without waiting), and a + * second call must join the first, not build a second World. + */ + init(): Promise { + this.drawerInit ??= this.drawer.init().then(() => { + this.drawerReady = true; + }); + return this.drawerInit; } /** Fresh out of the toaster, and now you need something to spread with. */ private openDrawer(): void { + if (!this.drawerReady) { + // The toast beat the wasm: on a slow device Rapier can still be + // compiling when the slice lands. Opening anyway dereferenced an + // undefined World mid-update — the round softlocked on a landed slice + // with no hint and a dead ENTER. Hold on the plate until physics is in; + // if it never arrives, skip the fishing and hand over the ordered tool. + this.kitchen.flash('rummaging for the drawer…'); + void this.init().then( + () => this.openDrawer(), + () => this.closeDrawer(this.order.tool), + ); + return; + } this.drawer.reset(this.order.tool); this.drawer.setTimerLabel('your toast is going cold'); this.drawer.warmth = this.kitchen.currentSlice.warmth; @@ -1523,6 +1547,18 @@ export class Game { /** The toaster half of the day: the loaf knife-trip, or straight to the bench. */ private enterToastFlow(): void { if (this.order.fromLoaf) { + if (!this.drawerReady) { + // Same wasm race as openDrawer, at the day's front door. Wait it out; + // a physics that never arrives skips the trip and hands them the knife. + void this.init().then( + () => this.enterToastFlow(), + () => { + this.knifeTrip = true; + this.closeDrawer('bread_knife'); + }, + ); + return; + } // Bakery bread: first find something to slice it with. this.knifeTrip = true; this.drawer.reset('bread_knife'); diff --git a/src/main.ts b/src/main.ts index 6933a0b..24a41ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,8 +13,10 @@ app.scene.background = new THREE.Color(0x241a15); const game = new Game(app); // Rapier's wasm has to be up before the drawer can be opened, but the toaster -// works without it — so boot the game now and let physics catch up. -void game.init(); +// works without it — so boot the game now and let physics catch up. A compile +// failure is handled at the drawer's door (it degrades to handing over the +// ordered tool); swallow it here so it isn't also an unhandled rejection. +void game.init().catch(() => {}); // The main harness ships in production too: it powers the in-game // demonstrations ("watch him do it"), and console cooks are welcome guests. diff --git a/src/scenes/kitchen.ts b/src/scenes/kitchen.ts index f2e6b88..a5aff79 100644 --- a/src/scenes/kitchen.ts +++ b/src/scenes/kitchen.ts @@ -62,6 +62,7 @@ export class KitchenView implements View { private vel = new THREE.Vector3(); private spin = 0; private toastSeconds = 0; + private flyingSeconds = 0; knife: Knife = newKnife(); /** Brand quality for the current order's bread. */ @@ -386,6 +387,7 @@ export class KitchenView implements View { const T = (vy + Math.sqrt(disc)) / g; this.vel.set((PLATE_X - p0.x) / T, vy, (PLATE_Z - p0.z) / T); this.spin = Math.PI / 2 / T; + this.flyingSeconds = 0; this.app.shake = 0.045; this.hintEl.textContent = ''; } @@ -423,11 +425,18 @@ export class KitchenView implements View { this.placeInSlot(this.leverT); } else if (this.phase === 'flying') { this.vel.y -= 9.8 * dt; + this.flyingSeconds += dt; this.slice.mesh.position.addScaledVector(this.vel, dt); this.slice.mesh.rotation.x = Math.max(0, this.slice.mesh.rotation.x - this.spin * dt); this.slice.mesh.rotation.z += dt * 1.1; this.slice.setHeatGlow(0); - if (this.slice.mesh.position.y <= PLATE_Y + 0.02 && this.vel.y < 0) { + // The arc is deterministic and takes ~1s, so the timeout is pure + // insurance: whatever leaves a slice hanging mid-air (a hitch, a bug, + // a hostile embed), the round must never be able to hang with it. + if ( + (this.slice.mesh.position.y <= PLATE_Y + 0.02 && this.vel.y < 0) || + this.flyingSeconds > 3 + ) { this.slice.mesh.position.set(PLATE_X, PLATE_Y + 0.02, PLATE_Z); this.slice.mesh.rotation.set(0, 0, 0); this.app.shake = 0.02;