feat(world): Orte-Liste mit Lage, Radien und Kollisionsmaterialien

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Haiku 4.5 committed 2026-09-14 10:59:02 +02:00
1 parent 6c291c4106
commit 62dbbe41d2
2 files changed
+207

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
// Orte: fest platzierte Modelle aus dem Editor three-d-stage, getrennt vom Chunk-Streaming
// (docs/superpowers/specs/2026-09-14-hood-yard-design.md). Aufbereitet werden sie mit
// tools/assets/places.mjs; hier stehen Lage, Laden und Kollision.
import { worldToThree } from "./coords.js";
// Chunks laden bei 400 m und bauen bei 600 m ab, gemessen zu ihrem Mittelpunkt (ChunkStreamer.update).
// Der fernste Chunk-Mittelpunkt unter dem Hood Yard liegt 148 m von dessen Mitte entfernt: erschiene
// der Hof schon bei 400 m, stünde er vom Startpunkt aus über dem Meer. Mit 250 m und 450 m ist sein
// Boden geladen, solange er in der Szene ist (tests/placesSite.test.js prüft das für jeden Ort).
// Heruntergeladen wird früher, damit das Einsetzen nicht auf die Datei wartet.
export const PLACE_PRELOAD_RADIUS = 600;
export const PLACE_SHOW_RADIUS = 250;
export const PLACE_HIDE_RADIUS = 450;
/**
* at: Welt-x und Welt-y in Metern, wie auf der Minimap — nie three-z. rotation: Grad gegen den
* Uhrzeigersinn, von oben mit Norden oben (wie add_building in corrections.toml). lift: Meter auf die
* Modellwurzel. bounds: Außenmaße in Modellkoordinaten (x, z); das Spiel braucht sie nicht, die Tests
* prüfen damit Datei (placesAsset.test.js) und Lage (placesSite.test.js) gegeneinander.
*/
export const PLACES = [
{
id: "hood-yard",
url: "./models/places/hood-yard.glb",
// Übergang Container -> Schuppen an derselben Stelle wie im Satellitenbild des Nutzers.
at: [1489, 1067.5],
rotation: 0,
// Die 34 cm dicke Kiesplatte liegt mit ihrer Unterseite auf 0; so ragt sie noch 6 cm heraus.
lift: -0.28,
bounds: { minX: -17.15, maxX: 17, minZ: -13.4, maxZ: 8.6 },
},
];
/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Kleinteile — läuft man durch. */
export const SOLID_MATERIALS = [
// Boden
"gravel", "asphalt", "curbAsphalt", "padAsphalt", "puddleAsphalt", "cobble", "grass", "concrete",
// Container
"cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end", "contFrame",
// Schuppen und Tore
"ironPlain", "ironTeal", "ironBlue", "galv", "rust", "darkTrim", "osb", "osbB",
"rollershutter_window_01", "rollershutter_window_01_graffiti",
// Zaun
"chainlink_post", "chainlink_wire",
// Masten, Fass, Kiste
"modular_electricity_poles", "barrel_03", "plastic_container",
// Bäume
"quiver_tree_01_trunk", "quiver_tree_02",
];
const SOLID = new Set(SOLID_MATERIALS);
export function isSolidMaterial(name) {
return SOLID.has(name);
}
/** Lage eines Orts in three-Koordinaten. Positives rotationY dreht +x nach −z, also nach Norden. */
export function placeTransform(place) {
const { x, z } = worldToThree(place.at[0], place.at[1]);
return { position: { x, y: place.lift, z }, rotationY: (place.rotation * Math.PI) / 180 };
}
/** Abstand in der Ebene zwischen einer three-Position und der Mitte eines Orts. */
export function placeDistance(position, place) {
const { x, z } = worldToThree(place.at[0], place.at[1]);
return Math.hypot(x - position.x, z - position.z);
}
/** Wie wantedChunks: add innerhalb von show, remove erst jenseits von hide. */
export function wantedPlaces(position, places, { show, hide, active }) {
const add = [];
const remove = [];
for (const place of places) {
const distance = placeDistance(position, place);
if (distance <= show && !active.has(place.id)) add.push(place.id);
if (distance > hide && active.has(place.id)) remove.push(place.id);
}
return { add, remove };
}
/**
* Setzt Lage und Schatten und nimmt nicht feste Meshes aus der Kollision (colliderBuild.ts im
* three-player-controller überspringt userData.excludeFromCollider). Gibt zurück, ob ein Teil fest ist.
*/
export function prepareModel(root, place) {
const { position, rotationY } = placeTransform(place);
root.position.set(position.x, position.y, position.z);
root.rotation.set(0, rotationY, 0);
let solid = false;
root.traverse((object) => {
if (!object.isMesh) return;
object.castShadow = true;
object.receiveShadow = true;
const materials = Array.isArray(object.material) ? object.material : [object.material];
if (materials.some((material) => isSolidMaterial(material?.name))) solid = true;
else object.userData.excludeFromCollider = true;
});
root.updateMatrixWorld(true);
return solid;
}