Fix the landed-toast softlock: the drawer no longer opens before Rapier is up
The toast could beat the wasm: main.ts boots the game without awaiting game.init(), and drawer.reset() dereferences the Rapier World. Pop early enough on a slow device and onPopped -> openDrawer threw mid-update, leaving the round on phase 'landed' with an empty hint and a dead ENTER (field report: toast apparently resting against the toaster, game stuck). - Game.init() is memoised with a drawerReady flag; a second call joins the first instead of building a second World. - openDrawer() and the fromLoaf knife-trip both gate on it: wait for physics if it's still compiling, and if it never arrives, degrade to handing over the ordered tool instead of softlocking. - Belt and braces in the kitchen: a 3s flying timeout force-lands the slice, so 'flying' can never hang mid-air whatever the cause. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
317c78be71
commit
e851900cc1
@ -659,12 +659,36 @@ export class Game {
|
||||
this.beginDay();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.drawer.init();
|
||||
private drawerInit?: Promise<void>;
|
||||
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<void> {
|
||||
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');
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user