import { describe, expect, it } from "vitest"; import { BoxGeometry, Group, Mesh, MeshStandardMaterial, Vector3 } from "three"; import { PLACES, SOLID_MATERIALS, isSolidMaterial, placeTransform, prepareModel, wantedPlaces } from "../src/world/places.js"; describe("placeTransform", () => { // Bruch: Welt-y nicht negiert (der Hof landete 2 km weiter südlich im Meer) oder lift nicht in y. it("rechnet Welt-x/-y in three um und hebt um lift", () => { const transform = placeTransform({ at: [1489, 1067.5], rotation: 0, lift: -0.28 }); expect(transform.position).toEqual({ x: 1489, y: -0.28, z: -1067.5 }); expect(transform.rotationY).toBe(0); }); // Bruch: Grad nicht ins Bogenmaß umgerechnet oder falsche Drehrichtung. "Gegen den Uhrzeigersinn, // Norden oben" heißt in three: +x (Osten) wird zu −z (Norden). it("dreht mit positivem Winkel Osten nach Norden", () => { const root = new Group(); root.rotation.y = placeTransform({ at: [0, 0], rotation: 90, lift: 0 }).rotationY; root.updateMatrixWorld(); const east = new Vector3(1, 0, 0).applyMatrix4(root.matrixWorld); expect(east.x).toBeCloseTo(0); expect(east.z).toBeCloseTo(-1); }); }); describe("wantedPlaces", () => { const place = { id: "hof", at: [1000, 1000], rotation: 0, lift: 0 }; const south = (distance) => ({ x: 1000, z: -1000 + distance }); const radii = { show: 250, hide: 450 }; it("setzt innerhalb des show-Radius ein", () => { expect(wantedPlaces(south(240), [place], { ...radii, active: new Set() }).add).toEqual(["hof"]); expect(wantedPlaces(south(260), [place], { ...radii, active: new Set() }).add).toEqual([]); }); // Bruch: ein Radius für Ein- und Ausblenden — an der Grenze flackerte der Hof mit jedem Schritt. it("nimmt erst jenseits des hide-Radius heraus", () => { const active = new Set(["hof"]); expect(wantedPlaces(south(400), [place], { ...radii, active }).remove).toEqual([]); expect(wantedPlaces(south(460), [place], { ...radii, active }).remove).toEqual(["hof"]); }); it("meldet einen aktiven Ort nicht doppelt an", () => { expect(wantedPlaces(south(10), [place], { ...radii, active: new Set(["hof"]) }).add).toEqual([]); }); }); describe("isSolidMaterial", () => { // Bruch: Gras oder Blätter im Collider (man bliebe im Gras hängen), oder ein fester Teil fehlt // (man liefe durch Container, Schuppen oder Zaun). it("lässt feste Teile kollidieren, Gras, Blätter und Kleinteile nicht", () => { for (const name of ["gravel", "cont_a_side", "ironTeal", "chainlink_wire", "barrel_03", "quiver_tree_01_trunk"]) { expect(isSolidMaterial(name)).toBe(true); } for (const name of ["grass_medium_01", "quiver_tree_01_leaf", "searsia_lucida_leaves", "trashbag", undefined]) { expect(isSolidMaterial(name)).toBe(false); } }); it("kennt jedes Material der Liste", () => { expect(SOLID_MATERIALS.every(isSolidMaterial)).toBe(true); }); }); /** Ein Mesh je Materialname unter einer Wurzel, wie GLTFLoader es nach join() liefert. */ function fakeModel(names = ["cont_a_side", "grass_medium_01"]) { const root = new Group(); for (const name of names) root.add(new Mesh(new BoxGeometry(), new MeshStandardMaterial({ name }))); return root; } describe("prepareModel", () => { // Bruch: Lage nicht gesetzt, Schatten aus, oder feste Teile fälschlich aus der Kollision genommen. it("setzt Lage und Schatten und nimmt nur nicht feste Teile aus der Kollision", () => { const root = fakeModel(); const [container, grass] = root.children; expect(prepareModel(root, { at: [1489, 1067.5], rotation: 0, lift: -0.28 })).toBe(true); expect(root.position.toArray()).toEqual([1489, -0.28, -1067.5]); expect(container.userData.excludeFromCollider).toBeUndefined(); expect(grass.userData.excludeFromCollider).toBe(true); for (const mesh of [container, grass]) { expect(mesh.castShadow).toBe(true); expect(mesh.receiveShadow).toBe(true); } }); it("meldet, wenn kein Teil fest ist", () => { expect(prepareModel(fakeModel(["grass_medium_01"]), { at: [0, 0], rotation: 0, lift: 0 })).toBe(false); }); }); describe("PLACES", () => { // Bruch: ein Eintrag ohne Pflichtfeld — placeTransform rechnete mit undefined, die Tests mit bounds // prüften nichts. it("hat für jeden Ort id, url, at, rotation, lift und bounds", () => { for (const entry of PLACES) { expect(entry).toEqual({ id: expect.any(String), url: expect.stringMatching(/^\.\/models\/places\/.+\.glb$/), at: [expect.any(Number), expect.any(Number)], rotation: expect.any(Number), lift: expect.any(Number), bounds: { minX: expect.any(Number), maxX: expect.any(Number), minZ: expect.any(Number), maxZ: expect.any(Number) }, }); } }); });