feat(world): Orte-Liste mit Lage, Radien und Kollisionsmaterialien
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6c291c4106
commit
62dbbe41d2
2 files changed
+207
No files matched your search
@@ -0,0 +1,108 @@
|
||||
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) },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user