# 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 `