diff --git a/docs/superpowers/specs/2026-09-13-minimap-design.md b/docs/superpowers/specs/2026-09-13-minimap-design.md new file mode 100644 index 0000000..330e51f --- /dev/null +++ b/docs/superpowers/specs/2026-09-13-minimap-design.md @@ -0,0 +1,131 @@ +# 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. | + +## 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": "" }` | + +`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. + +## 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 + +**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 + +- 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.