feat(world): Tribüne im Park J6 statt des alten Gebäudekastens
tribuene.glb mit places.mjs aufbereitet (6,3 MB -> 1,7 MB, 69 557 Dreiecke, 14 Primitive, 49 MB Grafikspeicher). PLACES-Eintrag bei (1409,9 | 1051,1), 45° gedreht, lift 0, replaces auf den 18,6 m hohen Kasten. Der Lage-Test übergeht ersetzte Grundrisse und prüft, dass jeder replaces-Punkt genau ein Gebäude unter dem Ort trifft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae0f6106bb
commit
190fe9c13c
5 files changed
+51
-8
No files matched your search
@@ -98,9 +98,12 @@ Die positive Drehung bringt +x nach Nordosten und die Front (+z) nach Südosten.
|
||||
|
||||
## Budget
|
||||
|
||||
- **Erwartet:** rund 70 000 Dreiecke, 14 Primitive (Draw Calls), unter 53 MB Grafikspeicher, Datei deutlich unter 30 MB.
|
||||
- **Gemessen an der Aufbereitung (2026-09-14):**
|
||||
- 1,7 MB, 69 557 Dreiecke, 14 Primitive (Draw Calls)
|
||||
- 39 WebP-Texturen: 4 × 768, 23 × 512, 12 × 256 px
|
||||
- geschätzt 49 MB Grafikspeicher, Außenmaße unverändert
|
||||
- Die sechs Kleinteil-Materialien haben 15 939 Dreiecke statt 64 298.
|
||||
- **Zum Vergleich der Hood Yard:** 657 653 Dreiecke, 43 Primitive, 231 MB.
|
||||
- Nach der Aufbereitung kommen die Werte aus dem Bericht von `places.mjs` hier hinein.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
Binary file not shown.
+17
-1
@@ -19,7 +19,9 @@ 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.
|
||||
* prüfen damit Datei (placesAsset.test.js) und Lage (placesSite.test.js) gegeneinander. replaces
|
||||
* (optional): Weltpunkte [x, y]; jeder Gebäudegrundriss aus der Extraktion, in dem einer liegt, wird beim
|
||||
* Chunk-Aufbau weggelassen (withoutReplacedBuildings in streaming.js).
|
||||
*/
|
||||
export const PLACES = [
|
||||
{
|
||||
@@ -32,6 +34,20 @@ export const PLACES = [
|
||||
lift: -0.28,
|
||||
bounds: { minX: -17.15, maxX: 17, minZ: -13.4, maxZ: 8.6 },
|
||||
},
|
||||
{
|
||||
id: "tribuene",
|
||||
url: "./models/places/tribuene.glb",
|
||||
// Längs mittig auf dem alten Gebäudekasten, Front 1 m vor dessen Feldseite (Vergleichsbild des Nutzers);
|
||||
// längs bleiben bis zur Fahrbahn nur 1,25 m nach Nordosten und 1,8 m nach Südwesten.
|
||||
at: [1409.9, 1051.1],
|
||||
// Die Sitzreihen schauen wie in echt nach Südosten auf die Home Plate.
|
||||
rotation: 45,
|
||||
// Keine Bodenplatte: der Betonkörper steht mit seiner Unterseite auf 0, flach auf 0 liegt nichts.
|
||||
lift: 0,
|
||||
bounds: { minX: -20.8, maxX: 20.8, minZ: -14.82, maxZ: 0.8 },
|
||||
// Der 18,6 m hohe Gebäudekasten aus der Extraktion an dieser Stelle (J6).
|
||||
replaces: [[1403.25, 1057.75]],
|
||||
},
|
||||
];
|
||||
|
||||
/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Sitzschalen, Kleinteile — läuft man durch. */
|
||||
|
||||
@@ -181,14 +181,15 @@ describe("prepareModel", () => {
|
||||
|
||||
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) {
|
||||
// prüften nichts; oder replaces ist keine Liste von Punkten [x, y], und der Streamer ließe nichts weg.
|
||||
it("hat für jeden Ort id, url, at, rotation, lift und bounds, dazu optional replaces mit Weltpunkten", () => {
|
||||
for (const { replaces = [], ...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) },
|
||||
});
|
||||
for (const point of replaces) expect(point).toEqual([expect.any(Number), expect.any(Number)]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { CHUNKS_PER_SIDE, CHUNK_SIZE, SLICE, chunkForWorld } from "../src/world/coords.js";
|
||||
import { PLACES, PLACE_HIDE_RADIUS, PLACE_SHOW_RADIUS, placeTransform } from "../src/world/places.js";
|
||||
import { lampPoints, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
|
||||
import { withoutReplacedBuildings } from "../src/world/streaming.js";
|
||||
|
||||
// Radien aus ChunkStreamer.update (src/world/streaming.js), gemessen zum Chunk-Mittelpunkt.
|
||||
const CHUNK_LOAD_RADIUS = 400;
|
||||
@@ -18,6 +19,16 @@ function chunk(i, j) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
// Grundrisse, die ein Ort ersetzt, baut der Streamer nicht (withoutReplacedBuildings, verdrahtet in
|
||||
// src/main.js) — für die Prüfung auf Gebäude zählen sie deshalb nicht.
|
||||
const REPLACED = PLACES.flatMap((place) => place.replaces ?? []);
|
||||
const standing = new Map();
|
||||
function standingBuildings(i, j) {
|
||||
const key = `${i}_${j}`;
|
||||
if (!standing.has(key)) standing.set(key, withoutReplacedBuildings(chunk(i, j), REPLACED).buildings);
|
||||
return standing.get(key);
|
||||
}
|
||||
|
||||
/** Modellpunkt (x, z) -> Welt (X, Y), mit derselben Drehung wie three (rotation.y). */
|
||||
function modelToWorld(place, x, z) {
|
||||
const { position, rotationY } = placeTransform(place);
|
||||
@@ -51,7 +62,7 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
||||
const under = keys.map((key) => key.split("_").map(Number));
|
||||
|
||||
// Bruch: der Ort steht auf einer Straße, in einem Gebäude oder über Wasser — nach einem Verschieben
|
||||
// in PLACES oder nach einem neuen Lauf der Extraktion.
|
||||
// in PLACES oder nach einem neuen Lauf der Extraktion. Gebäude, die ein Ort ersetzt, zählen nicht.
|
||||
it("liegt ganz auf Land, auf keiner Straße und in keinem Gebäude", () => {
|
||||
const bad = points.filter(({ X, Y }) => {
|
||||
const { i, j } = chunkForWorld(X, Y);
|
||||
@@ -59,12 +70,24 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
||||
const u = X - c.origin[0], v = -Y - c.origin[1];
|
||||
const onLand = c.land.some((rings) => pointInPolygon(u, v, rings));
|
||||
const onRoad = c.roads.some((road) => pointInPolygon(u, v, road.poly));
|
||||
const inBuilding = c.buildings.some((building) => pointInPolygon(u, v, building.poly));
|
||||
const inBuilding = standingBuildings(i, j).some((building) => pointInPolygon(u, v, building.poly));
|
||||
return !onLand || onRoad || inBuilding;
|
||||
});
|
||||
expect(bad.slice(0, 5)).toEqual([]);
|
||||
});
|
||||
|
||||
// Bruch: Nach einem neuen Lauf der Extraktion liegt das ersetzte Gebäude woanders oder ist geteilt. Der
|
||||
// Punkt träfe dann nichts (der alte Kasten stünde wieder im Ort), mehrere Grundrisse oder einen außerhalb.
|
||||
it.runIf((place.replaces ?? []).length > 0)("ersetzt mit jedem replaces-Punkt genau ein Gebäude unter sich", () => {
|
||||
const bad = place.replaces.filter(([X, Y]) => {
|
||||
const { i, j } = chunkForWorld(X, Y);
|
||||
const c = chunk(i, j);
|
||||
const hits = c.buildings.filter((building) => pointInPolygon(X - c.origin[0], -Y - c.origin[1], building.poly));
|
||||
return hits.length !== 1 || !insideBounds(place, X, Y);
|
||||
});
|
||||
expect(bad).toEqual([]);
|
||||
});
|
||||
|
||||
// Bruch: eine Palme oder Laterne mitten im Ort. Beide entstehen je Chunk und wissen nichts von Orten;
|
||||
// Laternen stehen bis 16 m neben ihrer Mittellinie, deshalb zählen auch die Nachbarchunks.
|
||||
it("enthält keine Palme und keine Laterne", () => {
|
||||
|
||||
Reference in new issue
Block a user