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>
13 KiB
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:
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)aussrc/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 ausplayer.getControllerMode() === 1, nicht ausgetActiveVehicle()— 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:
InputSystemregistriertkeydown,keyup,mousemoveundclickaufwindowin der Bubble-Phase.- Die Karte registriert beim Öffnen Listener auf
windowin der Capture-Phase, die diese Ereignisse mitstopPropagation()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 ControllerresetKeys()auf, damit eine beim Drücken von M gehaltene Bewegungstaste nicht hängen bleibt und die Figur weiterlaufen lässt. - Nicht
unbindEvents()verwenden: es setztisupdate = falseund hält damit die gesamte Figurenaktualisierung an, nicht nur die Eingabe. Das widerspräche der Entscheidung, dass die Welt weiterläuft.
- Die Karte registriert beim Öffnen Listener auf
-
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.pngnachgeschlagen.- 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 = 5px —nearestWalkableerhält den Radius in Pixeln, die Umrechnung aus Metern geschieht an genau einer Stelle übermap.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,10und 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 überchunkForWorld(i = 1, j = 8)und damitB9. An der bestehenden Funktion nachgerechnet.
Ohne Browser (vitest), für mapMath.js:
worldToMapundmapToWorldan 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.
nearestWalkablean 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.pnghat 3.000 × 3.000 px; ein bekannter Wasserpunkt in der Bucht ist wasserfarben, ein bekannter Landpunkt nicht.map-walkable.pnghat 750 × 750 px; die Fläche eines Gebäudegrundrisses ist 0, eine Straße daneben 255.map.jsonenthä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.pysie in einem eigenen Schritt nach.