G1: Drei-Lane-Layout mit Höhenfeld, Grotte, Aufmarschbereichen und MOBA-Kamera

Top windet sich über das Hochland, Mid quert die Kluft über die Mittelplattform,
Bottom führt durch das Tiefland über die Deltabrücke; Top und Bottom sind exakt
gleich lang, alles bleibt spiegelsymmetrisch. LuneTerrain liefert ein exakt
gespiegeltes Höhenfeld, auf dem Figuren stehen. Bossgrotte als Terrasse auf der
Achse zwischen Top und Mid, Aufmarschbereiche je Lane, perspektivische Kamera
mit Sprungtasten (F2–F4, B, G, H, O, R, Q/E). Bestandsassets wiederverwendet,
neue Formen als Blockout für G2/G4.

Geprüft: verify_g1 443/443, Regeln 91/91, M2 73/73, verify_m1 38/38,
verify_m2 14/14, Netz 43/43, Loopback 12/12, Balance 40 Partien (Seitentausch
20/20), Legacy 209/463. Vorher/Nachher-Aufnahmen und Messungen in G1-KARTE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin Stickelbroeck 2026-09-29 00:09:48 +02:00
parent c15533831a
commit 8b17563814
96 changed files with 5409 additions and 372 deletions

View file

@ -12,8 +12,8 @@ Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebra
## Eine Partie ## Eine Partie
1. **Neue Partie** → Bund (Glutpakt oder Wurzelbund) und deine Lane (Top, Mid, Bottom) wählen → **Partie beginnen**. Du spielst in Team Blau (Südwest) mit zwei Bot-Verbündeten gegen drei Bots in Team Rot. 1. **Neue Partie** → Bund (Glutpakt oder Wurzelbund) und deine Lane (Top, Mid, Bottom) wählen → **Partie beginnen**. Du spielst in Team Blau (Südwest) mit zwei Bot-Verbündeten gegen drei Bots in Team Rot. Top führt über das Hochland, Mid über die Mittelplattform, Bottom durch das Tiefland über die Deltabrücke; die Bossgrotte liegt zwischen Top und Mid.
2. **Planung:** Truppen kaufen (Knöpfe oder 1–5), mit T/M/B einer Lane zuweisen, mit ↑ aufwerten, mit × zurückgeben. Käufe aus derselben Planung werden voll erstattet, ältere mit 75 %. **Startaufstellung** kauft Front, Nahkampf und Fernkampf. **Bereit** (Leertaste) übernimmt alle Befehle verbindlich. 2. **Planung:** Truppen kaufen (Knöpfe oder 1–5), mit T/M/B einer Lane zuweisen (die blau umrandeten Aufmarschflächen vor den Basistoren zeigen, wo die Armee antritt), mit ↑ aufwerten, mit × zurückgeben. Käufe aus derselben Planung werden voll erstattet, ältere mit 75 %. **Startaufstellung** kauft Front, Nahkampf und Fernkampf. **Bereit** (Leertaste) übernimmt alle Befehle verbindlich.
3. **Kampf:** Er läuft automatisch. Deine Truppen rücken mit der eigenen Monsterwelle über ihre Lane vor. Leertaste oder P pausiert, Tempo 1×/2×/4×. 3. **Kampf:** Er läuft automatisch. Deine Truppen rücken mit der eigenen Monsterwelle über ihre Lane vor. Leertaste oder P pausiert, Tempo 1×/2×/4×.
4. **Abrechnung:** Getötete Monster und Gebäude bringen Gold, dazu kommt das Rundeneinkommen. Deine Truppen kehren geheilt zurück. Türme und Kerne behalten ihren Schaden. 4. **Abrechnung:** Getötete Monster und Gebäude bringen Gold, dazu kommt das Rundeneinkommen. Deine Truppen kehren geheilt zurück. Türme und Kerne behalten ihren Schaden.
5. Die Partie endet erst, wenn ein Ancient fällt. Es gibt kein Rundenlimit. 5. Die Partie endet erst, wenn ein Ancient fällt. Es gibt kein Rundenlimit.
@ -26,9 +26,15 @@ Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebra
| Leertaste | Bereit / im Kampf Pause | | Leertaste | Bereit / im Kampf Pause |
| P | Pause | | P | Pause |
| WASD, Pfeiltasten, mittlere Maustaste ziehen | Kamera verschieben | | WASD, Pfeiltasten, mittlere Maustaste ziehen | Kamera verschieben |
| Mausrad | Zoom | | Mausrad | Zoom (nah flacher, weit steiler) |
| F2 / F3 / F4 | Kamera auf Top / Mid / Bottom |
| Tab / Lane-Knöpfe links | Kamera auf die nächste Lane | | Tab / Lane-Knöpfe links | Kamera auf die nächste Lane |
| O | Kartenübersicht | | B | eigene Basis (Ancient) |
| G | Bossgrotte |
| H | eigener Held |
| O | ganze Karte |
| R | Kamera zurücksetzen (Drehung, Zoom, eigene Lane) |
| Q / E | Kamera um 45° drehen |
| Minimap klicken/ziehen | Kamera springt dorthin | | Minimap klicken/ziehen | Kamera springt dorthin |
| F1 | Optionen und Steuerung | | F1 | Optionen und Steuerung |
| M / F11 | Ton / Vollbild | | M / F11 | Ton / Vollbild |
@ -48,8 +54,10 @@ godot --headless --path . res://tools/luneraria/balance_run.tscn -- --matchups -
python3 tools/luneraria/analyze_balance.py docs/luneraria/logs/m3/balance.json python3 tools/luneraria/analyze_balance.py docs/luneraria/logs/m3/balance.json
godot --headless --path . res://tools/luneraria/stress_run.tscn # Wellen bis 10^6, Langlauf godot --headless --path . res://tools/luneraria/stress_run.tscn # Wellen bis 10^6, Langlauf
godot --path . res://tools/luneraria/people_lineup.tscn -- --out=res://docs/luneraria/screenshots godot --path . res://tools/luneraria/people_lineup.tscn -- --out=res://docs/luneraria/screenshots
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn -- --out=res://docs/luneraria/logs/g1/perf-nachher-1920x1080.json
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_review.tscn # Aufnahmen godot --path . --resolution 1920x1080 res://tools/luneraria/lune_review.tscn # Aufnahmen
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn # G1: Layout, Gelände, Kamera (443)
godot --path . --resolution 1920x1080 res://tools/luneraria/g_views.tscn -- --out=res://docs/luneraria/screenshots/g1/nachher # Vergleichsansichten
# Legacy (Saumwacht) unverändert: # Legacy (Saumwacht) unverändert:
godot --headless --path . res://tests/test_runner.tscn godot --headless --path . res://tests/test_runner.tscn
godot --headless --path . -- --smoke-test godot --headless --path . -- --smoke-test

View file

@ -26,3 +26,9 @@ Stand: 27. September 2026 (M0).
7. **Tests:** Neue Tests liegen unter `tests/luneraria/` und `tools/luneraria/`, die Legacy-Tests bleiben unverändert und werden als „Legacy“ geführt. Neue Referenzen werden erst nach fachlicher Prüfung festgeschrieben. 7. **Tests:** Neue Tests liegen unter `tests/luneraria/` und `tools/luneraria/`, die Legacy-Tests bleiben unverändert und werden als „Legacy“ geführt. Neue Referenzen werden erst nach fachlicher Prüfung festgeschrieben.
8. **M3-Nachtrag:** Bots planen Items/Champions vor der Armee und helfen deterministisch (kein geteilter `rng`), damit Spiegelmatches bit-symmetrisch bleiben. Kartenprops stammen aus `map_props.glb` (MultiMesh, ENV_*-Kluftshader). Volksrepräsentanten `p_*` sind Asset- und Datenziele ohne Matcheinbindung; siehe [VOELKER.md](VOELKER.md). Balance- und Stresswerkzeuge liegen unter `tools/luneraria/`. 8. **M3-Nachtrag:** Bots planen Items/Champions vor der Armee und helfen deterministisch (kein geteilter `rng`), damit Spiegelmatches bit-symmetrisch bleiben. Kartenprops stammen aus `map_props.glb` (MultiMesh, ENV_*-Kluftshader). Volksrepräsentanten `p_*` sind Asset- und Datenziele ohne Matcheinbindung; siehe [VOELKER.md](VOELKER.md). Balance- und Stresswerkzeuge liegen unter `tools/luneraria/`.
9. **M4-Nachtrag:** `scripts/luneraria/net/lune_session.gd` (Node) kapselt ENet Host/Join. Der Host besitzt `LuneMatch`, rechnet jeden Kampf und sendet je Sitz den öffentlichen Zustand plus nur dessen eigenen Entwurf (`rpc_state`), zum Kampfbeginn die Einheitenliste (`rpc_combat_begin`), danach Momentaufnahmen (`rpc_frame`, 20/s, `PackedFloat32Array`). Der Client baut daraus ein reines Anzeige-Abbild von `LuneCombat`, auf dem nie eine Regel läuft. Clients senden nur Befehle (`rpc_hello` mit Protokollversion, `rpc_submit`, `rpc_ready`). Peer-ID → Sitz, ein Peer ein Sitz. Disconnect: Bot-Übernahme, kein Kernverlust, keine Hostmigration; Wiedereinstieg über den Anzeigenamen. `LuneGame` spricht offline direkt mit `LuneMatch`, im Netzspiel ausschließlich mit der Sitzung. Protokolltests `tests/luneraria/test_lune_net.gd`, Mehr-Runden-Loopback `tools/luneraria/net_pair.tscn` mit Treiber `tools/luneraria/net_rounds.py`. Export-Presets `Luneraria-Linux` / `Luneraria-Windows`, Demo-Bau `tools/luneraria/build_luneraria_demo.py`. 9. **M4-Nachtrag:** `scripts/luneraria/net/lune_session.gd` (Node) kapselt ENet Host/Join. Der Host besitzt `LuneMatch`, rechnet jeden Kampf und sendet je Sitz den öffentlichen Zustand plus nur dessen eigenen Entwurf (`rpc_state`), zum Kampfbeginn die Einheitenliste (`rpc_combat_begin`), danach Momentaufnahmen (`rpc_frame`, 20/s, `PackedFloat32Array`). Der Client baut daraus ein reines Anzeige-Abbild von `LuneCombat`, auf dem nie eine Regel läuft. Clients senden nur Befehle (`rpc_hello` mit Protokollversion, `rpc_submit`, `rpc_ready`). Peer-ID → Sitz, ein Peer ein Sitz. Disconnect: Bot-Übernahme, kein Kernverlust, keine Hostmigration; Wiedereinstieg über den Anzeigenamen. `LuneGame` spricht offline direkt mit `LuneMatch`, im Netzspiel ausschließlich mit der Sitzung. Protokolltests `tests/luneraria/test_lune_net.gd`, Mehr-Runden-Loopback `tools/luneraria/net_pair.tscn` mit Treiber `tools/luneraria/net_rounds.py`. Export-Presets `Luneraria-Linux` / `Luneraria-Windows`, Demo-Bau `tools/luneraria/build_luneraria_demo.py`.
10. **G1-Nachtrag (Karte):**
- `LuneMap` baut jede Lane aus Catmull-Rom-Kontrollpunkten ihrer blauen Hälfte (Endpunkt auf der Achse, gespiegelter Phantompunkt, dadurch knickfrei). Bottom wird per Bisektion auf die Länge von Top gelöst. `LanePath` sucht Segmente binär.
- Die Simulation bleibt zweidimensional. Die Darstellung liest Höhen aus `scripts/luneraria/view/lune_terrain.gd` (`LuneTerrain`): Höhenfeld aus Achsenkoordinate, Achsenabstand und gespiegelten Kartenelementen, daher exakt symmetrisch. Es wird für die Blau-Hälfte berechnet und gespiegelt, je Prozess einmal erzeugt; die `walk_height` liefert Deckhöhen statt Flussbett.
- `lune_world.gd` baut Gelände, Wasser, Lanebeläge, Plattform, Deltabrücke, Basen, Grotte, Landmarken, Vegetationscluster und Aufmarschflächen aus Bestandsassets und Blockout-Primitiven.
- `lune_camera.gd` ist eine perspektivische Kamera, deren `zoom` die sichtbare Bodenhöhe bleibt; die Neigung hängt vom Zoom ab.
- Die Grottenpunkte behalten ihre M2-Abstände zum Grottenmittelpunkt.

182
docs/luneraria/G1-KARTE.md Normal file
View file

@ -0,0 +1,182 @@
# G1 · Map-Blockout und spielerisches Drei-Lane-Layout
Stand: 29. September 2026 · Branch `luneraria/g1` (von `luneraria/m4`, Commit `c155338`).
Grundlage: Grafikauftrag vom 28. September 2026, Konzeptbild `assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png` als visuelle Richtung (keine Vorlage zum Kopieren).
![Kartenplan (Draufsicht, nur Dokumentation)](screenshots/g1/nachher/00-kartenplan-draufsicht.png)
*Kartenplan: orthografische Draufsicht ohne HUD, nur zur Dokumentation. Alle Abnahmebilder unten stammen aus der echten Spielkamera.*
## Vorab: M4 gesichert und eingefroren
- M4-Stand dokumentiert (STATUS, QA-MATRIX, ARCHITEKTUR, ANLEITUNG) und als `c155338` auf `luneraria/m4` committet.
- Vor dem Commit erneut ausgeführt: `test_lune_net` 43/43, `net_rounds.py` 12/12.
- Offen und eingefroren: OpenGL-Kompatibilitätsmodus, nativer Windows-11-Test, Neubau der Demo-Pakete, Partie über mehrere Rechner.
- Während G1 keine Netzwerk-Featurearbeit. Einzige Änderung am Netzwerk-Testtreiber: Gast A trennt jetzt bei der ersten Momentaufnahme des Kampfes der Trennrunde statt erst nach sechs (kurze Kämpfe lieferten zu wenige; siehe Tests).
## Neues Kartenlayout
Die Karte bleibt ein Quadrat von 72 × 72 m mit Blau im Südwesten und Rot im Nordosten. Rot ist weiterhin das exakte Spiegelbild von Blau an der Achse x = z. Jede Lane wird aus ihrer blauen Hälfte und deren Spiegelung gebaut; Türme, Aufmarschpunkte und Grotteneingänge sind dadurch funktional gleich. Neu ist, dass die Lanes als geglättete Kurven (Catmull-Rom, alle 2 m abgetastet) entstehen und sich Top und Bottom in der Form unterscheiden.
| Lane | Länge | Identität | Querung der Kluft |
| --- | --- | --- | --- |
| Top | 96,82 m (M3: 94,5) | windet sich über das nordwestliche Hochland (bis 3,3 m), alte Wachtwege, Ruinentor, Mauerreste, Aussichtsturm an der Ecke | keine |
| Mid | 61,16 m (M3: 59,4) | direkteste, breitere Route (Belag 7,8 m statt 6), leicht gewellt, runde Mittelplattform mit Runenkreis und vier Runensteinen, Blick die Kluft hinauf zur Grotte | über die Mittelplattform (8 m Wasser) |
| Bottom | 96,82 m (exakt wie Top) | Tiefland im Südosten (bis −0,16 m), Talbogen, verwachsene Mauerstümpfe, Schilf am Ufer | über die Deltabrücke (16 m Wasser) |
Die Länge von Bottom wird beim Laden per Bisektion auf die Länge von Top gelöst, damit beide Lane-Rollen gleich lange Wege haben.
**Kluft (Fluss):** Sie entspringt am Fuß der Grottenterrasse, läuft genau auf der Symmetrieachse unter der Mittelplattform hindurch und mündet im Südosten in ein Delta mit See, das Bottom auf der Brücke quert und das durch das Randgebirge abfließt. Top berührt kein Wasser.
**Basen:** Plateau (Radius 10,6 m, 0,6 m hoch), Platz mit Teamring, Ringmauer mit drei Toren an den Lane-Ausgängen und Torsäulen mit Teamkristall. Die zwei Basistürme stehen 6 m vor dem Ancient, 40° beiderseits der Mid-Richtung.
**Türme:** je Team 2 Türme pro Lane plus 2 Basistürme, also 16 Türme und 2 Ancients auf der Karte, alle auf ebenen Sockeln in Höhe der Lane daneben. Die Position folgt wie bisher dem Anteil an der Lanelänge (innen 17 %, außen 35 %) mit 3,4 m Seitenabstand.
**Bossgrotte:** auf der Symmetrieachse zwischen Top und Mid, 19,1 m neben Mid. Terrasse (Radius 7,8 m, 1,7 m hoch) vor der Hochlandkante mit Runenkreis, elf Säulen mit Grottenkristallen um die Rückseite, dem M2-Felsbogen als Höhlenmund (ohne dessen alte Bodenscheibe und Findlinge) und Klippen dahinter. Je Team führt eine Treppe zum Eingang, dazwischen fällt die Quelle als kleiner Wasserfall in die Kluft. Die Abstände von Eingängen und Bosspunkt zum Grottenmittelpunkt sind seit M2 unverändert; die Grottenkämpfe sind also nur verschoben, nicht verändert. Bei gesperrter Grotte werden Licht und Runen gedimmt, bei Anspruch leuchten sie in Teamfarbe (Zustände im Detail: G2).
**Aufmarschbereiche:** je Team und Lane eine Fläche vor dem eigenen Basistor (Lanefortschritt 0–7,6 m, 4,8 m breit), mit Reihenlinien im Abstand von 1,5 m (vorne Front, dahinter Nahkampf, Flächenschaden, Fernkampf, Unterstützung, wie in `LuneMatch._spawn_armies`) und einem Pfeil in Marschrichtung. In der Planung sieht man die drei eigenen Flächen; Lanes mit eigenen Einheiten sind hervorgehoben. Die Grotteneingänge zeigen einen Ring, wenn die Grotte offen ist. Im Kampf ist alles ausgeblendet.
**Höhenfeld (`LuneTerrain`):** Hochland im Nordwesten hinter einer Abbruchkante, Tal in der Mitte, Tiefland im Südosten, Basisplateaus, Grottenterrasse, Randgebirge als abgerundetes Quadrat mit unregelmäßigem Fuß, feines Raster (1 m bis ±60 m) plus grober Außenring (4 m bis ±132 m). Jeder Term hängt nur von der Achsenkoordinate, dem Abstand zur Achse oder von gespiegelten Kartenelementen ab; das Gelände ist exakt spiegelsymmetrisch (Abweichung 0,0). Figuren stehen auf der „Laufhöhe“ (Lanebeläge, Plattform, Brücke, Terrasse), nie im Flussbett.
## Kamera
- Perspektivische MOBA-Kamera (34° Blickwinkel) statt der Orthokamera. Die Neigung wird beim Herauszoomen steiler: −48° nah, −70° in der Übersicht. `zoom` bedeutet weiterhin die sichtbare Bodenhöhe am Fokus, alle bisherigen Kamerarahmen behalten ihre Bedeutung.
- Der Fokus folgt der Geländehöhe; der Schattenbereich passt sich der Kameradistanz an.
- Tasten: **F2 / F3 / F4** Top / Mid / Bottom (F1 bleibt Optionen), **Tab** nächste Lane, **B** eigene Basis, **G** Bossgrotte, **H** eigener Held (Vorschau in der Planung, lebende Einheit im Kampf), **O** ganze Karte, **R** Rücksetzen (Drehung, Zoom, eigene Lane), **Q / E** Drehen in 45°-Schritten; WASD, Pfeiltasten, mittlere Maustaste und Mausrad wie bisher.
- Die Minimap zeigt den tatsächlichen Bodenausschnitt der Kamera (Trapez) und die Kluft mit Delta.
## Assets
**Wiederverwendet, unverändert:** Laneturm (`tower.glb`, Blender-Generator aus M1), Ancients (Saumkern-Modelle `core_team0/1`), Kartenrequisiten aus `map_props.glb` (drei Kiefern, drei Eichen, drei Felsen, zwei Büsche, Bordsteine, Brücke, Randklippe, Banner), Kluft-Oberflächenshader (`sanctuary.gdshader`) mit dem Kalkstein-Textursatz, Flussshader, Beleuchtung, alle Figuren und Animationen.
**Wiederverwendet, verändert eingesetzt:**
- M2-Grottenmodell (`grotto.glb`): nur Felsbogen, Höhlendunkel und Kristalle, Bodenscheibe und Findlinge ausgeblendet, Maßstab 1,05.
- Brücke: um 34 % gestreckt als Deltabrücke.
- Randklippe: gestaucht als Klippen hinter der Grotte.
- Büsche: gestreckt als Schilf.
**Neu (Blockout, kein neues Blender-Asset):**
- Höhenfeld mit Zonenfarben und eigenem Geländeshader (`lune_terrain.gdshader`).
- Lanebeläge mit Böschung.
- Mittelplattform, Runensteine und Runenkreis.
- Basisplätze, Ringmauern, Torsäulen und Torkristalle.
- Grottenterrasse, Säulen, Treppen und Wasserfall.
- Ruinentor, Mauerreste und Aussichtsturm.
- Aufmarschflächen (`lune_pad.gdshader`).
Diese Primitive ersetzen G2 (Ancients, Türme, Grotte) und G4 (Umgebung) durch Blender-Assets. Neue Blender-Assets entstanden in G1 bewusst nicht.
## Vorher / nachher (1920 × 1080, Spielkamera)
Gleiche Partie (Seed 4, Wurzelbund auf Mid), gleiche benannte Kamerarahmen, aufgenommen mit `tools/luneraria/g_views.tscn`.
| Ansicht | Vorher (M4) | Nachher (G1) |
| --- | --- | --- |
| 1 · ganze Karte | [vorher](screenshots/g1/vorher/01-karte.png) | [nachher](screenshots/g1/nachher/01-karte.png) |
| 2 · Top Lane (Kampf) | [vorher](screenshots/g1/vorher/02-top-lane.png) | [nachher](screenshots/g1/nachher/02-top-lane.png) |
| 3 · Mid Lane (Kampf) | [vorher](screenshots/g1/vorher/03-mid-lane.png) | [nachher](screenshots/g1/nachher/03-mid-lane.png) |
| 4 · Bottom Lane (Kampf) | [vorher](screenshots/g1/vorher/04-bottom-lane.png) | [nachher](screenshots/g1/nachher/04-bottom-lane.png) |
| 5 · eigene Basis | [vorher](screenshots/g1/vorher/05-eigene-basis.png) | [nachher](screenshots/g1/nachher/05-eigene-basis.png) |
| 6 · Gegnerbasis | [vorher](screenshots/g1/vorher/06-gegnerbasis.png) | [nachher](screenshots/g1/nachher/06-gegnerbasis.png) |
| 7 · Bossgrotte | [vorher](screenshots/g1/vorher/07-bossgrotte.png) | [nachher](screenshots/g1/nachher/07-bossgrotte.png) |
| 8 · Kampf mit vielen Einheiten | [vorher](screenshots/g1/vorher/08-kampf-viele-einheiten.png) | [nachher](screenshots/g1/nachher/08-kampf-viele-einheiten.png) |
| 9 · HUD (Planung) | [vorher](screenshots/g1/vorher/09-hud-planung.png) | [nachher](screenshots/g1/nachher/09-hud-planung.png) |
| 10 · Held ausgewählt | [vorher](screenshots/g1/vorher/10-held-ausgewaehlt.png) | [nachher](screenshots/g1/nachher/10-held-ausgewaehlt.png) |
| 11 · Zoom nah | [vorher](screenshots/g1/vorher/11-zoom-nah.png) | [nachher](screenshots/g1/nachher/11-zoom-nah.png) |
| 12 · Kamera in Bewegung | [vorher](screenshots/g1/vorher/12-kamera-bewegt.png) | [nachher](screenshots/g1/nachher/12-kamera-bewegt.png) |
| 13 · Grottenkampf (aus `verify_m2`) | [vorher](screenshots/g1/vorher/13-grottenkampf.png) | [nachher](screenshots/g1/nachher/13-grottenkampf.png) |
Die Ansichten 8 und 11 nutzen eine Aufnahme-Vorgabe (Runde 13, sechs volle Armeen, Gold nur für die Aufnahme).
## Gameplay-Befunde durch das neue Layout
- **Regeln unverändert:** Alle Regeltests bestehen ohne geänderte Erwartungswerte, auch der bitgenaue Spiegeltest über 12 Runden.
- **Wege etwas länger:** Top und Bottom +2,5 %, Mid +3 %. Türme stehen dadurch absolut minimal weiter auseinander. Die Partielänge ist unverändert: 9–13 Wellen, Median 11 (M3: 9–14, Median 11).
- **Neu, aber selten – Kampfphasen am Zeitlimit:** 4 von 442 Kampfphasen erreichen das 180-s-Fenster (M3: 0). Betroffen sind zwei fernkampflastige Zusammensetzungen, jeweils in Runde 12 und auf beiden Seiten gleich. Ein Zeitlimit beendet nur die Kampfphase, nie die Partie. Der Median der längsten Kampfphase je Partie sank von 113 auf 109 s. Beobachten, noch kein Handlungsbedarf.
- **Balance-Verschiebungen im Rahmen:**
- Profil „teamhilfe“ fällt von 46 auf 38 %, „ausgewogen“ steigt von 58 auf 62 %, „druck“ von 53 auf 59 %.
- Bünde 53/47 (M3 55/45).
- Bossentsendungen 12 statt 8 bei 40 Partien.
- Seitentausch weiterhin 20/20 konsistent: Die Karte bevorzugt kein Team.
- **Äußerer Mid-Turm nah an der Plattform:** Er steht 9,8 m vom Kartenmittelpunkt. Die Plattform ist damit gleichzeitig Kampfplatz und Vorfeld des Turms. Das ist gewollt, sollte aber bei G2 (Turmsilhouette) im Blick bleiben.
- **Grotte nah an der Top-Ecke:** Die Arena liegt 3,0 m außerhalb des Top-Lanekorridors. Kämpfe an der Top-Ecke und in der Grotte sind aus derselben Kamera sichtbar. Das unterstützt die gewünschte Blickbeziehung, verlangt in G2 aber, dass Grotteneffekte die Top-Figuren nicht überdecken.
- **Figuren am Ufer:** Wer im Kampf eine Lane verlässt und ans Ufer läuft, wird auf der Wasseroberfläche dargestellt, nicht im Flussbett. Das ist rein optisch; die Simulation ist zweidimensional.
## Leistung
Gleiche Messmethode für beide Stände (`tools/luneraria/lune_benchmark.tscn`). Fedora 44, Ryzen 5 5600X, RTX 5060 Ti, Forward+, 1920 × 1080, VSync aus, Tempo 2×, Runde 13 mit sechs vollen Armeen (120 bewegliche Einheiten zu Beginn). CPU = Hauptthread vom ersten `_process` bis zum fertig gezeichneten Bild.
| Messung | Vorher (M4) | Nachher (G1) |
| --- | --- | --- |
| Lane-Ansicht: Bildzeit Mittel / p95 | 6,16 / 8,58 ms | 6,95 / 9,66 ms |
| Lane-Ansicht: CPU / GPU | 6,04 / 3,17 ms | 6,82 / 3,29 ms |
| Lane-Ansicht: Draw Calls / Objekte | 3 609 / 7 060 | 3 340 / 7 438 |
| Übersicht: Bildzeit Mittel / p95 | 7,30 / 9,31 ms (Zoom 46, nur Teil der Karte) | 6,97 / 9,28 ms (Zoom 92, ganze Karte) |
| Übersicht: CPU / GPU | 7,17 / 4,11 ms | 6,84 / 3,52 ms |
| Übersicht: Draw Calls | 5 448 | 3 605 |
| Szenenaufbau (inkl. Höhenfeld) | 180 ms | 293 ms |
| Simulationsschritt | 2,54 ms | 2,71 ms |
| VRAM / Texturen / statischer RAM | 780 / 425 / 126 MB | 782 / 425 / 128 MB |
Optimiert statt entfernt:
- Schattenreichweite an die Kameradistanz gekoppelt (70–220 m). Das senkte die zusätzliche GPU-Zeit der Lane-Ansicht von +0,56 auf +0,12 ms.
- Kein Schattenwurf für den fernen Gebirgsring.
- Alle Wiederholteile als MultiMesh.
- Das Höhenfeld wird nur für die Blau-Hälfte berechnet und gespiegelt; es wird je Prozess einmal erzeugt und bei einer Revanche wiederverwendet.
- Lane-Abfragen per binärer Suche.
Das Bild bleibt CPU-gebunden, vor allem durch die rund 120 gerenderten Skelettfiguren. Logs: `logs/g1/perf-vorher-1920x1080.json`, `logs/g1/perf-nachher-1920x1080.json`.
## Tests
| Prüfung | Ergebnis |
| --- | --- |
| G1-Abnahme `verify_g1` (echte Anwendung, Tasten, Mausrad) | 443/443 |
| Regeln `test_lune_core` (inkl. Spiegeltest) | 91/91 |
| Regeln M2 (Helden, Items, Talente, Champions, Grotte, Boss, Bots) | 73/73 |
| Netzwerkprotokoll `test_lune_net` (Regression) | 43/43 |
| Mehr-Runden-Loopback `net_rounds.py` (Regression) | 12/12 (nach Anpassung des Trennzeitpunkts im Testtreiber, siehe oben) |
| Echte Anwendung M1 | 38/38 bei 1920 × 1080. Bei 1280 × 720: 5 von 7 Läufen 38/38; zwei Läufe scheiterten an je einem Klick (einmal im Menü vor dem Partiestart, einmal im Shop), danach vier Läufe in Folge bestanden. Der M4-Stand bestand 3/3. Da der Menü-Fehlklick vor jedem G1-Code auftrat, gehen wir von einer zeitweiligen Eingabestörung aus; nicht abschließend geklärt. |
| Echte Anwendung M2 | 14/14 (Partie endete vor der Bossentsendung, bekannte Variante) |
| Balance 40 Partien (wie M3) | Seitentausch 20/20, alle regulär nach 9–13 Wellen, 0 Testlimit |
| Legacy Saumwacht | 209 · 463 unverändert |
`verify_g1` prüft unter anderem:
- **Lanes und Wasser:** Jede Lane ist überall begehbar; Wasser unter einer Lane gibt es nur auf der Plattform und der Deltabrücke; Top bleibt trocken und erreicht das Hochland; Bottom liegt im Tiefland; Mid quert auf der Plattform.
- **Gebäude:** Alle 16 Türme und beide Ancients liegen im Spielfeld, stehen trocken, eben und mit Abstand zueinander; die Basistürme stehen vor ihrem Ancient.
- **Grotte:** Sie liegt auf der Achse, blockiert Mid nicht und hält alle Lanekorridore frei; Eingänge und Bosspunkt liegen auf der Terrasse, die Quelle entspringt an ihrem Fuß.
- **Aufmarschflächen:** Sie liegen trocken, innen und auf der eigenen Seite.
- **Gelände:** Es ist exakt gespiegelt.
- **Kamera:** Alle Sprungtasten treffen ihr Ziel; die Übersicht zeigt alle Gebäude und die Grotte; Drehen, Rücksetzen, Zoomgrenzen und die Neigung je Zoom funktionieren; der Minimap-Umriss ist korrekt; ein weicher Sprung kommt in 1,2 s an.
- **Planung und Kampf:** In der Planung sind nur die eigenen drei Flächen sichtbar, hervorgehoben dort, wo eigene Einheiten stehen. Im Kampf sind sie ausgeblendet. Alle Figuren stehen auf dem Gelände (Fehler < 0,001 m), und F3 zeigt im Kampf die eigene Mid-Armee.
## Vier Kategorien
**Implementiert:**
- Drei-Lane-Layout mit Lane-Identitäten, Höhenfeld, Kluft, Basen, 16 Türmen (12 Lane- und 4 Basistürme) und 2 Ancients, Bossgrotte (Blockout), Aufmarschbereiche.
- Perspektivische Spielkamera mit Sprungtasten.
- Minimap-Anpassung, Vergleichs- und Messwerkzeuge.
**Automatisiert geprüft:** siehe Tabelle „Tests“.
**Visuell geprüft:** alle 13 Vergleichsansichten und der Kartenplan in der Spielkamera bei 1920 × 1080, dazu Lane-Kampf, hohe Einheitenzahl, Nahzoom, bewegte Kamera und Grottenkampf.
**Ungetestet:**
- OpenGL-Kompatibilitätsmodus mit dem neuen Gelände.
- Windows 11.
- Schwächere GPUs.
- Menschliche Spieltests des neuen Layouts.
- Übertragungsverhalten des Netzspiels auf der neuen Karte über echte Netze (nur Loopback).
## Offen und nächste Schritte
- **G2 Ancients, Türme, Bossgrotte:**
- monumentale Ancients mit Zustandsstufen;
- modulares Turmsystem mit Angriffsbereich, Treffer- und Zerstörungszuständen;
- Grotte als echtes Blender-Asset mit den Zuständen neutral, verfügbar, umkämpft und Abklingzeit;
- Teamidentität über Banner, Runen und Architektur.
- **G3 HUD:** das vollständige neue Ingame-HUD. G1 hat hier nur Tastenhinweise und die Minimap angepasst.
- **G4 Umgebung:** Klippen- und Ruinen-Assets statt Primitiven, 3–5 neue Baumarten, Uferzonen, neuer Wassershader.
- **G5:** Licht und Materialien.
- **G6:** Einheitendarstellung und Völker.
- Beobachten: Kampfphasen am Zeitlimit bei fernkampflastigen Teams.

View file

@ -74,6 +74,23 @@ Weitere M2-Belege: `test_lune_m2` 73/73, darunter der Nachweis, dass die alte Ar
| Kompatibilitätsmodus (OpenGL) | ⏳ offen, M4 pausiert | — | | Kompatibilitätsmodus (OpenGL) | ⏳ offen, M4 pausiert | — |
| Sechs Menschen auf mehreren Rechnern / Internet | ⏳ nicht geprüft | nur lokaler Loopback | | Sechs Menschen auf mehreren Rechnern / Internet | ⏳ nicht geprüft | nur lokaler Loopback |
## G1 · Karte und Layout
| Nachweis | Stand | Beleg |
| --- | --- | --- |
| Drei Lanes, Top/Bottom gleich lang, Mid direkt | ✅ | `verify_g1`, `test_lune_core` symmetry |
| Lanes überall begehbar, Wasser nur an Plattform und Deltabrücke, Top trocken | ✅ | `verify_g1` |
| 16 Türme und 2 Ancients im Feld, trocken, eben, mit Abstand; Basistürme vor dem Ancient | ✅ | `verify_g1` |
| Grotte auf der Achse, frei von allen Lanekorridoren, blockiert Mid nicht, Eingänge auf der Terrasse | ✅ | `verify_g1` · 👁 `screenshots/g1/nachher/07-bossgrotte.png`, `13-grottenkampf.png` |
| Aufmarschbereiche trocken und auf der eigenen Seite; in der Planung nur die eigenen, im Kampf ausgeblendet | ✅ · 👁 | `verify_g1`, `09-hud-planung.png` |
| Gelände exakt gespiegelt, Figuren stehen auf dem Gelände | ✅ Abweichung 0,0 / < 0,001 m | `verify_g1` |
| Kamera: F2–F4, B, G, H, O, R, Q/E, Zoomgrenzen, Neigung je Zoom, Minimap-Umriss | ✅ echte Tastatur- und Mausereignisse | `verify_g1` |
| Regeln unverändert | ✅ 91/91 · 73/73 ohne geänderte Erwartungswerte | `test_lune_core`, `test_lune_m2` |
| Balance auf der neuen Karte | ✅ Seitentausch 20/20, 9–13 Wellen; 4/442 Kampfphasen am Zeitlimit (Befund) | `logs/g1/balance.json`, `balance-summary.txt` |
| Vorher/Nachher 1920 × 1080 aus der Spielkamera | 👁 13 Ansichten + Kartenplan | `screenshots/g1/vorher`, `screenshots/g1/nachher` |
| Leistung vorher/nachher, gleiche Methode | ✅ | `logs/g1/perf-*-1920x1080.json` |
| OpenGL, Windows 11, Spieltests | ⏳ offen | — |
## Legacy-Regressionen (Saumwacht) ## Legacy-Regressionen (Saumwacht)
Unverändert über die Umleitung der alten QA-Schalter: 209 · 33 · 463 · 206 · 1.665 · Menü 20A 68. Logs: `logs/m0-baseline/`, `logs/m1/legacy-*.log`. Unverändert über die Umleitung der alten QA-Schalter: 209 · 33 · 463 · 206 · 1.665 · Menü 20A 68. Logs: `logs/m0-baseline/`, `logs/m1/legacy-*.log`.

View file

@ -1,6 +1,6 @@
# STATUS · Champions of Luneraria # STATUS · Champions of Luneraria
Stand: 28. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4` · Luneraria-Demo 0.16.0-m4. Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1` · Luneraria-Demo 0.16.0-m4.
| Meilenstein | Zustand | | Meilenstein | Zustand |
| --- | --- | | --- | --- |
@ -9,7 +9,8 @@ Stand: 28. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `l
| M2 · Mechanikdurchlauf | **abgeschlossen** | | M2 · Mechanikdurchlauf | **abgeschlossen** |
| M3 · Qualität und Balance | **abgeschlossen** (siehe unten) | | M3 · Qualität und Balance | **abgeschlossen** (siehe unten) |
| M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: OpenGL, Windows 11) | | M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: OpenGL, Windows 11) |
| G1–G6 · Karte, Grafik, HUD | **als Nächstes** (Auftrag vom 28. September 2026) | | G1 · Map-Blockout und Drei-Lane-Layout | **abgeschlossen** ([G1-KARTE.md](G1-KARTE.md)) |
| G2–G6 · Ancients/Türme/Grotte, HUD, Umgebung, Licht, Einheiten | offen |
## M1 · abgeschlossen ## M1 · abgeschlossen
@ -124,6 +125,36 @@ Auf Entscheidung des Auftraggebers ruht M4 seit dem 28. September 2026 zugunsten
- Wiedereinstieg ist nur an den Anzeigenamen gebunden (keine Konten); Übertragungsrate bei vielen Gästen über das Internet ist nicht gemessen. - Wiedereinstieg ist nur an den Anzeigenamen gebunden (keine Konten); Übertragungsrate bei vielen Gästen über das Internet ist nicht gemessen.
- Öffentliches Matchmaking, Accounts, Ranglisten: außerhalb des Umfangs. - Öffentliches Matchmaking, Accounts, Ranglisten: außerhalb des Umfangs.
## G1 · abgeschlossen
Details, Vorher/Nachher-Bilder und Messwerte: [G1-KARTE.md](G1-KARTE.md).
**Implementiert**
- Neues Drei-Lane-Layout aus geglätteten, gespiegelten Lanes:
- Top über das Hochland im Nordwesten;
- Mid als breite, direkte Route über die Mittelplattform mit Runensteinen;
- Bottom durch das Tiefland über die Deltabrücke.
- Top und Bottom exakt gleich lang (96,82 m), Mid 61,16 m.
- Höhenfeld `LuneTerrain`: Hochland, Tal, Tiefland, Kluft auf der Symmetrieachse, Basisplateaus, Grottenterrasse, Randgebirge; exakt gespiegelt. Figuren stehen auf dem Gelände.
- Basen mit Plateau, Ringmauer und drei Lane-Toren. 16 Türme (12 Lane-, 4 Basistürme) und 2 Ancients auf ebenen Sockeln.
- Bossgrotte (Blockout) zwischen Top und Mid auf der Achse: Terrasse, Höhlenmund, Säulen, zwei Teamtreppen, Quelle der Kluft. Sie blockiert Mid nicht.
- Aufmarschbereiche je Team und Lane mit Reihenlinien, in der Planung sichtbar und hervorgehoben, wo eigene Einheiten stehen.
- Perspektivische MOBA-Kamera mit zoomabhängiger Neigung und Sprungtasten F2–F4, B, G, H, O, R, Q/E. Minimap mit Kamera-Umriss und Kluft.
- Werkzeuge: `g_views` (Vergleichsaufnahmen), `verify_g1` (Abnahme), erweiterter Benchmark (Lane-Ansicht und Übersicht).
**Automatisiert geprüft**
- `verify_g1` 443/443. Regeln 91/91 (inkl. Spiegeltest) und M2 73/73, jeweils ohne geänderte Erwartungswerte.
- Echte Anwendung: M1 38/38 (1080p; bei 720p zwei zeitweilige Klickfehler in 7 Läufen), M2 14/14.
- Netzwerk-Regression 43/43 und Loopback 12/12.
- Balance 40 Partien: Seitentausch 20/20, alle regulär nach 9–13 Wellen.
- Legacy 209 · 463.
**Visuell geprüft:** 13 Vergleichsansichten vorher/nachher aus der Spielkamera (1920 × 1080) und der Kartenplan.
**Befunde:** 4 von 442 Kampfphasen erreichen das Zeitlimit (M3: 0; fernkampflastige Teams, Runde 12). Wege 2,5–3 % länger, Partielänge unverändert (Median 11 Wellen). Leistung: Lane-Ansicht 6,95 ms statt 6,16 ms pro Bild, die ganze Karte in der Übersicht mit 6,97 ms.
**Ungetestet:** OpenGL-Modus, Windows 11, schwächere GPUs, menschliche Spieltests des Layouts.
## Nächster Schritt ## Nächster Schritt
Phasen G1–G6 (Karte, Ancients/Türme/Grotte, HUD, Umgebung, Licht/Material, Einheitendarstellung) nach dem Grafikauftrag vom 28. September 2026. M4 ruht; beim Wiederaufnehmen zuerst OpenGL-Sichtprüfung, Windows-11-Protokoll auf fremder Hardware und Neubau der Demos. G2 · Ancients, Türme und Bossgrotte: vorhandene Kristall- und Turmassets zu monumentalen Ancients (Zustandsstufen), einem modularen Turmsystem (Angriffsbereich, Treffer, Schaden, Zerstörung) und einer echten Grotte mit Zuständen ausbauen; Teamidentität über Banner, Runen und Architektur. Danach G3 (HUD). M4 ruht; beim Wiederaufnehmen zuerst OpenGL-Sichtprüfung, Windows-11-Protokoll auf fremder Hardware und Neubau der Demos.

View file

@ -0,0 +1,6 @@
Partien 40 · Ergebnisse {'blau': 20, 'rot': 20} · Testlimit 0 · Zeitfenster-Kämpfe 4 · längster Kampf 180 s
Wellen bis Ende: min 9 · median 11 · max 13
Seitentausch: dieselbe Zusammensetzung gewinnt beide Male in 20/20 Paaren
Profile (Siege/Einsätze): {'ausgewogen': '30/48 (62 %)', 'bossjagd': '8/34 (24 %)', 'druck': '20/34 (59 %)', 'fernkampf': '26/48 (54 %)', 'teamhilfe': '18/48 (38 %)', 'verteidigung': '18/28 (64 %)'}
Bünde: {'glutpakt': '66/124 (53 %)', 'wurzelbund': '54/116 (47 %)'}
Boss: {'won': 20, 'deployed': 12} · Heldenstufen am Ende (Median): 4

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
BALANCE 00 ["ausgewogen", "druck", "teamhilfe"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> blau (12) / swapped rot (12)
BALANCE 01 ["bossjagd", "bossjagd", "teamhilfe"] | ["fernkampf", "teamhilfe", "fernkampf"] -> rot (9) / swapped blau (9)
BALANCE 02 ["bossjagd", "ausgewogen", "teamhilfe"] | ["druck", "ausgewogen", "verteidigung"] -> rot (11) / swapped blau (11)
BALANCE 03 ["fernkampf", "druck", "ausgewogen"] | ["druck", "fernkampf", "verteidigung"] -> blau (12) / swapped rot (12)
BALANCE 04 ["ausgewogen", "teamhilfe", "fernkampf"] | ["ausgewogen", "verteidigung", "fernkampf"] -> blau (11) / swapped rot (11)
BALANCE 05 ["teamhilfe", "fernkampf", "verteidigung"] | ["fernkampf", "bossjagd", "bossjagd"] -> blau (11) / swapped rot (11)
BALANCE 06 ["bossjagd", "ausgewogen", "teamhilfe"] | ["fernkampf", "teamhilfe", "ausgewogen"] -> blau (9) / swapped rot (9)
BALANCE 07 ["ausgewogen", "ausgewogen", "ausgewogen"] | ["teamhilfe", "teamhilfe", "ausgewogen"] -> blau (12) / swapped rot (12)
BALANCE 08 ["teamhilfe", "ausgewogen", "bossjagd"] | ["druck", "ausgewogen", "druck"] -> rot (12) / swapped blau (12)
BALANCE 09 ["verteidigung", "druck", "druck"] | ["bossjagd", "ausgewogen", "teamhilfe"] -> blau (11) / swapped rot (11)
BALANCE 10 ["druck", "ausgewogen", "ausgewogen"] | ["teamhilfe", "bossjagd", "ausgewogen"] -> blau (12) / swapped rot (12)
BALANCE 11 ["bossjagd", "fernkampf", "fernkampf"] | ["teamhilfe", "verteidigung", "teamhilfe"] -> rot (10) / swapped blau (10)
BALANCE 12 ["fernkampf", "fernkampf", "teamhilfe"] | ["teamhilfe", "ausgewogen", "fernkampf"] -> rot (12) / swapped blau (12)
BALANCE 13 ["teamhilfe", "druck", "fernkampf"] | ["fernkampf", "druck", "ausgewogen"] -> rot (13) / swapped blau (13)
BALANCE 14 ["bossjagd", "druck", "fernkampf"] | ["bossjagd", "bossjagd", "bossjagd"] -> blau (10) / swapped rot (10)
BALANCE 15 ["fernkampf", "fernkampf", "verteidigung"] | ["druck", "fernkampf", "ausgewogen"] -> blau (12) / swapped rot (12)
BALANCE 16 ["teamhilfe", "verteidigung", "verteidigung"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> rot (13) / swapped blau (13)
BALANCE 17 ["fernkampf", "verteidigung", "fernkampf"] | ["druck", "fernkampf", "bossjagd"] -> blau (10) / swapped rot (10)
BALANCE 18 ["verteidigung", "ausgewogen", "bossjagd"] | ["druck", "teamhilfe", "druck"] -> blau (9) / swapped rot (9)
BALANCE 19 ["druck", "teamhilfe", "teamhilfe"] | ["fernkampf", "verteidigung", "bossjagd"] -> rot (10) / swapped blau (10)
WARNING: 38 ObjectDB instances were leaked at exit (run with `--verbose` for details).
at: cleanup (core/object/object.cpp:2536)
ERROR: 1 resources still in use at exit (run with --verbose for details).
at: clear (core/io/resource.cpp:822)

View file

@ -0,0 +1,39 @@
{
"buildings": 18,
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
"engine": "4.7.2-stable (fedora)",
"gpu": "NVIDIA GeForce RTX 5060 Ti",
"lane_view": {
"camera_zoom": 34.0,
"cpu_frame_ms_mean": 6.82015972222222,
"draw_calls_mean": 3340.02083333333,
"frame_ms_mean": 6.94640740740741,
"frame_ms_p95": 9.656,
"frames": 864,
"gpu_ms_mean": 3.29306944790814,
"mobile_units_mean": 117.75462962963,
"objects_mean": 7437.95949074074,
"primitives_mean": 16302012.3611111
},
"mobile_units_at_start": 120,
"overview": {
"camera_zoom": 92.0,
"cpu_frame_ms_mean": 6.84469489559164,
"draw_calls_mean": 3604.76450116009,
"frame_ms_mean": 6.9669071925754,
"frame_ms_p95": 9.283,
"frames": 862,
"gpu_ms_mean": 3.51868097826389,
"mobile_units_mean": 96.8259860788863,
"objects_mean": 8162.20765661253,
"primitives_mean": 16845863.4895592
},
"renderer": "forward_plus",
"scene_build_ms": 293.127,
"sim_speed": "2x (40 steps/s)",
"sim_step_ms": 2.71302,
"static_mem_mb": 128.053088188171,
"texture_mem_mb": 424.65576171875,
"video_mem_mb": 782.140380859375,
"window": "(1920, 1080)"
}

View file

@ -0,0 +1,39 @@
{
"buildings": 18,
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
"engine": "4.7.2-stable (fedora)",
"gpu": "NVIDIA GeForce RTX 5060 Ti",
"lane_view": {
"camera_zoom": 34.0,
"cpu_frame_ms_mean": 6.03641128205129,
"draw_calls_mean": 3608.65641025641,
"frame_ms_mean": 6.15782974358974,
"frame_ms_p95": 8.584,
"frames": 975,
"gpu_ms_mean": 3.16637640928611,
"mobile_units_mean": 117.465641025641,
"objects_mean": 7059.66871794872,
"primitives_mean": 14455054.2379487
},
"mobile_units_at_start": 120,
"overview": {
"camera_zoom": 46.0,
"cpu_frame_ms_mean": 7.17415674362089,
"draw_calls_mean": 5447.83110571081,
"frame_ms_mean": 7.29744106925881,
"frame_ms_p95": 9.307,
"frames": 823,
"gpu_ms_mean": 4.11414337940378,
"mobile_units_mean": 97.5261239368165,
"objects_mean": 10639.1822600243,
"primitives_mean": 27029403.7618469
},
"renderer": "forward_plus",
"scene_build_ms": 179.527,
"sim_speed": "2x (40 steps/s)",
"sim_step_ms": 2.53624,
"static_mem_mb": 126.140163421631,
"texture_mem_mb": 424.65576171875,
"video_mem_mb": 780.451141357422,
"window": "(1920, 1080)"
}

View file

@ -0,0 +1,40 @@
{
"checks": 443,
"facts": {
"grotto_clearance_to_lanes": 3.0,
"lane_height_range": {
"max": {
"bot": 0.600000023841858,
"mid": 0.620000004768372,
"top": 3.29657602310181
},
"min": {
"bot": -0.159999996423721,
"mid": 0.114149838685989,
"top": 0.081579864025116
}
},
"lane_lengths": {
"bot": 96.82,
"mid": 61.16,
"top": 96.82
},
"overview": {
"distance": 150.459213256836,
"pitch_deg": -69.4643032570342,
"zoom": 92.0
},
"pitch_range_deg": [
-48.000003795596,
-69.9999992757703
],
"terrain_mirror_error": 0.0,
"unit_ground_error": 0.0000000882349220354683,
"water_points_under_lanes": {
"bot": 16,
"mid": 8,
"top": 0
}
},
"failures": []
}

View file

@ -2,9 +2,9 @@
"checks": 38, "checks": 38,
"failures": [], "failures": [],
"object_counts": [ "object_counts": [
3408.0, 3620.0,
3404.0, 3620.0,
3404.0, 3620.0,
3406.0 3620.0
] ]
} }

View file

@ -7,7 +7,7 @@ NET_SPOOF REJECTED
NET_PLAN role=client round=1 seat=1 NET_PLAN role=client round=1 seat=1
NET_MSG OK Korkrücken gekauft. NET_MSG OK Korkrücken gekauft.
NET_ROUND role=client round=1 phase=0 sum=1392179697 NET_ROUND role=client round=1 phase=0 sum=1392179697
NET_CLIENT_COMBAT round=1 frames=32 local_steps=0 NET_CLIENT_COMBAT round=1 frames=17 local_steps=0
NET_PLAN role=client round=2 seat=1 NET_PLAN role=client round=2 seat=1
NET_MSG OK Samenschleuder gekauft. NET_MSG OK Samenschleuder gekauft.
NET_CLIENT_DROP round=2 seat=1 NET_CLIENT_DROP round=2 seat=1

View file

@ -4,18 +4,18 @@ NET_JOIN OK
NET_MSG OK Mit dem Host verbunden. NET_MSG OK Mit dem Host verbunden.
NET_SETUP seat=1 round=1 phase=0 NET_SETUP seat=1 round=1 phase=0
NET_SPOOF REJECTED NET_SPOOF REJECTED
NET_ROUND role=client round=2 phase=0 sum=612150297 NET_ROUND role=client round=2 phase=0 sum=3342070386
NET_CLIENT_COMBAT round=2 frames=8 local_steps=0 NET_CLIENT_COMBAT round=2 frames=10 local_steps=0
NET_PLAN role=client round=3 seat=1 NET_PLAN role=client round=3 seat=1
NET_MSG OK Sporenkelch gekauft. NET_MSG OK Sporenkelch gekauft.
NET_ROUND role=client round=3 phase=0 sum=87981900 NET_ROUND role=client round=3 phase=0 sum=711640101
NET_CLIENT_COMBAT round=3 frames=3 local_steps=0 NET_CLIENT_COMBAT round=3 frames=3 local_steps=0
NET_PLAN role=client round=4 seat=1 NET_PLAN role=client round=4 seat=1
NET_MSG OK Tauhüter gekauft. NET_MSG OK Tauhüter gekauft.
NET_ROUND role=client round=4 phase=0 sum=2202250974 NET_ROUND role=client round=4 phase=0 sum=346458639
NET_CLIENT_COMBAT round=4 frames=12 local_steps=0 NET_CLIENT_COMBAT round=4 frames=42 local_steps=0
NET_PLAN role=client round=5 seat=1 NET_PLAN role=client round=5 seat=1
NET_MSG OK Dornhopser gekauft. NET_MSG OK Dornhopser gekauft.
NET_ROUND role=client round=5 phase=0 sum=1883482295 NET_ROUND role=client round=5 phase=0 sum=254645978
NET_CLIENT_COMBAT round=5 frames=49 local_steps=0 NET_CLIENT_COMBAT round=5 frames=64 local_steps=0
NET_CLIENT_DONE round=6 seat=1 NET_CLIENT_DONE round=6 seat=1

View file

@ -7,17 +7,17 @@ WARNING: Sending 1718 bytes unreliably which is above the MTU (1392), this will
at: put_packet (modules/enet/enet_multiplayer_peer.cpp:365) at: put_packet (modules/enet/enet_multiplayer_peer.cpp:365)
GDScript backtrace (most recent call first): GDScript backtrace (most recent call first):
[0] host_tick (res://scripts/luneraria/net/lune_session.gd:291) [0] host_tick (res://scripts/luneraria/net/lune_session.gd:291)
[1] _process (res://scripts/luneraria/lune_game.gd:207) [1] _process (res://scripts/luneraria/lune_game.gd:258)
NET_ROUND role=host round=1 phase=0 sum=1392179697 NET_ROUND role=host round=1 phase=0 sum=1392179697
NET_PLAN role=host round=2 seat=0 NET_PLAN role=host round=2 seat=0
NET_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen. NET_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen.
NET_MSG OK Gast kehrt zurück auf Slot 1. NET_MSG OK Gast kehrt zurück auf Slot 1.
NET_ROUND role=host round=2 phase=0 sum=612150297 NET_ROUND role=host round=2 phase=0 sum=3342070386
NET_PLAN role=host round=3 seat=0 NET_PLAN role=host round=3 seat=0
NET_ROUND role=host round=3 phase=0 sum=87981900 NET_ROUND role=host round=3 phase=0 sum=711640101
NET_PLAN role=host round=4 seat=0 NET_PLAN role=host round=4 seat=0
NET_ROUND role=host round=4 phase=0 sum=2202250974 NET_ROUND role=host round=4 phase=0 sum=346458639
NET_PLAN role=host round=5 seat=0 NET_PLAN role=host round=5 seat=0
NET_ROUND role=host round=5 phase=0 sum=1883482295 NET_ROUND role=host round=5 phase=0 sum=254645978
NET_HOST_DONE round=6 seat=0 NET_HOST_DONE round=6 seat=0
NET_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen. NET_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen.

View file

@ -11,27 +11,27 @@
"mismatches": [], "mismatches": [],
"host_sums": { "host_sums": {
"1": "1392179697", "1": "1392179697",
"2": "612150297", "2": "3342070386",
"3": "87981900", "3": "711640101",
"4": "2202250974", "4": "346458639",
"5": "1883482295" "5": "254645978"
}, },
"guest_sums": { "guest_sums": {
"1": "1392179697", "1": "1392179697",
"2": "612150297", "2": "3342070386",
"3": "87981900", "3": "711640101",
"4": "2202250974", "4": "346458639",
"5": "1883482295" "5": "254645978"
}, },
"client_combats": [ "client_combats": [
{ {
"round": 1, "round": 1,
"frames": 32, "frames": 17,
"local_steps": 0 "local_steps": 0
}, },
{ {
"round": 2, "round": 2,
"frames": 8, "frames": 10,
"local_steps": 0 "local_steps": 0
}, },
{ {
@ -41,12 +41,12 @@
}, },
{ {
"round": 4, "round": 4,
"frames": 12, "frames": 42,
"local_steps": 0 "local_steps": 0
}, },
{ {
"round": 5, "round": 5,
"frames": 49, "frames": 64,
"local_steps": 0 "local_steps": 0
} }
], ],

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfnl5pipkm2n1"
path="res://.godot/imported/00-kartenplan-draufsicht.png-2ca4024de3d198d8d9f1bdca97d42481.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png"
dest_files=["res://.godot/imported/00-kartenplan-draufsicht.png-2ca4024de3d198d8d9f1bdca97d42481.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lr03js5nblhl"
path="res://.godot/imported/01-karte.png-96789aba0ff604bcb6f965065a48845a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/01-karte.png"
dest_files=["res://.godot/imported/01-karte.png-96789aba0ff604bcb6f965065a48845a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bcanui88p8qyc"
path="res://.godot/imported/02-top-lane.png-c3561a8df8623617cb94592961dfe916.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/02-top-lane.png"
dest_files=["res://.godot/imported/02-top-lane.png-c3561a8df8623617cb94592961dfe916.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3vim0lytocpb"
path="res://.godot/imported/03-mid-lane.png-5129bdbfec756167ee92071ec74b2274.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/03-mid-lane.png"
dest_files=["res://.godot/imported/03-mid-lane.png-5129bdbfec756167ee92071ec74b2274.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d2nb04jgi1v1u"
path="res://.godot/imported/04-bottom-lane.png-f9b2388fabe7f82e35bd2bf9f1ea9497.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png"
dest_files=["res://.godot/imported/04-bottom-lane.png-f9b2388fabe7f82e35bd2bf9f1ea9497.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://g84b6v1kmn6h"
path="res://.godot/imported/05-eigene-basis.png-4e0d2284a7c2306c7c0fb9c0e04036c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png"
dest_files=["res://.godot/imported/05-eigene-basis.png-4e0d2284a7c2306c7c0fb9c0e04036c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cm8bkm6avms4p"
path="res://.godot/imported/06-gegnerbasis.png-64604b6b6510da4a22907f9b3aec1d28.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png"
dest_files=["res://.godot/imported/06-gegnerbasis.png-64604b6b6510da4a22907f9b3aec1d28.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c4v7thusx4pvt"
path="res://.godot/imported/07-bossgrotte.png-43d8e3d9f3840489c0e0ae41c8125b28.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png"
dest_files=["res://.godot/imported/07-bossgrotte.png-43d8e3d9f3840489c0e0ae41c8125b28.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://61hr7ivbuoy4"
path="res://.godot/imported/08-kampf-viele-einheiten.png-6c44677e17afa017457daba5b0c1f5db.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png"
dest_files=["res://.godot/imported/08-kampf-viele-einheiten.png-6c44677e17afa017457daba5b0c1f5db.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://y0uqoo0yxogq"
path="res://.godot/imported/09-hud-planung.png-623af17d8be85dee24f95fc581742444.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/09-hud-planung.png"
dest_files=["res://.godot/imported/09-hud-planung.png-623af17d8be85dee24f95fc581742444.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://degcngk702v2u"
path="res://.godot/imported/10-held-ausgewaehlt.png-249a5e49026e60f57c8e4f3d00808178.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png"
dest_files=["res://.godot/imported/10-held-ausgewaehlt.png-249a5e49026e60f57c8e4f3d00808178.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwx6yu7ush063"
path="res://.godot/imported/11-zoom-nah.png-9775410ebc7c8ad8d576b96dfb83ba23.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png"
dest_files=["res://.godot/imported/11-zoom-nah.png-9775410ebc7c8ad8d576b96dfb83ba23.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dnl86k218fhgh"
path="res://.godot/imported/12-kamera-bewegt.png-d7acd59dc88f8fe4f40917dd6ea419d6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png"
dest_files=["res://.godot/imported/12-kamera-bewegt.png-d7acd59dc88f8fe4f40917dd6ea419d6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dimffw8h0djax"
path="res://.godot/imported/13-grottenkampf.png-a28b4b8f4694ff7f7d2d90f28253f83d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png"
dest_files=["res://.godot/imported/13-grottenkampf.png-a28b4b8f4694ff7f7d2d90f28253f83d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rj2lj35pqufx"
path="res://.godot/imported/01-karte.png-ce4b231a58bbbc9069487eaf7430f901.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/01-karte.png"
dest_files=["res://.godot/imported/01-karte.png-ce4b231a58bbbc9069487eaf7430f901.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blhy7juf6j0jm"
path="res://.godot/imported/02-top-lane.png-b101771ab0695a9bdc8da72b4c740099.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/02-top-lane.png"
dest_files=["res://.godot/imported/02-top-lane.png-b101771ab0695a9bdc8da72b4c740099.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bh2uayb61s3sg"
path="res://.godot/imported/03-mid-lane.png-6efe91ed0b4f9be3587e80ead94d8841.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/03-mid-lane.png"
dest_files=["res://.godot/imported/03-mid-lane.png-6efe91ed0b4f9be3587e80ead94d8841.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dod60foruihar"
path="res://.godot/imported/04-bottom-lane.png-983d87853d549d9578a9e908cd496896.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png"
dest_files=["res://.godot/imported/04-bottom-lane.png-983d87853d549d9578a9e908cd496896.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bb87354sjr2g6"
path="res://.godot/imported/05-eigene-basis.png-0341f91acde69f9fbaea20b07c87bffa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png"
dest_files=["res://.godot/imported/05-eigene-basis.png-0341f91acde69f9fbaea20b07c87bffa.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cakea1ngbqa43"
path="res://.godot/imported/06-gegnerbasis.png-c0617fbcf715a4e32ea68036266fc4dc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png"
dest_files=["res://.godot/imported/06-gegnerbasis.png-c0617fbcf715a4e32ea68036266fc4dc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cxfyqbhhvkykw"
path="res://.godot/imported/07-bossgrotte.png-7aac47cb6f5c1524b2c9492ff2229c02.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png"
dest_files=["res://.godot/imported/07-bossgrotte.png-7aac47cb6f5c1524b2c9492ff2229c02.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://psritdy1l4id"
path="res://.godot/imported/08-kampf-viele-einheiten.png-1a3289ca798daf9d328830c82bc47353.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png"
dest_files=["res://.godot/imported/08-kampf-viele-einheiten.png-1a3289ca798daf9d328830c82bc47353.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://f610hxnatmad"
path="res://.godot/imported/09-hud-planung.png-e7fb4082c2694f8e48084b61a6b2d815.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/09-hud-planung.png"
dest_files=["res://.godot/imported/09-hud-planung.png-e7fb4082c2694f8e48084b61a6b2d815.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://du2agks4hppy6"
path="res://.godot/imported/10-held-ausgewaehlt.png-6c5431de111cdd03cb5c4c0222902af5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png"
dest_files=["res://.godot/imported/10-held-ausgewaehlt.png-6c5431de111cdd03cb5c4c0222902af5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://70eokr0x40en"
path="res://.godot/imported/11-zoom-nah.png-8354307c8a1c11f60b3960ad46f68321.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png"
dest_files=["res://.godot/imported/11-zoom-nah.png-8354307c8a1c11f60b3960ad46f68321.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://deivn3vq6xsi3"
path="res://.godot/imported/12-kamera-bewegt.png-60a1c57e95178b578b39de4216c2678d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png"
dest_files=["res://.godot/imported/12-kamera-bewegt.png-60a1c57e95178b578b39de4216c2678d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://7khyclbceng8"
path="res://.godot/imported/13-grottenkampf.png-11d73b1ff7e4266621c000897f86dd55.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png"
dest_files=["res://.godot/imported/13-grottenkampf.png-11d73b1ff7e4266621c000897f86dd55.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt8ip3xno5rgg"
path="res://.godot/imported/net-guest-combat-r1.png-097b5ce28024fe9324574e91a83bc90a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/m4/net-guest-combat-r1.png"
dest_files=["res://.godot/imported/net-guest-combat-r1.png-097b5ce28024fe9324574e91a83bc90a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c6i6iqo818gs2"
path="res://.godot/imported/net-guest-combat-r2.png-b1c78cb66d1b4a660fba5f0ae4e0bba9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/m4/net-guest-combat-r2.png"
dest_files=["res://.godot/imported/net-guest-combat-r2.png-b1c78cb66d1b4a660fba5f0ae4e0bba9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://l8utxhs4pu26"
path="res://.godot/imported/net-guest-planning-r1.png-470b43201ee472c938978f0bfb5eae00.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/m4/net-guest-planning-r1.png"
dest_files=["res://.godot/imported/net-guest-planning-r1.png-470b43201ee472c938978f0bfb5eae00.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dvwvs1wt8d1pw"
path="res://.godot/imported/net-guest-planning-r2.png-f24d21945eb643fb4786dff29b2cef4f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/luneraria/screenshots/m4/net-guest-planning-r2.png"
dest_files=["res://.godot/imported/net-guest-planning-r2.png-f24d21945eb643fb4786dff29b2cef4f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -2,19 +2,29 @@ class_name LuneMap
extends RefCounted extends RefCounted
## Map data in ground coordinates Vector2(x, z): Blau (team 0) in the south-west, ## Map data in ground coordinates Vector2(x, z): Blau (team 0) in the south-west,
## Rot (team 1) in the north-east. Rot is the mirror image of Blau across x = z ## Rot (team 1) in the north-east. Rot is the mirror image of Blau across x = z
## (mirror(v) = Vector2(v.y, v.x)), Bottom is Top mirrored across the Mid line. ## (mirror(v) = Vector2(v.y, v.x)); every lane is built from its Blau half and that
## Both hold by construction, so strecken, towers and spawns are functionally equal. ## half mirrored, so strecken, towers and spawns are functionally equal by construction.
## G1 layout: Top climbs the north-western highland, Mid crosses the Kluft over the
## central platform, Bottom winds through the south-eastern lowland and fords the river.
## Top and Bottom differ in shape but have exactly the same length (solved at load).
const LANES: Array[String] = ["top", "mid", "bot"] const LANES: Array[String] = ["top", "mid", "bot"]
const LANE_NAMES: Dictionary = {"top": "Top", "mid": "Mid", "bot": "Bottom"} const LANE_NAMES: Dictionary = {"top": "Top", "mid": "Mid", "bot": "Bottom"}
# First halves from the Blau end to the centre line; the Rot half is mirrored. ## Catmull-Rom control points of each Blau half, from the Blau base to the symmetry axis.
const TOP_HALF: Array[Vector2] = [Vector2(-24, 22), Vector2(-26, 6), Vector2(-26.5, -12), Vector2(-25, -25)] ## The last point lies on x = z; the curve is sampled every ~2 m into the lane polyline.
const MID_HALF: Array[Vector2] = [Vector2(-21, 21), Vector2(-11, 10), Vector2(0, 0)] const TOP_CONTROL: Array[Vector2] = [Vector2(-27.4, 21.0), Vector2(-26.4, 13.0), Vector2(-28.4, 4.0), Vector2(-26.2, -5.0),
Vector2(-28.6, -13.5), Vector2(-28.4, -21.2), Vector2(-25.0, -25.0)]
const MID_CONTROL: Array[Vector2] = [Vector2(-21.6, 21.6), Vector2(-16.4, 17.2), Vector2(-10.9, 11.5), Vector2(-5.6, 5.9), Vector2(0, 0)]
const CORE_BLUE := Vector2(-30, 30) const CORE_BLUE := Vector2(-30, 30)
const BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-28.8, 24.6), Vector2(-24.6, 28.8)] ## Two base towers in front of each Ancient, 6 m out and 40° either side of the Mid direction.
## Neutral boss grotto, north-west of Mid and on the symmetry axis (M2). const BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-29.477, 24.023), Vector2(-24.023, 29.477)]
const GROTTO := Vector2(-11.5, -11.5) ## Neutral boss grotto on the symmetry axis between Top and Mid, beside the Mid lane.
const GROTTO := Vector2(-13.5, -13.5)
const HALF_EXTENT: float = 36.0 const HALF_EXTENT: float = 36.0
## Direction of the symmetry axis (north-west to south-east) and its Blau-side normal.
const AXIS := Vector2(0.70710678, 0.70710678)
const BLUE_SIDE := Vector2(-0.70710678, 0.70710678)
const SAMPLE_SPACING: float = 2.0
var balance: LuneBalance var balance: LuneBalance
var lanes: Dictionary = {} var lanes: Dictionary = {}
@ -33,31 +43,36 @@ class LanePath:
length += points[i].distance_to(points[i - 1]) length += points[i].distance_to(points[i - 1])
cumulative.append(length) cumulative.append(length)
## Index i of the segment [i - 1, i] that contains lane coordinate d (binary search).
func _segment(d: float) -> int:
var low: int = 1
var high: int = points.size() - 1
while low < high:
var middle: int = (low + high) >> 1
if d <= cumulative[middle]:
high = middle
else:
low = middle + 1
return low
func point_at(s: float) -> Vector2: func point_at(s: float) -> Vector2:
var d: float = clampf(s, 0.0, length) var d: float = clampf(s, 0.0, length)
for i: int in range(1, points.size()): var i: int = _segment(d)
if d <= cumulative[i] or i == points.size() - 1:
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1]) var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
return points[i - 1].lerp(points[i], clampf((d - cumulative[i - 1]) / span, 0.0, 1.0)) return points[i - 1].lerp(points[i], clampf((d - cumulative[i - 1]) / span, 0.0, 1.0))
return points[points.size() - 1]
func tangent_at(s: float) -> Vector2: func tangent_at(s: float) -> Vector2:
var d: float = clampf(s, 0.0, length) var i: int = _segment(clampf(s, 0.0, length))
for i: int in range(1, points.size()):
if d <= cumulative[i] or i == points.size() - 1:
return (points[i] - points[i - 1]).normalized() return (points[i] - points[i - 1]).normalized()
return Vector2.RIGHT
## Point and unit tangent at s in one pass. ## Point and unit tangent at s in one pass.
func sample(s: float) -> Array: func sample(s: float) -> Array:
var d: float = clampf(s, 0.0, length) var d: float = clampf(s, 0.0, length)
for i: int in range(1, points.size()): var i: int = _segment(d)
if d <= cumulative[i] or i == points.size() - 1:
var a: Vector2 = points[i - 1] var a: Vector2 = points[i - 1]
var b: Vector2 = points[i] var b: Vector2 = points[i]
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1]) var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
return [a.lerp(b, clampf((d - cumulative[i - 1]) / span, 0.0, 1.0)), (b - a) / span] return [a.lerp(b, clampf((d - cumulative[i - 1]) / span, 0.0, 1.0)), (b - a) / span]
return [points[points.size() - 1], Vector2.RIGHT]
func project(position: Vector2) -> float: func project(position: Vector2) -> float:
var best: float = INF var best: float = INF
@ -85,14 +100,50 @@ static func full_lane(half: Array[Vector2]) -> Array[Vector2]:
result.append(mirror(half[i])) result.append(mirror(half[i]))
return result return result
## Samples the Catmull-Rom curve through the control points of a Blau half. The phantom
## point after the axis point is the mirror of the one before it, so the joined lane has
## no kink on the axis (its tangent there is perpendicular to x = z).
static func sample_half(control: Array[Vector2]) -> Array[Vector2]:
var ext: Array[Vector2] = [control[0] * 2.0 - control[1]]
ext.append_array(control)
ext.append(mirror(control[control.size() - 2]))
var result: Array[Vector2] = []
for i: int in range(1, ext.size() - 2):
var p0: Vector2 = ext[i - 1]
var p1: Vector2 = ext[i]
var p2: Vector2 = ext[i + 1]
var p3: Vector2 = ext[i + 2]
var steps: int = maxi(2, ceili(p1.distance_to(p2) / SAMPLE_SPACING))
for k: int in range(steps):
var t: float = float(k) / steps
var t2: float = t * t
var t3: float = t2 * t
result.append(0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t3))
result.append(control[control.size() - 1])
return result
## Bottom half through the south-eastern lowland; `bend` pulls the valley meander inwards.
static func bottom_control(bend: float) -> Array[Vector2]:
return [Vector2(-21.0, 27.4), Vector2(-12.5, 27.0), Vector2(-4.0, 27.6), Vector2(4.0, 27.0 - bend),
Vector2(12.0, 27.4 - 0.6 * bend), Vector2(19.5, 27.0), Vector2(22.2, 27.8), Vector2(25.0, 25.0)]
## Bisection on the meander so that Bottom is exactly as long as Top (fair lane roles).
static func solve_bottom(target_length: float) -> Array[Vector2]:
var low: float = 0.0
var high: float = 8.0
for i: int in range(60):
var bend: float = (low + high) * 0.5
if LanePath.new(full_lane(sample_half(bottom_control(bend)))).length < target_length:
low = bend
else:
high = bend
return full_lane(sample_half(bottom_control((low + high) * 0.5)))
func _init(config: LuneBalance = null) -> void: func _init(config: LuneBalance = null) -> void:
balance = config if config != null else LuneBalance.default_balance() balance = config if config != null else LuneBalance.default_balance()
var bot_half: Array[Vector2] = [] lanes["top"] = LanePath.new(full_lane(sample_half(TOP_CONTROL)))
for point: Vector2 in TOP_HALF: lanes["mid"] = LanePath.new(full_lane(sample_half(MID_CONTROL)))
bot_half.append(mirror_mid(point)) lanes["bot"] = LanePath.new(solve_bottom(lanes["top"].length))
lanes["top"] = LanePath.new(full_lane(TOP_HALF))
lanes["mid"] = LanePath.new(full_lane(MID_HALF))
lanes["bot"] = LanePath.new(full_lane(bot_half))
_build_buildings() _build_buildings()
## Lane coordinate s for a team's own marching progress p (0 at its own base). ## Lane coordinate s for a team's own marching progress p (0 at its own base).
@ -118,6 +169,7 @@ func lane_forward(team: int, lane: String, progress: float) -> Vector2:
return tangent if team == 0 else mirror(tangent) return tangent if team == 0 else mirror(tangent)
## Grotto deployment points: one mirrored entrance per team, boss slightly inside. ## Grotto deployment points: one mirrored entrance per team, boss slightly inside.
## The offsets from GROTTO are unchanged since M2, so expedition fights are unchanged.
func grotto_entry(team: int) -> Vector2: func grotto_entry(team: int) -> Vector2:
var blue: Vector2 = GROTTO + Vector2(1.2, 4.6) var blue: Vector2 = GROTTO + Vector2(1.2, 4.6)
return blue if team == 0 else mirror(blue) return blue if team == 0 else mirror(blue)
@ -125,6 +177,14 @@ func grotto_entry(team: int) -> Vector2:
func grotto_boss_point() -> Vector2: func grotto_boss_point() -> Vector2:
return GROTTO + Vector2(-1.5, -1.5) return GROTTO + Vector2(-1.5, -1.5)
func grotto_center() -> Vector2:
return GROTTO
## Army formation area of a team on a lane (lane progress from/to and lateral half width).
## Matches the spawn rows in LuneMatch._spawn_armies: rows of four, 1.2 m apart.
func deployment_span() -> Dictionary:
return {"from": 0.4, "to": 7.2, "half_width": 2.4}
func core_position(team: int) -> Vector2: func core_position(team: int) -> Vector2:
return CORE_BLUE if team == 0 else mirror(CORE_BLUE) return CORE_BLUE if team == 0 else mirror(CORE_BLUE)

View file

@ -48,6 +48,7 @@ func start(setup_options: Dictionary) -> void:
add_child(_units_root) add_child(_units_root)
camera = CameraRig.new() camera = CameraRig.new()
camera.reduced_motion = Preferences.reduced_motion camera.reduced_motion = Preferences.reduced_motion
camera.ground = func(p: Vector2) -> float: return world.height_at(p)
add_child(camera) add_child(camera)
var layer := CanvasLayer.new() var layer := CanvasLayer.new()
add_child(layer) add_child(layer)
@ -85,14 +86,49 @@ func _is_client() -> bool:
func _send(command: Dictionary) -> Dictionary: func _send(command: Dictionary) -> Dictionary:
return session.submit(command) if _net() else match_state.submit(human_slot, command) return session.submit(command) if _net() else match_state.submit(human_slot, command)
func _own_team() -> int:
return int(match_state.players[maxi(0, human_slot)].team)
func focus_lane(lane: String, instant: bool = false) -> void: func focus_lane(lane: String, instant: bool = false) -> void:
var team: int = int(match_state.players[maxi(0, human_slot)].team)
var path: LuneMap.LanePath = match_state.map.lanes[lane] var path: LuneMap.LanePath = match_state.map.lanes[lane]
# Planning looks at the own deployment and outer tower, combat at the contested middle. # Planning looks at the own deployment and inner tower, combat at the contested middle.
var fraction: float = .2 if match_state.phase == LuneMatch.Phase.PLANNING else .42 var fraction: float = .2 if match_state.phase == LuneMatch.Phase.PLANNING else .42
var point: Vector2 = match_state.map.lane_point(team, lane, path.length * fraction) var point: Vector2 = match_state.map.lane_point(_own_team(), lane, path.length * fraction)
camera.look_at_point(Vector3(point.x, 0, point.y), 24.0, instant) camera.look_at_point(Vector3(point.x, 0, point.y), 24.0, instant)
func focus_base(instant: bool = false) -> void:
var core: Vector2 = match_state.map.core_position(_own_team())
camera.look_at_point(Vector3(core.x, 0, core.y), 26.0, instant)
func focus_grotto(instant: bool = false) -> void:
var center: Vector2 = match_state.map.grotto_center()
camera.look_at_point(Vector3(center.x, 0, center.y), 22.0, instant)
## The own hero: its planning preview or, in combat, the live unit.
func own_hero_unit() -> LuneUnit:
if human_slot < 0:
return null
var source: LuneCombat = match_state.combat if match_state.phase == LuneMatch.Phase.COMBAT else _preview
if source == null:
return null
for unit: LuneUnit in source.units:
if unit.kind == "hero" and unit.owner_slot == human_slot and unit.alive:
return unit
return null
func focus_hero(instant: bool = false) -> bool:
var unit: LuneUnit = own_hero_unit()
if unit == null:
hud.notify("Kein Held im Feld – im Reiter „Held“ anwerben.", true)
return false
camera.look_at_point(Vector3(unit.position.x, 0, unit.position.y), 16.0, instant)
return true
func reset_camera(instant: bool = false) -> void:
var lane: String = str(match_state.players[maxi(0, human_slot)].lane)
var point: Vector2 = match_state.map.lane_point(_own_team(), lane, match_state.map.lanes[lane].length * .2)
camera.reset_view(Vector3(point.x, 0, point.y), instant)
func _feedback(result: Dictionary) -> void: func _feedback(result: Dictionary) -> void:
hud.notify(str(result.message), not bool(result.ok)) hud.notify(str(result.message), not bool(result.ok))
Soundscape.play("place" if result.ok else "error") Soundscape.play("place" if result.ok else "error")
@ -137,6 +173,7 @@ func _enter_planning() -> void:
func _enter_combat() -> void: func _enter_combat() -> void:
_clear_views() _clear_views()
_preview = null _preview = null
world.show_deployment(false, _own_team(), {}, false)
_bound_combat = match_state.combat _bound_combat = match_state.combat
_accumulator = 0.0 _accumulator = 0.0
Soundscape.play("wave") Soundscape.play("wave")
@ -148,6 +185,7 @@ func _clear_views() -> void:
## Shows the human player's current draft army in its formation during planning. ## Shows the human player's current draft army in its formation during planning.
func _rebuild_preview() -> void: func _rebuild_preview() -> void:
_update_deployment()
if match_state.phase != LuneMatch.Phase.PLANNING or human_slot < 0: if match_state.phase != LuneMatch.Phase.PLANNING or human_slot < 0:
return return
_clear_views() _clear_views()
@ -173,8 +211,21 @@ func _rebuild_preview() -> void:
unit = _preview.spawn(entry[3], int(player.team), lane, entry[0], maxf(.4, 7.2 - float(index / 4) * 1.5), (float(index % 4) - 1.5) * 1.2, human_slot, entry[2]) unit = _preview.spawn(entry[3], int(player.team), lane, entry[0], maxf(.4, 7.2 - float(index / 4) * 1.5), (float(index % 4) - 1.5) * 1.2, human_slot, entry[2])
_add_view(unit) _add_view(unit)
## Planning shows the own formation areas; lanes that hold own units are highlighted.
func _update_deployment() -> void:
var planning: bool = match_state.phase == LuneMatch.Phase.PLANNING and human_slot >= 0
var used: Dictionary = {}
if human_slot >= 0:
var draft: Dictionary = match_state.players[human_slot].draft
for entry: Dictionary in draft.roster:
used[str(entry.lane)] = int(used.get(str(entry.lane), 0)) + 1
if not (draft.hero as Dictionary).is_empty():
used[str(draft.hero.lane)] = int(used.get(str(draft.hero.lane), 0)) + 1
world.show_deployment(planning, _own_team(), used, match_state.grotto_available())
func _add_view(unit: LuneUnit) -> void: func _add_view(unit: LuneUnit) -> void:
var view: Node3D = UnitView.new() var view: Node3D = UnitView.new()
view.terrain = world.terrain
_units_root.add_child(view) _units_root.add_child(view)
view.bind(unit) view.bind(unit)
_views[unit.id] = view _views[unit.id] = view
@ -230,6 +281,8 @@ func _process(delta: float) -> void:
world.sync_grotto(match_state.grotto_available(), int(claim.team) if not claim.is_empty() else -1) world.sync_grotto(match_state.grotto_available(), int(claim.team) if not claim.is_empty() else -1)
hud.minimap.camera_focus = camera.focus hud.minimap.camera_focus = camera.focus
hud.minimap.camera_span = camera.zoom hud.minimap.camera_span = camera.zoom
hud.minimap.camera_footprint = camera.footprint()
world.fit_shadows(camera.position.distance_to(camera.focus))
## Reacts to phase changes, whether they come from the local simulation or the host. ## Reacts to phase changes, whether they come from the local simulation or the host.
func _track_phase() -> void: func _track_phase() -> void:
@ -276,6 +329,22 @@ func _input(event: InputEvent) -> void:
KEY_TAB: KEY_TAB:
_lane_cycle = (_lane_cycle + 1) % 3 _lane_cycle = (_lane_cycle + 1) % 3
focus_lane(LuneMap.LANES[_lane_cycle]) focus_lane(LuneMap.LANES[_lane_cycle])
KEY_F2, KEY_F3, KEY_F4:
# F1 stays the options key; F2–F4 jump to Top, Mid and Bottom.
_lane_cycle = event.keycode - KEY_F2
focus_lane(LuneMap.LANES[_lane_cycle])
KEY_B:
focus_base()
KEY_G:
focus_grotto()
KEY_H:
focus_hero()
KEY_R:
reset_camera()
KEY_Q:
camera.rotate_step(-1)
KEY_E:
camera.rotate_step(1)
KEY_O: KEY_O:
camera.overview() camera.overview()
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5: KEY_1, KEY_2, KEY_3, KEY_4, KEY_5:

View file

@ -297,7 +297,7 @@ func notify(message: String, error: bool = false) -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
_toast_left -= delta _toast_left -= delta
if _toast_left <= 0.0: if _toast_left <= 0.0:
_toast.text = "WASD / mittlere Maustaste: Kamera · Mausrad: Zoom · Tab: Lanes · Minimap klicken" _toast.text = "WASD / mittlere Maustaste: Kamera · Mausrad: Zoom · F2–F4: Top/Mid/Bottom · B Basis · G Grotte · H Held · O Karte · R Zurück"
_toast.remove_theme_color_override("font_color") _toast.remove_theme_color_override("font_color")
if match_state != null: if match_state != null:
refresh() refresh()

View file

@ -6,6 +6,8 @@ const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55"), Color("b37c
var match_state: LuneMatch var match_state: LuneMatch
var camera_focus := Vector3.ZERO var camera_focus := Vector3.ZERO
var camera_span: float = 34.0 var camera_span: float = 34.0
## Ground outline of the game camera's view (perspective trapezoid); empty = rectangle.
var camera_footprint := PackedVector2Array()
func _ready() -> void: func _ready() -> void:
custom_minimum_size = Vector2(236, 236) custom_minimum_size = Vector2(236, 236)
@ -32,9 +34,10 @@ func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, size), Color("5c4a33"), false, 1.5) draw_rect(Rect2(Vector2.ZERO, size), Color("5c4a33"), false, 1.5)
if match_state == null: if match_state == null:
return return
var a: Vector2 = to_map(Vector2(-40, -40)) # The Kluft: from the grotto spring along the symmetry axis into the south-eastern delta.
var b: Vector2 = to_map(Vector2(40, 40)) var spring: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * LuneTerrain.ARENA_RADIUS
draw_line(a, b, Color(.12, .38, .42, .8), 7.0) draw_line(to_map(spring), to_map(Vector2(40, 40)), Color(.12, .38, .42, .8), 7.0)
draw_circle(to_map(Vector2(33, 33)), 11.0, Color(.12, .38, .42, .8))
for lane: String in LuneMap.LANES: for lane: String in LuneMap.LANES:
var path: LuneMap.LanePath = match_state.map.lanes[lane] var path: LuneMap.LanePath = match_state.map.lanes[lane]
var points := PackedVector2Array() var points := PackedVector2Array()
@ -65,6 +68,13 @@ func _draw() -> void:
else: else:
draw_arc(at, radius, 0, TAU, 16, color.darkened(.3), 1.5) draw_arc(at, radius, 0, TAU, 16, color.darkened(.3), 1.5)
draw_line(at - Vector2(radius, radius) * .6, at + Vector2(radius, radius) * .6, color.darkened(.3), 1.5) draw_line(at - Vector2(radius, radius) * .6, at + Vector2(radius, radius) * .6, color.darkened(.3), 1.5)
if camera_footprint.size() == 4:
var outline := PackedVector2Array()
for corner: Vector2 in camera_footprint:
outline.append(to_map(corner).clamp(Vector2.ZERO, size))
outline.append(outline[0])
draw_polyline(outline, Color(1, 1, 1, .6), 1.2, true)
else:
var center: Vector2 = to_map(Vector2(camera_focus.x, camera_focus.z)) var center: Vector2 = to_map(Vector2(camera_focus.x, camera_focus.z))
var span: float = camera_span / ((LuneMap.HALF_EXTENT + 4.0) * 2.0) * size.x var span: float = camera_span / ((LuneMap.HALF_EXTENT + 4.0) * 2.0) * size.x
draw_rect(Rect2(center - Vector2(span * .9, span * .55), Vector2(span * 1.8, span * 1.1)), Color(1, 1, 1, .55), false, 1.2) draw_rect(Rect2(center - Vector2(span * .9, span * .55), Vector2(span * 1.8, span * 1.1)), Color(1, 1, 1, .55), false, 1.2)

View file

@ -48,7 +48,7 @@ func _ready() -> void:
_checks[key] = check _checks[key] = check
column.add_child(HSeparator.new()) column.add_child(HSeparator.new())
_label("STEUERUNG", "SectionLabel", column) _label("STEUERUNG", "SectionLabel", column)
_label("Planung: 1–5 Truppe kaufen · Lane-Knöpfe T/M/B teilen ein · Leertaste Bereit\nKampf: Leertaste oder P Pause · Tempo 1×/2×/4× · Kämpfe laufen automatisch\nKamera: WASD / Pfeile / mittlere Maustaste · Mausrad Zoom · Tab nächste Lane · O Übersicht\nMinimap anklicken springt dorthin · F1 Optionen · M Ton · F11 Vollbild", "Label", column) _label("Planung: 1–5 Truppe kaufen · Lane-Knöpfe T/M/B teilen ein · Leertaste Bereit\nKampf: Leertaste oder P Pause · Tempo 1×/2×/4× · Kämpfe laufen automatisch\nKamera: WASD / Pfeile / mittlere Maustaste · Mausrad Zoom · Q/E drehen · R zurücksetzen\nSprünge: F2 Top · F3 Mid · F4 Bottom · Tab nächste Lane · B eigene Basis · G Bossgrotte · H Held · O Übersicht\nMinimap anklicken springt dorthin · F1 Optionen · M Ton · F11 Vollbild", "Label", column)
var back := Button.new() var back := Button.new()
back.text = "ZURÜCK [Esc]" back.text = "ZURÜCK [Esc]"
back.theme_type_variation = "PrimaryButton" back.theme_type_variation = "PrimaryButton"

View file

@ -1,40 +1,77 @@
extends Camera3D extends Camera3D
## Overview camera: orthographic three-quarter view over the whole map, pan, zoom and ## MOBA-style game camera: a perspective three-quarter view whose angle steepens as it
## lane focus. Blau's base sits bottom-left, Rot's top-right, as on the minimap. ## zooms out (flat and close for fights, steep for the whole map). `zoom` is the visible
## ground height at the focus, the same measure the old orthographic camera used, so lane,
## base and grotto framings keep their meaning. The focus follows the terrain height.
## Blau's base sits bottom-left, Rot's top-right, as on the minimap (yaw 0).
const FOV_DEGREES: float = 34.0
const MIN_ZOOM: float = 11.0
const MAX_ZOOM: float = 100.0
const DEFAULT_ZOOM: float = 34.0
## Pitch at the closest and the widest zoom.
const PITCH_NEAR: float = -48.0
const PITCH_FAR: float = -70.0
const FOCUS_LIMIT: float = 42.0
var focus := Vector3.ZERO var focus := Vector3.ZERO
var yaw: float = 0.0 var yaw: float = 0.0
var pitch: float = deg_to_rad(-56.0) var zoom: float = DEFAULT_ZOOM
var zoom: float = 34.0
var reduced_motion: bool = false var reduced_motion: bool = false
## Terrain height under a ground point; the world sets it (flat without a world).
var ground: Callable = func(_p: Vector2) -> float: return 0.0
var _goal_focus := Vector3.ZERO var _goal_focus := Vector3.ZERO
var _goal_zoom: float = 34.0 var _goal_zoom: float = DEFAULT_ZOOM
var _goal_yaw: float = 0.0
var _dragging: bool = false var _dragging: bool = false
func _ready() -> void: func _ready() -> void:
projection = PROJECTION_ORTHOGONAL projection = PROJECTION_PERSPECTIVE
near = .1 fov = FOV_DEGREES
far = 400.0 near = .5
far = 700.0
current = true current = true
_goal_focus = focus _goal_focus = focus
_apply() _apply()
static func pitch_for(value: float) -> float:
return deg_to_rad(lerpf(PITCH_NEAR, PITCH_FAR, smoothstep(14.0, MAX_ZOOM, value)))
func distance_for(value: float) -> float:
return value / (2.0 * tan(deg_to_rad(fov) * 0.5))
func look_at_point(point: Vector3, new_zoom: float = -1.0, instant: bool = false) -> void: func look_at_point(point: Vector3, new_zoom: float = -1.0, instant: bool = false) -> void:
_goal_focus = Vector3(clampf(point.x, -40, 40), 0, clampf(point.z, -40, 40)) _goal_focus = Vector3(clampf(point.x, -FOCUS_LIMIT, FOCUS_LIMIT), 0, clampf(point.z, -FOCUS_LIMIT, FOCUS_LIMIT))
if new_zoom > 0.0: if new_zoom > 0.0:
_goal_zoom = new_zoom _goal_zoom = clampf(new_zoom, MIN_ZOOM, MAX_ZOOM)
if instant or reduced_motion: if instant or reduced_motion:
focus = _goal_focus focus = _goal_focus
focus.y = ground.call(Vector2(focus.x, focus.z))
zoom = _goal_zoom zoom = _goal_zoom
yaw = _goal_yaw
_apply() _apply()
## The whole map (both bases, all lanes and the grotto) at once.
func overview(instant: bool = false) -> void: func overview(instant: bool = false) -> void:
look_at_point(Vector3.ZERO, 46.0, instant) look_at_point(Vector3.ZERO, 92.0, instant)
## Quarter turns around the focus; R returns to the map-aligned view.
func rotate_step(direction: int) -> void:
_goal_yaw += direction * PI * 0.25
if reduced_motion:
yaw = _goal_yaw
_apply()
func reset_view(point: Vector3, instant: bool = false) -> void:
_goal_yaw = 0.0
look_at_point(point, DEFAULT_ZOOM, instant)
if instant:
yaw = 0.0
_apply()
func _apply() -> void: func _apply() -> void:
size = zoom var back := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch_for(zoom)).rotated(Vector3.UP, yaw)
var back := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch).rotated(Vector3.UP, yaw) position = focus + back * distance_for(zoom)
position = focus + back * 80.0
look_at(focus, Vector3.UP) look_at(focus, Vector3.UP)
func _process(delta: float) -> void: func _process(delta: float) -> void:
@ -47,23 +84,43 @@ func _process(delta: float) -> void:
var right := Vector3(cos(yaw), 0, -sin(yaw)) var right := Vector3(cos(yaw), 0, -sin(yaw))
var forward := Vector3(-sin(yaw), 0, -cos(yaw)) var forward := Vector3(-sin(yaw), 0, -cos(yaw))
_goal_focus += (right * pan.x - forward * pan.y) * zoom * .9 * delta _goal_focus += (right * pan.x - forward * pan.y) * zoom * .9 * delta
_goal_focus.x = clampf(_goal_focus.x, -FOCUS_LIMIT, FOCUS_LIMIT)
_goal_focus.z = clampf(_goal_focus.z, -FOCUS_LIMIT, FOCUS_LIMIT)
var t: float = 1.0 if reduced_motion else minf(1.0, delta * 7.0) var t: float = 1.0 if reduced_motion else minf(1.0, delta * 7.0)
focus = focus.lerp(_goal_focus, t) var target_height: float = ground.call(Vector2(_goal_focus.x, _goal_focus.z))
focus = focus.lerp(Vector3(_goal_focus.x, target_height, _goal_focus.z), t)
zoom = lerpf(zoom, _goal_zoom, t) zoom = lerpf(zoom, _goal_zoom, t)
yaw = lerp_angle(yaw, _goal_yaw, t)
_apply() _apply()
func _text_focus() -> bool: func _text_focus() -> bool:
var owner: Control = get_viewport().gui_get_focus_owner() var owner: Control = get_viewport().gui_get_focus_owner()
return owner is LineEdit or owner is TextEdit return owner is LineEdit or owner is TextEdit
## Ground outline of the current view (four corners on the focus plane) for the minimap.
func footprint() -> PackedVector2Array:
var result := PackedVector2Array()
var viewport: Viewport = get_viewport()
if viewport == null:
return result
var extent: Vector2 = viewport.get_visible_rect().size
for corner: Vector2 in [Vector2.ZERO, Vector2(extent.x, 0), extent, Vector2(0, extent.y)]:
var origin: Vector3 = project_ray_origin(corner)
var direction: Vector3 = project_ray_normal(corner)
if direction.y > -0.02:
direction.y = -0.02
var hit: Vector3 = origin + direction * ((focus.y - origin.y) / direction.y)
result.append(Vector2(hit.x, hit.z))
return result
## World input only; the HUD consumes its own clicks before they reach here. ## World input only; the HUD consumes its own clicks before they reach here.
func handle_world_input(event: InputEvent) -> bool: func handle_world_input(event: InputEvent) -> bool:
if event is InputEventMouseButton and event.pressed: if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP: if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_goal_zoom = maxf(12.0, _goal_zoom - 2.5) _goal_zoom = maxf(MIN_ZOOM, _goal_zoom - maxf(2.5, _goal_zoom * .09))
return true return true
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_goal_zoom = minf(60.0, _goal_zoom + 2.5) _goal_zoom = minf(MAX_ZOOM, _goal_zoom + maxf(2.5, _goal_zoom * .09))
return true return true
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE:
_dragging = event.pressed _dragging = event.pressed
@ -72,5 +129,7 @@ func handle_world_input(event: InputEvent) -> bool:
var right := Vector3(cos(yaw), 0, -sin(yaw)) var right := Vector3(cos(yaw), 0, -sin(yaw))
var forward := Vector3(-sin(yaw), 0, -cos(yaw)) var forward := Vector3(-sin(yaw), 0, -cos(yaw))
_goal_focus -= (right * event.relative.x - forward * event.relative.y) * zoom / 700.0 _goal_focus -= (right * event.relative.x - forward * event.relative.y) * zoom / 700.0
_goal_focus.x = clampf(_goal_focus.x, -FOCUS_LIMIT, FOCUS_LIMIT)
_goal_focus.z = clampf(_goal_focus.z, -FOCUS_LIMIT, FOCUS_LIMIT)
return true return true
return false return false

View file

@ -0,0 +1,32 @@
// Deployment area on a lane: team-coloured border, faint fill and one line per
// formation row (1.5 m apart), plus chevrons at the front that point down the lane.
// UV.x runs across the pad (-1..1), UV.y along it in metres from the rear edge.
shader_type spatial;
render_mode unshaded, blend_mix, depth_draw_never, cull_disabled, shadows_disabled;
uniform vec4 tint : source_color = vec4(.35, .66, 1.0, 1.0);
uniform float pad_length = 7.4;
uniform float row_start = 7.2;
uniform float row_spacing = 1.5;
uniform float strength = 1.0;
uniform float highlight = 0.0;
void fragment() {
float across = abs(UV.x);
float along = UV.y;
// smoothstep needs edge0 < edge1 (reversed edges are undefined in GLSL).
float border = smoothstep(.86, .93, across) + (1.0 - smoothstep(.08, .22, along)) + smoothstep(pad_length - .22, pad_length - .08, along);
float rows = 0.0;
for (int i = 0; i < 5; i++) {
float at = row_start - float(i) * row_spacing;
rows += (1.0 - smoothstep(.0, .05, abs(along - at))) * step(across, .8) * .55;
}
float chevron = 0.0;
float front = along - (pad_length - 1.4);
if (front > 0.0 && front < 1.0) {
chevron = (1.0 - smoothstep(.0, .12, abs(front - (1.0 - across) * .7 - .15))) * step(across, .55);
}
float pulse = .75 + .25 * sin(TIME * 2.4);
float alpha = clamp(.035 + .045 * highlight + border * .5 + rows * .5 + chevron * .7, 0.0, 1.0);
// Unshaded colour stays at or below 1, so the pads never trigger bloom.
ALBEDO = min(tint.rgb * (.8 + .2 * highlight * pulse), vec3(1.0));
ALPHA = alpha * strength * (.8 + .2 * pulse * highlight);
}

View file

@ -0,0 +1 @@
uid://bwn3ulxjiwybb

View file

@ -0,0 +1,419 @@
class_name LuneTerrain
extends RefCounted
## Visual height field of the G1 map, derived from LuneMap alone. Every term depends on
## the axis coordinate u = (x + z) / √2 and the distance a = |z - x| / √2 from the axis,
## or on symmetric map features, so the ground is the exact mirror image across x = z.
## The simulation stays two-dimensional: views read walk_height() to stand units on it.
##
## Regions: the north-western highland (Top) behind an escarpment, the central valley
## with the Kluft river on the axis, the south-eastern lowland (Bottom) with the delta,
## base plateaus, the grotto terrace and a ring of mountains around the playable square.
const EXTENT: float = 60.0
const STEP: float = 1.0
## Coarse outer ring (mountains and the river outlet) around the fine grid.
const OUTER_EXTENT: float = 132.0
const OUTER_STEP: float = 4.0
const OUTER_HOLE: float = 56.0
const WATER_LEVEL: float = -0.35
const BED_LEVEL: float = -1.2
const BASE_HEIGHT: float = 0.6
const BASE_RADIUS: float = 10.6
const HIGHLAND: float = 3.0
const VALLEY: float = 0.18
const LOWLAND: float = -0.1
const ARENA_HEIGHT: float = 1.7
const ARENA_RADIUS: float = 7.8
const PLATFORM_HEIGHT: float = 0.62
const PLATFORM_RADIUS: float = 6.6
const FORD_HEIGHT: float = -0.16
## Lane corridor half widths (flat ground under the paving) per lane.
const CORRIDOR: Dictionary = {"top": 3.6, "mid": 4.5, "bot": 3.6}
const BLEND: float = 5.0
## River half width along the axis coordinate u (piecewise linear).
const RIVER_WIDTH: Array[Vector2] = [Vector2(-11.6, 1.6), Vector2(-9.0, 2.4), Vector2(-4.0, 3.1), Vector2(0.0, 3.5),
Vector2(12.0, 3.0), Vector2(26.0, 3.5), Vector2(32.0, 5.0), Vector2(36.5, 7.2), Vector2(42.0, 13.0), Vector2(60.0, 20.0)]
const RIVER_START: float = -11.6
var map: LuneMap
var size: int = 0
var ground := PackedFloat32Array()
var walk := PackedFloat32Array()
var colors := PackedColorArray()
## lane -> PackedFloat32Array of the lane surface height per metre of lane coordinate.
var lane_profiles: Dictionary = {}
var _noise := FastNoiseLite.new()
var _detail := FastNoiseLite.new()
var _segments: Array = []
var _buckets: Dictionary = {}
## [position, height] of every tower: a flat pad at the height of the lane beside it.
var _pads: Array = []
const BUCKET: float = 6.0
static var _cache: LuneTerrain
## One terrain per map layout and process; a rematch reuses it.
static func for_map(match_map: LuneMap) -> LuneTerrain:
if _cache == null:
_cache = LuneTerrain.new()
_cache.build(match_map)
return _cache
func build(match_map: LuneMap) -> void:
map = match_map
_noise.seed = 4417
_noise.frequency = 0.045
_noise.fractal_octaves = 3
_detail.seed = 911
_detail.frequency = 0.21
_detail.fractal_octaves = 2
for lane: String in LuneMap.LANES:
lane_profiles[lane] = _lane_profile(lane)
_index_segments()
for definition: Dictionary in map.buildings:
if str(definition.tier) in ["inner", "outer"]:
_pads.append([Vector2(definition.position), lane_height(str(definition.lane), map.s_for(int(definition.team), str(definition.lane), float(definition.progress)))])
elif str(definition.tier) == "base":
_pads.append([Vector2(definition.position), BASE_HEIGHT])
var grid: Dictionary = _grid(EXTENT, STEP, 0.0)
size = grid.size
ground = grid.ground
walk = grid.walk
colors = grid.colors
## Evaluates a square grid. Only the Blau side (z >= x) and the axis are computed; the
## grid is symmetric under i <-> j, so the Rot side is copied. `hole` skips the interior
## (used by the coarse outer ring, which also sinks slightly where it overlaps).
func _grid(extent: float, step: float, hole: float) -> Dictionary:
var n: int = int(round(extent * 2.0 / step)) + 1
var heights := PackedFloat32Array()
heights.resize(n * n)
var walks := PackedFloat32Array()
walks.resize(n * n)
var lane_distance := PackedFloat32Array()
lane_distance.resize(n * n)
var carved := PackedByteArray()
carved.resize(n * n)
for j: int in range(n):
for i: int in range(j + 1):
var p := Vector2(-extent + i * step, -extent + j * step)
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < hole - step:
continue
var sample: Array = _evaluate(p)
var height: float = sample[0]
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < EXTENT + 0.5:
height -= 0.5
for index: int in [j * n + i, i * n + j]:
heights[index] = height
walks[index] = sample[1]
lane_distance[index] = sample[2]
carved[index] = sample[3]
var grid_colors := PackedColorArray()
grid_colors.resize(n * n)
for j: int in range(n):
for i: int in range(j + 1):
var index: int = j * n + i
var left: float = heights[j * n + maxi(0, i - 1)]
var right: float = heights[j * n + mini(n - 1, i + 1)]
var up: float = heights[maxi(0, j - 1) * n + i]
var down: float = heights[mini(n - 1, j + 1) * n + i]
var slope: float = Vector2(right - left, down - up).length() / (2.0 * step)
var color: Color = _color(Vector2(-extent + i * step, -extent + j * step), heights[index], slope, lane_distance[index], carved[index] == 1)
grid_colors[index] = color
grid_colors[i * n + j] = color
return {"size": n, "extent": extent, "step": step, "hole": hole, "ground": heights, "walk": walks, "colors": grid_colors}
# ---------------------------------------------------------------- queries
static func axis_u(p: Vector2) -> float:
return (p.x + p.y) * 0.70710678
static func axis_a(p: Vector2) -> float:
return absf(p.y - p.x) * 0.70710678
func height_at(p: Vector2) -> float:
return _bilinear(ground, p)
## Height a unit stands on: the lane decks (bridges, ford, platform) instead of the river bed.
func walk_height(p: Vector2) -> float:
return _bilinear(walk, p)
func lane_height(lane: String, s: float) -> float:
var profile: PackedFloat32Array = lane_profiles[lane]
var f: float = clampf(s, 0.0, float(profile.size() - 1))
var i: int = mini(int(f), profile.size() - 2)
return lerpf(profile[i], profile[i + 1], f - i)
func river_half_width(u: float) -> float:
if u < RIVER_START:
return 0.0
for k: int in range(1, RIVER_WIDTH.size()):
if u <= RIVER_WIDTH[k].x:
var a: Vector2 = RIVER_WIDTH[k - 1]
var b: Vector2 = RIVER_WIDTH[k]
return lerpf(a.y, b.y, clampf((u - a.x) / (b.x - a.x), 0.0, 1.0))
return RIVER_WIDTH[RIVER_WIDTH.size() - 1].y
func is_water(p: Vector2) -> bool:
return height_at(p) < WATER_LEVEL - 0.02
func _bilinear(field: PackedFloat32Array, p: Vector2) -> float:
var fx: float = clampf((p.x + EXTENT) / STEP, 0.0, size - 1.001)
var fz: float = clampf((p.y + EXTENT) / STEP, 0.0, size - 1.001)
var i: int = int(fx)
var j: int = int(fz)
var tx: float = fx - i
var tz: float = fz - j
var a: float = lerpf(field[j * size + i], field[j * size + i + 1], tx)
var b: float = lerpf(field[(j + 1) * size + i], field[(j + 1) * size + i + 1], tx)
return lerpf(a, b, tz)
# ---------------------------------------------------------------- regions
func escarpment(a: float) -> float:
return -27.4 + 4.6 * smoothstep(6.0, 20.0, a) + 1.1 * sin(a * 0.43)
## Ground before lanes, river and arena: highland, valley, lowland, bases, mountains.
func _region(p: Vector2) -> float:
var u: float = axis_u(p)
var a: float = axis_a(p)
var edge: float = escarpment(a)
var hills: float = _noise.get_noise_2d(u, a)
var height: float = lerpf(HIGHLAND + hills * 0.5, VALLEY + hills * 0.35, smoothstep(edge - 1.3, edge + 1.3, u))
height = lerpf(height, LOWLAND + hills * 0.22, smoothstep(20.0, 33.0, u))
# The lookout hill beyond the Top corner.
height += 1.1 * (1.0 - smoothstep(4.0, 11.0, p.distance_to(Vector2(-31.0, -31.0))))
for team: int in range(2):
var d: float = p.distance_to(map.core_position(team))
height = lerpf(BASE_HEIGHT, height, smoothstep(BASE_RADIUS, BASE_RADIUS + 3.0, d))
# Mountains around the playable square: a rounded square (superellipse, symmetric under
# x <-> z) with a noisy foot line, so the frame does not read as a straight wall.
var border: float = pow(pow(absf(p.x), 5.0) + pow(absf(p.y), 5.0), 0.2) + hills * 3.4 + _detail.get_noise_2d(u * 0.35, a * 0.35) * 1.8
if border > 39.0:
var over: float = border - 39.0
height += minf(11.0, over * 0.62 + over * over * 0.035) * (0.75 + 0.25 * _detail.get_noise_2d(u * 0.5, a * 0.5))
return height
func _arena_blend(p: Vector2, height: float) -> float:
var d: float = p.distance_to(LuneMap.GROTTO)
if d > ARENA_RADIUS + 3.5:
return height
return lerpf(ARENA_HEIGHT, height, smoothstep(ARENA_RADIUS, ARENA_RADIUS + 3.5, d))
## Lane surface height: the smoothed ground along the lane, lifted onto platform and ford.
func _lane_profile(lane: String) -> PackedFloat32Array:
var path: LuneMap.LanePath = map.lanes[lane]
var count: int = int(ceil(path.length)) + 1
var raw := PackedFloat32Array()
raw.resize(count)
for k: int in range(count):
raw[k] = _arena_blend(path.point_at(float(k)), _region(path.point_at(float(k))))
var smooth: PackedFloat32Array = raw
for pass_index: int in range(3):
var next := PackedFloat32Array()
next.resize(count)
for k: int in range(count):
var total: float = 0.0
var weight: float = 0.0
for o: int in range(-6, 7):
var m: int = clampi(k + o, 0, count - 1)
total += smooth[m]
weight += 1.0
next[k] = total / weight
smooth = next
var middle: float = path.length * 0.5
for k: int in range(count):
var from_middle: float = absf(float(k) - middle)
# The lane leaves the base on the plateau and only then descends.
var at: Vector2 = path.point_at(float(k))
var core_d: float = minf(at.distance_to(map.core_position(0)), at.distance_to(map.core_position(1)))
smooth[k] = lerpf(BASE_HEIGHT, smooth[k], smoothstep(BASE_RADIUS - 1.0, BASE_RADIUS + 5.0, core_d))
if lane == "mid":
smooth[k] = lerpf(PLATFORM_HEIGHT, smooth[k], smoothstep(PLATFORM_RADIUS - 1.0, PLATFORM_RADIUS + 7.0, from_middle))
elif lane == "bot":
smooth[k] = lerpf(FORD_HEIGHT, smooth[k], smoothstep(6.0, 16.0, from_middle))
smooth[k] = maxf(smooth[k], WATER_LEVEL + 0.18)
return smooth
# ---------------------------------------------------------------- lanes (spatial index)
func _index_segments() -> void:
for lane: String in LuneMap.LANES:
var path: LuneMap.LanePath = map.lanes[lane]
for i: int in range(1, path.points.size()):
var a: Vector2 = path.points[i - 1]
var b: Vector2 = path.points[i]
var index: int = _segments.size()
_segments.append([a, b, lane, float(path.cumulative[i - 1]), a.distance_to(b)])
var reach: float = float(CORRIDOR[lane]) + BLEND + 1.0
var lo := Vector2(minf(a.x, b.x) - reach, minf(a.y, b.y) - reach)
var hi := Vector2(maxf(a.x, b.x) + reach, maxf(a.y, b.y) + reach)
for bx: int in range(floori(lo.x / BUCKET), floori(hi.x / BUCKET) + 1):
for bz: int in range(floori(lo.y / BUCKET), floori(hi.y / BUCKET) + 1):
var key := Vector2i(bx, bz)
if not _buckets.has(key):
_buckets[key] = []
(_buckets[key] as Array).append(index)
## [distance, lane, lane coordinate] of the closest lane centre line, or [INF].
func nearest_lane(p: Vector2) -> Array:
var key := Vector2i(floori(p.x / BUCKET), floori(p.y / BUCKET))
if not _buckets.has(key):
return [INF, "", 0.0]
var best: float = INF
var best_lane: String = ""
var best_s: float = 0.0
for index: int in _buckets[key]:
var segment: Array = _segments[index]
var a: Vector2 = segment[0]
var ab: Vector2 = (segment[1] as Vector2) - a
var t: float = clampf((p - a).dot(ab) / maxf(0.0001, ab.length_squared()), 0.0, 1.0)
var d: float = p.distance_to(a + ab * t)
if d < best:
best = d
best_lane = segment[2]
best_s = float(segment[3]) + float(segment[4]) * t
return [best, best_lane, best_s]
# ---------------------------------------------------------------- one grid point
func _evaluate(p: Vector2) -> Array:
var u: float = axis_u(p)
var a: float = axis_a(p)
var height: float = _arena_blend(p, _region(p))
var lane_info: Array = nearest_lane(p)
var lane_d: float = lane_info[0]
var on_lane: bool = false
var lane_surface: float = 0.0
if lane_d < INF:
var corridor: float = CORRIDOR[lane_info[1]]
lane_surface = lane_height(lane_info[1], lane_info[2])
height = lerpf(lane_surface, height, smoothstep(corridor, corridor + BLEND, lane_d))
on_lane = lane_d < corridor
for pad: Array in _pads:
var pad_d: float = p.distance_to(pad[0])
if pad_d < 3.8:
height = lerpf(float(pad[1]), height, smoothstep(2.3, 3.8, pad_d))
# The river is carved last so bridges and the ford span open water.
var width: float = river_half_width(u)
var carved: bool = false
if width > 0.0:
var bank: float = lerpf(BED_LEVEL, height, smoothstep(width - 0.9, width + 1.9, a))
# Round spring pool at the foot of the grotto terrace.
var start: float = smoothstep(RIVER_START - 0.2, RIVER_START + 1.6, u)
bank = lerpf(height, bank, start)
if bank < height:
carved = height - bank > 0.25
height = bank
var walk_height: float = lane_surface if on_lane else maxf(height, WATER_LEVEL)
if p.length() < PLATFORM_RADIUS:
walk_height = maxf(walk_height, PLATFORM_HEIGHT)
return [height, walk_height, lane_d, 1 if carved else 0]
## Blockout palette per zone; alpha carries rockiness for the terrain shader.
func _color(p: Vector2, height: float, slope: float, lane_d: float, carved: bool) -> Color:
var u: float = axis_u(p)
var a: float = axis_a(p)
var variation: float = _detail.get_noise_2d(u * 1.7, a * 1.7)
var grass := Color(.2, .31, .12).lerp(Color(.25, .33, .14), variation * .5 + .5)
var edge: float = escarpment(a)
var color: Color = grass
var rock: float = 0.0
if u < edge + 1.4:
color = Color(.29, .32, .17).lerp(Color(.35, .34, .25), clampf(variation * 1.4, 0.0, 1.0))
rock = clampf(variation * 1.2, 0.0, .6)
if u > 20.0:
color = color.lerp(Color(.17, .25, .12).lerp(Color(.24, .22, .15), clampf(-variation, 0.0, 1.0)), smoothstep(20.0, 30.0, u))
# Steep ground (escarpment, terrace walls, mountains) reads as rock.
if slope > 0.55:
color = color.lerp(Color(.37, .34, .29), smoothstep(0.55, 1.1, slope))
rock = maxf(rock, smoothstep(0.55, 1.1, slope))
if maxf(absf(p.x), absf(p.y)) > 40.0:
rock = maxf(rock, 0.35 + variation * 0.3)
if carved or height < WATER_LEVEL + 0.25:
color = Color(.31, .29, .21).lerp(Color(.15, .16, .12), clampf((WATER_LEVEL + 0.2 - height) / 0.8, 0.0, 1.0))
rock = 0.25
if lane_d < 5.2:
color = color.lerp(Color(.31, .26, .18), 1.0 - smoothstep(3.8, 5.2, lane_d))
for team: int in range(2):
if p.distance_to(map.core_position(team)) < BASE_RADIUS + 0.3:
color = Color(.41, .38, .32).lerp(Color(.36, .33, .28), variation * .5 + .5)
rock = 0.45
if p.distance_to(LuneMap.GROTTO) < ARENA_RADIUS + 0.2:
color = Color(.36, .34, .33)
rock = 0.55
# The palette is authored in sRGB; vertex colours reach the shader as linear values.
var linear: Color = Color(color.r, color.g, color.b).srgb_to_linear()
linear.a = rock
return linear
# ---------------------------------------------------------------- mesh
var _inner_mesh: ArrayMesh
var _outer_mesh: ArrayMesh
func build_mesh() -> ArrayMesh:
return _mesh({"size": size, "extent": EXTENT, "step": STEP, "hole": 0.0, "ground": ground, "colors": colors})
## The distant mountains and the river outlet at 4 m resolution, with a hole for the fine grid.
func build_outer_mesh() -> ArrayMesh:
return _mesh(_grid(OUTER_EXTENT, OUTER_STEP, OUTER_HOLE))
## Cached meshes: a rematch or a second match in the same process reuses them.
func inner_mesh() -> ArrayMesh:
if _inner_mesh == null:
_inner_mesh = build_mesh()
return _inner_mesh
func outer_mesh() -> ArrayMesh:
if _outer_mesh == null:
_outer_mesh = build_outer_mesh()
return _outer_mesh
func _mesh(grid: Dictionary) -> ArrayMesh:
var n: int = grid.size
var extent: float = grid.extent
var step: float = grid.step
var hole: float = grid.hole
var heights: PackedFloat32Array = grid.ground
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
vertices.resize(n * n)
normals.resize(n * n)
for j: int in range(n):
for i: int in range(n):
var index: int = j * n + i
vertices[index] = Vector3(-extent + i * step, heights[index], -extent + j * step)
var left: float = heights[j * n + maxi(0, i - 1)]
var right: float = heights[j * n + mini(n - 1, i + 1)]
var up: float = heights[maxi(0, j - 1) * n + i]
var down: float = heights[mini(n - 1, j + 1) * n + i]
normals[index] = Vector3(left - right, 2.0 * step, up - down).normalized()
var indices := PackedInt32Array()
indices.resize((n - 1) * (n - 1) * 6)
var cursor: int = 0
for j: int in range(n - 1):
for i: int in range(n - 1):
if hole > 0.0:
var cx: float = absf(-extent + (i + 0.5) * step)
var cz: float = absf(-extent + (j + 0.5) * step)
if maxf(cx, cz) < hole:
continue
var a: int = j * n + i
var b: int = a + 1
var c: int = a + n
var d: int = c + 1
# Alternating diagonals; cell (i, j) and its mirror (j, i) share the parity,
# so mirrored cells triangulate as mirror images (clockwise front faces).
if (i + j) % 2 == 0:
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = d
indices[cursor + 3] = a; indices[cursor + 4] = d; indices[cursor + 5] = c
else:
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = c
indices[cursor + 3] = b; indices[cursor + 4] = d; indices[cursor + 5] = c
cursor += 6
indices.resize(cursor)
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_COLOR] = grid.colors
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh

View file

@ -0,0 +1 @@
uid://ccaumfwbolrec

View file

@ -0,0 +1,36 @@
// G1 ground: zone colours from the height field (vertex colour), rockiness in alpha.
// Rock areas use the shared limestone texture triplanar, like the Kluft surfaces.
shader_type spatial;
render_mode diffuse_burley, specular_schlick_ggx;
uniform sampler2D stone_map : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
varying vec3 world;
varying vec3 world_normal;
float hash(vec3 p) { p = fract(p * .1031); p += dot(p, p.yzx + 33.33); return fract((p.x + p.y) * p.z); }
float noise(vec3 p) {
vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y), f.z);
}
void vertex() {
world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_normal = normalize(mat3(MODEL_MATRIX) * NORMAL);
}
void fragment() {
vec3 weights = pow(abs(world_normal), vec3(5.0));
weights /= max(dot(weights, vec3(1.0)), .0001);
vec3 p = world * .21;
vec3 stone = texture(stone_map, p.zy).rgb * weights.x + texture(stone_map, p.xz).rgb * weights.y + texture(stone_map, p.xy).rgb * weights.z;
float relief = dot(stone, vec3(.333));
float broad = noise(world * .35);
float grain = noise(world * 6.0);
float rock = COLOR.a;
vec3 soil = COLOR.rgb * mix(.84, 1.14, broad) * mix(.9, 1.07, grain);
vec3 rocky = COLOR.rgb * mix(vec3(.72), vec3(1.45), stone);
ALBEDO = mix(soil, rocky, rock);
ROUGHNESS = mix(.93, .84, rock);
vec3 n = NORMAL, dx = dFdx(VERTEX), dy = dFdy(VERTEX);
vec3 rx = cross(dy, n), ry = cross(n, dx);
float det = dot(dx, rx);
vec3 gradient = dFdx(relief) * rx + dFdy(relief) * ry;
NORMAL = normalize(abs(det) * n - sign(det) * gradient * (.006 + rock * .014));
}

View file

@ -0,0 +1 @@
uid://ih7v3fqacdf8

View file

@ -6,6 +6,8 @@ const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55"), Color("b37c
const HERO_GOLD := Color("f2c96b") const HERO_GOLD := Color("f2c96b")
var unit: LuneUnit var unit: LuneUnit
## Ground the figure stands on (lane decks, terrain, grotto terrace); flat without it.
var terrain: LuneTerrain
var model: Node3D var model: Node3D
var skeletal: bool = false var skeletal: bool = false
var _fill: MeshInstance3D var _fill: MeshInstance3D
@ -90,7 +92,8 @@ func bind(source: LuneUnit) -> void:
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
tag.no_depth_test = true tag.no_depth_test = true
tag.fixed_size = true tag.fixed_size = true
tag.pixel_size = .0011 # Fixed screen size under the perspective game camera (about 18 px at 1080p).
tag.pixel_size = .00034
tag.font_size = 30 tag.font_size = 30
tag.outline_size = 10 tag.outline_size = 10
tag.modulate = HERO_GOLD if unit.kind == "hero" else TEAM_COLORS[unit.team].lightened(.3) tag.modulate = HERO_GOLD if unit.kind == "hero" else TEAM_COLORS[unit.team].lightened(.3)
@ -102,7 +105,7 @@ func bind(source: LuneUnit) -> void:
add_child(tag) add_child(tag)
_last_attack = unit.attack_serial _last_attack = unit.attack_serial
_last_ability = unit.ability_serial _last_ability = unit.ability_serial
position = Vector3(unit.position.x, 0, unit.position.y) position = Vector3(unit.position.x, _ground(unit.position), unit.position.y)
rotation.y = unit.facing rotation.y = unit.facing
_last_visual = position _last_visual = position
@ -111,9 +114,9 @@ func sync(alpha: float, delta: float) -> void:
return return
_time += delta _time += delta
var at: Vector2 = unit.previous_position.lerp(unit.position, clampf(alpha, 0.0, 1.0)) var at: Vector2 = unit.previous_position.lerp(unit.position, clampf(alpha, 0.0, 1.0))
position = Vector3(at.x, 0, at.y) position = Vector3(at.x, _ground(at), at.y)
rotation.y = lerp_angle(rotation.y, unit.facing, minf(1.0, delta * 12.0)) rotation.y = lerp_angle(rotation.y, unit.facing, minf(1.0, delta * 12.0))
var moved: float = position.distance_to(_last_visual) var moved: float = Vector2(position.x - _last_visual.x, position.z - _last_visual.z).length()
_last_visual = position _last_visual = position
if unit.attack_serial != _last_attack: if unit.attack_serial != _last_attack:
_last_attack = unit.attack_serial _last_attack = unit.attack_serial
@ -136,6 +139,9 @@ func sync(alpha: float, delta: float) -> void:
if not skeletal: if not skeletal:
visible = unit.alive visible = unit.alive
func _ground(at: Vector2) -> float:
return terrain.walk_height(at) + .03 if terrain != null else 0.0
class EnemyVisualsFallback: class EnemyVisualsFallback:
static func make(color: Color) -> Node3D: static func make(color: Color) -> Node3D:
var body := MeshInstance3D.new() var body := MeshInstance3D.new()

File diff suppressed because it is too large Load diff

136
tools/luneraria/g_views.gd Normal file
View file

@ -0,0 +1,136 @@
extends Node
## Comparison captures for the graphics phases G1–G6, always from the real game camera
## and the real HUD at the running window size (use 1920x1080).
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/g_views.tscn -- --out=res://docs/luneraria/screenshots/g1/nachher
## Views: 01 map, 02–04 lanes in combat, 05/06 bases, 07 grotto, 08 many units, 09 HUD,
## 10 hero selected, 11 close zoom, 12 camera in motion. Match and fixture are fixed
## (seed 4, Wurzelbund on Mid) so every phase is photographed the same way. Fixture gold
## only exists in this capture run; the rules are the ones of the running build.
const Game = preload("res://scripts/luneraria/lune_game.gd")
var out: String = "res://docs/luneraria/screenshots/g1/nachher/"
var main: Node
var shots: Array[String] = []
func _ready() -> void:
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out = arg.substr(6)
if not out.ends_with("/"):
out += "/"
call_deferred("run")
func shot(label: String, frames: int = 14) -> void:
for i: int in frames:
await get_tree().process_frame
await RenderingServer.frame_post_draw
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out))
get_viewport().get_texture().get_image().save_png(out + label + ".png")
shots.append(label)
print("G_VIEW ", label)
## Camera helpers that only use public map data, so they frame old and new layouts alike.
func grotto_point(game: Node3D) -> Vector2:
var map: LuneMap = game.match_state.map
return map.grotto_center() if map.has_method("grotto_center") else LuneMap.GROTTO
func look(game: Node3D, at: Vector2, zoom: float) -> void:
game.camera.look_at_point(Vector3(at.x, 0, at.y), zoom, true)
## Runs the combat clock quickly (more fixed steps per frame, rules unchanged) and holds it.
func advance_combat(game: Node3D, seconds: float) -> void:
var state: LuneMatch = game.match_state
game.paused = false
while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < seconds:
for i: int in range(20):
if state.phase != LuneMatch.Phase.COMBAT or state.combat.elapsed >= seconds:
break
state.step_combat()
await get_tree().process_frame
game.paused = true
## Documentation only (not a game view): orthographic top-down plan without the HUD.
func plan_view(game: Node3D) -> void:
var plan := Camera3D.new()
plan.projection = Camera3D.PROJECTION_ORTHOGONAL
plan.size = 86.0
plan.far = 400.0
game.add_child(plan)
plan.position = Vector3(0, 150, 0)
plan.rotation = Vector3(-PI * .5, 0, 0)
plan.current = true
game.hud.visible = false
await shot("00-kartenplan-draufsicht", 10)
game.hud.visible = true
game.camera.current = true
plan.queue_free()
func run() -> void:
Soundscape.enabled = false
main = load("res://scenes/luneraria.tscn").instantiate()
add_child(main)
var menu: MainMenu = main.menu
while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")):
await get_tree().process_frame
menu.bund = "wurzelbund"
menu.lane = "mid"
main.start_match({"seed": 4})
await get_tree().create_timer(.8).timeout
var game: Node3D = main.game
var state: LuneMatch = game.match_state
var slot: int = game.human_slot
# Fixture: enough gold for a hero with one item next to the starting army.
state.players[slot].draft.gold = 900
game._on_command({"type": "hero_buy", "hero_id": "held_ringborke"})
game._on_command({"type": "item_buy", "item_id": "basaltschild"})
game.hud._starter.pressed.emit()
game.hud.show_page(0)
game.focus_lane("mid", true)
await shot("09-hud-planung", 24)
game.hud.show_page(1)
await shot("10-held-ausgewaehlt")
game.hud.show_page(0)
game.camera.overview(true)
await shot("01-karte", 20)
await plan_view(game)
look(game, state.map.core_position(0), 26.0)
await shot("05-eigene-basis")
look(game, state.map.core_position(1), 26.0)
await shot("06-gegnerbasis")
look(game, grotto_point(game), 22.0)
await shot("07-bossgrotte")
game._on_ready()
await get_tree().process_frame
await advance_combat(game, 16.0)
var names := {"top": "02-top-lane", "mid": "03-mid-lane", "bot": "04-bottom-lane"}
for lane: String in LuneMap.LANES:
game.focus_lane(lane, true)
await shot(names[lane])
# Camera in motion: a real, non-instant pan from Mid towards the own base.
game.focus_lane("mid", true)
await get_tree().process_frame
var base: Vector2 = state.map.core_position(0)
game.camera.look_at_point(Vector3(base.x, 0, base.y), 30.0, false)
await shot("12-kamera-bewegt", 5)
# Many units: all six armies at the supply cap in a round-13 wave (capture fixture).
main.queue_free()
await get_tree().process_frame
var crowd: Node3D = Game.new()
add_child(crowd)
crowd.start({"seed": 5, "human_slot": 1})
var crowd_state: LuneMatch = crowd.match_state
crowd_state.round_number = 13
for other: int in range(6):
crowd_state.players[other].draft.gold = 100000
crowd_state.players[other].draft.roster.clear()
LuneBot.plan(crowd_state, other, "ausgewogen")
crowd._on_ready()
await get_tree().process_frame
await advance_combat(crowd, 13.0)
crowd.focus_lane("mid", true)
await shot("08-kampf-viele-einheiten", 20)
var fight: Vector2 = crowd_state.map.lane_point(0, "mid", crowd_state.map.lanes["mid"].length * .5)
look(crowd, fight, 13.0)
await shot("11-zoom-nah")
print("G_VIEWS_DONE ", shots.size())
get_tree().quit()

View file

@ -0,0 +1 @@
uid://cwt2eokernmvs

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tools/luneraria/g_views.gd" id="1"]
[node name="GViews" type="Node"]
script = ExtResource("1")

View file

@ -1,11 +1,69 @@
extends Node extends Node
## Frame and simulation cost in a real match scene at representative maximum occupation: ## Frame and simulation cost in a real match scene at representative maximum occupation:
## six armies at the supply cap and a round-13 wave (synthetic fixture, gold granted to the ## six armies at the supply cap and a round-13 wave on all three lanes (synthetic fixture,
## fixture only). VSync off; figures apply to this machine only. ## gold granted to the fixture only). Two camera setups are measured one after the other:
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn ## a lane view (Mid, Blau side, grotto in the background) and the whole-map overview,
## where every lane, tower, base and the grotto are drawn at once. VSync off; the
## figures apply to this machine only.
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn -- --out=res://docs/luneraria/logs/g1/perf.json
const Game = preload("res://scripts/luneraria/lune_game.gd") const Game = preload("res://scripts/luneraria/lune_game.gd")
const PHASE_SECONDS: float = 6.0
var out: String = ""
## Start of the current frame's main-thread work (set by a node that processes first).
var frame_start: int = 0
class FrameClock:
extends Node
var owner_bench: Node
func _process(_delta: float) -> void:
owner_bench.frame_start = Time.get_ticks_usec()
func _ready() -> void: func _ready() -> void:
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out = arg.substr(6)
var clock := FrameClock.new()
clock.owner_bench = self
clock.process_priority = -1000
add_child(clock)
call_deferred("run") call_deferred("run")
func mean(values: Array) -> float:
var total: float = 0.0
for value: Variant in values:
total += float(value)
return total / maxf(1.0, values.size())
func measure(game: Node3D, state: LuneMatch) -> Dictionary:
for i: int in 30:
await get_tree().process_frame
var frames: Array[float] = []
var cpu: Array[float] = []
var gpu: Array[float] = []
var draws: Array[float] = []
var primitives: Array[float] = []
var objects: Array[float] = []
var mobile: Array[int] = []
var last: int = Time.get_ticks_usec()
var end: int = Time.get_ticks_msec() + int(PHASE_SECONDS * 1000.0)
while Time.get_ticks_msec() < end and state.phase == LuneMatch.Phase.COMBAT:
await RenderingServer.frame_post_draw
var now: int = Time.get_ticks_usec()
frames.append((now - last) / 1000.0)
last = now
# Main-thread time from the first _process of the frame until the frame was drawn.
cpu.append((now - frame_start) / 1000.0)
gpu.append(RenderingServer.viewport_get_measured_render_time_gpu(get_viewport().get_viewport_rid()))
draws.append(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
primitives.append(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
objects.append(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME))
mobile.append(state.combat.mobile_count())
frames.sort()
return {"frames": frames.size(), "frame_ms_mean": mean(frames), "frame_ms_p95": frames[int(frames.size() * .95)] if not frames.is_empty() else -1.0,
"cpu_frame_ms_mean": mean(cpu), "gpu_ms_mean": mean(gpu), "draw_calls_mean": mean(draws), "primitives_mean": mean(primitives),
"objects_mean": mean(objects), "mobile_units_mean": mean(mobile), "camera_zoom": game.camera.zoom}
func run() -> void: func run() -> void:
Soundscape.enabled = false Soundscape.enabled = false
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
@ -21,27 +79,12 @@ func run() -> void:
state.players[slot].draft.roster.clear() state.players[slot].draft.roster.clear()
LuneBot.plan(state, slot, "ausgewogen") LuneBot.plan(state, slot, "ausgewogen")
game._on_ready() game._on_ready()
game.camera.look_at_point(Vector3(-10, 0, 10), 34.0, true)
for i in 30: await get_tree().process_frame
RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true) RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true)
var frames: Array[float] = []
var gpu: Array[float] = []
var sim: Array[float] = []
var draws: Array[float] = []
var mobile: Array[int] = []
var last: int = Time.get_ticks_usec()
var end: int = Time.get_ticks_msec() + 8000
game.speed_index = 1 game.speed_index = 1
while Time.get_ticks_msec() < end and state.phase == LuneMatch.Phase.COMBAT: game.camera.look_at_point(Vector3(-10, 0, 10), 34.0, true)
var s0: int = Time.get_ticks_usec() var lane_view: Dictionary = await measure(game, state)
var before: int = state.combat.steps game.camera.overview(true)
await RenderingServer.frame_post_draw var overview: Dictionary = await measure(game, state)
var now: int = Time.get_ticks_usec()
frames.append((now - last) / 1000.0)
last = now
gpu.append(RenderingServer.viewport_get_measured_render_time_gpu(get_viewport().get_viewport_rid()))
draws.append(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
mobile.append(state.combat.mobile_count())
# Pure simulation cost of one fixed step at this occupation, measured separately. # Pure simulation cost of one fixed step at this occupation, measured separately.
var probe := LuneMatch.new() var probe := LuneMatch.new()
probe.setup({"seed": 5, "human_slot": -1}) probe.setup({"seed": 5, "human_slot": -1})
@ -52,22 +95,19 @@ func run() -> void:
probe.begin_combat() probe.begin_combat()
var start_units: int = probe.combat.mobile_count() var start_units: int = probe.combat.mobile_count()
var s1: int = Time.get_ticks_usec() var s1: int = Time.get_ticks_usec()
for i in 200: for i: int in 200:
probe.step_combat() probe.step_combat()
var step_ms: float = (Time.get_ticks_usec() - s1) / 1000.0 / 200.0 var step_ms: float = (Time.get_ticks_usec() - s1) / 1000.0 / 200.0
frames.sort()
var mean := func(v: Array) -> float:
var t: float = 0.0
for x in v: t += float(x)
return t / maxf(1, v.size())
var report := {"window": get_window().size, "renderer": RenderingServer.get_current_rendering_method(), "gpu": RenderingServer.get_video_adapter_name(), var report := {"window": get_window().size, "renderer": RenderingServer.get_current_rendering_method(), "gpu": RenderingServer.get_video_adapter_name(),
"cpu": OS.get_processor_name(), "engine": Engine.get_version_info().string, "scene_build_ms": build_ms, "cpu": OS.get_processor_name(), "engine": Engine.get_version_info().string, "scene_build_ms": build_ms,
"mobile_units_at_start": start_units, "mobile_units_mean_measured": mean.call(mobile), "buildings": 18, "mobile_units_at_start": start_units, "buildings": 18, "sim_step_ms": step_ms, "sim_speed": "2x (40 steps/s)",
"frames": frames.size(), "frame_ms_mean": mean.call(frames), "frame_ms_p95": frames[int(frames.size() * .95)] if frames.size() > 0 else -1, "lane_view": lane_view, "overview": overview,
"gpu_ms_mean": mean.call(gpu), "draw_calls_mean": mean.call(draws), "sim_step_ms": step_ms, "sim_speed": "2x (40 steps/s)",
"video_mem_mb": Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1048576.0, "video_mem_mb": Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1048576.0,
"texture_mem_mb": Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED) / 1048576.0,
"static_mem_mb": Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0} "static_mem_mb": Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0}
var file := FileAccess.open("res://docs/luneraria/logs/perf-%dx%d.json" % [get_window().size.x, get_window().size.y], FileAccess.WRITE) var path: String = out if not out.is_empty() else "res://docs/luneraria/logs/perf-%dx%d.json" % [get_window().size.x, get_window().size.y]
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path.get_base_dir()))
var file := FileAccess.open(path, FileAccess.WRITE)
file.store_string(JSON.stringify(report, "\t")) file.store_string(JSON.stringify(report, "\t"))
file.close() file.close()
print("LUNE_BENCH ", JSON.stringify(report)) print("LUNE_BENCH ", JSON.stringify(report))

View file

@ -137,7 +137,8 @@ func _process(_delta: float) -> void:
if state.phase != LuneMatch.Phase.COMBAT: if state.phase != LuneMatch.Phase.COMBAT:
break break
state.step_combat() state.step_combat()
elif drop_round == state.round_number and session.frames_received > frames_at_combat_start + 5: elif drop_round == state.round_number and session.frames_received > frames_at_combat_start:
# Leave on the first snapshot of the combat: short combats may send only a few.
print("NET_CLIENT_DROP round=%d seat=%d" % [state.round_number, session.local_slot]) print("NET_CLIENT_DROP round=%d seat=%d" % [state.round_number, session.local_slot])
session.close() session.close()
get_tree().quit(0) get_tree().quit(0)

View file

@ -0,0 +1,294 @@
extends Node
## G1 acceptance: the three-lane layout plays correctly and the game camera reaches it.
## Part 1 checks the map and the terrain (lanes, water crossings, towers, bases, grotto,
## deployment areas, mirror symmetry). Part 2 runs the real game with real key and mouse
## events: lane, base, grotto, hero and overview jumps, reset, rotation, zoom limits,
## deployment areas in planning, figures standing on the ground in combat.
## Run (window needed): godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn
const OUT := "res://docs/luneraria/logs/g1/verify-g1.json"
var checks: int = 0
var failures: Array[String] = []
var facts: Dictionary = {}
func check(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures.append(label)
printerr("G1_FAIL: ", label)
func _ready() -> void:
call_deferred("run")
func run() -> void:
Soundscape.enabled = false
var map := LuneMap.new()
var terrain: LuneTerrain = LuneTerrain.for_map(map)
_layout(map, terrain)
await _game()
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://docs/luneraria/logs/g1"))
var file := FileAccess.open(OUT, FileAccess.WRITE)
file.store_string(JSON.stringify({"checks": checks, "failures": failures, "facts": facts}, "\t"))
file.close()
print("G1_VERIFY_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()])
get_tree().quit(0 if failures.is_empty() else 1)
# ---------------------------------------------------------------- part 1: layout
func _layout(map: LuneMap, terrain: LuneTerrain) -> void:
var lengths: Dictionary = {}
for lane: String in LuneMap.LANES:
lengths[lane] = snappedf(map.lanes[lane].length, .01)
facts.lane_lengths = lengths
check(absf(map.lanes["top"].length - map.lanes["bot"].length) < .001, "Top and Bottom have the same length")
check(map.lanes["mid"].length < map.lanes["top"].length * .75, "Mid is the direct route (clearly shorter than Top/Bottom)")
# Lanes are always walkable; open water lies under a lane only at the two decks.
var water_points: Dictionary = {}
var min_height: Dictionary = {}
var max_height: Dictionary = {}
for lane: String in LuneMap.LANES:
var path: LuneMap.LanePath = map.lanes[lane]
var dry: bool = true
var s: float = 0.0
water_points[lane] = 0
min_height[lane] = INF
max_height[lane] = -INF
while s <= path.length:
var p: Vector2 = path.point_at(s)
var surface: float = terrain.lane_height(lane, s)
min_height[lane] = minf(min_height[lane], surface)
max_height[lane] = maxf(max_height[lane], surface)
dry = dry and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .15
if terrain.height_at(p) < LuneTerrain.WATER_LEVEL:
water_points[lane] = int(water_points[lane]) + 1
var on_deck: bool = (lane == "mid" and p.length() < LuneTerrain.PLATFORM_RADIUS) or (lane == "bot" and p.distance_to(path.point_at(path.length * .5)) < 7.2)
check(on_deck, "%s crosses water only on its deck (at %s)" % [lane, p.snapped(Vector2(.1, .1))])
s += 1.0
check(dry, "%s lane is walkable everywhere" % lane)
facts.lane_height_range = {"min": min_height, "max": max_height}
facts.water_points_under_lanes = water_points
check(int(water_points.top) == 0, "Top never touches the river (highland route)")
check(int(water_points.mid) > 0 and int(water_points.bot) > 0, "Mid and Bottom cross the Kluft")
check(float(max_height.top) > 2.5, "Top climbs onto the highland (%.2f m)" % float(max_height.top))
check(float(min_height.bot) < 0.0, "Bottom runs through the lowland (%.2f m)" % float(min_height.bot))
check(is_equal_approx(terrain.lane_height("mid", map.lanes["mid"].length * .5), LuneTerrain.PLATFORM_HEIGHT), "Mid crosses on the central platform")
# Buildings: inside the playable square, on dry flat pads, not crowding each other.
var cores: Array[Vector2] = [map.core_position(0), map.core_position(1)]
for definition: Dictionary in map.buildings:
var p: Vector2 = definition.position
var id: String = definition.id
check(absf(p.x) < LuneMap.HALF_EXTENT - 2.0 and absf(p.y) < LuneMap.HALF_EXTENT - 2.0, id + " lies inside the playable square")
check(terrain.height_at(p) > LuneTerrain.WATER_LEVEL + .25, id + " stands on dry ground")
var flat: bool = true
for offset: Vector2 in [Vector2(1.4, 0), Vector2(-1.4, 0), Vector2(0, 1.4), Vector2(0, -1.4)]:
flat = flat and absf(terrain.height_at(p + offset) - terrain.height_at(p)) < .15
check(flat, id + " stands on a flat pad")
for other: Dictionary in map.buildings:
if other.id != id:
check(p.distance_to(other.position) > 3.8, "%s keeps its distance to %s" % [id, other.id])
if str(definition.tier) == "base":
var team: int = int(definition.team)
check((p - cores[team]).dot(cores[1 - team] - cores[team]) > 0.0, id + " stands in front of its Ancient")
# Grotto: on the symmetry axis, beside Mid, clear of every lane corridor, entries on the terrace.
var grotto: Vector2 = map.grotto_center()
check(LuneMap.mirror(grotto).distance_to(grotto) < .001, "grotto lies on the symmetry axis")
var nearest: Array = terrain.nearest_lane(grotto + LuneMap.AXIS * 0.0)
var clearance: float = INF
for angle_step: int in range(24):
var edge: Vector2 = grotto + Vector2.from_angle(TAU * angle_step / 24.0) * LuneTerrain.ARENA_RADIUS
var lane: Array = terrain.nearest_lane(edge)
if float(lane[0]) < INF:
clearance = minf(clearance, float(lane[0]) - float(LuneTerrain.CORRIDOR[lane[1]]))
facts.grotto_clearance_to_lanes = snappedf(clearance, .01)
check(clearance > 2.0, "grotto arena stays clear of every lane corridor (%.1f m)" % clearance)
var mid: LuneMap.LanePath = map.lanes["mid"]
check(mid.point_at(mid.project(grotto)).distance_to(grotto) > 12.0, "grotto does not block Mid")
for team: int in range(2):
check(absf(terrain.walk_height(map.grotto_entry(team)) - LuneTerrain.ARENA_HEIGHT) < .05, "grotto entry of team %d lies on the terrace" % team)
check(absf(terrain.walk_height(map.grotto_boss_point()) - LuneTerrain.ARENA_HEIGHT) < .05, "boss point lies on the terrace")
check(terrain.is_water(grotto + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS + 1.5)), "the Kluft springs at the foot of the grotto terrace")
check(not terrain.is_water(Vector2(-25, -25)), "no water at the Top corner")
check(terrain.is_water(Vector2(20, 20)) and terrain.is_water(Vector2(-5, -5)), "the river runs along the axis")
# Deployment areas: dry, inside the square, in front of the own base.
var span: Dictionary = map.deployment_span()
for team: int in range(2):
for lane: String in LuneMap.LANES:
var ok: bool = true
for progress: float in [float(span.from), float(span.to)]:
for lateral: float in [-float(span.half_width), 0.0, float(span.half_width)]:
var p: Vector2 = map.lane_point(team, lane, progress, lateral)
ok = ok and absf(p.x) < LuneMap.HALF_EXTENT and absf(p.y) < LuneMap.HALF_EXTENT
ok = ok and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .2
ok = ok and p.distance_to(cores[team]) < p.distance_to(cores[1 - team])
check(ok, "deployment area %d/%s is dry, inside and on the own side" % [team, lane])
# The ground is the exact mirror image across x = z.
var worst: float = 0.0
for j: int in range(terrain.size):
for i: int in range(j):
worst = maxf(worst, absf(terrain.ground[j * terrain.size + i] - terrain.ground[i * terrain.size + j]))
facts.terrain_mirror_error = worst
check(worst == 0.0, "terrain is the exact mirror image across x = z")
# ---------------------------------------------------------------- part 2: game camera
func key(code: Key) -> void:
var event := InputEventKey.new()
event.keycode = code
event.physical_keycode = code
event.pressed = true
get_viewport().push_input(event)
await get_tree().process_frame
var release := event.duplicate()
release.pressed = false
get_viewport().push_input(release)
await get_tree().process_frame
func wheel(up: bool) -> void:
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_WHEEL_UP if up else MOUSE_BUTTON_WHEEL_DOWN
event.pressed = true
event.position = Vector2(960, 560)
get_viewport().push_input(event)
await get_tree().process_frame
func settle(frames: int = 3) -> void:
for i: int in frames:
await get_tree().process_frame
func near_focus(game: Node3D, target: Vector2, tolerance: float = .6) -> bool:
return Vector2(game.camera.focus.x, game.camera.focus.z).distance_to(target) < tolerance
func on_screen(game: Node3D, world: Vector3) -> bool:
var camera: Camera3D = game.camera
if camera.is_position_behind(world):
return false
var screen: Vector2 = camera.unproject_position(world)
return get_viewport().get_visible_rect().has_point(screen)
func _game() -> void:
var main: Node = load("res://scenes/luneraria.tscn").instantiate()
add_child(main)
var menu: MainMenu = main.menu
while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")):
await get_tree().process_frame
menu.bund = "wurzelbund"
menu.lane = "mid"
main.start_match({"seed": 4})
await get_tree().create_timer(.8).timeout
var game: Node3D = main.game
var state: LuneMatch = game.match_state
var map: LuneMap = state.map
var team: int = int(state.players[game.human_slot].team)
check(game.camera.projection == Camera3D.PROJECTION_PERSPECTIVE, "the game camera is a perspective MOBA camera")
game.camera.reduced_motion = true
# Lane jumps F2/F3/F4 frame the own deployment side of each lane.
for index: int in range(3):
await key([KEY_F2, KEY_F3, KEY_F4][index])
var lane: String = LuneMap.LANES[index]
var target: Vector2 = map.lane_point(team, lane, map.lanes[lane].length * .2)
check(near_focus(game, target), "F%d centres the camera on %s" % [index + 2, lane])
check(on_screen(game, Vector3(target.x, game.world.height_at(target), target.y)), "%s is on screen after its hotkey" % lane)
await key(KEY_B)
check(near_focus(game, map.core_position(team)), "B centres the own Ancient")
check(on_screen(game, game.world.to3(map.core_position(team))), "own Ancient is on screen")
await key(KEY_G)
check(near_focus(game, map.grotto_center()), "G centres the boss grotto")
await key(KEY_H)
check(game.own_hero_unit() == null, "H without a hero reports that there is none")
# Hero jump after hiring one through the HUD command path.
state.players[game.human_slot].draft.gold = 600
game._on_command({"type": "hero_buy", "hero_id": "held_ringborke"})
await settle()
var hero: LuneUnit = game.own_hero_unit()
check(hero != null, "the hired hero appears in the planning formation")
if hero != null:
await key(KEY_H)
check(near_focus(game, hero.position), "H centres the selected hero")
# Overview: every building and the grotto are on screen at once.
await key(KEY_O)
await settle(4)
var all_visible: bool = true
for definition: Dictionary in map.buildings:
all_visible = all_visible and on_screen(game, game.world.to3(definition.position))
check(all_visible, "O shows all 16 towers and both Ancients")
check(on_screen(game, game.world.to3(map.grotto_center())), "O shows the boss grotto")
facts.overview = {"zoom": game.camera.zoom, "pitch_deg": rad_to_deg(LuneCameraPitch.of(game.camera)), "distance": game.camera.position.distance_to(game.camera.focus)}
# Rotation and reset.
await key(KEY_Q)
await settle()
check(is_equal_approx(game.camera.yaw, -PI * .25), "Q turns the camera by 45°")
await key(KEY_E)
await key(KEY_E)
check(is_equal_approx(game.camera.yaw, PI * .25), "E turns the camera back and further")
await key(KEY_R)
await settle()
var home: Vector2 = map.lane_point(team, "mid", map.lanes["mid"].length * .2)
check(is_zero_approx(game.camera.yaw) and near_focus(game, home) and is_equal_approx(game.camera.zoom, game.camera.DEFAULT_ZOOM), "R resets rotation, zoom and focus to the own lane")
# Zoom limits with the mouse wheel.
for i: int in 40:
await wheel(true)
check(is_equal_approx(game.camera.zoom, game.camera.MIN_ZOOM), "wheel zoom stops at the closest limit")
var near_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera))
for i: int in 60:
await wheel(false)
check(is_equal_approx(game.camera.zoom, game.camera.MAX_ZOOM), "wheel zoom stops at the widest limit")
var far_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera))
facts.pitch_range_deg = [near_pitch, far_pitch]
check(far_pitch < near_pitch - 10.0, "the view steepens when zooming out (%.0f° → %.0f°)" % [near_pitch, far_pitch])
var footprint: PackedVector2Array = game.camera.footprint()
check(footprint.size() == 4 and Geometry2D.is_point_in_polygon(Vector2(game.camera.focus.x, game.camera.focus.z), footprint), "the minimap outline contains the camera focus")
await key(KEY_R)
# Deployment areas: the own team's three pads in planning, highlighted where units stand.
var world: Node3D = game.world
var own_pads: int = 0
var other_pads: int = 0
for key_name: String in world._pads:
var pad: MeshInstance3D = world._pads[key_name]
if pad.visible:
if key_name.begins_with("%d:" % team):
own_pads += 1
else:
other_pads += 1
check(own_pads == 3 and other_pads == 0, "planning shows the own three deployment areas only")
var mid_pad: ShaderMaterial = (world._pads["%d:mid" % team] as MeshInstance3D).material_override
var top_pad: ShaderMaterial = (world._pads["%d:top" % team] as MeshInstance3D).material_override
check(float(mid_pad.get_shader_parameter("highlight")) > .5 and float(top_pad.get_shader_parameter("highlight")) < .5, "the lane that holds own units is highlighted")
# Combat: pads hide, figures stand on the ground, the lane view shows the own army.
game.hud._starter.pressed.emit()
await settle()
await key(KEY_SPACE)
await settle(2)
check(state.phase == LuneMatch.Phase.COMBAT, "Space starts the combat")
var hidden: bool = true
for key_name: String in world._pads:
hidden = hidden and not (world._pads[key_name] as MeshInstance3D).visible
check(hidden, "deployment areas hide during combat")
game.paused = true
for i: int in range(240):
state.step_combat()
await settle(3)
var grounded: bool = true
var worst: float = 0.0
for view: Node3D in game._views.values():
var expected: float = world.height_at(Vector2(view.position.x, view.position.z)) + .03
worst = maxf(worst, absf(view.position.y - expected))
grounded = grounded and absf(view.position.y - expected) < .05
facts.unit_ground_error = worst
check(grounded and game._views.size() > 20, "all %d figures stand on the terrain (max error %.3f m)" % [game._views.size(), worst])
await key(KEY_F3)
await settle(3)
var own_visible: int = 0
for unit: LuneUnit in state.combat.units:
if unit.alive and unit.team == team and unit.lane == "mid" and on_screen(game, game.world.to3(unit.position)):
own_visible += 1
check(own_visible > 0, "F3 in combat shows the own Mid army (%d on screen)" % own_visible)
# Real-time camera motion reaches its goal.
game.camera.reduced_motion = false
await key(KEY_B)
await get_tree().create_timer(1.2).timeout
check(near_focus(game, map.core_position(team), 1.0), "a smooth camera jump arrives within 1.2 s")
main.queue_free()
await settle()
class LuneCameraPitch:
static func of(camera: Camera3D) -> float:
var direction: Vector3 = (camera.focus - camera.position).normalized()
return asin(clampf(direction.y, -1.0, 1.0))

View file

@ -0,0 +1 @@
uid://c36slssmwrcrh

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tools/luneraria/verify_g1.gd" id="1"]
[node name="VerifyG1" type="Node"]
script = ExtResource("1")