diff --git a/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md new file mode 100644 index 0000000..3f42f27 --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md @@ -0,0 +1,2278 @@ +# Leonida Vice-City-Ausschnitt: Umsetzungsplan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ein 3 × 3 km großer, zu Fuß und mit dem Auto spielbarer 3D-Blockout von Vice City und Vice Beach, erzeugt aus den Kacheln der Community-Karte YANIS v15. + +**Architecture:** Python liest die Kartenkacheln einmalig aus und schreibt Vektordaten je 250-m-Chunk. Die Vite-App lädt diese Chunks im Umkreis des Spielers, baut daraus zur Laufzeit Meshes (Land, Straßen, Brücken, Gebäude) und meldet je Chunk genau einen Collider beim `three-player-controller` an. Der Geometrie-Baustein ist reines JavaScript ohne Browser-Abhängigkeiten und läuft deshalb auch in Node, damit derselbe Code später GLB-Chunks vorbacken kann. + +**Tech Stack:** Node 26, Vite, three 0.184.0, three-mesh-bvh, three-player-controller (lokal), vitest, @gltf-transform/cli 4.5.0 · Python 3.12 über uv mit numpy, scipy, scikit-image, shapely, networkx, pillow, pytest + +**Spec:** `docs/superpowers/specs/2026-09-12-leonida-vice-slice-design.md` + +## Global Constraints + +- Eine Spieleinheit ist ein Meter. Umrechnung in three.js: `x = X`, `z = −Y`. Norden liegt bei `−Z`. +- Ausschnitt A: `X −1000 … 2000`, `Y −500 … 2500`. Kacheln: Spalten 60–71, Zeilen 54–65, Dateiname `5,{zeile},{spalte}.jpg`. +- Kartenquelle ist ausschließlich `github.com/rolux/gtadb.org` beim Commit `4093e8c874e480d47c38b37fe7a404f8b0c5275e`. Niemals `map.stateofleonida.net` abfragen. +- three exakt auf `0.184.0` pinnen. In `vite.config.js` sind `resolve.dedupe: ['three','three-mesh-bvh']` und `optimizeDeps.exclude: ['three-mesh-bvh']` zwingend. +- Verboten als Abhängigkeit: `skan`, `buildingregulariser`, OpenCV, GDAL, rasterio. +- Chunkgröße 250 m. Laden ab 400 m, Entladen ab 600 m. Pro Frame höchstens ein Chunk-Bau, eine Collider-Anmeldung und vier Abmeldungen. +- Budgets: mindestens 55 FPS, unter 150 Draw Calls, Chunk-Bau unter 20 ms im Hauptthread. +- Die Namensnennung im HUD ist Pflicht (CC BY 4.0) und darf in keinem Schritt entfernt werden. +- Jeder Commit endet mit `Co-Authored-By: Claude Opus 5 `. + +## Dateistruktur + +| Datei | Verantwortung | +|---|---| +| `package.json`, `vite.config.js`, `index.html` | App-Gerüst, Abhängigkeiten, Vite-Sonderfälle | +| `src/main.js` | Renderer, Kamera, Licht, Spielschleife, Zusammenbau | +| `src/world/coords.js` | Umrechnung Welt ↔ Kachel ↔ Chunk. Reine Funktionen | +| `src/world/geometry.js` | Chunk-Daten → BufferGeometry (Land, Straßen, Brücken, Gebäude) | +| `src/world/streaming.js` | Laden und Entladen von Chunks, Collider-Verwaltung | +| `src/world/materials.js` | Poly-Haven-Materialien, Welt-UVs, Wasser | +| `src/world/props.js` | Palmen und Requisiten als InstancedMesh | +| `src/characters/index.js` | Figurenauswahl, Retargeting, Hitbox (aus showcase.js) | +| `src/ui/panel.js`, `src/ui/hud.js` | Debug-Panel, Namensnennung, Ladeanzeige | +| `tools/extract/leonida/coords.py` | dieselbe Umrechnung wie `coords.js`, Referenz für Tests | +| `tools/extract/leonida/tiles.py` | Kacheln laden, Cache, Mosaik | +| `tools/extract/leonida/classify.py` | Palette, Lab-Zuordnung, Auffüllen | +| `tools/extract/leonida/vectors.py` | Masken → Polygone (Land, Straßen, Gebäude) | +| `tools/extract/leonida/heights.py` | Höhen aus Wahrzeichen und Vierteln | +| `tools/extract/leonida/emit.py` | Chunk-JSON, Manifest, Debug-PNG | +| `tools/extract/run.py` | Ablaufsteuerung | +| `tools/assets/fetch.mjs` | Poly-Haven-Downloads, Palme wandeln, vereinfachen | + +--- + +### Task 1: App-Gerüst mit laufendem Spieler + +**Files:** +- Create: `package.json`, `vite.config.js`, `index.html`, `src/main.js`, `src/world/coords.js`, `tests/coords.test.js` +- Modify: `.gitmodules` (durch `git submodule add`) +- Test: `tests/coords.test.js` + +**Interfaces:** +- Consumes: nichts +- Produces: `worldToThree(X, Y) -> {x, z}`, `threeToWorld(x, z) -> {X, Y}`, `tileForWorld(X, Y) -> {row, col}`, `chunkForWorld(X, Y) -> {i, j}`, `CHUNK_SIZE = 250`, `SLICE = {x0:-1000, x1:2000, y0:-500, y1:2500}` aus `src/world/coords.js` + +- [ ] **Step 1: Controller als Submodul einbinden** + +```bash +cd /Users/Kenso/Developer/gta6 +git -C three-player-controller checkout ac39682 # unveränderter Upstream-Stand +git submodule add https://github.com/hh-hang/three-player-controller.git three-player-controller +git -C three-player-controller checkout leonida/showcase-figuren # Arbeitskopie zurück auf den Branch +``` + +Der Submodul-Eintrag zeigt damit auf `ac39682`; die Arbeitskopie steht auf dem Branch, damit die Showcase weiter läuft. `git status` meldet das Submodul dann als „modified content" — das ist erwartet. + +- [ ] **Step 2: Abhängigkeiten anlegen** + +`package.json`: + +```json +{ + "name": "leonida", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "vitest run" + }, + "dependencies": { + "three": "0.184.0", + "three-mesh-bvh": "^0.9.14", + "three-player-controller": "file:three-player-controller", + "lil-gui": "^0.21.0" + }, + "devDependencies": { + "vite": "^7.3.0", + "vitest": "^3.2.0" + } +} +``` + +`vite.config.js`: + +```js +import { defineConfig } from "vite"; + +export default defineConfig({ + // Ohne dedupe lädt der Controller three aus seinem eigenen node_modules -> zwei Kopien. + resolve: { dedupe: ["three", "three-mesh-bvh"] }, + // Der BVH-Worker wird dynamisch aus three-mesh-bvh/src/workers/ geladen und darf nicht vorgebündelt werden. + optimizeDeps: { exclude: ["three-mesh-bvh", "three-player-controller"] }, + server: { host: true }, +}); +``` + +Dann `npm install` ausführen. + +- [ ] **Step 3: Den fehlschlagenden Test schreiben** + +`tests/coords.test.js`: + +```js +import { describe, expect, it } from "vitest"; +import { CHUNK_SIZE, chunkForWorld, threeToWorld, tileForWorld, worldToThree } from "../src/world/coords.js"; + +describe("coords", () => { + it("dreht Y zur three-z-Achse", () => { + expect(worldToThree(1699, 743)).toEqual({ x: 1699, z: -743 }); + expect(threeToWorld(1699, -743)).toEqual({ X: 1699, Y: 743 }); + }); + + it("findet die Kachel von Vice Beach", () => { + expect(tileForWorld(1409.4, 2803.2)).toEqual({ row: 53, col: 69 }); + }); + + it("deckt den Ausschnitt mit Spalten 60..71 und Zeilen 54..65 ab", () => { + expect(tileForWorld(-1000, 2500)).toEqual({ row: 54, col: 60 }); + expect(tileForWorld(1999, -499)).toEqual({ row: 65, col: 71 }); + }); + + it("teilt den Ausschnitt in 250-m-Chunks", () => { + expect(CHUNK_SIZE).toBe(250); + expect(chunkForWorld(-1000, 2500)).toEqual({ i: 0, j: 0 }); + expect(chunkForWorld(1999, -499)).toEqual({ i: 11, j: 11 }); + }); +}); +``` + +- [ ] **Step 4: Test laufen lassen, er muss scheitern** + +Run: `npm test` +Expected: FAIL, `Failed to resolve import "../src/world/coords.js"` + +- [ ] **Step 5: `src/world/coords.js` schreiben** + +```js +export const TILE_PX = 256; +export const WORLD_HALF = 16384; // gtadb-Weltgrenze in Metern +export const CHUNK_SIZE = 250; +export const SLICE = { x0: -1000, x1: 2000, y0: -500, y1: 2500 }; +export const CHUNKS_PER_SIDE = (SLICE.x1 - SLICE.x0) / CHUNK_SIZE; // 12 + +export const worldToThree = (X, Y) => ({ x: X, z: -Y }); +export const threeToWorld = (x, z) => ({ X: x, Y: -z }); + +export function tileForWorld(X, Y) { + return { + row: Math.floor((WORLD_HALF - Y) / TILE_PX), + col: Math.floor((X + WORLD_HALF) / TILE_PX), + }; +} + +export function chunkForWorld(X, Y) { + return { + i: Math.floor((X - SLICE.x0) / CHUNK_SIZE), + j: Math.floor((SLICE.y1 - Y) / CHUNK_SIZE), + }; +} + +/** Nordwest-Ecke eines Chunks in Weltkoordinaten. */ +export function chunkOrigin(i, j) { + return { X: SLICE.x0 + i * CHUNK_SIZE, Y: SLICE.y1 - j * CHUNK_SIZE }; +} +``` + +- [ ] **Step 6: Test laufen lassen, er muss bestehen** + +Run: `npm test` +Expected: PASS, 4 Tests + +- [ ] **Step 7: Minimale Szene mit Spieler** + +`public/models/ual.glb` aus `three-player-controller/example/public/glb/ual.glb` kopieren. `index.html` mit `
` und `