diff --git a/src/main.js b/src/main.js index 50c41bf..c0b354b 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ import { createSky } from "./world/sky.js"; import { createSunShadows, prepareLitMaterials } from "./world/shadows.js"; import { PropLayer } from "./world/props.js"; import { PLACES, PlaceLayer } from "./world/places.js"; +import { SPAWN, VEHICLE_SPAWN_POINTS } from "./world/spawn.js"; import { chunkForWorld, threeToWorld } from "./world/coords.js"; import { createCharacterModule } from "./characters/index.js"; import { activeVehicleOrNull, playerIsGrounded, playerPosition } from "./player.js"; @@ -69,20 +70,8 @@ water.rotation.x = -Math.PI / 2; water.position.set(500, -0.6, -1000); scene.add(water); -// Vice Beach, Rasen am Südrand des Parks (Beschriftungs-Plan, Task 7). Auch initPos, damit der Streamer von Anfang an den richtigen -// Chunk anfragt statt erst nach dem späteren reset() umzuschwenken. -const SPAWN = { x: 1600, y: 3, z: -690 }; - -// Task 11, Schritt 5: vier SUVs an echten Mittellinienpunkten aus chunk_10_7.json (derselbe -// Chunk wie SPAWN), auf der Straße verteilt und weit genug von SPAWN entfernt, um den Spieler -// beim Start nicht zu überlappen. y=1: kleine Fallhöhe bis zur Fahrbahn (y=0), die Federung -// übernimmt den Rest — wie schon bei SPAWN selbst braucht es keine exakte Bodenhöhe im Voraus. -const VEHICLE_SPAWNS = [ - new Vector3(1560, 1, -658), - new Vector3(1590, 1, -659), - new Vector3(1630, 1, -661), - new Vector3(1655, 1, -663), -]; +// Startpunkt und Fahrzeugpunkte stehen in src/world/spawn.js. +const VEHICLE_SPAWNS = VEHICLE_SPAWN_POINTS.map(([x, y, z]) => new Vector3(x, y, z)); // Konfiguration unverändert aus spawnShowcaseVehicles/VEHICLE_TUNING in // three-player-controller/example/showcase.js — dieselbe suv.glb, dieselbe Abstimmung. const VEHICLE_SCALE = 0.5; diff --git a/src/world/spawn.js b/src/world/spawn.js new file mode 100644 index 0000000..5e23043 --- /dev/null +++ b/src/world/spawn.js @@ -0,0 +1,17 @@ +// Startpunkt und Fahrzeugpunkte, aus src/main.js herausgelöst, damit tests/parkData.test.js sie gegen die +// Chunk-Daten prüfen kann: main.js lädt beim Import Texturen und Modelle und lässt sich nicht in Tests laden. + +// Vice Beach, Rasen am Südrand des Parks (Beschriftungs-Plan, Task 7). Seit Flamingo Park nach OpenStreetMap +// (docs/superpowers/specs/2026-09-15-flamingo-park-design.md) liegt er im neu eingesetzten Park. Auch initPos, +// damit der Streamer von Anfang an den richtigen Chunk anfragt statt erst nach dem späteren reset() umzuschwenken. +export const SPAWN = { x: 1600, y: 3, z: -690 }; + +// Task 11, Schritt 5: vier SUVs an echten Mittellinienpunkten aus chunk_10_7.json (derselbe Chunk wie SPAWN), +// auf der Straße verteilt und weit genug von SPAWN entfernt, um den Spieler beim Start nicht zu überlappen. +// y=1: kleine Fallhöhe bis zur Fahrbahn (y=0), die Federung übernimmt den Rest. Als [x, y, three-z]. +export const VEHICLE_SPAWN_POINTS = [ + [1560, 1, -658], + [1590, 1, -659], + [1630, 1, -661], + [1655, 1, -663], +]; diff --git a/tests/parkData.test.js b/tests/parkData.test.js new file mode 100644 index 0000000..ecd5d82 --- /dev/null +++ b/tests/parkData.test.js @@ -0,0 +1,58 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { chunkForWorld } from "../src/world/coords.js"; +import { PLACES } from "../src/world/places.js"; +import { pointInPolygon } from "../src/world/props.js"; +import { SPAWN, VEHICLE_SPAWN_POINTS } from "../src/world/spawn.js"; + +const PARK_FILE = readFileSync(new URL("../tools/extract/data/flamingo-park.toml", import.meta.url), "utf8"); + +/** Die [[keepout]]-Blöcke der Parkdatei. Ihr Format steht in der Spec fest; ein allgemeiner TOML-Leser wäre zu viel. */ +function keepouts(text) { + const number = "(-?[\\d.]+)"; + return text.split("[[keepout]]").slice(1).map((block) => { + const body = block.split(/\n\[/)[0]; + const at = body.match(new RegExp(`\\nat\\s*=\\s*\\[${number},\\s*${number}\\]`)); + const rotation = body.match(new RegExp(`\\nrotation\\s*=\\s*${number}`)); + const bounds = body.match(new RegExp( + `bounds\\s*=\\s*\\{\\s*min_x\\s*=\\s*${number},\\s*max_x\\s*=\\s*${number},\\s*min_z\\s*=\\s*${number},\\s*max_z\\s*=\\s*${number}\\s*\\}`)); + return { + name: body.match(/name\s*=\s*"([^"]+)"/)[1], + at: [Number(at[1]), Number(at[2])], + rotation: Number(rotation[1]), + bounds: { minX: Number(bounds[1]), maxX: Number(bounds[2]), minZ: Number(bounds[3]), maxZ: Number(bounds[4]) }, + }; + }); +} + +function chunkAt(X, Y) { + const { i, j } = chunkForWorld(X, Y); + return JSON.parse(readFileSync(new URL(`../public/data/vice-a/chunk_${i}_${j}.json`, import.meta.url), "utf8")); +} + +describe("Flamingo-Park-Daten", () => { + // Bruch: ein Ort wird verschoben oder gedreht, die Pipeline hält aber die alte Fläche frei — Palmen, + // Wege oder OSM-Gebäude landeten dann im Ort. + it("hat für jeden Ort eine Sperrfläche mit dessen Lage, Drehung und Außenmaßen", () => { + const byName = Object.fromEntries(keepouts(PARK_FILE).map((keepout) => [keepout.name, keepout])); + for (const place of PLACES) { + expect(byName[place.id], place.id).toEqual({ name: place.id, at: place.at, rotation: place.rotation, bounds: place.bounds }); + } + }); + + // Bruch: der Startpunkt liegt nach dem Umbau des Parks in einem Gebäude oder über Wasser. + it("setzt den Startpunkt auf Land und in kein Gebäude", () => { + const c = chunkAt(SPAWN.x, -SPAWN.z); + const u = SPAWN.x - c.origin[0], v = SPAWN.z - c.origin[1]; + expect(c.land.some((rings) => pointInPolygon(u, v, rings))).toBe(true); + expect(c.buildings.some((building) => pointInPolygon(u, v, building.poly))).toBe(false); + }); + + // Bruch: das Abschneiden am Park nimmt der 11th Street ein Stück, und ein Auto fällt beim Start durch. + it("lässt die vier Autos auf einer Straße stehen", () => { + for (const [x, , z] of VEHICLE_SPAWN_POINTS) { + const c = chunkAt(x, -z); + expect(c.roads.some((road) => pointInPolygon(x - c.origin[0], z - c.origin[1], road.poly)), `${x} / ${-z}`).toBe(true); + } + }); +}); diff --git a/tests/placesSite.test.js b/tests/placesSite.test.js index 603a8f9..517fd72 100644 --- a/tests/placesSite.test.js +++ b/tests/placesSite.test.js @@ -79,6 +79,16 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte", expect(bad.slice(0, 5)).toEqual([]); }); + // Bruch: ein OSM-Weg läuft durch einen Ort, weil seine Sperrfläche in flamingo-park.toml fehlt oder verrutscht ist. + it("liegt auf keinem Parkweg", () => { + const bad = points.filter(({ X, Y }) => { + const { i, j } = chunkForWorld(X, Y); + const c = chunk(i, j); + return (c.paths ?? []).some((path) => pointInPolygon(X - c.origin[0], -Y - c.origin[1], path.poly)); + }); + expect(bad.slice(0, 5)).toEqual([]); + }); + // Bruch: Nach einem neuen Lauf der Extraktion liegt das ersetzte Gebäude woanders oder ist geteilt, oder // der Punkt trifft ein fremdes oder mit einem Nachbarn zusammengelegtes Gebäude. Der Punkt träfe dann // nichts (der alte Kasten stünde wieder im Ort), mehrere Grundrisse, einen außerhalb, oder einen mit