feat(world): PlaceLayer lädt vor, setzt ein und nimmt heraus

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-14 11:26:34 +02:00
1 parent 16eb651308
commit ef2001542a
2 files changed
+191 -3

No files matched your search

+114 -3
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { BoxGeometry, Group, Mesh, MeshStandardMaterial, Vector3 } from "three";
import { PLACES, SOLID_MATERIALS, isSolidMaterial, placeTransform, prepareModel,
import { afterEach, describe, expect, it, vi } from "vitest";
import { BoxGeometry, Group, Mesh, MeshStandardMaterial, Scene, Vector3 } from "three";
import { PLACES, PlaceLayer, SOLID_MATERIALS, isSolidMaterial, placeTransform, prepareModel,
wantedPlaces } from "../src/world/places.js";
describe("placeTransform", () => {
@@ -106,3 +106,114 @@ describe("PLACES", () => {
}
});
});
describe("PlaceLayer", () => {
afterEach(() => vi.restoreAllMocks());
const place = { id: "hof", url: "./hof.glb", at: [1489, 1067.5], rotation: 0, lift: -0.28 };
// Spieler distance Meter südlich der Ortsmitte (three: +z ist Süden).
const south = (distance) => ({ x: 1489, y: 0, z: -1067.5 + distance });
function makeLayer(load) {
const scene = new Scene();
const player = { addCollider: vi.fn(() => ({ id: 7 })), removeCollider: vi.fn() };
const layer = new PlaceLayer({ scene, player, places: [place], load });
return { scene, player, layer };
}
// Bruch: Einsetzen schon beim Vorladen (der Hof stünde über ungeladenem Boden) oder erst nach
// einem zweiten Laden.
it("lädt ab 600 m vor und setzt erst ab 250 m ein", async () => {
const model = fakeModel();
const load = vi.fn(async () => model);
const { scene, player, layer } = makeLayer(load);
layer.update(south(500));
await vi.waitFor(() => expect(layer.models.has("hof")).toBe(true));
expect(load).toHaveBeenCalledWith("./hof.glb");
expect(scene.children).not.toContain(model);
expect(player.addCollider).not.toHaveBeenCalled();
layer.update(south(240));
expect(scene.children).toContain(model);
expect(player.addCollider).toHaveBeenCalledWith({
motion: "static", useWorker: true, shape: { kind: "mesh", source: model },
});
});
// Bruch: Collider bleibt nach dem Herausnehmen stehen (unsichtbare Wand) oder Flackern an der Grenze.
it("nimmt den Ort erst jenseits von 450 m heraus und meldet den Collider ab", async () => {
const model = fakeModel();
const { scene, player, layer } = makeLayer(vi.fn(async () => model));
layer.update(south(100));
await vi.waitFor(() => expect(scene.children).toContain(model));
layer.update(south(400));
expect(scene.children).toContain(model);
expect(player.removeCollider).not.toHaveBeenCalled();
layer.update(south(460));
expect(scene.children).not.toContain(model);
expect(player.removeCollider).toHaveBeenCalledWith({ id: 7 });
});
it("setzt nach der Rückkehr ohne neues Laden wieder ein", async () => {
const model = fakeModel();
const load = vi.fn(async () => model);
const { scene, player, layer } = makeLayer(load);
layer.update(south(100));
await vi.waitFor(() => expect(scene.children).toContain(model));
layer.update(south(700));
layer.update(south(100));
expect(scene.children).toContain(model);
expect(load).toHaveBeenCalledTimes(1);
expect(player.addCollider).toHaveBeenCalledTimes(2);
});
// Bruch: ein spät fertiges Laden setzt den Ort ein, obwohl der Spieler längst weitergefahren ist.
it("lässt den Ort draußen, wenn der Spieler beim Ende des Ladens schon fort ist", async () => {
const model = fakeModel();
let finish;
const { scene, player, layer } = makeLayer(vi.fn(() => new Promise((resolve) => { finish = resolve; })));
layer.update(south(100));
layer.update(south(500));
finish(model);
await vi.waitFor(() => expect(layer.models.has("hof")).toBe(true));
expect(scene.children).not.toContain(model);
expect(player.addCollider).not.toHaveBeenCalled();
layer.update(south(100));
expect(scene.children).toContain(model);
});
it("meldet ohne feste Teile keinen Collider an", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const model = fakeModel(["grass_medium_01"]);
const { scene, player, layer } = makeLayer(vi.fn(async () => model));
layer.update(south(100));
await vi.waitFor(() => expect(scene.children).toContain(model));
layer.update(south(460));
expect(player.addCollider).not.toHaveBeenCalled();
expect(player.removeCollider).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(expect.stringContaining("hof"));
});
// Bruch: ein fehlender Ort bricht das Spiel ab oder wird in jedem Frame neu angefragt.
it("warnt bei einem Ladefehler, wirft nicht und versucht es nicht erneut", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const load = vi.fn(async () => { throw new Error("404"); });
const { scene, layer } = makeLayer(load);
expect(() => layer.update(south(100))).not.toThrow();
await vi.waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("hof"), expect.any(Error)));
layer.update(south(100));
expect(load).toHaveBeenCalledTimes(1);
expect(scene.children).toHaveLength(0);
});
});