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();
|
this.beginDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
private drawerInit?: Promise<void>;
|
||||||
await this.drawer.init();
|
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. */
|
/** Fresh out of the toaster, and now you need something to spread with. */
|
||||||
private openDrawer(): void {
|
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.reset(this.order.tool);
|
||||||
this.drawer.setTimerLabel('your toast is going cold');
|
this.drawer.setTimerLabel('your toast is going cold');
|
||||||
this.drawer.warmth = this.kitchen.currentSlice.warmth;
|
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. */
|
/** The toaster half of the day: the loaf knife-trip, or straight to the bench. */
|
||||||
private enterToastFlow(): void {
|
private enterToastFlow(): void {
|
||||||
if (this.order.fromLoaf) {
|
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.
|
// Bakery bread: first find something to slice it with.
|
||||||
this.knifeTrip = true;
|
this.knifeTrip = true;
|
||||||
this.drawer.reset('bread_knife');
|
this.drawer.reset('bread_knife');
|
||||||
|
|||||||
@ -13,8 +13,10 @@ app.scene.background = new THREE.Color(0x241a15);
|
|||||||
|
|
||||||
const game = new Game(app);
|
const game = new Game(app);
|
||||||
// Rapier's wasm has to be up before the drawer can be opened, but the toaster
|
// 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.
|
// works without it — so boot the game now and let physics catch up. A compile
|
||||||
void game.init();
|
// 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
|
// The main harness ships in production too: it powers the in-game
|
||||||
// demonstrations ("watch him do it"), and console cooks are welcome guests.
|
// 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 vel = new THREE.Vector3();
|
||||||
private spin = 0;
|
private spin = 0;
|
||||||
private toastSeconds = 0;
|
private toastSeconds = 0;
|
||||||
|
private flyingSeconds = 0;
|
||||||
|
|
||||||
knife: Knife = newKnife();
|
knife: Knife = newKnife();
|
||||||
/** Brand quality for the current order's bread. */
|
/** Brand quality for the current order's bread. */
|
||||||
@ -386,6 +387,7 @@ export class KitchenView implements View {
|
|||||||
const T = (vy + Math.sqrt(disc)) / g;
|
const T = (vy + Math.sqrt(disc)) / g;
|
||||||
this.vel.set((PLATE_X - p0.x) / T, vy, (PLATE_Z - p0.z) / T);
|
this.vel.set((PLATE_X - p0.x) / T, vy, (PLATE_Z - p0.z) / T);
|
||||||
this.spin = Math.PI / 2 / T;
|
this.spin = Math.PI / 2 / T;
|
||||||
|
this.flyingSeconds = 0;
|
||||||
this.app.shake = 0.045;
|
this.app.shake = 0.045;
|
||||||
this.hintEl.textContent = '';
|
this.hintEl.textContent = '';
|
||||||
}
|
}
|
||||||
@ -423,11 +425,18 @@ export class KitchenView implements View {
|
|||||||
this.placeInSlot(this.leverT);
|
this.placeInSlot(this.leverT);
|
||||||
} else if (this.phase === 'flying') {
|
} else if (this.phase === 'flying') {
|
||||||
this.vel.y -= 9.8 * dt;
|
this.vel.y -= 9.8 * dt;
|
||||||
|
this.flyingSeconds += dt;
|
||||||
this.slice.mesh.position.addScaledVector(this.vel, 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.x = Math.max(0, this.slice.mesh.rotation.x - this.spin * dt);
|
||||||
this.slice.mesh.rotation.z += dt * 1.1;
|
this.slice.mesh.rotation.z += dt * 1.1;
|
||||||
this.slice.setHeatGlow(0);
|
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.position.set(PLATE_X, PLATE_Y + 0.02, PLATE_Z);
|
||||||
this.slice.mesh.rotation.set(0, 0, 0);
|
this.slice.mesh.rotation.set(0, 0, 0);
|
||||||
this.app.shake = 0.02;
|
this.app.shake = 0.02;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user