diff --git a/src/world/places.js b/src/world/places.js index 570167d..4273b2a 100644 --- a/src/world/places.js +++ b/src/world/places.js @@ -185,15 +185,28 @@ export class PlaceLayer { } #show(id) { + if (this.failed.has(id)) return; const model = this.models.get(id); if (!model || this.active.has(id)) return; + // Collider vor der Szene bauen: prepareModel hat schon updateMatrixWorld(true) auf der Wurzel + // aufgerufen, die Quelle hat also eine gültige Weltmatrix, obwohl sie noch nicht in der Szene + // hängt. Wirft addCollider (z. B. toter Worker), bleibt die Szene unverändert statt eine + // Wurzel ohne active-Eintrag (und damit nie wieder entfernbar) stehen zu lassen — und aus + // update(), das #show synchron aufruft, entkäme sonst eine Ausnahme in die Spielschleife. + let handle; + try { + // Ohne feste Teile keinen Collider: eine leere Quelle liefert im Controller nur ein Handle, + // das nie fertig wird (vgl. Ruling 51 in streaming.js). + handle = model.solid + ? this.player.addCollider({ motion: "static", useWorker: true, shape: { kind: "mesh", source: model.root } }) + : null; + } catch (err) { + this.failed.add(id); + console.warn(`Ort ${id} konnte nicht eingesetzt werden:`, err); + return; + } this.scene.add(model.root); model.root.updateMatrixWorld(true); - // Ohne feste Teile keinen Collider: eine leere Quelle liefert im Controller nur ein Handle, - // das nie fertig wird (vgl. Ruling 51 in streaming.js). - const handle = model.solid - ? this.player.addCollider({ motion: "static", useWorker: true, shape: { kind: "mesh", source: model.root } }) - : null; this.active.set(id, { root: model.root, handle }); } diff --git a/tests/places.test.js b/tests/places.test.js index 98d98f1..f35e429 100644 --- a/tests/places.test.js +++ b/tests/places.test.js @@ -292,4 +292,29 @@ describe("PlaceLayer", () => { expect(warn).toHaveBeenCalledWith(expect.stringContaining("hof"), expect.any(Error)); }); + + // Bruch: addCollider wirft, scene.add lief aber schon davor — die Wurzel bliebe unsichtbar und + // für immer in der Szene, ohne active-Eintrag also nie mehr entfernbar; oder die Ausnahme entkäme + // synchron aus update() in die Spielschleife (#show läuft hier direkt, nicht über die Ladekette). + it("hält die Szene unverändert und warnt, wenn addCollider beim Einsetzen wirft", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const model = fakeModel(); + const load = vi.fn(async () => model); + const { scene, player, layer } = makeLayer(load); + player.addCollider.mockImplementation(() => { throw new Error("Worker tot"); }); + + layer.update(south(500)); + await vi.waitFor(() => expect(layer.models.has("hof")).toBe(true)); + + expect(() => layer.update(south(100))).not.toThrow(); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("hof"), expect.any(Error)); + expect(scene.children).not.toContain(model); + + const callsBefore = player.addCollider.mock.calls.length; + layer.update(south(500)); + layer.update(south(100)); + + expect(scene.children).not.toContain(model); + expect(player.addCollider.mock.calls.length).toBe(callsBefore); + }); });