diff --git a/docs/luneraria/ANLEITUNG.md b/docs/luneraria/ANLEITUNG.md index c0692fc..d3600c8 100644 --- a/docs/luneraria/ANLEITUNG.md +++ b/docs/luneraria/ANLEITUNG.md @@ -12,8 +12,8 @@ Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebra ## 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. -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. +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 (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×. 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. @@ -26,9 +26,15 @@ Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebra | Leertaste | Bereit / im Kampf Pause | | P | Pause | | 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 | -| 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 | | F1 | Optionen und Steuerung | | 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 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 . --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/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: godot --headless --path . res://tests/test_runner.tscn godot --headless --path . -- --smoke-test diff --git a/docs/luneraria/ARCHITEKTUR.md b/docs/luneraria/ARCHITEKTUR.md index 9117b26..4fd32b0 100644 --- a/docs/luneraria/ARCHITEKTUR.md +++ b/docs/luneraria/ARCHITEKTUR.md @@ -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. 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`. +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. diff --git a/docs/luneraria/G1-KARTE.md b/docs/luneraria/G1-KARTE.md new file mode 100644 index 0000000..9778ed9 --- /dev/null +++ b/docs/luneraria/G1-KARTE.md @@ -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. diff --git a/docs/luneraria/QA-MATRIX.md b/docs/luneraria/QA-MATRIX.md index 81a3724..6d5ba9b 100644 --- a/docs/luneraria/QA-MATRIX.md +++ b/docs/luneraria/QA-MATRIX.md @@ -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 | — | | 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) 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`. diff --git a/docs/luneraria/STATUS.md b/docs/luneraria/STATUS.md index 8dfc1ba..b7d2122 100644 --- a/docs/luneraria/STATUS.md +++ b/docs/luneraria/STATUS.md @@ -1,6 +1,6 @@ # 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 | | --- | --- | @@ -9,7 +9,8 @@ Stand: 28. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `l | M2 · Mechanikdurchlauf | **abgeschlossen** | | M3 · Qualität und Balance | **abgeschlossen** (siehe unten) | | 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 @@ -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. - Ö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 -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. diff --git a/docs/luneraria/logs/g1/balance-summary.txt b/docs/luneraria/logs/g1/balance-summary.txt new file mode 100644 index 0000000..6c5a001 --- /dev/null +++ b/docs/luneraria/logs/g1/balance-summary.txt @@ -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 diff --git a/docs/luneraria/logs/g1/balance.json b/docs/luneraria/logs/g1/balance.json new file mode 100644 index 0000000..c0f0b19 --- /dev/null +++ b/docs/luneraria/logs/g1/balance.json @@ -0,0 +1,1756 @@ +[ + { + "bunds": [ + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "wurzelbund" + ], + "index": 0, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 5, + 5, + 4, + 3, + 2, + 3 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 110.849999999996, + "result": "blau", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 743, + 745, + 895, + 762, + 958, + 923, + 1005, + 1115, + 1338, + 1570, + 2436, + 1501 + ] + }, + "profiles": [ + "ausgewogen", + "druck", + "teamhilfe", + "teamhilfe", + "verteidigung", + "ausgewogen" + ], + "seed": 1000, + "swapped": { + "boss": [], + "hero_levels": [ + 3, + 2, + 3, + 5, + 5, + 4 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 110.849999999996, + "result": "rot", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 743, + 745, + 895, + 762, + 958, + 923, + 1005, + 1115, + 1338, + 1570, + 2436, + 1501 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt" + ], + "index": 1, + "matchup": false, + "normal": { + "boss": [ + "9:won:0" + ], + "hero_levels": [ + 2, + 2, + 3, + 3, + 3, + 3 + ], + "lane_losses_at_end": [ + 1, + 0 + ], + "max_combat": 100.049999999996, + "result": "rot", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 843, + 750, + 970, + 812, + 1018, + 1143, + 1125, + 2044, + 1020 + ] + }, + "profiles": [ + "bossjagd", + "bossjagd", + "teamhilfe", + "fernkampf", + "teamhilfe", + "fernkampf" + ], + "seed": 1001, + "swapped": { + "boss": [ + "9:won:1" + ], + "hero_levels": [ + 3, + 3, + 3, + 2, + 2, + 3 + ], + "lane_losses_at_end": [ + 0, + 1 + ], + "max_combat": 100.049999999996, + "result": "blau", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 843, + 750, + 970, + 812, + 1018, + 1143, + 1125, + 2044, + 1020 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 2, + "matchup": false, + "normal": { + "boss": [ + "10:won:0", + "11:deployed:0" + ], + "hero_levels": [ + 3, + 0, + 2, + 2, + 3, + 3 + ], + "lane_losses_at_end": [ + 3, + 1 + ], + "max_combat": 107.549999999996, + "result": "rot", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 753, + 780, + 920, + 892, + 968, + 963, + 1095, + 1390, + 1492, + 2324, + 2280 + ] + }, + "profiles": [ + "bossjagd", + "ausgewogen", + "teamhilfe", + "druck", + "ausgewogen", + "verteidigung" + ], + "seed": 1002, + "swapped": { + "boss": [ + "10:won:1", + "11:deployed:1" + ], + "hero_levels": [ + 2, + 3, + 3, + 3, + 0, + 2 + ], + "lane_losses_at_end": [ + 1, + 3 + ], + "max_combat": 107.549999999996, + "result": "blau", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 753, + 780, + 920, + 892, + 968, + 963, + 1095, + 1390, + 1492, + 2324, + 2280 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "wurzelbund" + ], + "index": 3, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 7, + 5, + 4, + 4, + 6, + 3 + ], + "lane_losses_at_end": [ + 1, + 2 + ], + "max_combat": 180.000000000007, + "result": "blau", + "rounds": 12, + "timeouts": 1, + "total_gold_curve": [ + 838, + 695, + 865, + 907, + 953, + 963, + 1140, + 1420, + 1496, + 2068, + 2361, + 702 + ] + }, + "profiles": [ + "fernkampf", + "druck", + "ausgewogen", + "druck", + "fernkampf", + "verteidigung" + ], + "seed": 1003, + "swapped": { + "boss": [], + "hero_levels": [ + 4, + 6, + 3, + 7, + 5, + 4 + ], + "lane_losses_at_end": [ + 2, + 1 + ], + "max_combat": 180.000000000007, + "result": "rot", + "rounds": 12, + "timeouts": 1, + "total_gold_curve": [ + 838, + 695, + 865, + 907, + 953, + 963, + 1140, + 1420, + 1496, + 2068, + 2361, + 702 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt", + "glutpakt" + ], + "index": 4, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 5, + 4, + 4, + 3, + 6, + 6 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 101.099999999996, + "result": "blau", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 848, + 725, + 975, + 827, + 978, + 1108, + 1315, + 1511, + 1722, + 1604, + 1447 + ] + }, + "profiles": [ + "ausgewogen", + "teamhilfe", + "fernkampf", + "ausgewogen", + "verteidigung", + "fernkampf" + ], + "seed": 1004, + "swapped": { + "boss": [], + "hero_levels": [ + 3, + 6, + 6, + 5, + 4, + 4 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 101.099999999996, + "result": "rot", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 848, + 725, + 975, + 827, + 978, + 1108, + 1315, + 1511, + 1722, + 1604, + 1447 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "glutpakt" + ], + "index": 5, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 6, + 6, + 5, + 0, + 0, + 0 + ], + "lane_losses_at_end": [ + 0, + 3 + ], + "max_combat": 113.349999999996, + "result": "blau", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 853, + 785, + 985, + 837, + 1008, + 1028, + 1115, + 1480, + 1823, + 1720, + 1615 + ] + }, + "profiles": [ + "teamhilfe", + "fernkampf", + "verteidigung", + "fernkampf", + "bossjagd", + "bossjagd" + ], + "seed": 1005, + "swapped": { + "boss": [], + "hero_levels": [ + 0, + 0, + 0, + 6, + 6, + 5 + ], + "lane_losses_at_end": [ + 3, + 0 + ], + "max_combat": 113.349999999996, + "result": "rot", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 853, + 785, + 985, + 837, + 1008, + 1028, + 1115, + 1480, + 1823, + 1720, + 1615 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "glutpakt" + ], + "index": 6, + "matchup": false, + "normal": { + "boss": [ + "8:won:0", + "9:deployed:0" + ], + "hero_levels": [ + 3, + 5, + 4, + 3, + 2, + 3 + ], + "lane_losses_at_end": [ + 2, + 1 + ], + "max_combat": 104.399999999996, + "result": "blau", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 803, + 755, + 980, + 842, + 1098, + 958, + 1464, + 1969, + 1301 + ] + }, + "profiles": [ + "bossjagd", + "ausgewogen", + "teamhilfe", + "fernkampf", + "teamhilfe", + "ausgewogen" + ], + "seed": 1006, + "swapped": { + "boss": [ + "8:won:1", + "9:deployed:1" + ], + "hero_levels": [ + 3, + 2, + 3, + 3, + 5, + 4 + ], + "lane_losses_at_end": [ + 1, + 2 + ], + "max_combat": 104.399999999996, + "result": "rot", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 803, + 755, + 980, + 842, + 1098, + 958, + 1464, + 1969, + 1301 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 7, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 4, + 7, + 4, + 0, + 2, + 0 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 95.2999999999967, + "result": "blau", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 740, + 965, + 837, + 1028, + 878, + 1342, + 1427, + 1429, + 1411, + 1499, + 1816 + ] + }, + "profiles": [ + "ausgewogen", + "ausgewogen", + "ausgewogen", + "teamhilfe", + "teamhilfe", + "ausgewogen" + ], + "seed": 1007, + "swapped": { + "boss": [], + "hero_levels": [ + 0, + 2, + 0, + 4, + 7, + 4 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 95.2999999999967, + "result": "rot", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 740, + 965, + 837, + 1028, + 878, + 1342, + 1427, + 1429, + 1411, + 1499, + 1816 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt", + "glutpakt" + ], + "index": 8, + "matchup": false, + "normal": { + "boss": [ + "12:won:0" + ], + "hero_levels": [ + 3, + 2, + 3, + 3, + 5, + 4 + ], + "lane_losses_at_end": [ + 2, + 0 + ], + "max_combat": 110.699999999996, + "result": "rot", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 755, + 875, + 807, + 958, + 1073, + 1100, + 1050, + 1303, + 1310, + 2356, + 1749 + ] + }, + "profiles": [ + "teamhilfe", + "ausgewogen", + "bossjagd", + "druck", + "ausgewogen", + "druck" + ], + "seed": 1008, + "swapped": { + "boss": [ + "12:won:1" + ], + "hero_levels": [ + 3, + 5, + 4, + 3, + 2, + 3 + ], + "lane_losses_at_end": [ + 0, + 2 + ], + "max_combat": 110.699999999996, + "result": "blau", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 755, + 875, + 807, + 958, + 1073, + 1100, + 1050, + 1303, + 1310, + 2356, + 1749 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "glutpakt", + "glutpakt", + "wurzelbund", + "glutpakt" + ], + "index": 9, + "matchup": false, + "normal": { + "boss": [ + "11:won:1" + ], + "hero_levels": [ + 4, + 0, + 3, + 3, + 5, + 3 + ], + "lane_losses_at_end": [ + 2, + 1 + ], + "max_combat": 155.500000000001, + "result": "blau", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 758, + 790, + 895, + 882, + 988, + 1028, + 1120, + 1235, + 1392, + 2109, + 1733 + ] + }, + "profiles": [ + "verteidigung", + "druck", + "druck", + "bossjagd", + "ausgewogen", + "teamhilfe" + ], + "seed": 1009, + "swapped": { + "boss": [ + "11:won:0" + ], + "hero_levels": [ + 3, + 5, + 3, + 4, + 0, + 3 + ], + "lane_losses_at_end": [ + 1, + 2 + ], + "max_combat": 155.500000000001, + "result": "rot", + "rounds": 11, + "timeouts": 0, + "total_gold_curve": [ + 758, + 790, + 895, + 882, + 988, + 1028, + 1120, + 1235, + 1392, + 2109, + 1733 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 10, + "matchup": false, + "normal": { + "boss": [ + "12:won:1" + ], + "hero_levels": [ + 5, + 5, + 4, + 3, + 3, + 0 + ], + "lane_losses_at_end": [ + 0, + 2 + ], + "max_combat": 90.599999999997, + "result": "blau", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 755, + 915, + 832, + 983, + 968, + 1040, + 1020, + 1148, + 1530, + 1788, + 1861 + ] + }, + "profiles": [ + "druck", + "ausgewogen", + "ausgewogen", + "teamhilfe", + "bossjagd", + "ausgewogen" + ], + "seed": 1010, + "swapped": { + "boss": [ + "12:won:0" + ], + "hero_levels": [ + 3, + 3, + 0, + 5, + 5, + 4 + ], + "lane_losses_at_end": [ + 2, + 0 + ], + "max_combat": 90.599999999997, + "result": "rot", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 753, + 755, + 915, + 832, + 983, + 968, + 1040, + 1020, + 1148, + 1530, + 1788, + 1861 + ] + } + }, + { + "bunds": [ + "glutpakt", + "glutpakt", + "glutpakt", + "glutpakt", + "glutpakt", + "wurzelbund" + ], + "index": 11, + "matchup": false, + "normal": { + "boss": [ + "9:won:0", + "10:deployed:0" + ], + "hero_levels": [ + 2, + 5, + 5, + 4, + 3, + 2 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 102.549999999996, + "result": "rot", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 853, + 770, + 1005, + 837, + 1028, + 1153, + 1275, + 1614, + 2209, + 1415 + ] + }, + "profiles": [ + "bossjagd", + "fernkampf", + "fernkampf", + "teamhilfe", + "verteidigung", + "teamhilfe" + ], + "seed": 1011, + "swapped": { + "boss": [ + "9:won:1", + "10:deployed:1" + ], + "hero_levels": [ + 4, + 3, + 2, + 2, + 5, + 5 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 102.549999999996, + "result": "blau", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 853, + 770, + 1005, + 837, + 1028, + 1153, + 1275, + 1614, + 2209, + 1415 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "glutpakt", + "glutpakt", + "glutpakt", + "glutpakt", + "glutpakt" + ], + "index": 12, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 7, + 6, + 6, + 8, + 6, + 8 + ], + "lane_losses_at_end": [ + 3, + 1 + ], + "max_combat": 104.349999999996, + "result": "rot", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 903, + 710, + 1020, + 852, + 933, + 1178, + 1230, + 1734, + 1640, + 1362, + 2161, + 2038 + ] + }, + "profiles": [ + "fernkampf", + "fernkampf", + "teamhilfe", + "teamhilfe", + "ausgewogen", + "fernkampf" + ], + "seed": 1012, + "swapped": { + "boss": [], + "hero_levels": [ + 8, + 6, + 8, + 7, + 6, + 6 + ], + "lane_losses_at_end": [ + 1, + 3 + ], + "max_combat": 104.349999999996, + "result": "blau", + "rounds": 12, + "timeouts": 0, + "total_gold_curve": [ + 903, + 710, + 1020, + 852, + 933, + 1178, + 1230, + 1734, + 1640, + 1362, + 2161, + 2038 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "wurzelbund", + "wurzelbund", + "glutpakt", + "wurzelbund", + "glutpakt" + ], + "index": 13, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 6, + 7, + 4, + 6, + 7, + 6 + ], + "lane_losses_at_end": [ + 2, + 0 + ], + "max_combat": 105.299999999996, + "result": "rot", + "rounds": 13, + "timeouts": 0, + "total_gold_curve": [ + 843, + 690, + 890, + 932, + 923, + 1003, + 1165, + 1165, + 1546, + 1918, + 1982, + 2013, + 1448 + ] + }, + "profiles": [ + "teamhilfe", + "druck", + "fernkampf", + "fernkampf", + "druck", + "ausgewogen" + ], + "seed": 1013, + "swapped": { + "boss": [], + "hero_levels": [ + 6, + 7, + 6, + 6, + 7, + 4 + ], + "lane_losses_at_end": [ + 0, + 2 + ], + "max_combat": 105.299999999996, + "result": "blau", + "rounds": 13, + "timeouts": 0, + "total_gold_curve": [ + 843, + 690, + 890, + 932, + 923, + 1003, + 1165, + 1165, + 1546, + 1918, + 1982, + 2013, + 1448 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "glutpakt", + "wurzelbund", + "glutpakt", + "glutpakt", + "wurzelbund" + ], + "index": 14, + "matchup": false, + "normal": { + "boss": [ + "9:won:0", + "10:deployed:0" + ], + "hero_levels": [ + 3, + 0, + 0, + 2, + 3, + 2 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 119.799999999995, + "result": "blau", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 803, + 810, + 920, + 847, + 1063, + 1098, + 1150, + 1300, + 1573, + 1404 + ] + }, + "profiles": [ + "bossjagd", + "druck", + "fernkampf", + "bossjagd", + "bossjagd", + "bossjagd" + ], + "seed": 1014, + "swapped": { + "boss": [ + "9:won:1", + "10:deployed:1" + ], + "hero_levels": [ + 2, + 3, + 2, + 3, + 0, + 0 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 119.799999999995, + "result": "rot", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 803, + 810, + 920, + 847, + 1063, + 1098, + 1150, + 1300, + 1573, + 1404 + ] + } + }, + { + "bunds": [ + "glutpakt", + "wurzelbund", + "glutpakt", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 15, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 7, + 5, + 4, + 4, + 4, + 4 + ], + "lane_losses_at_end": [ + 0, + 1 + ], + "max_combat": 180.000000000007, + "result": "blau", + "rounds": 12, + "timeouts": 1, + "total_gold_curve": [ + 898, + 705, + 950, + 902, + 988, + 1023, + 1140, + 1335, + 1367, + 2474, + 1817, + 887 + ] + }, + "profiles": [ + "fernkampf", + "fernkampf", + "verteidigung", + "druck", + "fernkampf", + "ausgewogen" + ], + "seed": 1015, + "swapped": { + "boss": [], + "hero_levels": [ + 4, + 4, + 4, + 7, + 5, + 4 + ], + "lane_losses_at_end": [ + 1, + 0 + ], + "max_combat": 180.000000000007, + "result": "rot", + "rounds": 12, + "timeouts": 1, + "total_gold_curve": [ + 898, + 705, + 950, + 902, + 988, + 1023, + 1140, + 1335, + 1367, + 2474, + 1817, + 887 + ] + } + }, + { + "bunds": [ + "glutpakt", + "glutpakt", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 16, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 5, + 3, + 0, + 6, + 3, + 6 + ], + "lane_losses_at_end": [ + 2, + 1 + ], + "max_combat": 131.749999999996, + "result": "rot", + "rounds": 13, + "timeouts": 0, + "total_gold_curve": [ + 758, + 825, + 980, + 812, + 928, + 978, + 1060, + 1100, + 1198, + 1545, + 1990, + 1766, + 2066 + ] + }, + "profiles": [ + "teamhilfe", + "verteidigung", + "verteidigung", + "teamhilfe", + "verteidigung", + "ausgewogen" + ], + "seed": 1016, + "swapped": { + "boss": [], + "hero_levels": [ + 6, + 3, + 6, + 5, + 3, + 0 + ], + "lane_losses_at_end": [ + 1, + 2 + ], + "max_combat": 131.749999999996, + "result": "blau", + "rounds": 13, + "timeouts": 0, + "total_gold_curve": [ + 758, + 825, + 980, + 812, + 928, + 978, + 1060, + 1100, + 1198, + 1545, + 1990, + 1766, + 2066 + ] + } + }, + { + "bunds": [ + "glutpakt", + "glutpakt", + "glutpakt", + "wurzelbund", + "glutpakt", + "wurzelbund" + ], + "index": 17, + "matchup": false, + "normal": { + "boss": [], + "hero_levels": [ + 5, + 3, + 4, + 0, + 0, + 0 + ], + "lane_losses_at_end": [ + 0, + 2 + ], + "max_combat": 121.599999999995, + "result": "blau", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 893, + 720, + 940, + 882, + 1033, + 1053, + 1170, + 1495, + 1617, + 1533 + ] + }, + "profiles": [ + "fernkampf", + "verteidigung", + "fernkampf", + "druck", + "fernkampf", + "bossjagd" + ], + "seed": 1017, + "swapped": { + "boss": [], + "hero_levels": [ + 0, + 0, + 0, + 5, + 3, + 4 + ], + "lane_losses_at_end": [ + 2, + 0 + ], + "max_combat": 121.599999999995, + "result": "rot", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 893, + 720, + 940, + 882, + 1033, + 1053, + 1170, + 1495, + 1617, + 1533 + ] + } + }, + { + "bunds": [ + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund", + "glutpakt", + "glutpakt" + ], + "index": 18, + "matchup": false, + "normal": { + "boss": [ + "8:won:0", + "9:deployed:0" + ], + "hero_levels": [ + 2, + 3, + 4, + 2, + 2, + 5 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 102.349999999996, + "result": "blau", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 753, + 780, + 880, + 867, + 898, + 1223, + 1215, + 1701, + 1564 + ] + }, + "profiles": [ + "verteidigung", + "ausgewogen", + "bossjagd", + "druck", + "teamhilfe", + "druck" + ], + "seed": 1018, + "swapped": { + "boss": [ + "8:won:1", + "9:deployed:1" + ], + "hero_levels": [ + 2, + 2, + 5, + 2, + 3, + 4 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 102.349999999996, + "result": "rot", + "rounds": 9, + "timeouts": 0, + "total_gold_curve": [ + 753, + 780, + 880, + 867, + 898, + 1223, + 1215, + 1701, + 1564 + ] + } + }, + { + "bunds": [ + "glutpakt", + "glutpakt", + "wurzelbund", + "glutpakt", + "wurzelbund", + "wurzelbund" + ], + "index": 19, + "matchup": false, + "normal": { + "boss": [ + "9:won:1", + "10:deployed:1" + ], + "hero_levels": [ + 7, + 3, + 4, + 4, + 5, + 3 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 127.249999999995, + "result": "rot", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 798, + 770, + 930, + 787, + 958, + 1348, + 1080, + 1910, + 1473, + 1384 + ] + }, + "profiles": [ + "druck", + "teamhilfe", + "teamhilfe", + "fernkampf", + "verteidigung", + "bossjagd" + ], + "seed": 1019, + "swapped": { + "boss": [ + "9:won:0", + "10:deployed:0" + ], + "hero_levels": [ + 4, + 5, + 3, + 7, + 3, + 4 + ], + "lane_losses_at_end": [ + 1, + 1 + ], + "max_combat": 127.249999999995, + "result": "blau", + "rounds": 10, + "timeouts": 0, + "total_gold_curve": [ + 798, + 770, + 930, + 787, + 958, + 1348, + 1080, + 1910, + 1473, + 1384 + ] + } + } +] \ No newline at end of file diff --git a/docs/luneraria/logs/g1/balance.log b/docs/luneraria/logs/g1/balance.log new file mode 100644 index 0000000..559a846 --- /dev/null +++ b/docs/luneraria/logs/g1/balance.log @@ -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) diff --git a/docs/luneraria/logs/g1/perf-nachher-1920x1080.json b/docs/luneraria/logs/g1/perf-nachher-1920x1080.json new file mode 100644 index 0000000..4496762 --- /dev/null +++ b/docs/luneraria/logs/g1/perf-nachher-1920x1080.json @@ -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)" +} \ No newline at end of file diff --git a/docs/luneraria/logs/g1/perf-vorher-1920x1080.json b/docs/luneraria/logs/g1/perf-vorher-1920x1080.json new file mode 100644 index 0000000..03a6524 --- /dev/null +++ b/docs/luneraria/logs/g1/perf-vorher-1920x1080.json @@ -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)" +} \ No newline at end of file diff --git a/docs/luneraria/logs/g1/verify-g1.json b/docs/luneraria/logs/g1/verify-g1.json new file mode 100644 index 0000000..9b2ca7d --- /dev/null +++ b/docs/luneraria/logs/g1/verify-g1.json @@ -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": [] +} \ No newline at end of file diff --git a/docs/luneraria/logs/m1-acceptance.json b/docs/luneraria/logs/m1-acceptance.json index b4eac04..6149b60 100644 --- a/docs/luneraria/logs/m1-acceptance.json +++ b/docs/luneraria/logs/m1-acceptance.json @@ -2,9 +2,9 @@ "checks": 38, "failures": [], "object_counts": [ - 3408.0, - 3404.0, - 3404.0, - 3406.0 + 3620.0, + 3620.0, + 3620.0, + 3620.0 ] } \ No newline at end of file diff --git a/docs/luneraria/logs/m4/net-rounds-gast-a.log b/docs/luneraria/logs/m4/net-rounds-gast-a.log index 0d2096e..3ef25b9 100644 --- a/docs/luneraria/logs/m4/net-rounds-gast-a.log +++ b/docs/luneraria/logs/m4/net-rounds-gast-a.log @@ -7,7 +7,7 @@ NET_SPOOF REJECTED NET_PLAN role=client round=1 seat=1 NET_MSG OK Korkrücken gekauft. 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_MSG OK Samenschleuder gekauft. NET_CLIENT_DROP round=2 seat=1 diff --git a/docs/luneraria/logs/m4/net-rounds-gast-b.log b/docs/luneraria/logs/m4/net-rounds-gast-b.log index 46d19de..773c2c5 100644 --- a/docs/luneraria/logs/m4/net-rounds-gast-b.log +++ b/docs/luneraria/logs/m4/net-rounds-gast-b.log @@ -4,18 +4,18 @@ NET_JOIN OK NET_MSG OK Mit dem Host verbunden. NET_SETUP seat=1 round=1 phase=0 NET_SPOOF REJECTED -NET_ROUND role=client round=2 phase=0 sum=612150297 -NET_CLIENT_COMBAT round=2 frames=8 local_steps=0 +NET_ROUND role=client round=2 phase=0 sum=3342070386 +NET_CLIENT_COMBAT round=2 frames=10 local_steps=0 NET_PLAN role=client round=3 seat=1 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_PLAN role=client round=4 seat=1 NET_MSG OK Tauhüter gekauft. -NET_ROUND role=client round=4 phase=0 sum=2202250974 -NET_CLIENT_COMBAT round=4 frames=12 local_steps=0 +NET_ROUND role=client round=4 phase=0 sum=346458639 +NET_CLIENT_COMBAT round=4 frames=42 local_steps=0 NET_PLAN role=client round=5 seat=1 NET_MSG OK Dornhopser gekauft. -NET_ROUND role=client round=5 phase=0 sum=1883482295 -NET_CLIENT_COMBAT round=5 frames=49 local_steps=0 +NET_ROUND role=client round=5 phase=0 sum=254645978 +NET_CLIENT_COMBAT round=5 frames=64 local_steps=0 NET_CLIENT_DONE round=6 seat=1 diff --git a/docs/luneraria/logs/m4/net-rounds-host.log b/docs/luneraria/logs/m4/net-rounds-host.log index f2fc1c7..68d46fd 100644 --- a/docs/luneraria/logs/m4/net-rounds-host.log +++ b/docs/luneraria/logs/m4/net-rounds-host.log @@ -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) GDScript backtrace (most recent call first): [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_PLAN role=host round=2 seat=0 NET_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen. 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_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_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_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_MSG OK Gast hat die Verbindung getrennt – Slot wird vom Bot übernommen. diff --git a/docs/luneraria/logs/m4/net-rounds.json b/docs/luneraria/logs/m4/net-rounds.json index c34aa12..76a86b9 100644 --- a/docs/luneraria/logs/m4/net-rounds.json +++ b/docs/luneraria/logs/m4/net-rounds.json @@ -11,27 +11,27 @@ "mismatches": [], "host_sums": { "1": "1392179697", - "2": "612150297", - "3": "87981900", - "4": "2202250974", - "5": "1883482295" + "2": "3342070386", + "3": "711640101", + "4": "346458639", + "5": "254645978" }, "guest_sums": { "1": "1392179697", - "2": "612150297", - "3": "87981900", - "4": "2202250974", - "5": "1883482295" + "2": "3342070386", + "3": "711640101", + "4": "346458639", + "5": "254645978" }, "client_combats": [ { "round": 1, - "frames": 32, + "frames": 17, "local_steps": 0 }, { "round": 2, - "frames": 8, + "frames": 10, "local_steps": 0 }, { @@ -41,12 +41,12 @@ }, { "round": 4, - "frames": 12, + "frames": 42, "local_steps": 0 }, { "round": 5, - "frames": 49, + "frames": 64, "local_steps": 0 } ], diff --git a/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png b/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png new file mode 100644 index 0000000..1482906 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png.import b/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png.import new file mode 100644 index 0000000..99ddf48 --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/00-kartenplan-draufsicht.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/01-karte.png b/docs/luneraria/screenshots/g1/nachher/01-karte.png new file mode 100644 index 0000000..e4cf267 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/01-karte.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/01-karte.png.import b/docs/luneraria/screenshots/g1/nachher/01-karte.png.import new file mode 100644 index 0000000..b4d5e3d --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/01-karte.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/02-top-lane.png b/docs/luneraria/screenshots/g1/nachher/02-top-lane.png new file mode 100644 index 0000000..48141b2 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/02-top-lane.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/02-top-lane.png.import b/docs/luneraria/screenshots/g1/nachher/02-top-lane.png.import new file mode 100644 index 0000000..e31f79b --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/02-top-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png b/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png new file mode 100644 index 0000000..334ca9d Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png.import b/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png.import new file mode 100644 index 0000000..3b767ab --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/03-mid-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png b/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png new file mode 100644 index 0000000..2eb1a1c Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png.import b/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png.import new file mode 100644 index 0000000..3acb80a --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png b/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png new file mode 100644 index 0000000..e849cfa Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png.import b/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png.import new file mode 100644 index 0000000..6aa3001 --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png b/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png new file mode 100644 index 0000000..7148d59 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png.import b/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png.import new file mode 100644 index 0000000..9a17f1a --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png b/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png new file mode 100644 index 0000000..562d6aa Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png.import b/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png.import new file mode 100644 index 0000000..41f540d --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png b/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png new file mode 100644 index 0000000..37e7895 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png.import b/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png.import new file mode 100644 index 0000000..3f01e0e --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/08-kampf-viele-einheiten.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png b/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png new file mode 100644 index 0000000..4f581b5 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png.import b/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png.import new file mode 100644 index 0000000..99c9ebf --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/09-hud-planung.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png b/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png new file mode 100644 index 0000000..0975c68 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png.import b/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png.import new file mode 100644 index 0000000..397db75 --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png b/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png new file mode 100644 index 0000000..43a2329 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png.import b/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png.import new file mode 100644 index 0000000..c39fe22 --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png b/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png new file mode 100644 index 0000000..608e267 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png.import b/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png.import new file mode 100644 index 0000000..936cff1 --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png b/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png new file mode 100644 index 0000000..b15f5d8 Binary files /dev/null and b/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png differ diff --git a/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png.import b/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png.import new file mode 100644 index 0000000..43cd71c --- /dev/null +++ b/docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/01-karte.png b/docs/luneraria/screenshots/g1/vorher/01-karte.png new file mode 100644 index 0000000..070cbf5 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/01-karte.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/01-karte.png.import b/docs/luneraria/screenshots/g1/vorher/01-karte.png.import new file mode 100644 index 0000000..6b61899 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/01-karte.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/02-top-lane.png b/docs/luneraria/screenshots/g1/vorher/02-top-lane.png new file mode 100644 index 0000000..31ca27d Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/02-top-lane.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/02-top-lane.png.import b/docs/luneraria/screenshots/g1/vorher/02-top-lane.png.import new file mode 100644 index 0000000..3997b73 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/02-top-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png b/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png new file mode 100644 index 0000000..c0884c8 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png.import b/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png.import new file mode 100644 index 0000000..0b6c2e4 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/03-mid-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png b/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png new file mode 100644 index 0000000..3d97e74 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png.import b/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png.import new file mode 100644 index 0000000..f02bd8b --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png b/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png new file mode 100644 index 0000000..0d6ecce Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png.import b/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png.import new file mode 100644 index 0000000..069da55 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png b/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png new file mode 100644 index 0000000..7ffb1db Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png.import b/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png.import new file mode 100644 index 0000000..cb4939c --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png b/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png new file mode 100644 index 0000000..0ef15c8 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png.import b/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png.import new file mode 100644 index 0000000..ca943db --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png b/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png new file mode 100644 index 0000000..355b28b Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png.import b/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png.import new file mode 100644 index 0000000..bedf2ac --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/08-kampf-viele-einheiten.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png b/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png new file mode 100644 index 0000000..a445b64 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png.import b/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png.import new file mode 100644 index 0000000..2913ade --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/09-hud-planung.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png b/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png new file mode 100644 index 0000000..7d1d91f Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png.import b/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png.import new file mode 100644 index 0000000..6c5a6bc --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png b/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png new file mode 100644 index 0000000..76dbb81 Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png.import b/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png.import new file mode 100644 index 0000000..ae8d139 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png b/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png new file mode 100644 index 0000000..84e870d Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png.import b/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png.import new file mode 100644 index 0000000..0db5bfe --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png.import @@ -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 diff --git a/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png b/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png new file mode 100644 index 0000000..adf672f Binary files /dev/null and b/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png differ diff --git a/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png.import b/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png.import new file mode 100644 index 0000000..2934ae9 --- /dev/null +++ b/docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png.import @@ -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 diff --git a/docs/luneraria/screenshots/m4/net-guest-combat-r1.png.import b/docs/luneraria/screenshots/m4/net-guest-combat-r1.png.import new file mode 100644 index 0000000..600d949 --- /dev/null +++ b/docs/luneraria/screenshots/m4/net-guest-combat-r1.png.import @@ -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 diff --git a/docs/luneraria/screenshots/m4/net-guest-combat-r2.png.import b/docs/luneraria/screenshots/m4/net-guest-combat-r2.png.import new file mode 100644 index 0000000..42f8ef5 --- /dev/null +++ b/docs/luneraria/screenshots/m4/net-guest-combat-r2.png.import @@ -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 diff --git a/docs/luneraria/screenshots/m4/net-guest-planning-r1.png.import b/docs/luneraria/screenshots/m4/net-guest-planning-r1.png.import new file mode 100644 index 0000000..8dd8428 --- /dev/null +++ b/docs/luneraria/screenshots/m4/net-guest-planning-r1.png.import @@ -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 diff --git a/docs/luneraria/screenshots/m4/net-guest-planning-r2.png.import b/docs/luneraria/screenshots/m4/net-guest-planning-r2.png.import new file mode 100644 index 0000000..f49bf92 --- /dev/null +++ b/docs/luneraria/screenshots/m4/net-guest-planning-r2.png.import @@ -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 diff --git a/scripts/luneraria/core/lune_map.gd b/scripts/luneraria/core/lune_map.gd index fe4068e..419c95d 100644 --- a/scripts/luneraria/core/lune_map.gd +++ b/scripts/luneraria/core/lune_map.gd @@ -2,19 +2,29 @@ class_name LuneMap extends RefCounted ## 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 -## (mirror(v) = Vector2(v.y, v.x)), Bottom is Top mirrored across the Mid line. -## Both hold by construction, so strecken, towers and spawns are functionally equal. +## (mirror(v) = Vector2(v.y, v.x)); every lane is built from its Blau half and that +## 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 LANE_NAMES: Dictionary = {"top": "Top", "mid": "Mid", "bot": "Bottom"} -# First halves from the Blau end to the centre line; the Rot half is mirrored. -const TOP_HALF: Array[Vector2] = [Vector2(-24, 22), Vector2(-26, 6), Vector2(-26.5, -12), Vector2(-25, -25)] -const MID_HALF: Array[Vector2] = [Vector2(-21, 21), Vector2(-11, 10), Vector2(0, 0)] +## Catmull-Rom control points of each Blau half, from the Blau base to the symmetry axis. +## The last point lies on x = z; the curve is sampled every ~2 m into the lane polyline. +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 BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-28.8, 24.6), Vector2(-24.6, 28.8)] -## Neutral boss grotto, north-west of Mid and on the symmetry axis (M2). -const GROTTO := Vector2(-11.5, -11.5) +## Two base towers in front of each Ancient, 6 m out and 40° either side of the Mid direction. +const BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-29.477, 24.023), Vector2(-24.023, 29.477)] +## 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 +## 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 lanes: Dictionary = {} @@ -33,31 +43,36 @@ class LanePath: length += points[i].distance_to(points[i - 1]) 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: var d: float = clampf(s, 0.0, length) - for i: int in range(1, points.size()): - if d <= cumulative[i] or i == points.size() - 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[points.size() - 1] + var i: int = _segment(d) + 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)) func tangent_at(s: float) -> Vector2: - var d: float = 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 Vector2.RIGHT + var i: int = _segment(clampf(s, 0.0, length)) + return (points[i] - points[i - 1]).normalized() ## Point and unit tangent at s in one pass. func sample(s: float) -> Array: var d: float = clampf(s, 0.0, length) - for i: int in range(1, points.size()): - if d <= cumulative[i] or i == points.size() - 1: - var a: Vector2 = points[i - 1] - var b: Vector2 = points[i] - 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 [points[points.size() - 1], Vector2.RIGHT] + var i: int = _segment(d) + var a: Vector2 = points[i - 1] + var b: Vector2 = points[i] + 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] func project(position: Vector2) -> float: var best: float = INF @@ -85,14 +100,50 @@ static func full_lane(half: Array[Vector2]) -> Array[Vector2]: result.append(mirror(half[i])) 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: balance = config if config != null else LuneBalance.default_balance() - var bot_half: Array[Vector2] = [] - for point: Vector2 in TOP_HALF: - bot_half.append(mirror_mid(point)) - lanes["top"] = LanePath.new(full_lane(TOP_HALF)) - lanes["mid"] = LanePath.new(full_lane(MID_HALF)) - lanes["bot"] = LanePath.new(full_lane(bot_half)) + lanes["top"] = LanePath.new(full_lane(sample_half(TOP_CONTROL))) + lanes["mid"] = LanePath.new(full_lane(sample_half(MID_CONTROL))) + lanes["bot"] = LanePath.new(solve_bottom(lanes["top"].length)) _build_buildings() ## 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) ## 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: var blue: Vector2 = GROTTO + Vector2(1.2, 4.6) return blue if team == 0 else mirror(blue) @@ -125,6 +177,14 @@ func grotto_entry(team: int) -> Vector2: func grotto_boss_point() -> Vector2: 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: return CORE_BLUE if team == 0 else mirror(CORE_BLUE) diff --git a/scripts/luneraria/lune_game.gd b/scripts/luneraria/lune_game.gd index 283e138..9b67239 100644 --- a/scripts/luneraria/lune_game.gd +++ b/scripts/luneraria/lune_game.gd @@ -48,6 +48,7 @@ func start(setup_options: Dictionary) -> void: add_child(_units_root) camera = CameraRig.new() camera.reduced_motion = Preferences.reduced_motion + camera.ground = func(p: Vector2) -> float: return world.height_at(p) add_child(camera) var layer := CanvasLayer.new() add_child(layer) @@ -85,14 +86,49 @@ func _is_client() -> bool: func _send(command: Dictionary) -> Dictionary: 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: - var team: int = int(match_state.players[maxi(0, human_slot)].team) 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 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) +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: hud.notify(str(result.message), not bool(result.ok)) Soundscape.play("place" if result.ok else "error") @@ -137,6 +173,7 @@ func _enter_planning() -> void: func _enter_combat() -> void: _clear_views() _preview = null + world.show_deployment(false, _own_team(), {}, false) _bound_combat = match_state.combat _accumulator = 0.0 Soundscape.play("wave") @@ -148,6 +185,7 @@ func _clear_views() -> void: ## Shows the human player's current draft army in its formation during planning. func _rebuild_preview() -> void: + _update_deployment() if match_state.phase != LuneMatch.Phase.PLANNING or human_slot < 0: return _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]) _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: var view: Node3D = UnitView.new() + view.terrain = world.terrain _units_root.add_child(view) view.bind(unit) _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) hud.minimap.camera_focus = camera.focus 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. func _track_phase() -> void: @@ -276,6 +329,22 @@ func _input(event: InputEvent) -> void: KEY_TAB: _lane_cycle = (_lane_cycle + 1) % 3 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: camera.overview() KEY_1, KEY_2, KEY_3, KEY_4, KEY_5: diff --git a/scripts/luneraria/ui/lune_hud.gd b/scripts/luneraria/ui/lune_hud.gd index 0244429..a0e20fe 100644 --- a/scripts/luneraria/ui/lune_hud.gd +++ b/scripts/luneraria/ui/lune_hud.gd @@ -297,7 +297,7 @@ func notify(message: String, error: bool = false) -> void: func _process(delta: float) -> void: _toast_left -= delta 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") if match_state != null: refresh() diff --git a/scripts/luneraria/ui/lune_minimap.gd b/scripts/luneraria/ui/lune_minimap.gd index ede19dd..bccfdba 100644 --- a/scripts/luneraria/ui/lune_minimap.gd +++ b/scripts/luneraria/ui/lune_minimap.gd @@ -6,6 +6,8 @@ const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55"), Color("b37c var match_state: LuneMatch var camera_focus := Vector3.ZERO 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: custom_minimum_size = Vector2(236, 236) @@ -32,9 +34,10 @@ func _draw() -> void: draw_rect(Rect2(Vector2.ZERO, size), Color("5c4a33"), false, 1.5) if match_state == null: return - var a: Vector2 = to_map(Vector2(-40, -40)) - var b: Vector2 = to_map(Vector2(40, 40)) - draw_line(a, b, Color(.12, .38, .42, .8), 7.0) + # The Kluft: from the grotto spring along the symmetry axis into the south-eastern delta. + var spring: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * LuneTerrain.ARENA_RADIUS + 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: var path: LuneMap.LanePath = match_state.map.lanes[lane] var points := PackedVector2Array() @@ -65,6 +68,13 @@ func _draw() -> void: else: 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) - 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 - draw_rect(Rect2(center - Vector2(span * .9, span * .55), Vector2(span * 1.8, span * 1.1)), Color(1, 1, 1, .55), false, 1.2) + 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 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) diff --git a/scripts/luneraria/ui/lune_settings.gd b/scripts/luneraria/ui/lune_settings.gd index f8d2e9c..7f252e9 100644 --- a/scripts/luneraria/ui/lune_settings.gd +++ b/scripts/luneraria/ui/lune_settings.gd @@ -48,7 +48,7 @@ func _ready() -> void: _checks[key] = check column.add_child(HSeparator.new()) _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() back.text = "ZURÜCK [Esc]" back.theme_type_variation = "PrimaryButton" diff --git a/scripts/luneraria/view/lune_camera.gd b/scripts/luneraria/view/lune_camera.gd index 6703815..bfeee91 100644 --- a/scripts/luneraria/view/lune_camera.gd +++ b/scripts/luneraria/view/lune_camera.gd @@ -1,40 +1,77 @@ extends Camera3D -## Overview camera: orthographic three-quarter view over the whole map, pan, zoom and -## lane focus. Blau's base sits bottom-left, Rot's top-right, as on the minimap. +## MOBA-style game camera: a perspective three-quarter view whose angle steepens as it +## 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 yaw: float = 0.0 -var pitch: float = deg_to_rad(-56.0) -var zoom: float = 34.0 +var zoom: float = DEFAULT_ZOOM 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_zoom: float = 34.0 +var _goal_zoom: float = DEFAULT_ZOOM +var _goal_yaw: float = 0.0 var _dragging: bool = false func _ready() -> void: - projection = PROJECTION_ORTHOGONAL - near = .1 - far = 400.0 + projection = PROJECTION_PERSPECTIVE + fov = FOV_DEGREES + near = .5 + far = 700.0 current = true _goal_focus = focus _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: - _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: - _goal_zoom = new_zoom + _goal_zoom = clampf(new_zoom, MIN_ZOOM, MAX_ZOOM) if instant or reduced_motion: focus = _goal_focus + focus.y = ground.call(Vector2(focus.x, focus.z)) zoom = _goal_zoom + yaw = _goal_yaw _apply() +## The whole map (both bases, all lanes and the grotto) at once. 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: - size = zoom - var back := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch).rotated(Vector3.UP, yaw) - position = focus + back * 80.0 + var back := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch_for(zoom)).rotated(Vector3.UP, yaw) + position = focus + back * distance_for(zoom) look_at(focus, Vector3.UP) func _process(delta: float) -> void: @@ -47,23 +84,43 @@ func _process(delta: float) -> void: var right := Vector3(cos(yaw), 0, -sin(yaw)) var forward := Vector3(-sin(yaw), 0, -cos(yaw)) _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) - 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) + yaw = lerp_angle(yaw, _goal_yaw, t) _apply() func _text_focus() -> bool: var owner: Control = get_viewport().gui_get_focus_owner() 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. func handle_world_input(event: InputEvent) -> bool: if event is InputEventMouseButton and event.pressed: 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 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 if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE: _dragging = event.pressed @@ -72,5 +129,7 @@ func handle_world_input(event: InputEvent) -> bool: var right := Vector3(cos(yaw), 0, -sin(yaw)) var forward := Vector3(-sin(yaw), 0, -cos(yaw)) _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 false diff --git a/scripts/luneraria/view/lune_pad.gdshader b/scripts/luneraria/view/lune_pad.gdshader new file mode 100644 index 0000000..89bd260 --- /dev/null +++ b/scripts/luneraria/view/lune_pad.gdshader @@ -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); +} diff --git a/scripts/luneraria/view/lune_pad.gdshader.uid b/scripts/luneraria/view/lune_pad.gdshader.uid new file mode 100644 index 0000000..c177a9b --- /dev/null +++ b/scripts/luneraria/view/lune_pad.gdshader.uid @@ -0,0 +1 @@ +uid://bwn3ulxjiwybb diff --git a/scripts/luneraria/view/lune_terrain.gd b/scripts/luneraria/view/lune_terrain.gd new file mode 100644 index 0000000..5b72ba3 --- /dev/null +++ b/scripts/luneraria/view/lune_terrain.gd @@ -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 diff --git a/scripts/luneraria/view/lune_terrain.gd.uid b/scripts/luneraria/view/lune_terrain.gd.uid new file mode 100644 index 0000000..46f9b7b --- /dev/null +++ b/scripts/luneraria/view/lune_terrain.gd.uid @@ -0,0 +1 @@ +uid://ccaumfwbolrec diff --git a/scripts/luneraria/view/lune_terrain.gdshader b/scripts/luneraria/view/lune_terrain.gdshader new file mode 100644 index 0000000..329240a --- /dev/null +++ b/scripts/luneraria/view/lune_terrain.gdshader @@ -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)); +} diff --git a/scripts/luneraria/view/lune_terrain.gdshader.uid b/scripts/luneraria/view/lune_terrain.gdshader.uid new file mode 100644 index 0000000..da18851 --- /dev/null +++ b/scripts/luneraria/view/lune_terrain.gdshader.uid @@ -0,0 +1 @@ +uid://ih7v3fqacdf8 diff --git a/scripts/luneraria/view/lune_unit_view.gd b/scripts/luneraria/view/lune_unit_view.gd index 1e0adba..286a390 100644 --- a/scripts/luneraria/view/lune_unit_view.gd +++ b/scripts/luneraria/view/lune_unit_view.gd @@ -6,6 +6,8 @@ const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55"), Color("b37c const HERO_GOLD := Color("f2c96b") var unit: LuneUnit +## Ground the figure stands on (lane decks, terrain, grotto terrace); flat without it. +var terrain: LuneTerrain var model: Node3D var skeletal: bool = false var _fill: MeshInstance3D @@ -90,7 +92,8 @@ func bind(source: LuneUnit) -> void: tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED tag.no_depth_test = 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.outline_size = 10 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) _last_attack = unit.attack_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 _last_visual = position @@ -111,9 +114,9 @@ func sync(alpha: float, delta: float) -> void: return _time += delta 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)) - 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 if unit.attack_serial != _last_attack: _last_attack = unit.attack_serial @@ -136,6 +139,9 @@ func sync(alpha: float, delta: float) -> void: if not skeletal: visible = unit.alive +func _ground(at: Vector2) -> float: + return terrain.walk_height(at) + .03 if terrain != null else 0.0 + class EnemyVisualsFallback: static func make(color: Color) -> Node3D: var body := MeshInstance3D.new() diff --git a/scripts/luneraria/view/lune_world.gd b/scripts/luneraria/view/lune_world.gd index a5583c5..1c583b6 100644 --- a/scripts/luneraria/view/lune_world.gd +++ b/scripts/luneraria/view/lune_world.gd @@ -1,41 +1,60 @@ extends Node3D -## 3D presentation of LuneMap built from existing assets: Kluft terrain shaders, the -## Saumkern models as Ancients, compare-pack rocks and trees, and the new lane tower. -## Purely visual; the simulation never reads anything from this node. +## 3D presentation of LuneMap (G1 layout). The ground is LuneTerrain's height field: +## the north-western highland carries Top, the Kluft river runs on the symmetry axis +## from the grotto spring under the central Mid platform into the south-eastern delta +## that Bottom crosses on a bridge. Existing assets are reused: the lane tower, the +## Saumkern Ancients, the grotto rock arc, map props (trees, rocks, curbs, bridge, +## banners) and the Kluft surface shader. New shapes are simple blockout primitives +## that G2/G4 replace. Purely visual; the simulation never reads anything from here. const Lighting = preload("res://scripts/presentation/lighting.gd") const Sanctuary = preload("res://scripts/presentation/sanctuary.gd") const RiverShader = preload("res://scripts/presentation/river.gdshader") +const GroundShader = preload("res://scripts/luneraria/view/lune_terrain.gdshader") +const PadShader = preload("res://scripts/luneraria/view/lune_pad.gdshader") +const Stone = preload("res://assets/materials/phase10/limestone-albedo.png") const TOWER := "res://assets/luneraria/tower.glb" -const ROCK := "res://assets/compare/compare-rock.glb" -const TREE := "res://assets/compare/compare-tree.glb" -const GROTTO := "res://assets/luneraria/grotto.glb" +const GROTTO_SCENE := "res://assets/luneraria/grotto.glb" const PROPS := "res://assets/luneraria/map_props.glb" -## Lane crossings over the river (x = z); all three lie on the symmetry axis. -const CROSSINGS: Array[Vector2] = [Vector2(-25, -25), Vector2(0, 0), Vector2(25, 25)] const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55")] const TEAM_NAMES: Array[String] = ["Blau", "Rot"] +const GROTTO_COLOR := Color("b37cff") +## Visible paving half width per lane; Mid is the broad monumental route. +const PAVING: Dictionary = {"top": 3.0, "mid": 3.9, "bot": 3.0} +## Lane identity: old grey flagstones on Top, pale limestone on Mid, packed earth on Bottom. +const PAVING_MATERIAL: Dictionary = {"top": ["ENV_paving_top", Color(.33, .31, .27)], "mid": ["ENV_paving_mid", Color(.42, .39, .33)], + "bot": ["ENV_soil_bot", Color(.31, .25, .17)]} var map: LuneMap +var terrain: LuneTerrain +var sun: DirectionalLight3D var building_nodes: Dictionary = {} -var _terrain: Dictionary = {} +var grotto_light: OmniLight3D +var _grotto_runes: StandardMaterial3D +var _materials: Dictionary = {} var _crystal_materials: Array[StandardMaterial3D] = [] +var _props: Dictionary = {} +## "team:lane" -> deployment pad, team -> grotto entrance ring +var _pads: Dictionary = {} +var _grotto_pads: Dictionary = {} func build(match_map: LuneMap) -> void: map = match_map name = "LuneWorld" + terrain = LuneTerrain.for_map(map) _environment() _ground() - _river() - for lane: String in LuneMap.LANES: - _lane_ribbon(map.lanes[lane]) - for team: int in range(2): - _base_plaza(team) + _water() _load_props() - _bridges() + for lane: String in LuneMap.LANES: + _paving(lane) + _central_platform() + _delta_bridge() _curbs() - _decoration() - _edges() + for team: int in range(2): + _base(team) + _landmarks() + _vegetation() for team: int in range(2): var crystal := StandardMaterial3D.new() crystal.albedo_color = TEAM_COLORS[team] @@ -47,33 +66,81 @@ func build(match_map: LuneMap) -> void: for definition: Dictionary in map.buildings: _building(definition) _grotto() + _deployment_pads() -func terrain(key: String, color: Color) -> ShaderMaterial: - if not _terrain.has(key): +# ---------------------------------------------------------------- helpers +## Height a unit or building stands on (lane decks instead of the river bed). +func height_at(p: Vector2) -> float: + return terrain.walk_height(p) + +func to3(v: Vector2, lift: float = 0.0) -> Vector3: + return Vector3(v.x, terrain.walk_height(v) + lift, v.y) + +func _ground3(v: Vector2, lift: float = 0.0) -> Vector3: + return Vector3(v.x, terrain.height_at(v) + lift, v.y) + +## Kluft surface material for ENV_* keys (flags come from the key: paving, soil, leaf, bark). +func surface(key: String, color: Color) -> ShaderMaterial: + if not _materials.has(key): var source := StandardMaterial3D.new() source.resource_name = key source.albedo_color = color - _terrain[key] = Sanctuary.terrain_material(source) - return _terrain[key] + _materials[key] = Sanctuary.terrain_material(source) + return _materials[key] -func to3(v: Vector2, y: float = 0.0) -> Vector3: - return Vector3(v.x, y, v.y) +static func _glow(color: Color, energy: float) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.emission_enabled = true + material.emission = color + material.emission_energy_multiplier = energy + material.roughness = .3 + return material +func _multi(mesh: Mesh, material: Material, transforms: Array[Transform3D], label: String, shadows: bool = true) -> MultiMeshInstance3D: + var multimesh := MultiMesh.new() + multimesh.transform_format = MultiMesh.TRANSFORM_3D + multimesh.mesh = mesh + multimesh.instance_count = transforms.size() + for index: int in range(transforms.size()): + multimesh.set_instance_transform(index, transforms[index]) + var instance := MultiMeshInstance3D.new() + instance.name = label + instance.multimesh = multimesh + if material != null: + instance.material_override = material + instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(instance) + return instance + +func _prop_multi(prop: String, transforms: Array[Transform3D], shadows: bool = true) -> void: + if not transforms.is_empty(): + _multi(_props[prop], null, transforms, "Props_" + prop, shadows) + +static func _yaw_basis(yaw: float, scale: Vector3 = Vector3.ONE) -> Basis: + return Basis(Vector3.UP, yaw) * Basis.from_scale(scale) + +## Yaw that turns a model's +Z towards the given ground direction. +static func _facing(direction: Vector2) -> float: + return atan2(direction.x, direction.y) + +# ---------------------------------------------------------------- environment and ground func _environment() -> void: var environment := WorldEnvironment.new() var settings := Environment.new() Lighting.configure(settings) - # The overview camera sits ~80 m away; distance fog would wash out the whole board. + # The camera sits 40–170 m away; distance fog would wash out the board (G5 revisits). settings.fog_enabled = false environment.environment = settings add_child(environment) - var sun := DirectionalLight3D.new() + sun = DirectionalLight3D.new() sun.rotation_degrees = Vector3(-55, -30, 0) sun.light_color = Color("ffe2bc") sun.light_energy = 1.7 sun.shadow_enabled = true - sun.directional_shadow_max_distance = 90.0 - sun.shadow_blur = 1.8 + sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS + sun.directional_shadow_max_distance = 190.0 + sun.shadow_blur = 1.6 add_child(sun) var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-35, 150, 0) @@ -82,177 +149,194 @@ func _environment() -> void: add_child(fill) func _ground() -> void: + var material := ShaderMaterial.new() + material.shader = GroundShader + material.set_shader_parameter("stone_map", Stone) var ground := MeshInstance3D.new() - var plane := PlaneMesh.new() - plane.size = Vector2(LuneMap.HALF_EXTENT * 2.0 + 30.0, LuneMap.HALF_EXTENT * 2.0 + 30.0) - plane.subdivide_width = 32 - plane.subdivide_depth = 32 - ground.mesh = plane - ground.material_override = terrain("ENV_soil", Color(.10, .15, .065)) ground.name = "Ground" + ground.mesh = terrain.inner_mesh() + ground.material_override = material add_child(ground) + var outer := MeshInstance3D.new() + outer.name = "Mountains" + outer.mesh = terrain.outer_mesh() + outer.material_override = material + # The distant ring lies outside the board; its shadows would only cost shadow-map time. + outer.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(outer) -func _river() -> void: - # The river follows the symmetry axis x = z and passes under all three lanes. +func _water() -> void: + var water_material := ShaderMaterial.new() + water_material.shader = RiverShader + var water := MeshInstance3D.new() + water.name = "Water" + var plane := PlaneMesh.new() + plane.size = Vector2(LuneTerrain.OUTER_EXTENT * 2.0, LuneTerrain.OUTER_EXTENT * 2.0) + water.mesh = plane + water.position.y = LuneTerrain.WATER_LEVEL + water.material_override = water_material + water.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(water) + # The spring leaves the grotto terrace between the two stairs as a short waterfall. + var top: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS - 0.5) + var bottom: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS + 1.7) + var across: Vector2 = LuneMap.BLUE_SIDE * .9 + var corners: Array[Vector3] = [Vector3(top.x + across.x, LuneTerrain.ARENA_HEIGHT - 0.05, top.y + across.y), + Vector3(top.x - across.x, LuneTerrain.ARENA_HEIGHT - 0.05, top.y - across.y), + Vector3(bottom.x - across.x, LuneTerrain.WATER_LEVEL + .02, bottom.y - across.y), + Vector3(bottom.x + across.x, LuneTerrain.WATER_LEVEL + .02, bottom.y + across.y)] var tool := SurfaceTool.new() tool.begin(Mesh.PRIMITIVE_TRIANGLES) - var a := Vector2(-40, -40) - var b := Vector2(40, 40) - var side := Vector2(1, -1).normalized() * 3.2 - var steps: int = 40 - for i: int in range(steps): - # Godot front faces wind clockwise seen from above. - for quad: Array in [[i, -1], [i + 1, 1], [i + 1, -1], [i, -1], [i, 1], [i + 1, 1]]: - var t: float = float(quad[0]) / steps - var wobble: float = sin(t * 18.0) * .6 - var p: Vector2 = a.lerp(b, t) + side * (float(quad[1]) + wobble * .15) - tool.set_normal(Vector3.UP) - tool.add_vertex(to3(p, .012)) - var river := MeshInstance3D.new() - river.mesh = tool.commit() - var water := ShaderMaterial.new() - water.shader = RiverShader - river.material_override = water - river.name = "River" - add_child(river) + # Both windings, so the fall is visible from every camera angle. + for index: int in [0, 1, 2, 0, 2, 3, 0, 2, 1, 0, 3, 2]: + tool.add_vertex(corners[index]) + tool.generate_normals() + var fall := MeshInstance3D.new() + fall.name = "SpringFall" + fall.mesh = tool.commit() + var foam := StandardMaterial3D.new() + foam.albedo_color = Color(.72, .86, .9, .82) + foam.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + foam.emission_enabled = true + foam.emission = Color(.35, .5, .55) + foam.emission_energy_multiplier = .35 + foam.roughness = .2 + fall.material_override = foam + fall.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(fall) -func _lane_ribbon(path: LuneMap.LanePath) -> void: +## Shadows cover what the camera shows: close views keep the shadow map dense and cheap, +## the overview reaches the whole board. +func fit_shadows(camera_distance: float) -> void: + sun.directional_shadow_max_distance = clampf(camera_distance * 1.3 + 25.0, 70.0, 220.0) + +# ---------------------------------------------------------------- lanes +## Lane sections that a deck replaces: the central platform on Mid, the delta bridge on Bottom. +func _deck_gap(lane: String, point: Vector2) -> bool: + if lane == "mid": + return point.length() < LuneTerrain.PLATFORM_RADIUS - 0.4 + if lane == "bot": + return point.distance_to(delta_point()) < 7.2 + return false + +## Where Bottom crosses the Kluft delta (its midpoint, on the symmetry axis). +func delta_point() -> Vector2: + var path: LuneMap.LanePath = map.lanes["bot"] + return path.point_at(path.length * 0.5) + +func _paving(lane: String) -> void: + var path: LuneMap.LanePath = map.lanes[lane] + var half: float = PAVING[lane] var tool := SurfaceTool.new() tool.begin(Mesh.PRIMITIVE_TRIANGLES) - var steps: int = int(ceil(path.length / .8)) - var half: float = map.balance.lane_half_width + .6 - var previous_left := Vector3.ZERO - var previous_right := Vector3.ZERO + var steps: int = int(ceil(path.length / 0.8)) + var previous: Array[Vector3] = [] + var previous_gap: bool = true for i: int in range(steps + 1): var s: float = path.length * float(i) / steps var point: Vector2 = path.point_at(s) - var tangent: Vector2 = (path.point_at(minf(path.length, s + .5)) - path.point_at(maxf(0.0, s - .5))).normalized() + var tangent: Vector2 = (path.point_at(minf(path.length, s + .6)) - path.point_at(maxf(0.0, s - .6))).normalized() var normal := Vector2(-tangent.y, tangent.x) - var left: Vector3 = to3(point + normal * half, .03) - var right: Vector3 = to3(point - normal * half, .03) - if i > 0: - for vertex: Vector3 in [previous_left, right, left, previous_left, previous_right, right]: - tool.set_normal(Vector3.UP) - tool.set_uv(Vector2(vertex.x, vertex.z) * .25) - tool.add_vertex(vertex) - previous_left = left - previous_right = right + var y: float = terrain.lane_height(lane, s) + 0.05 + # Outer shoulder, paving edge, paving edge, outer shoulder (a bevel into the ground). + var section: Array[Vector3] = [] + for offset: Array in [[half + .45, -.17], [half, 0.0], [-half, 0.0], [-half - .45, -.17]]: + var at: Vector2 = point + normal * float(offset[0]) + section.append(Vector3(at.x, y + float(offset[1]), at.y)) + var gap: bool = _deck_gap(lane, point) + if i > 0 and not gap and not previous_gap: + for k: int in range(3): + for vertex: Vector3 in [previous[k], section[k + 1], section[k], previous[k], previous[k + 1], section[k + 1]]: + tool.add_vertex(vertex) + previous = section + previous_gap = gap + tool.generate_normals() var ribbon := MeshInstance3D.new() + ribbon.name = "Paving_" + lane ribbon.mesh = tool.commit() - ribbon.material_override = terrain("ENV_paving", Color(.19, .185, .15)) + var spec: Array = PAVING_MATERIAL[lane] + ribbon.material_override = surface(spec[0], spec[1]) ribbon.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(ribbon) -func _base_plaza(team: int) -> void: - var plaza := MeshInstance3D.new() +## Mid crosses the Kluft on a round platform: pier, parapet, rune circle, four standing stones. +func _central_platform() -> void: + var y: float = LuneTerrain.PLATFORM_HEIGHT + var pier := MeshInstance3D.new() + pier.name = "CentralPlatform" + var cylinder := CylinderMesh.new() + cylinder.top_radius = LuneTerrain.PLATFORM_RADIUS + cylinder.bottom_radius = LuneTerrain.PLATFORM_RADIUS + .7 + cylinder.height = 2.6 + cylinder.radial_segments = 56 + pier.mesh = cylinder + pier.position = Vector3(0, y + .05 - 1.3, 0) + pier.material_override = surface("ENV_limestone_platform", Color(.56, .52, .46)) + add_child(pier) + var inlay := MeshInstance3D.new() var disc := CylinderMesh.new() - disc.top_radius = 9.5 - disc.bottom_radius = 9.8 - disc.height = .12 - disc.radial_segments = 48 - plaza.mesh = disc - plaza.material_override = terrain("ENV_limestone", Color(.25, .225, .17)) - plaza.position = to3(map.core_position(team), .02) - add_child(plaza) - var rim := MeshInstance3D.new() - var ring := TorusMesh.new() - ring.inner_radius = 9.35 - ring.outer_radius = 9.6 - ring.rings = 64 - rim.mesh = ring - var glow := StandardMaterial3D.new() - glow.albedo_color = TEAM_COLORS[team] - glow.emission_enabled = true - glow.emission = TEAM_COLORS[team] - glow.emission_energy_multiplier = 1.2 - rim.material_override = glow - rim.position = to3(map.core_position(team), .1) - rim.scale = Vector3(1, .3, 1) - add_child(rim) + disc.top_radius = LuneTerrain.PLATFORM_RADIUS - 1.0 + disc.bottom_radius = disc.top_radius + disc.height = .04 + disc.radial_segments = 56 + inlay.mesh = disc + inlay.position = Vector3(0, y + .07, 0) + inlay.material_override = surface("ENV_paving_plaza", Color(.6, .56, .49)) + add_child(inlay) + var ring := MeshInstance3D.new() + var torus := TorusMesh.new() + torus.inner_radius = 3.25 + torus.outer_radius = 3.5 + torus.rings = 64 + ring.mesh = torus + ring.scale = Vector3(1, .12, 1) + ring.position = Vector3(0, y + .1, 0) + ring.material_override = _glow(Color("d9ccff"), .9) + ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(ring) + var parapet := MeshInstance3D.new() + var rim := TorusMesh.new() + rim.inner_radius = LuneTerrain.PLATFORM_RADIUS - .35 + rim.outer_radius = LuneTerrain.PLATFORM_RADIUS + .05 + rim.rings = 64 + parapet.mesh = rim + parapet.scale = Vector3(1, .9, 1) + parapet.position = Vector3(0, y + .15, 0) + parapet.material_override = surface("ENV_limestone_parapet", Color(.5, .47, .41)) + add_child(parapet) + var stones: Array[Transform3D] = [] + for direction: Vector2 in [Vector2(1, 0), Vector2(0, 1), Vector2(-1, 0), Vector2(0, -1)]: + var at: Vector2 = direction * 5.3 + var lean: float = .05 if direction.x + direction.y > 0.0 else -.05 + stones.append(Transform3D(_yaw_basis(_facing(-direction)) * Basis(Vector3.RIGHT, lean), Vector3(at.x, y + 1.75, at.y))) + var slab := BoxMesh.new() + slab.size = Vector3(1.0, 3.6, .62) + _multi(slab, surface("ENV_cliff_runestone", Color(.44, .42, .45)), stones, "RuneStones") + var runes: Array[Transform3D] = [] + for stone: Transform3D in stones: + runes.append(Transform3D(stone.basis, stone.origin + stone.basis.z * .32 + Vector3(0, .5, 0))) + var rune := BoxMesh.new() + rune.size = Vector3(.34, 1.5, .04) + _multi(rune, _glow(Color("cdb8ff"), 1.3), runes, "RuneGlyphs", false) -var grotto_light: OmniLight3D +## Bottom crosses the delta on the existing bridge prop, stretched to span the wider water. +func _delta_bridge() -> void: + var at: Vector2 = delta_point() + var path: LuneMap.LanePath = map.lanes["bot"] + var deck: float = terrain.lane_height("bot", path.length * 0.5) + 0.05 + var bridge := MeshInstance3D.new() + bridge.name = "DeltaBridge" + bridge.mesh = _props["Bridge"] + # The prop's deck surface lies 0.10 m above its origin. + bridge.transform = Transform3D(_yaw_basis(PI * .25, Vector3(1.34, 1.0, 1.0)), Vector3(at.x, deck - 0.1, at.y)) + add_child(bridge) -func _grotto() -> void: - var grotto: Node3D = (load(GROTTO) as PackedScene).instantiate() - grotto.name = "BossGrotto" - grotto.position = to3(LuneMap.GROTTO) - _dress(grotto) - add_child(grotto) - grotto_light = OmniLight3D.new() - grotto_light.position = to3(LuneMap.GROTTO + Vector2(-2.0, -2.0), 2.4) - grotto_light.light_color = Color("b37cff") - grotto_light.light_energy = 2.2 - grotto_light.omni_range = 9.0 - add_child(grotto_light) - -## Grotto glow: violet when open, dim when locked, team colour while a claim is pending. -func sync_grotto(available: bool, claim_team: int) -> void: - if grotto_light == null: - return - grotto_light.light_color = TEAM_COLORS[claim_team] if claim_team >= 0 else Color("b37cff") - grotto_light.light_energy = 2.2 if available or claim_team >= 0 else .5 - -func _far_from_lanes(point: Vector2, margin: float) -> bool: - for lane: String in LuneMap.LANES: - var path: LuneMap.LanePath = map.lanes[lane] - if path.point_at(path.project(point)).distance_to(point) < margin: - return false - for team: int in range(2): - if map.core_position(team).distance_to(point) < 12.0: - return false - if absf(point.x - point.y) < 5.0: - return false - if point.distance_to(LuneMap.GROTTO) < 9.0: - return false - return true - -var _props: Dictionary = {} - -## Prop meshes from the Blender generator, with the Kluft shaders applied per surface. -func _load_props() -> void: - var scene: Node3D = (load(PROPS) as PackedScene).instantiate() - for child: Node in scene.get_children(): - if child is MeshInstance3D: - var mesh: ArrayMesh = (child as MeshInstance3D).mesh.duplicate() - for surface: int in range(mesh.get_surface_count()): - var source := mesh.surface_get_material(surface) as StandardMaterial3D - if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"): - mesh.surface_set_material(surface, terrain(source.resource_name, source.albedo_color)) - _props[str(child.name)] = mesh - scene.free() - -func _multimesh(prop: String, transforms: Array[Transform3D], shadows: bool = true) -> MultiMeshInstance3D: - var multimesh := MultiMesh.new() - multimesh.transform_format = MultiMesh.TRANSFORM_3D - multimesh.mesh = _props[prop] - multimesh.instance_count = transforms.size() - for index: int in range(transforms.size()): - multimesh.set_instance_transform(index, transforms[index]) - var instance := MultiMeshInstance3D.new() - instance.name = "Props_" + prop - instance.multimesh = multimesh - instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - add_child(instance) - return instance - -func _bridges() -> void: - var transforms: Array[Transform3D] = [] - for point: Vector2 in CROSSINGS: - transforms.append(Transform3D(Basis(Vector3.UP, PI * .25), to3(point))) - _multimesh("Bridge", transforms) - -func _near_crossing(point: Vector2, margin: float) -> bool: - for crossing: Vector2 in CROSSINGS: - if crossing.distance_to(point) < margin: - return true - return false - -## Curb stones along both lane edges, skipped on bridges, at bases and around towers. +## Curb stones along Top and Mid, skipped on decks, at bases and around towers. func _curbs() -> void: var transforms: Array[Transform3D] = [] - var offset: float = map.balance.lane_half_width + 1.0 - for lane: String in LuneMap.LANES: + for lane: String in ["top", "mid"]: var path: LuneMap.LanePath = map.lanes[lane] + var offset: float = float(PAVING[lane]) + .45 var s: float = 0.0 while s < path.length: var sampled: Array = path.sample(s) @@ -260,92 +344,476 @@ func _curbs() -> void: var normal := Vector2(-tangent.y, tangent.x) for side: float in [-1.0, 1.0]: var at: Vector2 = sampled[0] + normal * offset * side - var clear: bool = not _near_crossing(at, 6.5) and map.core_position(0).distance_to(at) > 11.0 and map.core_position(1).distance_to(at) > 11.0 + var clear: bool = not _deck_gap(lane, at) and at.length() > LuneTerrain.PLATFORM_RADIUS + .6 + for team: int in range(2): + clear = clear and map.core_position(team).distance_to(at) > LuneTerrain.BASE_RADIUS + 0.8 for definition: Dictionary in map.buildings: - clear = clear and Vector2(definition.position).distance_to(at) > 2.2 + clear = clear and Vector2(definition.position).distance_to(at) > 2.3 if clear: - transforms.append(Transform3D(Basis(Vector3.UP, atan2(-tangent.y, tangent.x)), to3(at))) + transforms.append(Transform3D(_yaw_basis(atan2(-tangent.y, tangent.x)), Vector3(at.x, terrain.lane_height(lane, s) - .06, at.y))) s += 1.55 - _multimesh("Curb", transforms, false) + _prop_multi("Curb", transforms, false) -func _decoration() -> void: - # Scatter one half deterministically and mirror it, so decoration never favours a team. +# ---------------------------------------------------------------- bases +## Plateau plaza with the team rim, a ring wall with three lane gates and gate crystals. +func _base(team: int) -> void: + var core: Vector2 = map.core_position(team) + var y: float = LuneTerrain.BASE_HEIGHT + var plaza := MeshInstance3D.new() + plaza.name = "BasePlaza_%d" % team + var disc := CylinderMesh.new() + disc.top_radius = 8.4 + disc.bottom_radius = 8.7 + disc.height = .12 + disc.radial_segments = 56 + plaza.mesh = disc + plaza.position = Vector3(core.x, y + .02, core.y) + plaza.material_override = surface("ENV_limestone_plaza", Color(.52, .48, .4)) + add_child(plaza) + var rim := MeshInstance3D.new() + var ring := TorusMesh.new() + ring.inner_radius = 8.25 + ring.outer_radius = 8.5 + ring.rings = 64 + rim.mesh = ring + rim.material_override = _glow(TEAM_COLORS[team], 1.2) + rim.position = Vector3(core.x, y + .1, core.y) + rim.scale = Vector3(1, .3, 1) + rim.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(rim) + var gates: Array[float] = [] + for lane: String in LuneMap.LANES: + var start: Vector2 = map.lane_point(team, lane, 0.0) + gates.append(atan2(start.y - core.y, start.x - core.x)) + var walls: Array[Transform3D] = [] + var pillars: Array[Transform3D] = [] + var gems: Array[Transform3D] = [] + var radius: float = LuneTerrain.BASE_RADIUS - .2 + var segments: int = 40 + for index: int in range(segments): + var angle: float = TAU * index / segments + var near_gate: float = PI + for gate: float in gates: + near_gate = minf(near_gate, absf(wrapf(angle - gate, -PI, PI))) + var at: Vector2 = core + Vector2(cos(angle), sin(angle)) * radius + if near_gate < deg_to_rad(15.0): + continue + if near_gate < deg_to_rad(24.0): + pillars.append(Transform3D(Basis.IDENTITY, Vector3(at.x, y + 1.7, at.y))) + gems.append(Transform3D(Basis(Vector3.UP, angle), Vector3(at.x, y + 3.75, at.y))) + continue + walls.append(Transform3D(_yaw_basis(-angle + PI * .5), Vector3(at.x, y + .5, at.y))) + var wall := BoxMesh.new() + wall.size = Vector3(1.72, 1.2, .7) + _multi(wall, surface("ENV_limestone_wall", Color(.5, .47, .42)), walls, "BaseWall_%d" % team) + var pillar := CylinderMesh.new() + pillar.top_radius = .42 + pillar.bottom_radius = .55 + pillar.height = 3.4 + pillar.radial_segments = 10 + _multi(pillar, surface("ENV_limestone_gate", Color(.55, .51, .45)), pillars, "BaseGates_%d" % team) + var gem := SphereMesh.new() + gem.radius = .45 + gem.height = 1.1 + gem.radial_segments = 6 + gem.rings = 2 + _multi(gem, _glow(TEAM_COLORS[team], 2.4), gems, "GateCrystals_%d" % team, false) + +# ---------------------------------------------------------------- landmarks (blockout) +## Top: a ruined lookout on the highland corner, an old gate and broken watch walls. +## Bottom: overgrown wall stubs in the lowland. Every piece is mirrored for Rot. +func _landmarks() -> void: + var walls: Array[Transform3D] = [] + var columns: Array[Transform3D] = [] + for team: int in range(2): + # Old gate where Top reaches the highland. + for side: float in [-1.0, 1.0]: + var post: Vector2 = map.lane_point(team, "top", 36.0, side * 4.7) + columns.append(Transform3D(Basis.from_scale(Vector3(1, 1.25, 1)), _ground3(post, 1.4))) + # Broken watch walls on the outer edge of the highland path. + for spec: Vector3 in [Vector3(29.0, -6.2, 3.4), Vector3(41.5, -6.4, 2.6), Vector3(45.0, 5.8, 2.0)]: + var at: Vector2 = map.lane_point(team, "top", spec.x, spec.y) + var along: Vector2 = map.lane_forward(team, "top", spec.x) + walls.append(Transform3D(_yaw_basis(atan2(-along.y, along.x), Vector3(spec.z, 1.0, 1.0)), _ground3(at, .45))) + # Overgrown stubs beside Bottom in the lowland. + for spec: Vector3 in [Vector3(24.0, 6.4, 2.2), Vector3(39.0, -6.3, 1.6)]: + var at: Vector2 = map.lane_point(team, "bot", spec.x, spec.y) + var along: Vector2 = map.lane_forward(team, "bot", spec.x) + walls.append(Transform3D(_yaw_basis(atan2(-along.y, along.x) + .3, Vector3(spec.z, .7, 1.0)), _ground3(at, .2))) + var block := BoxMesh.new() + block.size = Vector3(1.0, 1.1, .7) + _multi(block, surface("ENV_limestone_ruin", Color(.5, .48, .44)), walls, "RuinWalls") + var column := CylinderMesh.new() + column.top_radius = .38 + column.bottom_radius = .48 + column.height = 2.4 + column.radial_segments = 10 + _multi(column, surface("ENV_limestone_ruin", Color(.5, .48, .44)), columns, "RuinColumns") + # The lookout on the Top corner hill, on the symmetry axis. + var lookout := Vector2(-31.0, -31.0) + var tower := MeshInstance3D.new() + tower.name = "Lookout" + var shaft := CylinderMesh.new() + shaft.top_radius = 2.0 + shaft.bottom_radius = 2.6 + shaft.height = 4.6 + shaft.radial_segments = 14 + tower.mesh = shaft + tower.position = _ground3(lookout, 2.1) + tower.material_override = surface("ENV_limestone_ruin", Color(.5, .48, .44)) + add_child(tower) + var crown := MeshInstance3D.new() + var broken := CylinderMesh.new() + broken.top_radius = 1.6 + broken.bottom_radius = 2.0 + broken.height = 1.4 + broken.radial_segments = 7 + crown.mesh = broken + crown.position = _ground3(lookout, 5.0) + crown.rotation = Vector3(.12, .4, -.12) + crown.material_override = tower.material_override + add_child(crown) + +# ---------------------------------------------------------------- vegetation +## Clusters, not scatter: a noise field decides where forests stand; dense cores carry +## the big trees, edges the small ones and shrubs, clearings stay open. The species +## follow the zone (highland pines, valley mixed wood, lowland oaks and reeds, mountain +## pines). The Blau side is generated and mirrored, so decoration never favours a team. +func _vegetation() -> void: + var noise := FastNoiseLite.new() + noise.seed = 1203 + noise.frequency = .055 + noise.fractal_octaves = 2 var rng := RandomNumberGenerator.new() rng.seed = 90210 - var kinds: Array[String] = ["Pine_0", "Pine_1", "Pine_2", "Oak_0", "Oak_1", "Oak_2", "Rock_0", "Rock_1", "Rock_2", "Shrub_0", "Shrub_1"] - var weights: Array[float] = [1.2, 1.0, 1.0, 1.2, 1.0, .8, .7, .5, .8, 1.0, 1.0] - var total: float = 0.0 - for weight: float in weights: - total += weight var buckets: Dictionary = {} - var placed: Array[Vector2] = [] - var tries: int = 0 - while placed.size() < 150 and tries < 8000: - tries += 1 - var point := Vector2(rng.randf_range(-LuneMap.HALF_EXTENT - 2, LuneMap.HALF_EXTENT + 2), rng.randf_range(-LuneMap.HALF_EXTENT - 2, LuneMap.HALF_EXTENT + 2)) - if point.y < point.x or not _far_from_lanes(point, 4.9): - continue - var clear: bool = true - for other: Vector2 in placed: - if other.distance_to(point) < 1.9: - clear = false - break - if not clear: - continue - placed.append(point) - var roll: float = rng.randf() * total - var kind: String = kinds[-1] - for index: int in range(kinds.size()): - roll -= weights[index] - if roll <= 0.0: - kind = kinds[index] - break - var size: float = rng.randf_range(.85, 1.25) - var yaw: float = rng.randf() * TAU + var add := func(kind: String, p: Vector2, size: float, yaw: float, squash: Vector3 = Vector3.ONE) -> void: if not buckets.has(kind): buckets[kind] = [] as Array[Transform3D] for mirrored: bool in [false, true]: - var at: Vector2 = LuneMap.mirror(point) if mirrored else point - var basis := Basis(Vector3.UP, -yaw if mirrored else yaw).scaled(Vector3.ONE * size) - (buckets[kind] as Array[Transform3D]).append(Transform3D(basis, to3(at))) + var at: Vector2 = LuneMap.mirror(p) if mirrored else p + var basis: Basis = _yaw_basis(-yaw if mirrored else yaw, squash * size) + (buckets[kind] as Array[Transform3D]).append(Transform3D(basis, _ground3(at, -.06))) + var spacing: float = 2.25 + var x: float = -50.0 + while x <= 50.0: + var z: float = -50.0 + while z <= 50.0: + var p := Vector2(x + rng.randf_range(-.95, .95), z + rng.randf_range(-.95, .95)) + var roll: float = rng.randf() + var yaw: float = rng.randf() * TAU + var jitter: float = rng.randf_range(.85, 1.2) + z += spacing + if p.y <= p.x or not _plantable(p): + continue + var density: float = noise.get_noise_2d(p.x, p.y) * .5 + .5 + var u: float = LuneTerrain.axis_u(p) + var a: float = LuneTerrain.axis_a(p) + var edge: float = terrain.escarpment(a) + var border: float = pow(pow(absf(p.x), 5.0) + pow(absf(p.y), 5.0), .2) + if border > 39.5: + if density > .56 and roll < .55: + add.call(["Pine_0", "Pine_1", "Pine_2"][int(roll * 6.0) % 3], p, jitter * 1.15, yaw) + elif roll > .93: + add.call("Rock_1", p, jitter * 1.6, yaw) + continue + if absf(u - edge) < 1.9: + if roll < .35: + add.call(["Rock_0", "Rock_1", "Rock_2"][int(roll * 9.0) % 3], p, jitter * 1.35, yaw) + continue + if density < .43: + if roll > .965: + add.call("Rock_2", p, jitter, yaw) + continue + var core: bool = density > .62 + if u < edge: + if core or roll < .6: + add.call(["Pine_0", "Pine_1", "Pine_2"][int(roll * 7.0) % 3], p, jitter * (1.25 if core else .95), yaw) + elif roll < .82: + add.call("Rock_0", p, jitter, yaw) + elif u < 20.0: + if core: + add.call(["Oak_0", "Oak_1", "Pine_1"][int(roll * 7.0) % 3], p, jitter * 1.2, yaw) + elif roll < .45: + add.call(["Oak_2", "Pine_2"][int(roll * 5.0) % 2], p, jitter * .9, yaw) + elif roll < .8: + add.call(["Shrub_0", "Shrub_1"][int(roll * 5.0) % 2], p, jitter, yaw) + else: + if core: + add.call(["Oak_0", "Oak_1", "Oak_2"][int(roll * 7.0) % 3], p, jitter * 1.1, yaw) + elif roll < .55: + add.call(["Shrub_0", "Shrub_1"][int(roll * 5.0) % 2], p, jitter * 1.1, yaw) + elif roll < .75: + add.call("Shrub_0", p, jitter * .8, yaw, Vector3(.5, 1.9, .5)) + x += spacing + # River banks: stones along the Kluft, reeds in the lowland. + var u_bank: float = -8.0 + while u_bank < 44.0: + var a_bank: float = terrain.river_half_width(u_bank) + .5 + var p: Vector2 = LuneMap.AXIS * u_bank + LuneMap.BLUE_SIDE * a_bank + var roll: float = rng.randf() + if _bank_clear(p): + if u_bank > 18.0 and roll < .6: + add.call("Shrub_0", p, rng.randf_range(.7, 1.0), rng.randf() * TAU, Vector3(.45, 2.0, .45)) + elif roll < .7: + add.call(["Rock_0", "Rock_2"][int(roll * 4.0) % 2], p, rng.randf_range(.7, 1.05), rng.randf() * TAU) + u_bank += rng.randf_range(2.0, 3.4) for kind: String in buckets: - _multimesh(kind, buckets[kind], not kind.begins_with("Shrub")) + _prop_multi(kind, buckets[kind], not kind.begins_with("Shrub")) -## A ring of cliff segments frames the playable square. -func _edges() -> void: - var transforms: Array[Transform3D] = [] - var edge: float = LuneMap.HALF_EXTENT + 5.0 - for side: int in range(4): - for index: int in range(7): - var along: float = -edge + 6.0 + index * 12.0 - var at: Vector2 - var yaw: float - match side: - 0: at = Vector2(along, -edge); yaw = 0.0 - 1: at = Vector2(edge, along); yaw = PI * .5 - 2: at = Vector2(-along, edge); yaw = PI - _: at = Vector2(-edge, -along); yaw = PI * 1.5 - transforms.append(Transform3D(Basis(Vector3.UP, yaw), to3(at))) - _multimesh("EdgeCliff", transforms) +## Keeps lanes, bases, towers, the grotto, the platform, the river and open water clear. +func _plantable(p: Vector2) -> bool: + if maxf(absf(p.x), absf(p.y)) > 50.0: + return false + var lane: Array = terrain.nearest_lane(p) + if float(lane[0]) < float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + 1.9: + return false + for team: int in range(2): + if p.distance_to(map.core_position(team)) < LuneTerrain.BASE_RADIUS + 1.8: + return false + if p.distance_to(LuneMap.GROTTO) < LuneTerrain.ARENA_RADIUS + 4.6 or p.length() < LuneTerrain.PLATFORM_RADIUS + 3.0: + return false + if p.distance_to(Vector2(-31.0, -31.0)) < 4.2: + return false + var u: float = LuneTerrain.axis_u(p) + if LuneTerrain.axis_a(p) < terrain.river_half_width(u) + 1.6: + return false + if terrain.height_at(p) < LuneTerrain.WATER_LEVEL + .12: + return false + for definition: Dictionary in map.buildings: + if Vector2(definition.position).distance_to(p) < 4.2: + return false + return true +func _bank_clear(p: Vector2) -> bool: + if p.length() < LuneTerrain.PLATFORM_RADIUS + 1.5 or p.distance_to(delta_point()) < 8.5: + return false + var lane: Array = terrain.nearest_lane(p) + return float(lane[0]) > float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + .8 + +# ---------------------------------------------------------------- props var _vertex_colored: StandardMaterial3D +## Prop meshes from the Blender generator, with the Kluft shaders applied per surface. +func _load_props() -> void: + var scene: Node3D = (load(PROPS) as PackedScene).instantiate() + for child: Node in scene.get_children(): + if child is MeshInstance3D: + var mesh: ArrayMesh = (child as MeshInstance3D).mesh.duplicate() + for index: int in range(mesh.get_surface_count()): + var source := mesh.surface_get_material(index) as StandardMaterial3D + if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"): + mesh.surface_set_material(index, surface(source.resource_name, source.albedo_color)) + _props[str(child.name)] = mesh + scene.free() + func _dress(node: Node) -> void: if node is MeshInstance3D: - for surface: int in range(node.mesh.get_surface_count()): - var source := node.mesh.surface_get_material(surface) as StandardMaterial3D + for index: int in range(node.mesh.get_surface_count()): + var source := node.mesh.surface_get_material(index) as StandardMaterial3D if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"): - node.set_surface_override_material(surface, terrain(source.resource_name, source.albedo_color)) + node.set_surface_override_material(index, surface(source.resource_name, source.albedo_color)) elif source != null and source.resource_name.begins_with("CV_"): # Compare-pack props carry their colours as vertex colours. if _vertex_colored == null: _vertex_colored = StandardMaterial3D.new() _vertex_colored.vertex_color_use_as_albedo = true _vertex_colored.roughness = .86 - node.set_surface_override_material(surface, _vertex_colored) + node.set_surface_override_material(index, _vertex_colored) for child: Node in node.get_children(): _dress(child) +# ---------------------------------------------------------------- grotto +## Blockout of the boss grotto: the terrace arena in front of the escarpment, the rock +## arc as cave mouth, a ring of pillars around the back, cliffs for the silhouette, +## one stair per team up to its entrance and the spring falling towards Mid. +func _grotto() -> void: + var center: Vector2 = LuneMap.GROTTO + var y: float = LuneTerrain.ARENA_HEIGHT + var arena := MeshInstance3D.new() + arena.name = "GrottoArena" + var disc := CylinderMesh.new() + disc.top_radius = LuneTerrain.ARENA_RADIUS - .25 + disc.bottom_radius = LuneTerrain.ARENA_RADIUS + disc.height = .1 + disc.radial_segments = 56 + arena.mesh = disc + arena.position = Vector3(center.x, y + .02, center.y) + arena.material_override = surface("ENV_paving_arena", Color(.4, .37, .42)) + add_child(arena) + _grotto_runes = _glow(GROTTO_COLOR, 1.0) + var runes := MeshInstance3D.new() + var ring := TorusMesh.new() + ring.inner_radius = 4.3 + ring.outer_radius = 4.6 + ring.rings = 64 + runes.mesh = ring + runes.scale = Vector3(1, .12, 1) + runes.position = Vector3(center.x, y + .09, center.y) + runes.material_override = _grotto_runes + runes.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(runes) + var cave := MeshInstance3D.new() + cave.name = "BossGrotto" + cave.mesh = _cave_mesh() + cave.scale = Vector3.ONE * 1.05 + cave.position = Vector3(center.x, y - .15, center.y) - Vector3(LuneMap.AXIS.x, 0, LuneMap.AXIS.y) * 1.2 + _dress(cave) + add_child(cave) + var pillars: Array[Transform3D] = [] + var caps: Array[Transform3D] = [] + # Pillars around the back half; the front towards Mid stays open for stairs and spring. + var back: float = atan2(-LuneMap.AXIS.y, -LuneMap.AXIS.x) + for index: int in range(11): + var angle: float = back + deg_to_rad(-120.0 + 24.0 * index) + var at: Vector2 = center + Vector2(cos(angle), sin(angle)) * (LuneTerrain.ARENA_RADIUS - .15) + pillars.append(Transform3D(Basis.IDENTITY, Vector3(at.x, y + 1.6, at.y))) + caps.append(Transform3D(Basis(Vector3.UP, angle), Vector3(at.x, y + 3.45, at.y))) + var pillar := CylinderMesh.new() + pillar.top_radius = .36 + pillar.bottom_radius = .46 + pillar.height = 3.2 + pillar.radial_segments = 10 + _multi(pillar, surface("ENV_limestone_grotto", Color(.46, .43, .47)), pillars, "GrottoPillars") + var cap := SphereMesh.new() + cap.radius = .34 + cap.height = .9 + cap.radial_segments = 6 + cap.rings = 2 + _multi(cap, _grotto_runes, caps, "GrottoCrystals", false) + # Cliffs behind the arena give the grotto its silhouette from the game camera; any + # piece that would reach into a lane corridor is left out. + var cliffs: Array[Transform3D] = [] + for step: int in range(-1, 2): + var angle: float = back + step * deg_to_rad(38.0) + var at: Vector2 = center + Vector2(cos(angle), sin(angle)) * 9.6 + var inward: Vector2 = (center - at).normalized() + if _clear_of_lanes(at, 5.2): + cliffs.append(Transform3D(_yaw_basis(_facing(inward), Vector3(.62, 2.2 - absf(step) * .35, 1.1)), _ground3(at, -1.3))) + _prop_multi("EdgeCliff", cliffs) + var rocks: Array[Transform3D] = [] + for spec: Vector3 in [Vector3(-9.0, 5.8, 2.2), Vector3(-10.8, 0.0, 2.6), Vector3(-9.0, -5.8, 2.2), Vector3(-4.5, 8.8, 1.6), Vector3(-4.5, -8.8, 1.6)]: + var at: Vector2 = center + LuneMap.AXIS * spec.x + LuneMap.BLUE_SIDE * spec.y + if _clear_of_lanes(at, spec.z * 1.4): + rocks.append(Transform3D(_yaw_basis(spec.y * .4, Vector3.ONE * spec.z), _ground3(at, -.3))) + _prop_multi("Rock_1", rocks) + # One stair per team from the valley up to its entrance. + var steps: Array[Transform3D] = [] + for team: int in range(2): + var entry: Vector2 = map.grotto_entry(team) + var outward: Vector2 = (entry - center).normalized() + for k: int in range(5): + var at: Vector2 = center + outward * (LuneTerrain.ARENA_RADIUS + .15 + k * .72) + steps.append(Transform3D(_yaw_basis(_facing(outward)), _ground3(at, -.08))) + var stair := BoxMesh.new() + stair.size = Vector3(3.0, .34, .78) + _multi(stair, surface("ENV_limestone_stair", Color(.5, .47, .44)), steps, "GrottoStairs") + grotto_light = OmniLight3D.new() + grotto_light.position = Vector3(center.x, y + 2.6, center.y) - Vector3(LuneMap.AXIS.x, 0, LuneMap.AXIS.y) * 3.0 + grotto_light.light_color = GROTTO_COLOR + grotto_light.light_energy = 2.4 + grotto_light.omni_range = 12.0 + add_child(grotto_light) + +## True if a prop of the given radius at p stays outside every lane corridor. +func _clear_of_lanes(p: Vector2, radius: float) -> bool: + var lane: Array = terrain.nearest_lane(p) + return float(lane[0]) > float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + radius + +## The M2 grotto model without its floor disc and loose boulders (the G1 arena has its +## own floor): only the rock arc, the dark cave and the crystals remain. +func _cave_mesh() -> ArrayMesh: + var scene: Node3D = (load(GROTTO_SCENE) as PackedScene).instantiate() + var source: Mesh = (scene.get_node("BossGrotto") as MeshInstance3D).mesh + var mesh := ArrayMesh.new() + for index: int in range(source.get_surface_count()): + var material: Material = source.surface_get_material(index) + var key: String = material.resource_name if material != null else "" + if key in ["ENV_soil", "ENV_limestone"]: + continue + mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source.surface_get_arrays(index)) + mesh.surface_set_material(mesh.get_surface_count() - 1, material) + scene.free() + return mesh + +## Grotto glow: violet when open, dim when locked, team colour while a claim is pending. +func sync_grotto(available: bool, claim_team: int) -> void: + if grotto_light == null: + return + var color: Color = TEAM_COLORS[claim_team] if claim_team >= 0 else GROTTO_COLOR + var lit: bool = available or claim_team >= 0 + grotto_light.light_color = color + grotto_light.light_energy = 2.4 if lit else .5 + _grotto_runes.emission = color + _grotto_runes.albedo_color = color + _grotto_runes.emission_energy_multiplier = 1.2 if lit else .15 + +# ---------------------------------------------------------------- deployment areas +## One pad per team and lane where the army forms up (rows of four, front row ahead), +## and one ring per team at its grotto entrance. Shown during planning only. +func _deployment_pads() -> void: + var span: Dictionary = map.deployment_span() + var start: float = maxf(0.0, float(span.from) - .4) + var finish: float = float(span.to) + .4 + var half: float = float(span.half_width) + for team: int in range(2): + for lane: String in LuneMap.LANES: + var tool := SurfaceTool.new() + tool.begin(Mesh.PRIMITIVE_TRIANGLES) + var previous: Array = [] + var progress: float = start + while progress <= finish + 0.001: + var left: Vector2 = map.lane_point(team, lane, progress, half) + var right: Vector2 = map.lane_point(team, lane, progress, -half) + var y: float = terrain.walk_height(map.lane_point(team, lane, progress)) + .09 + var section: Array = [[Vector3(left.x, y, left.y), Vector2(1.0, progress - start)], [Vector3(right.x, y, right.y), Vector2(-1.0, progress - start)]] + if not previous.is_empty(): + for corner: Array in [previous[0], section[1], section[0], previous[0], previous[1], section[1]]: + tool.set_uv(corner[1]) + tool.add_vertex(corner[0]) + previous = section + progress += .5 + var pad := MeshInstance3D.new() + pad.name = "Deployment_%d_%s" % [team, lane] + pad.mesh = tool.commit() + var material := ShaderMaterial.new() + material.shader = PadShader + material.set_shader_parameter("tint", TEAM_COLORS[team]) + material.set_shader_parameter("pad_length", finish - start) + material.set_shader_parameter("row_start", float(span.to) - start) + pad.material_override = material + pad.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + pad.visible = false + add_child(pad) + _pads["%d:%s" % [team, lane]] = pad + var ring := MeshInstance3D.new() + ring.name = "GrottoEntry_%d" % team + var torus := TorusMesh.new() + torus.inner_radius = 1.9 + torus.outer_radius = 2.15 + torus.rings = 40 + ring.mesh = torus + ring.scale = Vector3(1, .15, 1) + var entry: Vector2 = map.grotto_entry(team) + ring.position = Vector3(entry.x, LuneTerrain.ARENA_HEIGHT + .12, entry.y) + ring.material_override = _glow(TEAM_COLORS[team], 1.6) + ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + ring.visible = false + add_child(ring) + _grotto_pads[team] = ring + +## Planning shows the own team's formation areas; lanes with own units are highlighted. +func show_deployment(visible_now: bool, team: int, used: Dictionary, grotto_open: bool) -> void: + for key: String in _pads: + var pad: MeshInstance3D = _pads[key] + var parts: PackedStringArray = key.split(":") + pad.visible = visible_now and int(parts[0]) == team + if pad.visible: + (pad.material_override as ShaderMaterial).set_shader_parameter("highlight", 1.0 if int(used.get(parts[1], 0)) > 0 else 0.0) + for side: int in _grotto_pads: + (_grotto_pads[side] as MeshInstance3D).visible = visible_now and grotto_open and side == team + +# ---------------------------------------------------------------- buildings func _building(definition: Dictionary) -> void: var root := Node3D.new() root.name = str(definition.id) @@ -379,10 +847,10 @@ func _building(definition: Dictionary) -> void: cloth.albedo_color = TEAM_COLORS[team] cloth.cull_mode = BaseMaterial3D.CULL_DISABLED cloth.roughness = .8 - for surface: int in range(banner.mesh.get_surface_count()): - var source: Material = banner.mesh.surface_get_material(surface) + for index: int in range(banner.mesh.get_surface_count()): + var source: Material = banner.mesh.surface_get_material(index) if source != null and source.resource_name == "PR_Banner": - banner.set_surface_override_material(surface, cloth) + banner.set_surface_override_material(index, cloth) var toward: Vector2 = (map.core_position(1 - team) - map.core_position(team)).normalized() var across := Vector2(-toward.y, toward.x) * side * 3.6 banner.position = Vector3(across.x, 0, across.y) + Vector3(toward.x, 0, toward.y) * 2.4 @@ -409,10 +877,10 @@ func _building(definition: Dictionary) -> void: func _tint_crystal(node: Node, crystal: StandardMaterial3D) -> void: if node is MeshInstance3D: - for surface: int in range(node.mesh.get_surface_count()): - var source: Material = node.mesh.surface_get_material(surface) + for index: int in range(node.mesh.get_surface_count()): + var source: Material = node.mesh.surface_get_material(index) if source != null and source.resource_name == "TW_Crystal": - node.set_surface_override_material(surface, crystal) + node.set_surface_override_material(index, crystal) for child: Node in node.get_children(): _tint_crystal(child, crystal) diff --git a/tools/luneraria/g_views.gd b/tools/luneraria/g_views.gd new file mode 100644 index 0000000..b51c10a --- /dev/null +++ b/tools/luneraria/g_views.gd @@ -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() diff --git a/tools/luneraria/g_views.gd.uid b/tools/luneraria/g_views.gd.uid new file mode 100644 index 0000000..54a87fe --- /dev/null +++ b/tools/luneraria/g_views.gd.uid @@ -0,0 +1 @@ +uid://cwt2eokernmvs diff --git a/tools/luneraria/g_views.tscn b/tools/luneraria/g_views.tscn new file mode 100644 index 0000000..8926d71 --- /dev/null +++ b/tools/luneraria/g_views.tscn @@ -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") diff --git a/tools/luneraria/lune_benchmark.gd b/tools/luneraria/lune_benchmark.gd index 3f386e9..c92a3e6 100644 --- a/tools/luneraria/lune_benchmark.gd +++ b/tools/luneraria/lune_benchmark.gd @@ -1,11 +1,69 @@ extends Node ## 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 -## fixture only). VSync off; figures apply to this machine only. -## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn +## six armies at the supply cap and a round-13 wave on all three lanes (synthetic fixture, +## gold granted to the fixture only). Two camera setups are measured one after the other: +## 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 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: + 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") + +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: Soundscape.enabled = false DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) @@ -21,27 +79,12 @@ func run() -> void: state.players[slot].draft.roster.clear() LuneBot.plan(state, slot, "ausgewogen") 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) - 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 - while Time.get_ticks_msec() < end and state.phase == LuneMatch.Phase.COMBAT: - var s0: int = Time.get_ticks_usec() - var before: int = state.combat.steps - await RenderingServer.frame_post_draw - 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()) + game.camera.look_at_point(Vector3(-10, 0, 10), 34.0, true) + var lane_view: Dictionary = await measure(game, state) + game.camera.overview(true) + var overview: Dictionary = await measure(game, state) # Pure simulation cost of one fixed step at this occupation, measured separately. var probe := LuneMatch.new() probe.setup({"seed": 5, "human_slot": -1}) @@ -52,22 +95,19 @@ func run() -> void: probe.begin_combat() var start_units: int = probe.combat.mobile_count() var s1: int = Time.get_ticks_usec() - for i in 200: + for i: int in 200: probe.step_combat() 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(), "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, - "frames": frames.size(), "frame_ms_mean": mean.call(frames), "frame_ms_p95": frames[int(frames.size() * .95)] if frames.size() > 0 else -1, - "gpu_ms_mean": mean.call(gpu), "draw_calls_mean": mean.call(draws), "sim_step_ms": step_ms, "sim_speed": "2x (40 steps/s)", + "mobile_units_at_start": start_units, "buildings": 18, "sim_step_ms": step_ms, "sim_speed": "2x (40 steps/s)", + "lane_view": lane_view, "overview": overview, "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} - 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.close() print("LUNE_BENCH ", JSON.stringify(report)) diff --git a/tools/luneraria/net_pair.gd b/tools/luneraria/net_pair.gd index c989b3e..6641ce6 100644 --- a/tools/luneraria/net_pair.gd +++ b/tools/luneraria/net_pair.gd @@ -137,7 +137,8 @@ func _process(_delta: float) -> void: if state.phase != LuneMatch.Phase.COMBAT: break 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]) session.close() get_tree().quit(0) diff --git a/tools/luneraria/verify_g1.gd b/tools/luneraria/verify_g1.gd new file mode 100644 index 0000000..8f23ff5 --- /dev/null +++ b/tools/luneraria/verify_g1.gd @@ -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)) diff --git a/tools/luneraria/verify_g1.gd.uid b/tools/luneraria/verify_g1.gd.uid new file mode 100644 index 0000000..76493ee --- /dev/null +++ b/tools/luneraria/verify_g1.gd.uid @@ -0,0 +1 @@ +uid://c36slssmwrcrh diff --git a/tools/luneraria/verify_g1.tscn b/tools/luneraria/verify_g1.tscn new file mode 100644 index 0000000..11de1a4 --- /dev/null +++ b/tools/luneraria/verify_g1.tscn @@ -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")