Files
gta6/docs/superpowers/specs/2026-09-13-minimap-design.md
T
knsoandClaude Opus 5 c0767375fe docs: Minimap-Spec um benanntes Chunk-Raster ergänzen
Der Nutzer will Chunks im Chat für die Feinkalibrierung ansprechen. Das
Raster wird auf der Karte sichtbar, jeder Chunk heißt als Planquadrat
A1 bis L12 — A1 ist die Nordwest-Ecke, gegen chunkOrigin geprüft. Die
Umrechnung steht in JavaScript und Python mit demselben Beispielsatz,
dazu eine Anzeige von Chunk-Name und Weltkoordinate unter dem Mauszeiger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:09:54 +02:00

168 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Minimap — Entwurf
**Datum:** 2026-09-13
**Zusammenhang:** Teil des Boden-Updates, das der Nutzer nach dem Spielen angefordert hat. Reihenfolge vom Nutzer entschieden: Schichten und Minimap parallel, danach Straßenbau (Rissrichtung, Bordstein, Gehweg).
**Baut auf:** `docs/superpowers/specs/2026-09-12-leonida-vice-slice-design.md` und dem umgesetzten Plan dazu.
## Ziel
Eine große Kartenansicht des gesamten Vice-City-Ausschnitts, die sich über dem laufenden Spiel öffnet, wenn man **M** drückt.
## Entscheidungen des Nutzers
| Frage | Entscheidung |
|---|---|
| Darstellung | Große Karte als Überlagerung, bildschirmfüllend. M oder Esc schließt. Keine Eckkarte. |
| Inhalt | Eigene Position mit Blickrichtung, Fahrzeuge, Zoomen und Verschieben, Klick zum Hinspringen. **Keine** Wahrzeichen. |
| Kartenbild | Vorab erzeugt, 1 Pixel je Meter. |
| Welt während offener Karte | Läuft weiter, keine Pause. |
| Klick auf Gebäude oder Wasser | Landung auf dem nächsten begehbaren Punkt im Umkreis von 20 m, sonst Hinweis und kein Sprung. |
| Chunks | Raster auf der Karte sichtbar, jeder Chunk benannt als **Planquadrat A1 bis L12**. Zweck: der Nutzer spricht Chunks im Chat für die Feinkalibrierung an. |
## Daten
Drei neue Dateien neben `manifest.json` in `public/data/vice-a/`:
| Datei | Inhalt |
|---|---|
| `map.png` | 3.000 × 3.000 px, 1 px/m. Wasser, Land, Sand, Park, Stadtfläche, Straßen und Brücken, Gebäude nach Höhe getönt — dieselben Farben wie die Gesamtansicht. **Ohne Palmen**: bei 1 px/m wären sie nur Rauschen. Löcher werden wie in der Gesamtansicht über eine Maske korrekt ausgespart. |
| `map-walkable.png` | 750 × 750 px, 4 m/px, Graustufen. 255 = begehbar (Land, Straße, Brücke, Sand, Park, Stadtfläche außerhalb von Gebäuden), 0 = nicht begehbar (Wasser oder Gebäude). |
| `map.json` | `{ "scale": 1.0, "walkable_scale": 0.25, "slice": { "x0": -1000, "x1": 2000, "y0": -500, "y1": 2500 }, "manifest_sha256": "<SHA-256 von manifest.json>" }` |
`map.json` ist die **einzige** Quelle für Maßstab und Ausschnittsgrenzen der Karte. Der Browser liest sie, statt die Werte ein zweites Mal im JavaScript festzuschreiben.
**Erzeugung:** vorerst als eigener Schritt `cd tools/extract && uv run python overview.py`, **nicht** in `run.py`. Grund: die Beschriftungs-Aufgabe task_999fed4a arbeitet gleichzeitig in einer anderen Sitzung an der Pipeline, und zwei Sitzungen sollen nicht in dieselbe Datei schreiben. Der Preis dieser Entkopplung ist, dass die Karte nach einem Pipeline-Lauf veralten kann — abgesichert über `manifest_sha256` (siehe Fehlerfälle). Das Einhängen in `run.py` folgt, sobald die Pipeline frei ist.
## Aufbau
| Einheit | Aufgabe |
|---|---|
| `tools/extract/overview.py` | Zusätzlicher Ausgang für `map.png`, `map-walkable.png` und `map.json`. Die bestehende Debug-Gesamtansicht bleibt unverändert erhalten. |
| `src/ui/mapMath.js` | Reine Funktionen ohne DOM, vollständig ohne Browser testbar: `worldToMap`, `mapToWorld`, Zoom- und Verschiebungsgrenzen, Unterscheidung Klick gegen Ziehen, `nearestWalkable(mask, px, py, radiusPx)`. |
| `src/ui/map.js` | Die Kartenkomponente: ein Canvas über dem Spiel, zeichnet jedes Bild Kartenbild, Spielerpfeil und Fahrzeugsymbole. Öffnen, Schließen, Offen-Zustand. |
| `src/main.js` | Nur Einhängen: Taste M und Esc, Position, Blickrichtung, Fahrzeuge, Sprung. |
Die Kartenkomponente bekommt ihre Abhängigkeiten übergeben, statt selbst in Controller oder Streamer zu greifen:
```js
createMap({
mapInfo, // Inhalt von map.json
manifest, // für den Fingerabdruck-Vergleich
getPosition, // () => { x, z } in three-Koordinaten
getHeading, // () => Winkel in Radiant
getVehicles, // () => [{ x, z, heading, occupied }]
onTeleport, // ({ x, z }) => void
}) // -> { open(), close(), toggle(), isOpen() }
```
## Koordinaten
three.js (x, z) → Welt (x, y = −z) → Kartenpixel:
```
px = (x − slice.x0) · scale
py = (slice.y1 − y) · scale = (slice.y1 + z) · scale
```
Die Umkehrung dient den Klicks. `mapMath.js` ist die **einzige** Stelle, an der die Achsenumkehr zwischen three.js-z und Karten-y steht — genau dort würde sonst eine gespiegelte Karte entstehen.
## Chunk-Raster und Namen
Die Namen sind die Schnittstelle zwischen Nutzer und Feinkalibrierung: was im Chat „C10" heißt, muss auf der Karte, in den Dateinamen und in der Pipeline eindeutig dasselbe sein.
**Schema:** Spalte als Buchstabe `A` bis `L` aus `i`, Zeile als Zahl `1` bis `12` aus `j`.
```
name(i, j) = "ABCDEFGHIJKL"[i] + (j + 1)
```
**Richtung, gegen die Daten geprüft:** `chunkOrigin(i, j)` liefert `ox = x0 + i · 250` und `oy = y1 − j · 250`. `i` wächst also nach Osten, `j` von der Nordkante nach Süden. Damit gilt:
| Name | Datei | Lage |
|---|---|---|
| A1 | `chunk_0_0.json` | Nordwest-Ecke, oben links auf der Karte |
| L1 | `chunk_11_0.json` | Nordost-Ecke |
| A12 | `chunk_0_11.json` | Südwest-Ecke |
| L12 | `chunk_11_11.json` | Südost-Ecke |
| C10 | `chunk_2_9.json` | Beispiel |
**Eine Umrechnung, zweimal geschrieben, einmal geprüft:** `chunkName(i, j)` und `parseChunkName(name) -> { i, j }` stehen in `src/world/coords.js` neben `chunkForWorld`, und dieselben zwei Funktionen als `chunk_name` und `parse_chunk_name` in `tools/extract/leonida/coords.py` — die Feinkalibrierung wird sie in der Pipeline brauchen. Beide Seiten prüfen denselben festen Satz an Beispielen aus der Tabelle oben, damit JavaScript und Python nicht auseinanderlaufen können. `parseChunkName` akzeptiert Groß- und Kleinschreibung (`c10` = `C10`) und weist alles außerhalb von A1–L12 ab.
**Auf der Karte:**
- Rasterlinien an allen Chunk-Grenzen.
- Der Name steht in der Mitte jedes Chunks, sobald der Chunk auf dem Bildschirm mindestens 40 px breit ist; darunter nur die Linien. Beim Öffnen passt der ganze Ausschnitt ins Fenster, dann ist jeder Chunk auf gängigen Bildschirmen breit genug.
- Unter dem Mauszeiger eine Anzeige mit Chunk-Name und Weltkoordinate, etwa `C10 · x 412 / y 1830`. Die Koordinate erlaubt, eine Stelle innerhalb eines Chunks genau zu benennen.
- Der Chunk, in dem der Spieler gerade steht, ist hervorgehoben und sein Name steht fest in einer Ecke der Karte.
## Anbindung an vorhandene Funktionen
- **Position:** über `playerPosition(player)` aus `src/player.js`. Damit fährt der Pfeil auch im Fahrzeug korrekt mit; dort liefert die Kapselposition nur einen lokalen Sitzplatz-Offset.
- **Blickrichtung:** aus der Weltrichtung der Kamera, auf die Bodenebene projiziert. Das gilt zu Fuß wie im Fahrzeug gleichermaßen, weil die Kamera der Fahrzeugrichtung folgt (`followVehicleDirection: true`).
- **Fahrzeuge:** über `player.getAllVehicles()`. Das Fahrzeug, in dem der Spieler sitzt, wird nicht zusätzlich als eigenes Symbol gezeichnet. Ob der Spieler gerade fährt, kommt aus `player.getControllerMode() === 1`, **nicht** aus `getActiveVehicle()` — letzteres wird beim Aussteigen nie zurückgesetzt.
- **Sprung:** über das vorhandene `requestTeleport`, mit seiner Wartewache, bis der Ziel-Chunk bereit ist.
## Bedienung
- **Öffnen und Schließen:** M öffnet und schließt, Esc schließt. M wird ignoriert, solange ein Eingabefeld (`input`, `textarea`, `select`) den Fokus hat — sonst öffnete sich die Karte beim Eintippen eines Werts im Panel.
- **Beim Öffnen** immer der ganze Ausschnitt, eingepasst ins Fenster.
- **Mausfang:** beim Öffnen wird der Pointer-Lock gelöst. Beim Schließen wird er nicht automatisch neu angefordert; Browser erlauben das nur nach einer Nutzergeste, ein Klick ins Spiel fängt ihn wie gewohnt.
- **Steuerung ruht, Welt läuft.** Solange die Karte offen ist, erreichen Tastatur- und Mausereignisse die Figur nicht; Streaming, Fahrzeugphysik und Animation laufen weiter.
Umsetzung, am Quelltext des Controllers belegt: `InputSystem` registriert `keydown`, `keyup`, `mousemove` und `click` auf `window` in der Bubble-Phase.
- Die Karte registriert beim Öffnen Listener auf `window` **in der Capture-Phase**, die diese Ereignisse mit `stopPropagation()` abfangen, und entfernt sie beim Schließen wieder. Die eigenen Kartenereignisse auf dem Canvas bleiben davon unberührt.
- Beim Öffnen wird einmal `window.dispatchEvent(new Event("blur"))` ausgelöst. Das ruft im Controller `resetKeys()` auf, damit eine beim Drücken von M gehaltene Bewegungstaste nicht hängen bleibt und die Figur weiterlaufen lässt.
- **Nicht** `unbindEvents()` verwenden: es setzt `isupdate = false` und hält damit die gesamte Figurenaktualisierung an, nicht nur die Eingabe. Das widerspräche der Entscheidung, dass die Welt weiterläuft.
- **Zoomen:** Mausrad, um den Mauszeiger herum, von „ganzer Ausschnitt passt ins Fenster" bis vierfach.
- **Verschieben:** Ziehen mit gedrückter Maustaste, begrenzt auf den Kartenrand.
- **Hinspringen:** Ein Klick — Maustaste gedrückt und losgelassen mit weniger als 4 px Bewegung dazwischen — springt an die Stelle und schließt die Karte. Mit 4 px oder mehr Bewegung ist es ein Ziehen, und es wird nicht gesprungen.
- **Landeplatz:** Der Klickpunkt wird in `map-walkable.png` nachgeschlagen.
- Begehbar: Sprung genau dorthin.
- Nicht begehbar (Gebäude oder Wasser): Sprung auf den nächsten begehbaren Pixel im Umkreis von 20 m, gemessen als euklidischer Abstand. In Maskenpixeln sind das `20 · walkable_scale = 5` px — `nearestWalkable` erhält den Radius in Pixeln, die Umrechnung aus Metern geschieht an genau einer Stelle über `map.json`.
- Kein begehbarer Pixel im Umkreis: kein Sprung, die Karte bleibt offen, und für etwa zwei Sekunden erscheint „Kein Landeplatz".
## Fehlerfälle
| Fall | Verhalten |
|---|---|
| `map.png` lässt sich nicht laden | M öffnet die Überlagerung mit dem Hinweis „Kartenbild fehlt — `uv run python overview.py` ausführen". Kein Absturz, das Spiel läuft weiter. |
| `map-walkable.png` lässt sich nicht laden | Anschauen, Zoomen und Verschieben funktionieren. Klick zum Hinspringen ist abgeschaltet, ein Hinweis nennt den Grund. |
| `map.json` fehlt oder ist ungültig | Wie fehlendes Kartenbild: ohne Maßstab und Grenzen lässt sich nichts korrekt platzieren. |
| `manifest_sha256` passt nicht zum geladenen Manifest | Die Karte funktioniert vollständig, zeigt aber sichtbar „Karte veraltet — `overview.py` erneut ausführen". |
## Prüfung
**Chunk-Namen, in beiden Sprachen mit demselben Beispielsatz (vitest und pytest):**
- `A1 ↔ (0, 0)`, `L1 ↔ (11, 0)`, `A12 ↔ (0, 11)`, `L12 ↔ (11, 11)`, `C10 ↔ (2, 9)` in beide Richtungen.
- Kleinschreibung wird akzeptiert; `M1`, `A0`, `A13`, `A`, `10` und die leere Eingabe werden abgewiesen.
- Für jeden der 144 Chunks ergibt `parseChunkName(chunkName(i, j))` wieder `(i, j)`.
- Ein Test, der die Namen mit der tatsächlichen Lage verbindet statt nur mit den Indizes: der Downtown-Kern bei Welt `(−511, 254)` ergibt über `chunkForWorld` `(i = 1, j = 8)` und damit `B9`. An der bestehenden Funktion nachgerechnet.
**Ohne Browser (vitest), für `mapMath.js`:**
- `worldToMap` und `mapToWorld` an bekannten Punkten: Spawn `(1699, 3, −743)` und alle vier Ecken des Ausschnitts; dazu Hin- und Rückweg ergibt wieder den Ausgangspunkt. Ein Test, der eine gespiegelte Achse sicher fängt.
- Zoomgrenzen: nie kleiner als „ganzer Ausschnitt", nie größer als vierfach; Verschiebung bleibt innerhalb des Kartenrands.
- Klick gegen Ziehen an der 4-px-Schwelle, knapp darunter und knapp darüber.
- `nearestWalkable` an einer künstlichen Maske: begehbarer Treffer bleibt, wo er ist; ein Gebäudepixel neben einer Straße findet die Straße; nur Wasser im Umkreis ergibt kein Ergebnis; ein Treffer knapp außerhalb von 20 m zählt nicht.
**Pipeline (pytest), für `overview.py`:**
- `map.png` hat 3.000 × 3.000 px; ein bekannter Wasserpunkt in der Bucht ist wasserfarben, ein bekannter Landpunkt nicht.
- `map-walkable.png` hat 750 × 750 px; die Fläche eines Gebäudegrundrisses ist 0, eine Straße daneben 255.
- `map.json` enthält den korrekten SHA-256 des aktuellen Manifests.
**Im Browser, mit Bildern im Bericht:**
- M öffnet, M und Esc schließen; bei fokussiertem Eingabefeld öffnet M nicht.
- Beim Öffnen mit gehaltener W-Taste bleibt die Figur stehen, statt weiterzulaufen.
- Zoomen, Verschieben, Kartenrand.
- Klick auf eine Straße, auf ein Gebäude (Landung daneben) und mitten in die Bucht („Kein Landeplatz").
- Im Auto fahren und prüfen, dass Pfeil und Richtung mitfahren und das eigene Fahrzeug nicht doppelt erscheint.
- Mit absichtlich verändertem Manifest erscheint „Karte veraltet".
## Nicht Teil dieses Entwurfs
- **Die Feinkalibrierung selbst** — wie Korrekturen je Chunk gespeichert und angewendet werden. Eigenes Teilsystem mit eigenem Entwurf; diese Spec liefert dafür nur die Namen, das Raster und die Koordinatenanzeige.
- Wahrzeichen auf der Karte (vom Nutzer abgewählt).
- Eine kleine Eckkarte.
- Pausieren des Spiels.
- Wegmarken und Routen.
- Einhängen der Kartenerzeugung in `run.py` — folgt, sobald die Beschriftungs-Aufgabe gelandet ist.
- Bordstein, Gehweg und Rissrichtung — der anschließende Straßenbau. Kommen dort neue Flächen hinzu, zeichnet `overview.py` sie in einem eigenen Schritt nach.