G1: Drei-Lane-Layout mit Höhenfeld, Grotte, Aufmarschbereichen und MOBA-Kamera
Top windet sich über das Hochland, Mid quert die Kluft über die Mittelplattform, Bottom führt durch das Tiefland über die Deltabrücke; Top und Bottom sind exakt gleich lang, alles bleibt spiegelsymmetrisch. LuneTerrain liefert ein exakt gespiegeltes Höhenfeld, auf dem Figuren stehen. Bossgrotte als Terrasse auf der Achse zwischen Top und Mid, Aufmarschbereiche je Lane, perspektivische Kamera mit Sprungtasten (F2–F4, B, G, H, O, R, Q/E). Bestandsassets wiederverwendet, neue Formen als Blockout für G2/G4. Geprüft: verify_g1 443/443, Regeln 91/91, M2 73/73, verify_m1 38/38, verify_m2 14/14, Netz 43/43, Loopback 12/12, Balance 40 Partien (Seitentausch 20/20), Legacy 209/463. Vorher/Nachher-Aufnahmen und Messungen in G1-KARTE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
182
docs/luneraria/G1-KARTE.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# G1 · Map-Blockout und spielerisches Drei-Lane-Layout
|
||||
|
||||
Stand: 29. September 2026 · Branch `luneraria/g1` (von `luneraria/m4`, Commit `c155338`).
|
||||
Grundlage: Grafikauftrag vom 28. September 2026, Konzeptbild `assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png` als visuelle Richtung (keine Vorlage zum Kopieren).
|
||||
|
||||

|
||||
|
||||
*Kartenplan: 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.
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
6
docs/luneraria/logs/g1/balance-summary.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Partien 40 · Ergebnisse {'blau': 20, 'rot': 20} · Testlimit 0 · Zeitfenster-Kämpfe 4 · längster Kampf 180 s
|
||||
Wellen bis Ende: min 9 · median 11 · max 13
|
||||
Seitentausch: dieselbe Zusammensetzung gewinnt beide Male in 20/20 Paaren
|
||||
Profile (Siege/Einsätze): {'ausgewogen': '30/48 (62 %)', 'bossjagd': '8/34 (24 %)', 'druck': '20/34 (59 %)', 'fernkampf': '26/48 (54 %)', 'teamhilfe': '18/48 (38 %)', 'verteidigung': '18/28 (64 %)'}
|
||||
Bünde: {'glutpakt': '66/124 (53 %)', 'wurzelbund': '54/116 (47 %)'}
|
||||
Boss: {'won': 20, 'deployed': 12} · Heldenstufen am Ende (Median): 4
|
||||
1756
docs/luneraria/logs/g1/balance.json
Normal file
26
docs/luneraria/logs/g1/balance.log
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||
|
||||
BALANCE 00 ["ausgewogen", "druck", "teamhilfe"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||
BALANCE 01 ["bossjagd", "bossjagd", "teamhilfe"] | ["fernkampf", "teamhilfe", "fernkampf"] -> rot (9) / swapped blau (9)
|
||||
BALANCE 02 ["bossjagd", "ausgewogen", "teamhilfe"] | ["druck", "ausgewogen", "verteidigung"] -> rot (11) / swapped blau (11)
|
||||
BALANCE 03 ["fernkampf", "druck", "ausgewogen"] | ["druck", "fernkampf", "verteidigung"] -> blau (12) / swapped rot (12)
|
||||
BALANCE 04 ["ausgewogen", "teamhilfe", "fernkampf"] | ["ausgewogen", "verteidigung", "fernkampf"] -> blau (11) / swapped rot (11)
|
||||
BALANCE 05 ["teamhilfe", "fernkampf", "verteidigung"] | ["fernkampf", "bossjagd", "bossjagd"] -> blau (11) / swapped rot (11)
|
||||
BALANCE 06 ["bossjagd", "ausgewogen", "teamhilfe"] | ["fernkampf", "teamhilfe", "ausgewogen"] -> blau (9) / swapped rot (9)
|
||||
BALANCE 07 ["ausgewogen", "ausgewogen", "ausgewogen"] | ["teamhilfe", "teamhilfe", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||
BALANCE 08 ["teamhilfe", "ausgewogen", "bossjagd"] | ["druck", "ausgewogen", "druck"] -> rot (12) / swapped blau (12)
|
||||
BALANCE 09 ["verteidigung", "druck", "druck"] | ["bossjagd", "ausgewogen", "teamhilfe"] -> blau (11) / swapped rot (11)
|
||||
BALANCE 10 ["druck", "ausgewogen", "ausgewogen"] | ["teamhilfe", "bossjagd", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||
BALANCE 11 ["bossjagd", "fernkampf", "fernkampf"] | ["teamhilfe", "verteidigung", "teamhilfe"] -> rot (10) / swapped blau (10)
|
||||
BALANCE 12 ["fernkampf", "fernkampf", "teamhilfe"] | ["teamhilfe", "ausgewogen", "fernkampf"] -> rot (12) / swapped blau (12)
|
||||
BALANCE 13 ["teamhilfe", "druck", "fernkampf"] | ["fernkampf", "druck", "ausgewogen"] -> rot (13) / swapped blau (13)
|
||||
BALANCE 14 ["bossjagd", "druck", "fernkampf"] | ["bossjagd", "bossjagd", "bossjagd"] -> blau (10) / swapped rot (10)
|
||||
BALANCE 15 ["fernkampf", "fernkampf", "verteidigung"] | ["druck", "fernkampf", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||
BALANCE 16 ["teamhilfe", "verteidigung", "verteidigung"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> rot (13) / swapped blau (13)
|
||||
BALANCE 17 ["fernkampf", "verteidigung", "fernkampf"] | ["druck", "fernkampf", "bossjagd"] -> blau (10) / swapped rot (10)
|
||||
BALANCE 18 ["verteidigung", "ausgewogen", "bossjagd"] | ["druck", "teamhilfe", "druck"] -> blau (9) / swapped rot (9)
|
||||
BALANCE 19 ["druck", "teamhilfe", "teamhilfe"] | ["fernkampf", "verteidigung", "bossjagd"] -> rot (10) / swapped blau (10)
|
||||
WARNING: 38 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||
at: cleanup (core/object/object.cpp:2536)
|
||||
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||
at: clear (core/io/resource.cpp:822)
|
||||
39
docs/luneraria/logs/g1/perf-nachher-1920x1080.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"buildings": 18,
|
||||
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||
"engine": "4.7.2-stable (fedora)",
|
||||
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||
"lane_view": {
|
||||
"camera_zoom": 34.0,
|
||||
"cpu_frame_ms_mean": 6.82015972222222,
|
||||
"draw_calls_mean": 3340.02083333333,
|
||||
"frame_ms_mean": 6.94640740740741,
|
||||
"frame_ms_p95": 9.656,
|
||||
"frames": 864,
|
||||
"gpu_ms_mean": 3.29306944790814,
|
||||
"mobile_units_mean": 117.75462962963,
|
||||
"objects_mean": 7437.95949074074,
|
||||
"primitives_mean": 16302012.3611111
|
||||
},
|
||||
"mobile_units_at_start": 120,
|
||||
"overview": {
|
||||
"camera_zoom": 92.0,
|
||||
"cpu_frame_ms_mean": 6.84469489559164,
|
||||
"draw_calls_mean": 3604.76450116009,
|
||||
"frame_ms_mean": 6.9669071925754,
|
||||
"frame_ms_p95": 9.283,
|
||||
"frames": 862,
|
||||
"gpu_ms_mean": 3.51868097826389,
|
||||
"mobile_units_mean": 96.8259860788863,
|
||||
"objects_mean": 8162.20765661253,
|
||||
"primitives_mean": 16845863.4895592
|
||||
},
|
||||
"renderer": "forward_plus",
|
||||
"scene_build_ms": 293.127,
|
||||
"sim_speed": "2x (40 steps/s)",
|
||||
"sim_step_ms": 2.71302,
|
||||
"static_mem_mb": 128.053088188171,
|
||||
"texture_mem_mb": 424.65576171875,
|
||||
"video_mem_mb": 782.140380859375,
|
||||
"window": "(1920, 1080)"
|
||||
}
|
||||
39
docs/luneraria/logs/g1/perf-vorher-1920x1080.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"buildings": 18,
|
||||
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||
"engine": "4.7.2-stable (fedora)",
|
||||
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||
"lane_view": {
|
||||
"camera_zoom": 34.0,
|
||||
"cpu_frame_ms_mean": 6.03641128205129,
|
||||
"draw_calls_mean": 3608.65641025641,
|
||||
"frame_ms_mean": 6.15782974358974,
|
||||
"frame_ms_p95": 8.584,
|
||||
"frames": 975,
|
||||
"gpu_ms_mean": 3.16637640928611,
|
||||
"mobile_units_mean": 117.465641025641,
|
||||
"objects_mean": 7059.66871794872,
|
||||
"primitives_mean": 14455054.2379487
|
||||
},
|
||||
"mobile_units_at_start": 120,
|
||||
"overview": {
|
||||
"camera_zoom": 46.0,
|
||||
"cpu_frame_ms_mean": 7.17415674362089,
|
||||
"draw_calls_mean": 5447.83110571081,
|
||||
"frame_ms_mean": 7.29744106925881,
|
||||
"frame_ms_p95": 9.307,
|
||||
"frames": 823,
|
||||
"gpu_ms_mean": 4.11414337940378,
|
||||
"mobile_units_mean": 97.5261239368165,
|
||||
"objects_mean": 10639.1822600243,
|
||||
"primitives_mean": 27029403.7618469
|
||||
},
|
||||
"renderer": "forward_plus",
|
||||
"scene_build_ms": 179.527,
|
||||
"sim_speed": "2x (40 steps/s)",
|
||||
"sim_step_ms": 2.53624,
|
||||
"static_mem_mb": 126.140163421631,
|
||||
"texture_mem_mb": 424.65576171875,
|
||||
"video_mem_mb": 780.451141357422,
|
||||
"window": "(1920, 1080)"
|
||||
}
|
||||
40
docs/luneraria/logs/g1/verify-g1.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"checks": 443,
|
||||
"facts": {
|
||||
"grotto_clearance_to_lanes": 3.0,
|
||||
"lane_height_range": {
|
||||
"max": {
|
||||
"bot": 0.600000023841858,
|
||||
"mid": 0.620000004768372,
|
||||
"top": 3.29657602310181
|
||||
},
|
||||
"min": {
|
||||
"bot": -0.159999996423721,
|
||||
"mid": 0.114149838685989,
|
||||
"top": 0.081579864025116
|
||||
}
|
||||
},
|
||||
"lane_lengths": {
|
||||
"bot": 96.82,
|
||||
"mid": 61.16,
|
||||
"top": 96.82
|
||||
},
|
||||
"overview": {
|
||||
"distance": 150.459213256836,
|
||||
"pitch_deg": -69.4643032570342,
|
||||
"zoom": 92.0
|
||||
},
|
||||
"pitch_range_deg": [
|
||||
-48.000003795596,
|
||||
-69.9999992757703
|
||||
],
|
||||
"terrain_mirror_error": 0.0,
|
||||
"unit_ground_error": 0.0000000882349220354683,
|
||||
"water_points_under_lanes": {
|
||||
"bot": 16,
|
||||
"mid": 8,
|
||||
"top": 0
|
||||
}
|
||||
},
|
||||
"failures": []
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/01-karte.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
40
docs/luneraria/screenshots/g1/nachher/01-karte.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://lr03js5nblhl"
|
||||
path="res://.godot/imported/01-karte.png-96789aba0ff604bcb6f965065a48845a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/nachher/01-karte.png"
|
||||
dest_files=["res://.godot/imported/01-karte.png-96789aba0ff604bcb6f965065a48845a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/02-top-lane.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
40
docs/luneraria/screenshots/g1/nachher/02-top-lane.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bcanui88p8qyc"
|
||||
path="res://.godot/imported/02-top-lane.png-c3561a8df8623617cb94592961dfe916.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/nachher/02-top-lane.png"
|
||||
dest_files=["res://.godot/imported/02-top-lane.png-c3561a8df8623617cb94592961dfe916.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/03-mid-lane.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
40
docs/luneraria/screenshots/g1/nachher/03-mid-lane.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3vim0lytocpb"
|
||||
path="res://.godot/imported/03-mid-lane.png-5129bdbfec756167ee92071ec74b2274.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/nachher/03-mid-lane.png"
|
||||
dest_files=["res://.godot/imported/03-mid-lane.png-5129bdbfec756167ee92071ec74b2274.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/04-bottom-lane.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/05-eigene-basis.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/06-gegnerbasis.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/07-bossgrotte.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/09-hud-planung.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/10-held-ausgewaehlt.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
40
docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dwx6yu7ush063"
|
||||
path="res://.godot/imported/11-zoom-nah.png-9775410ebc7c8ad8d576b96dfb83ba23.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/nachher/11-zoom-nah.png"
|
||||
dest_files=["res://.godot/imported/11-zoom-nah.png-9775410ebc7c8ad8d576b96dfb83ba23.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/12-kamera-bewegt.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/nachher/13-grottenkampf.png
Normal file
|
After Width: | Height: | Size: 2 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/01-karte.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/g1/vorher/01-karte.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://rj2lj35pqufx"
|
||||
path="res://.godot/imported/01-karte.png-ce4b231a58bbbc9069487eaf7430f901.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/vorher/01-karte.png"
|
||||
dest_files=["res://.godot/imported/01-karte.png-ce4b231a58bbbc9069487eaf7430f901.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/02-top-lane.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/g1/vorher/02-top-lane.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://blhy7juf6j0jm"
|
||||
path="res://.godot/imported/02-top-lane.png-b101771ab0695a9bdc8da72b4c740099.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/vorher/02-top-lane.png"
|
||||
dest_files=["res://.godot/imported/02-top-lane.png-b101771ab0695a9bdc8da72b4c740099.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/03-mid-lane.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/g1/vorher/03-mid-lane.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bh2uayb61s3sg"
|
||||
path="res://.godot/imported/03-mid-lane.png-6efe91ed0b4f9be3587e80ead94d8841.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/vorher/03-mid-lane.png"
|
||||
dest_files=["res://.godot/imported/03-mid-lane.png-6efe91ed0b4f9be3587e80ead94d8841.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/04-bottom-lane.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/05-eigene-basis.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/06-gegnerbasis.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/07-bossgrotte.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -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
|
||||
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/09-hud-planung.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/10-held-ausgewaehlt.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
40
docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://70eokr0x40en"
|
||||
path="res://.godot/imported/11-zoom-nah.png-8354307c8a1c11f60b3960ad46f68321.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/g1/vorher/11-zoom-nah.png"
|
||||
dest_files=["res://.godot/imported/11-zoom-nah.png-8354307c8a1c11f60b3960ad46f68321.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/12-kamera-bewegt.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -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
|
||||
BIN
docs/luneraria/screenshots/g1/vorher/13-grottenkampf.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -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
|
||||
40
docs/luneraria/screenshots/m4/net-guest-combat-r1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dt8ip3xno5rgg"
|
||||
path="res://.godot/imported/net-guest-combat-r1.png-097b5ce28024fe9324574e91a83bc90a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/m4/net-guest-combat-r1.png"
|
||||
dest_files=["res://.godot/imported/net-guest-combat-r1.png-097b5ce28024fe9324574e91a83bc90a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
docs/luneraria/screenshots/m4/net-guest-combat-r2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c6i6iqo818gs2"
|
||||
path="res://.godot/imported/net-guest-combat-r2.png-b1c78cb66d1b4a660fba5f0ae4e0bba9.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/luneraria/screenshots/m4/net-guest-combat-r2.png"
|
||||
dest_files=["res://.godot/imported/net-guest-combat-r2.png-b1c78cb66d1b4a660fba5f0ae4e0bba9.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 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))
|
||||
return points[points.size() - 1]
|
||||
|
||||
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:
|
||||
var i: int = _segment(clampf(s, 0.0, length))
|
||||
return (points[i] - points[i - 1]).normalized()
|
||||
return Vector2.RIGHT
|
||||
|
||||
## 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 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]
|
||||
return [points[points.size() - 1], Vector2.RIGHT]
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
32
scripts/luneraria/view/lune_pad.gdshader
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Deployment area on a lane: team-coloured border, faint fill and one line per
|
||||
// formation row (1.5 m apart), plus chevrons at the front that point down the lane.
|
||||
// UV.x runs across the pad (-1..1), UV.y along it in metres from the rear edge.
|
||||
shader_type spatial;
|
||||
render_mode unshaded, blend_mix, depth_draw_never, cull_disabled, shadows_disabled;
|
||||
uniform vec4 tint : source_color = vec4(.35, .66, 1.0, 1.0);
|
||||
uniform float pad_length = 7.4;
|
||||
uniform float row_start = 7.2;
|
||||
uniform float row_spacing = 1.5;
|
||||
uniform float strength = 1.0;
|
||||
uniform float highlight = 0.0;
|
||||
void fragment() {
|
||||
float across = abs(UV.x);
|
||||
float along = UV.y;
|
||||
// smoothstep needs edge0 < edge1 (reversed edges are undefined in GLSL).
|
||||
float border = smoothstep(.86, .93, across) + (1.0 - smoothstep(.08, .22, along)) + smoothstep(pad_length - .22, pad_length - .08, along);
|
||||
float rows = 0.0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float at = row_start - float(i) * row_spacing;
|
||||
rows += (1.0 - smoothstep(.0, .05, abs(along - at))) * step(across, .8) * .55;
|
||||
}
|
||||
float chevron = 0.0;
|
||||
float front = along - (pad_length - 1.4);
|
||||
if (front > 0.0 && front < 1.0) {
|
||||
chevron = (1.0 - smoothstep(.0, .12, abs(front - (1.0 - across) * .7 - .15))) * step(across, .55);
|
||||
}
|
||||
float pulse = .75 + .25 * sin(TIME * 2.4);
|
||||
float alpha = clamp(.035 + .045 * highlight + border * .5 + rows * .5 + chevron * .7, 0.0, 1.0);
|
||||
// Unshaded colour stays at or below 1, so the pads never trigger bloom.
|
||||
ALBEDO = min(tint.rgb * (.8 + .2 * highlight * pulse), vec3(1.0));
|
||||
ALPHA = alpha * strength * (.8 + .2 * pulse * highlight);
|
||||
}
|
||||
1
scripts/luneraria/view/lune_pad.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bwn3ulxjiwybb
|
||||
419
scripts/luneraria/view/lune_terrain.gd
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
class_name LuneTerrain
|
||||
extends RefCounted
|
||||
## Visual height field of the G1 map, derived from LuneMap alone. Every term depends on
|
||||
## the axis coordinate u = (x + z) / √2 and the distance a = |z - x| / √2 from the axis,
|
||||
## or on symmetric map features, so the ground is the exact mirror image across x = z.
|
||||
## The simulation stays two-dimensional: views read walk_height() to stand units on it.
|
||||
##
|
||||
## Regions: the north-western highland (Top) behind an escarpment, the central valley
|
||||
## with the Kluft river on the axis, the south-eastern lowland (Bottom) with the delta,
|
||||
## base plateaus, the grotto terrace and a ring of mountains around the playable square.
|
||||
|
||||
const EXTENT: float = 60.0
|
||||
const STEP: float = 1.0
|
||||
## Coarse outer ring (mountains and the river outlet) around the fine grid.
|
||||
const OUTER_EXTENT: float = 132.0
|
||||
const OUTER_STEP: float = 4.0
|
||||
const OUTER_HOLE: float = 56.0
|
||||
const WATER_LEVEL: float = -0.35
|
||||
const BED_LEVEL: float = -1.2
|
||||
const BASE_HEIGHT: float = 0.6
|
||||
const BASE_RADIUS: float = 10.6
|
||||
const HIGHLAND: float = 3.0
|
||||
const VALLEY: float = 0.18
|
||||
const LOWLAND: float = -0.1
|
||||
const ARENA_HEIGHT: float = 1.7
|
||||
const ARENA_RADIUS: float = 7.8
|
||||
const PLATFORM_HEIGHT: float = 0.62
|
||||
const PLATFORM_RADIUS: float = 6.6
|
||||
const FORD_HEIGHT: float = -0.16
|
||||
## Lane corridor half widths (flat ground under the paving) per lane.
|
||||
const CORRIDOR: Dictionary = {"top": 3.6, "mid": 4.5, "bot": 3.6}
|
||||
const BLEND: float = 5.0
|
||||
## River half width along the axis coordinate u (piecewise linear).
|
||||
const RIVER_WIDTH: Array[Vector2] = [Vector2(-11.6, 1.6), Vector2(-9.0, 2.4), Vector2(-4.0, 3.1), Vector2(0.0, 3.5),
|
||||
Vector2(12.0, 3.0), Vector2(26.0, 3.5), Vector2(32.0, 5.0), Vector2(36.5, 7.2), Vector2(42.0, 13.0), Vector2(60.0, 20.0)]
|
||||
const RIVER_START: float = -11.6
|
||||
|
||||
var map: LuneMap
|
||||
var size: int = 0
|
||||
var ground := PackedFloat32Array()
|
||||
var walk := PackedFloat32Array()
|
||||
var colors := PackedColorArray()
|
||||
## lane -> PackedFloat32Array of the lane surface height per metre of lane coordinate.
|
||||
var lane_profiles: Dictionary = {}
|
||||
var _noise := FastNoiseLite.new()
|
||||
var _detail := FastNoiseLite.new()
|
||||
var _segments: Array = []
|
||||
var _buckets: Dictionary = {}
|
||||
## [position, height] of every tower: a flat pad at the height of the lane beside it.
|
||||
var _pads: Array = []
|
||||
const BUCKET: float = 6.0
|
||||
|
||||
static var _cache: LuneTerrain
|
||||
|
||||
## One terrain per map layout and process; a rematch reuses it.
|
||||
static func for_map(match_map: LuneMap) -> LuneTerrain:
|
||||
if _cache == null:
|
||||
_cache = LuneTerrain.new()
|
||||
_cache.build(match_map)
|
||||
return _cache
|
||||
|
||||
func build(match_map: LuneMap) -> void:
|
||||
map = match_map
|
||||
_noise.seed = 4417
|
||||
_noise.frequency = 0.045
|
||||
_noise.fractal_octaves = 3
|
||||
_detail.seed = 911
|
||||
_detail.frequency = 0.21
|
||||
_detail.fractal_octaves = 2
|
||||
for lane: String in LuneMap.LANES:
|
||||
lane_profiles[lane] = _lane_profile(lane)
|
||||
_index_segments()
|
||||
for definition: Dictionary in map.buildings:
|
||||
if str(definition.tier) in ["inner", "outer"]:
|
||||
_pads.append([Vector2(definition.position), lane_height(str(definition.lane), map.s_for(int(definition.team), str(definition.lane), float(definition.progress)))])
|
||||
elif str(definition.tier) == "base":
|
||||
_pads.append([Vector2(definition.position), BASE_HEIGHT])
|
||||
var grid: Dictionary = _grid(EXTENT, STEP, 0.0)
|
||||
size = grid.size
|
||||
ground = grid.ground
|
||||
walk = grid.walk
|
||||
colors = grid.colors
|
||||
|
||||
## Evaluates a square grid. Only the Blau side (z >= x) and the axis are computed; the
|
||||
## grid is symmetric under i <-> j, so the Rot side is copied. `hole` skips the interior
|
||||
## (used by the coarse outer ring, which also sinks slightly where it overlaps).
|
||||
func _grid(extent: float, step: float, hole: float) -> Dictionary:
|
||||
var n: int = int(round(extent * 2.0 / step)) + 1
|
||||
var heights := PackedFloat32Array()
|
||||
heights.resize(n * n)
|
||||
var walks := PackedFloat32Array()
|
||||
walks.resize(n * n)
|
||||
var lane_distance := PackedFloat32Array()
|
||||
lane_distance.resize(n * n)
|
||||
var carved := PackedByteArray()
|
||||
carved.resize(n * n)
|
||||
for j: int in range(n):
|
||||
for i: int in range(j + 1):
|
||||
var p := Vector2(-extent + i * step, -extent + j * step)
|
||||
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < hole - step:
|
||||
continue
|
||||
var sample: Array = _evaluate(p)
|
||||
var height: float = sample[0]
|
||||
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < EXTENT + 0.5:
|
||||
height -= 0.5
|
||||
for index: int in [j * n + i, i * n + j]:
|
||||
heights[index] = height
|
||||
walks[index] = sample[1]
|
||||
lane_distance[index] = sample[2]
|
||||
carved[index] = sample[3]
|
||||
var grid_colors := PackedColorArray()
|
||||
grid_colors.resize(n * n)
|
||||
for j: int in range(n):
|
||||
for i: int in range(j + 1):
|
||||
var index: int = j * n + i
|
||||
var left: float = heights[j * n + maxi(0, i - 1)]
|
||||
var right: float = heights[j * n + mini(n - 1, i + 1)]
|
||||
var up: float = heights[maxi(0, j - 1) * n + i]
|
||||
var down: float = heights[mini(n - 1, j + 1) * n + i]
|
||||
var slope: float = Vector2(right - left, down - up).length() / (2.0 * step)
|
||||
var color: Color = _color(Vector2(-extent + i * step, -extent + j * step), heights[index], slope, lane_distance[index], carved[index] == 1)
|
||||
grid_colors[index] = color
|
||||
grid_colors[i * n + j] = color
|
||||
return {"size": n, "extent": extent, "step": step, "hole": hole, "ground": heights, "walk": walks, "colors": grid_colors}
|
||||
|
||||
# ---------------------------------------------------------------- queries
|
||||
static func axis_u(p: Vector2) -> float:
|
||||
return (p.x + p.y) * 0.70710678
|
||||
|
||||
static func axis_a(p: Vector2) -> float:
|
||||
return absf(p.y - p.x) * 0.70710678
|
||||
|
||||
func height_at(p: Vector2) -> float:
|
||||
return _bilinear(ground, p)
|
||||
|
||||
## Height a unit stands on: the lane decks (bridges, ford, platform) instead of the river bed.
|
||||
func walk_height(p: Vector2) -> float:
|
||||
return _bilinear(walk, p)
|
||||
|
||||
func lane_height(lane: String, s: float) -> float:
|
||||
var profile: PackedFloat32Array = lane_profiles[lane]
|
||||
var f: float = clampf(s, 0.0, float(profile.size() - 1))
|
||||
var i: int = mini(int(f), profile.size() - 2)
|
||||
return lerpf(profile[i], profile[i + 1], f - i)
|
||||
|
||||
func river_half_width(u: float) -> float:
|
||||
if u < RIVER_START:
|
||||
return 0.0
|
||||
for k: int in range(1, RIVER_WIDTH.size()):
|
||||
if u <= RIVER_WIDTH[k].x:
|
||||
var a: Vector2 = RIVER_WIDTH[k - 1]
|
||||
var b: Vector2 = RIVER_WIDTH[k]
|
||||
return lerpf(a.y, b.y, clampf((u - a.x) / (b.x - a.x), 0.0, 1.0))
|
||||
return RIVER_WIDTH[RIVER_WIDTH.size() - 1].y
|
||||
|
||||
func is_water(p: Vector2) -> bool:
|
||||
return height_at(p) < WATER_LEVEL - 0.02
|
||||
|
||||
func _bilinear(field: PackedFloat32Array, p: Vector2) -> float:
|
||||
var fx: float = clampf((p.x + EXTENT) / STEP, 0.0, size - 1.001)
|
||||
var fz: float = clampf((p.y + EXTENT) / STEP, 0.0, size - 1.001)
|
||||
var i: int = int(fx)
|
||||
var j: int = int(fz)
|
||||
var tx: float = fx - i
|
||||
var tz: float = fz - j
|
||||
var a: float = lerpf(field[j * size + i], field[j * size + i + 1], tx)
|
||||
var b: float = lerpf(field[(j + 1) * size + i], field[(j + 1) * size + i + 1], tx)
|
||||
return lerpf(a, b, tz)
|
||||
|
||||
# ---------------------------------------------------------------- regions
|
||||
func escarpment(a: float) -> float:
|
||||
return -27.4 + 4.6 * smoothstep(6.0, 20.0, a) + 1.1 * sin(a * 0.43)
|
||||
|
||||
## Ground before lanes, river and arena: highland, valley, lowland, bases, mountains.
|
||||
func _region(p: Vector2) -> float:
|
||||
var u: float = axis_u(p)
|
||||
var a: float = axis_a(p)
|
||||
var edge: float = escarpment(a)
|
||||
var hills: float = _noise.get_noise_2d(u, a)
|
||||
var height: float = lerpf(HIGHLAND + hills * 0.5, VALLEY + hills * 0.35, smoothstep(edge - 1.3, edge + 1.3, u))
|
||||
height = lerpf(height, LOWLAND + hills * 0.22, smoothstep(20.0, 33.0, u))
|
||||
# The lookout hill beyond the Top corner.
|
||||
height += 1.1 * (1.0 - smoothstep(4.0, 11.0, p.distance_to(Vector2(-31.0, -31.0))))
|
||||
for team: int in range(2):
|
||||
var d: float = p.distance_to(map.core_position(team))
|
||||
height = lerpf(BASE_HEIGHT, height, smoothstep(BASE_RADIUS, BASE_RADIUS + 3.0, d))
|
||||
# Mountains around the playable square: a rounded square (superellipse, symmetric under
|
||||
# x <-> z) with a noisy foot line, so the frame does not read as a straight wall.
|
||||
var border: float = pow(pow(absf(p.x), 5.0) + pow(absf(p.y), 5.0), 0.2) + hills * 3.4 + _detail.get_noise_2d(u * 0.35, a * 0.35) * 1.8
|
||||
if border > 39.0:
|
||||
var over: float = border - 39.0
|
||||
height += minf(11.0, over * 0.62 + over * over * 0.035) * (0.75 + 0.25 * _detail.get_noise_2d(u * 0.5, a * 0.5))
|
||||
return height
|
||||
|
||||
func _arena_blend(p: Vector2, height: float) -> float:
|
||||
var d: float = p.distance_to(LuneMap.GROTTO)
|
||||
if d > ARENA_RADIUS + 3.5:
|
||||
return height
|
||||
return lerpf(ARENA_HEIGHT, height, smoothstep(ARENA_RADIUS, ARENA_RADIUS + 3.5, d))
|
||||
|
||||
## Lane surface height: the smoothed ground along the lane, lifted onto platform and ford.
|
||||
func _lane_profile(lane: String) -> PackedFloat32Array:
|
||||
var path: LuneMap.LanePath = map.lanes[lane]
|
||||
var count: int = int(ceil(path.length)) + 1
|
||||
var raw := PackedFloat32Array()
|
||||
raw.resize(count)
|
||||
for k: int in range(count):
|
||||
raw[k] = _arena_blend(path.point_at(float(k)), _region(path.point_at(float(k))))
|
||||
var smooth: PackedFloat32Array = raw
|
||||
for pass_index: int in range(3):
|
||||
var next := PackedFloat32Array()
|
||||
next.resize(count)
|
||||
for k: int in range(count):
|
||||
var total: float = 0.0
|
||||
var weight: float = 0.0
|
||||
for o: int in range(-6, 7):
|
||||
var m: int = clampi(k + o, 0, count - 1)
|
||||
total += smooth[m]
|
||||
weight += 1.0
|
||||
next[k] = total / weight
|
||||
smooth = next
|
||||
var middle: float = path.length * 0.5
|
||||
for k: int in range(count):
|
||||
var from_middle: float = absf(float(k) - middle)
|
||||
# The lane leaves the base on the plateau and only then descends.
|
||||
var at: Vector2 = path.point_at(float(k))
|
||||
var core_d: float = minf(at.distance_to(map.core_position(0)), at.distance_to(map.core_position(1)))
|
||||
smooth[k] = lerpf(BASE_HEIGHT, smooth[k], smoothstep(BASE_RADIUS - 1.0, BASE_RADIUS + 5.0, core_d))
|
||||
if lane == "mid":
|
||||
smooth[k] = lerpf(PLATFORM_HEIGHT, smooth[k], smoothstep(PLATFORM_RADIUS - 1.0, PLATFORM_RADIUS + 7.0, from_middle))
|
||||
elif lane == "bot":
|
||||
smooth[k] = lerpf(FORD_HEIGHT, smooth[k], smoothstep(6.0, 16.0, from_middle))
|
||||
smooth[k] = maxf(smooth[k], WATER_LEVEL + 0.18)
|
||||
return smooth
|
||||
|
||||
# ---------------------------------------------------------------- lanes (spatial index)
|
||||
func _index_segments() -> void:
|
||||
for lane: String in LuneMap.LANES:
|
||||
var path: LuneMap.LanePath = map.lanes[lane]
|
||||
for i: int in range(1, path.points.size()):
|
||||
var a: Vector2 = path.points[i - 1]
|
||||
var b: Vector2 = path.points[i]
|
||||
var index: int = _segments.size()
|
||||
_segments.append([a, b, lane, float(path.cumulative[i - 1]), a.distance_to(b)])
|
||||
var reach: float = float(CORRIDOR[lane]) + BLEND + 1.0
|
||||
var lo := Vector2(minf(a.x, b.x) - reach, minf(a.y, b.y) - reach)
|
||||
var hi := Vector2(maxf(a.x, b.x) + reach, maxf(a.y, b.y) + reach)
|
||||
for bx: int in range(floori(lo.x / BUCKET), floori(hi.x / BUCKET) + 1):
|
||||
for bz: int in range(floori(lo.y / BUCKET), floori(hi.y / BUCKET) + 1):
|
||||
var key := Vector2i(bx, bz)
|
||||
if not _buckets.has(key):
|
||||
_buckets[key] = []
|
||||
(_buckets[key] as Array).append(index)
|
||||
|
||||
## [distance, lane, lane coordinate] of the closest lane centre line, or [INF].
|
||||
func nearest_lane(p: Vector2) -> Array:
|
||||
var key := Vector2i(floori(p.x / BUCKET), floori(p.y / BUCKET))
|
||||
if not _buckets.has(key):
|
||||
return [INF, "", 0.0]
|
||||
var best: float = INF
|
||||
var best_lane: String = ""
|
||||
var best_s: float = 0.0
|
||||
for index: int in _buckets[key]:
|
||||
var segment: Array = _segments[index]
|
||||
var a: Vector2 = segment[0]
|
||||
var ab: Vector2 = (segment[1] as Vector2) - a
|
||||
var t: float = clampf((p - a).dot(ab) / maxf(0.0001, ab.length_squared()), 0.0, 1.0)
|
||||
var d: float = p.distance_to(a + ab * t)
|
||||
if d < best:
|
||||
best = d
|
||||
best_lane = segment[2]
|
||||
best_s = float(segment[3]) + float(segment[4]) * t
|
||||
return [best, best_lane, best_s]
|
||||
|
||||
# ---------------------------------------------------------------- one grid point
|
||||
func _evaluate(p: Vector2) -> Array:
|
||||
var u: float = axis_u(p)
|
||||
var a: float = axis_a(p)
|
||||
var height: float = _arena_blend(p, _region(p))
|
||||
var lane_info: Array = nearest_lane(p)
|
||||
var lane_d: float = lane_info[0]
|
||||
var on_lane: bool = false
|
||||
var lane_surface: float = 0.0
|
||||
if lane_d < INF:
|
||||
var corridor: float = CORRIDOR[lane_info[1]]
|
||||
lane_surface = lane_height(lane_info[1], lane_info[2])
|
||||
height = lerpf(lane_surface, height, smoothstep(corridor, corridor + BLEND, lane_d))
|
||||
on_lane = lane_d < corridor
|
||||
for pad: Array in _pads:
|
||||
var pad_d: float = p.distance_to(pad[0])
|
||||
if pad_d < 3.8:
|
||||
height = lerpf(float(pad[1]), height, smoothstep(2.3, 3.8, pad_d))
|
||||
# The river is carved last so bridges and the ford span open water.
|
||||
var width: float = river_half_width(u)
|
||||
var carved: bool = false
|
||||
if width > 0.0:
|
||||
var bank: float = lerpf(BED_LEVEL, height, smoothstep(width - 0.9, width + 1.9, a))
|
||||
# Round spring pool at the foot of the grotto terrace.
|
||||
var start: float = smoothstep(RIVER_START - 0.2, RIVER_START + 1.6, u)
|
||||
bank = lerpf(height, bank, start)
|
||||
if bank < height:
|
||||
carved = height - bank > 0.25
|
||||
height = bank
|
||||
var walk_height: float = lane_surface if on_lane else maxf(height, WATER_LEVEL)
|
||||
if p.length() < PLATFORM_RADIUS:
|
||||
walk_height = maxf(walk_height, PLATFORM_HEIGHT)
|
||||
return [height, walk_height, lane_d, 1 if carved else 0]
|
||||
|
||||
## Blockout palette per zone; alpha carries rockiness for the terrain shader.
|
||||
func _color(p: Vector2, height: float, slope: float, lane_d: float, carved: bool) -> Color:
|
||||
var u: float = axis_u(p)
|
||||
var a: float = axis_a(p)
|
||||
var variation: float = _detail.get_noise_2d(u * 1.7, a * 1.7)
|
||||
var grass := Color(.2, .31, .12).lerp(Color(.25, .33, .14), variation * .5 + .5)
|
||||
var edge: float = escarpment(a)
|
||||
var color: Color = grass
|
||||
var rock: float = 0.0
|
||||
if u < edge + 1.4:
|
||||
color = Color(.29, .32, .17).lerp(Color(.35, .34, .25), clampf(variation * 1.4, 0.0, 1.0))
|
||||
rock = clampf(variation * 1.2, 0.0, .6)
|
||||
if u > 20.0:
|
||||
color = color.lerp(Color(.17, .25, .12).lerp(Color(.24, .22, .15), clampf(-variation, 0.0, 1.0)), smoothstep(20.0, 30.0, u))
|
||||
# Steep ground (escarpment, terrace walls, mountains) reads as rock.
|
||||
if slope > 0.55:
|
||||
color = color.lerp(Color(.37, .34, .29), smoothstep(0.55, 1.1, slope))
|
||||
rock = maxf(rock, smoothstep(0.55, 1.1, slope))
|
||||
if maxf(absf(p.x), absf(p.y)) > 40.0:
|
||||
rock = maxf(rock, 0.35 + variation * 0.3)
|
||||
if carved or height < WATER_LEVEL + 0.25:
|
||||
color = Color(.31, .29, .21).lerp(Color(.15, .16, .12), clampf((WATER_LEVEL + 0.2 - height) / 0.8, 0.0, 1.0))
|
||||
rock = 0.25
|
||||
if lane_d < 5.2:
|
||||
color = color.lerp(Color(.31, .26, .18), 1.0 - smoothstep(3.8, 5.2, lane_d))
|
||||
for team: int in range(2):
|
||||
if p.distance_to(map.core_position(team)) < BASE_RADIUS + 0.3:
|
||||
color = Color(.41, .38, .32).lerp(Color(.36, .33, .28), variation * .5 + .5)
|
||||
rock = 0.45
|
||||
if p.distance_to(LuneMap.GROTTO) < ARENA_RADIUS + 0.2:
|
||||
color = Color(.36, .34, .33)
|
||||
rock = 0.55
|
||||
# The palette is authored in sRGB; vertex colours reach the shader as linear values.
|
||||
var linear: Color = Color(color.r, color.g, color.b).srgb_to_linear()
|
||||
linear.a = rock
|
||||
return linear
|
||||
|
||||
# ---------------------------------------------------------------- mesh
|
||||
var _inner_mesh: ArrayMesh
|
||||
var _outer_mesh: ArrayMesh
|
||||
|
||||
func build_mesh() -> ArrayMesh:
|
||||
return _mesh({"size": size, "extent": EXTENT, "step": STEP, "hole": 0.0, "ground": ground, "colors": colors})
|
||||
|
||||
## The distant mountains and the river outlet at 4 m resolution, with a hole for the fine grid.
|
||||
func build_outer_mesh() -> ArrayMesh:
|
||||
return _mesh(_grid(OUTER_EXTENT, OUTER_STEP, OUTER_HOLE))
|
||||
|
||||
## Cached meshes: a rematch or a second match in the same process reuses them.
|
||||
func inner_mesh() -> ArrayMesh:
|
||||
if _inner_mesh == null:
|
||||
_inner_mesh = build_mesh()
|
||||
return _inner_mesh
|
||||
|
||||
func outer_mesh() -> ArrayMesh:
|
||||
if _outer_mesh == null:
|
||||
_outer_mesh = build_outer_mesh()
|
||||
return _outer_mesh
|
||||
|
||||
func _mesh(grid: Dictionary) -> ArrayMesh:
|
||||
var n: int = grid.size
|
||||
var extent: float = grid.extent
|
||||
var step: float = grid.step
|
||||
var hole: float = grid.hole
|
||||
var heights: PackedFloat32Array = grid.ground
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
vertices.resize(n * n)
|
||||
normals.resize(n * n)
|
||||
for j: int in range(n):
|
||||
for i: int in range(n):
|
||||
var index: int = j * n + i
|
||||
vertices[index] = Vector3(-extent + i * step, heights[index], -extent + j * step)
|
||||
var left: float = heights[j * n + maxi(0, i - 1)]
|
||||
var right: float = heights[j * n + mini(n - 1, i + 1)]
|
||||
var up: float = heights[maxi(0, j - 1) * n + i]
|
||||
var down: float = heights[mini(n - 1, j + 1) * n + i]
|
||||
normals[index] = Vector3(left - right, 2.0 * step, up - down).normalized()
|
||||
var indices := PackedInt32Array()
|
||||
indices.resize((n - 1) * (n - 1) * 6)
|
||||
var cursor: int = 0
|
||||
for j: int in range(n - 1):
|
||||
for i: int in range(n - 1):
|
||||
if hole > 0.0:
|
||||
var cx: float = absf(-extent + (i + 0.5) * step)
|
||||
var cz: float = absf(-extent + (j + 0.5) * step)
|
||||
if maxf(cx, cz) < hole:
|
||||
continue
|
||||
var a: int = j * n + i
|
||||
var b: int = a + 1
|
||||
var c: int = a + n
|
||||
var d: int = c + 1
|
||||
# Alternating diagonals; cell (i, j) and its mirror (j, i) share the parity,
|
||||
# so mirrored cells triangulate as mirror images (clockwise front faces).
|
||||
if (i + j) % 2 == 0:
|
||||
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = d
|
||||
indices[cursor + 3] = a; indices[cursor + 4] = d; indices[cursor + 5] = c
|
||||
else:
|
||||
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = c
|
||||
indices[cursor + 3] = b; indices[cursor + 4] = d; indices[cursor + 5] = c
|
||||
cursor += 6
|
||||
indices.resize(cursor)
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_COLOR] = grid.colors
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
1
scripts/luneraria/view/lune_terrain.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ccaumfwbolrec
|
||||
36
scripts/luneraria/view/lune_terrain.gdshader
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// G1 ground: zone colours from the height field (vertex colour), rockiness in alpha.
|
||||
// Rock areas use the shared limestone texture triplanar, like the Kluft surfaces.
|
||||
shader_type spatial;
|
||||
render_mode diffuse_burley, specular_schlick_ggx;
|
||||
uniform sampler2D stone_map : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
varying vec3 world;
|
||||
varying vec3 world_normal;
|
||||
float hash(vec3 p) { p = fract(p * .1031); p += dot(p, p.yzx + 33.33); return fract((p.x + p.y) * p.z); }
|
||||
float noise(vec3 p) {
|
||||
vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
|
||||
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y), f.z);
|
||||
}
|
||||
void vertex() {
|
||||
world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
world_normal = normalize(mat3(MODEL_MATRIX) * NORMAL);
|
||||
}
|
||||
void fragment() {
|
||||
vec3 weights = pow(abs(world_normal), vec3(5.0));
|
||||
weights /= max(dot(weights, vec3(1.0)), .0001);
|
||||
vec3 p = world * .21;
|
||||
vec3 stone = texture(stone_map, p.zy).rgb * weights.x + texture(stone_map, p.xz).rgb * weights.y + texture(stone_map, p.xy).rgb * weights.z;
|
||||
float relief = dot(stone, vec3(.333));
|
||||
float broad = noise(world * .35);
|
||||
float grain = noise(world * 6.0);
|
||||
float rock = COLOR.a;
|
||||
vec3 soil = COLOR.rgb * mix(.84, 1.14, broad) * mix(.9, 1.07, grain);
|
||||
vec3 rocky = COLOR.rgb * mix(vec3(.72), vec3(1.45), stone);
|
||||
ALBEDO = mix(soil, rocky, rock);
|
||||
ROUGHNESS = mix(.93, .84, rock);
|
||||
vec3 n = NORMAL, dx = dFdx(VERTEX), dy = dFdy(VERTEX);
|
||||
vec3 rx = cross(dy, n), ry = cross(n, dx);
|
||||
float det = dot(dx, rx);
|
||||
vec3 gradient = dFdx(relief) * rx + dFdy(relief) * ry;
|
||||
NORMAL = normalize(abs(det) * n - sign(det) * gradient * (.006 + rock * .014));
|
||||
}
|
||||
1
scripts/luneraria/view/lune_terrain.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ih7v3fqacdf8
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
136
tools/luneraria/g_views.gd
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
extends Node
|
||||
## Comparison captures for the graphics phases G1–G6, always from the real game camera
|
||||
## and the real HUD at the running window size (use 1920x1080).
|
||||
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/g_views.tscn -- --out=res://docs/luneraria/screenshots/g1/nachher
|
||||
## Views: 01 map, 02–04 lanes in combat, 05/06 bases, 07 grotto, 08 many units, 09 HUD,
|
||||
## 10 hero selected, 11 close zoom, 12 camera in motion. Match and fixture are fixed
|
||||
## (seed 4, Wurzelbund on Mid) so every phase is photographed the same way. Fixture gold
|
||||
## only exists in this capture run; the rules are the ones of the running build.
|
||||
const Game = preload("res://scripts/luneraria/lune_game.gd")
|
||||
|
||||
var out: String = "res://docs/luneraria/screenshots/g1/nachher/"
|
||||
var main: Node
|
||||
var shots: Array[String] = []
|
||||
|
||||
func _ready() -> void:
|
||||
for arg: String in OS.get_cmdline_user_args():
|
||||
if arg.begins_with("--out="):
|
||||
out = arg.substr(6)
|
||||
if not out.ends_with("/"):
|
||||
out += "/"
|
||||
call_deferred("run")
|
||||
|
||||
func shot(label: String, frames: int = 14) -> void:
|
||||
for i: int in frames:
|
||||
await get_tree().process_frame
|
||||
await RenderingServer.frame_post_draw
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out))
|
||||
get_viewport().get_texture().get_image().save_png(out + label + ".png")
|
||||
shots.append(label)
|
||||
print("G_VIEW ", label)
|
||||
|
||||
## Camera helpers that only use public map data, so they frame old and new layouts alike.
|
||||
func grotto_point(game: Node3D) -> Vector2:
|
||||
var map: LuneMap = game.match_state.map
|
||||
return map.grotto_center() if map.has_method("grotto_center") else LuneMap.GROTTO
|
||||
|
||||
func look(game: Node3D, at: Vector2, zoom: float) -> void:
|
||||
game.camera.look_at_point(Vector3(at.x, 0, at.y), zoom, true)
|
||||
|
||||
## Runs the combat clock quickly (more fixed steps per frame, rules unchanged) and holds it.
|
||||
func advance_combat(game: Node3D, seconds: float) -> void:
|
||||
var state: LuneMatch = game.match_state
|
||||
game.paused = false
|
||||
while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < seconds:
|
||||
for i: int in range(20):
|
||||
if state.phase != LuneMatch.Phase.COMBAT or state.combat.elapsed >= seconds:
|
||||
break
|
||||
state.step_combat()
|
||||
await get_tree().process_frame
|
||||
game.paused = true
|
||||
|
||||
## Documentation only (not a game view): orthographic top-down plan without the HUD.
|
||||
func plan_view(game: Node3D) -> void:
|
||||
var plan := Camera3D.new()
|
||||
plan.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||
plan.size = 86.0
|
||||
plan.far = 400.0
|
||||
game.add_child(plan)
|
||||
plan.position = Vector3(0, 150, 0)
|
||||
plan.rotation = Vector3(-PI * .5, 0, 0)
|
||||
plan.current = true
|
||||
game.hud.visible = false
|
||||
await shot("00-kartenplan-draufsicht", 10)
|
||||
game.hud.visible = true
|
||||
game.camera.current = true
|
||||
plan.queue_free()
|
||||
|
||||
func run() -> void:
|
||||
Soundscape.enabled = false
|
||||
main = load("res://scenes/luneraria.tscn").instantiate()
|
||||
add_child(main)
|
||||
var menu: MainMenu = main.menu
|
||||
while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")):
|
||||
await get_tree().process_frame
|
||||
menu.bund = "wurzelbund"
|
||||
menu.lane = "mid"
|
||||
main.start_match({"seed": 4})
|
||||
await get_tree().create_timer(.8).timeout
|
||||
var game: Node3D = main.game
|
||||
var state: LuneMatch = game.match_state
|
||||
var slot: int = game.human_slot
|
||||
# Fixture: enough gold for a hero with one item next to the starting army.
|
||||
state.players[slot].draft.gold = 900
|
||||
game._on_command({"type": "hero_buy", "hero_id": "held_ringborke"})
|
||||
game._on_command({"type": "item_buy", "item_id": "basaltschild"})
|
||||
game.hud._starter.pressed.emit()
|
||||
game.hud.show_page(0)
|
||||
game.focus_lane("mid", true)
|
||||
await shot("09-hud-planung", 24)
|
||||
game.hud.show_page(1)
|
||||
await shot("10-held-ausgewaehlt")
|
||||
game.hud.show_page(0)
|
||||
game.camera.overview(true)
|
||||
await shot("01-karte", 20)
|
||||
await plan_view(game)
|
||||
look(game, state.map.core_position(0), 26.0)
|
||||
await shot("05-eigene-basis")
|
||||
look(game, state.map.core_position(1), 26.0)
|
||||
await shot("06-gegnerbasis")
|
||||
look(game, grotto_point(game), 22.0)
|
||||
await shot("07-bossgrotte")
|
||||
game._on_ready()
|
||||
await get_tree().process_frame
|
||||
await advance_combat(game, 16.0)
|
||||
var names := {"top": "02-top-lane", "mid": "03-mid-lane", "bot": "04-bottom-lane"}
|
||||
for lane: String in LuneMap.LANES:
|
||||
game.focus_lane(lane, true)
|
||||
await shot(names[lane])
|
||||
# Camera in motion: a real, non-instant pan from Mid towards the own base.
|
||||
game.focus_lane("mid", true)
|
||||
await get_tree().process_frame
|
||||
var base: Vector2 = state.map.core_position(0)
|
||||
game.camera.look_at_point(Vector3(base.x, 0, base.y), 30.0, false)
|
||||
await shot("12-kamera-bewegt", 5)
|
||||
# Many units: all six armies at the supply cap in a round-13 wave (capture fixture).
|
||||
main.queue_free()
|
||||
await get_tree().process_frame
|
||||
var crowd: Node3D = Game.new()
|
||||
add_child(crowd)
|
||||
crowd.start({"seed": 5, "human_slot": 1})
|
||||
var crowd_state: LuneMatch = crowd.match_state
|
||||
crowd_state.round_number = 13
|
||||
for other: int in range(6):
|
||||
crowd_state.players[other].draft.gold = 100000
|
||||
crowd_state.players[other].draft.roster.clear()
|
||||
LuneBot.plan(crowd_state, other, "ausgewogen")
|
||||
crowd._on_ready()
|
||||
await get_tree().process_frame
|
||||
await advance_combat(crowd, 13.0)
|
||||
crowd.focus_lane("mid", true)
|
||||
await shot("08-kampf-viele-einheiten", 20)
|
||||
var fight: Vector2 = crowd_state.map.lane_point(0, "mid", crowd_state.map.lanes["mid"].length * .5)
|
||||
look(crowd, fight, 13.0)
|
||||
await shot("11-zoom-nah")
|
||||
print("G_VIEWS_DONE ", shots.size())
|
||||
get_tree().quit()
|
||||
1
tools/luneraria/g_views.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwt2eokernmvs
|
||||
6
tools/luneraria/g_views.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/luneraria/g_views.gd" id="1"]
|
||||
|
||||
[node name="GViews" type="Node"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
294
tools/luneraria/verify_g1.gd
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
extends Node
|
||||
## G1 acceptance: the three-lane layout plays correctly and the game camera reaches it.
|
||||
## Part 1 checks the map and the terrain (lanes, water crossings, towers, bases, grotto,
|
||||
## deployment areas, mirror symmetry). Part 2 runs the real game with real key and mouse
|
||||
## events: lane, base, grotto, hero and overview jumps, reset, rotation, zoom limits,
|
||||
## deployment areas in planning, figures standing on the ground in combat.
|
||||
## Run (window needed): godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn
|
||||
const OUT := "res://docs/luneraria/logs/g1/verify-g1.json"
|
||||
|
||||
var checks: int = 0
|
||||
var failures: Array[String] = []
|
||||
var facts: Dictionary = {}
|
||||
|
||||
func check(ok: bool, label: String) -> void:
|
||||
checks += 1
|
||||
if not ok:
|
||||
failures.append(label)
|
||||
printerr("G1_FAIL: ", label)
|
||||
|
||||
func _ready() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func run() -> void:
|
||||
Soundscape.enabled = false
|
||||
var map := LuneMap.new()
|
||||
var terrain: LuneTerrain = LuneTerrain.for_map(map)
|
||||
_layout(map, terrain)
|
||||
await _game()
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://docs/luneraria/logs/g1"))
|
||||
var file := FileAccess.open(OUT, FileAccess.WRITE)
|
||||
file.store_string(JSON.stringify({"checks": checks, "failures": failures, "facts": facts}, "\t"))
|
||||
file.close()
|
||||
print("G1_VERIFY_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()])
|
||||
get_tree().quit(0 if failures.is_empty() else 1)
|
||||
|
||||
# ---------------------------------------------------------------- part 1: layout
|
||||
func _layout(map: LuneMap, terrain: LuneTerrain) -> void:
|
||||
var lengths: Dictionary = {}
|
||||
for lane: String in LuneMap.LANES:
|
||||
lengths[lane] = snappedf(map.lanes[lane].length, .01)
|
||||
facts.lane_lengths = lengths
|
||||
check(absf(map.lanes["top"].length - map.lanes["bot"].length) < .001, "Top and Bottom have the same length")
|
||||
check(map.lanes["mid"].length < map.lanes["top"].length * .75, "Mid is the direct route (clearly shorter than Top/Bottom)")
|
||||
# Lanes are always walkable; open water lies under a lane only at the two decks.
|
||||
var water_points: Dictionary = {}
|
||||
var min_height: Dictionary = {}
|
||||
var max_height: Dictionary = {}
|
||||
for lane: String in LuneMap.LANES:
|
||||
var path: LuneMap.LanePath = map.lanes[lane]
|
||||
var dry: bool = true
|
||||
var s: float = 0.0
|
||||
water_points[lane] = 0
|
||||
min_height[lane] = INF
|
||||
max_height[lane] = -INF
|
||||
while s <= path.length:
|
||||
var p: Vector2 = path.point_at(s)
|
||||
var surface: float = terrain.lane_height(lane, s)
|
||||
min_height[lane] = minf(min_height[lane], surface)
|
||||
max_height[lane] = maxf(max_height[lane], surface)
|
||||
dry = dry and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .15
|
||||
if terrain.height_at(p) < LuneTerrain.WATER_LEVEL:
|
||||
water_points[lane] = int(water_points[lane]) + 1
|
||||
var on_deck: bool = (lane == "mid" and p.length() < LuneTerrain.PLATFORM_RADIUS) or (lane == "bot" and p.distance_to(path.point_at(path.length * .5)) < 7.2)
|
||||
check(on_deck, "%s crosses water only on its deck (at %s)" % [lane, p.snapped(Vector2(.1, .1))])
|
||||
s += 1.0
|
||||
check(dry, "%s lane is walkable everywhere" % lane)
|
||||
facts.lane_height_range = {"min": min_height, "max": max_height}
|
||||
facts.water_points_under_lanes = water_points
|
||||
check(int(water_points.top) == 0, "Top never touches the river (highland route)")
|
||||
check(int(water_points.mid) > 0 and int(water_points.bot) > 0, "Mid and Bottom cross the Kluft")
|
||||
check(float(max_height.top) > 2.5, "Top climbs onto the highland (%.2f m)" % float(max_height.top))
|
||||
check(float(min_height.bot) < 0.0, "Bottom runs through the lowland (%.2f m)" % float(min_height.bot))
|
||||
check(is_equal_approx(terrain.lane_height("mid", map.lanes["mid"].length * .5), LuneTerrain.PLATFORM_HEIGHT), "Mid crosses on the central platform")
|
||||
# Buildings: inside the playable square, on dry flat pads, not crowding each other.
|
||||
var cores: Array[Vector2] = [map.core_position(0), map.core_position(1)]
|
||||
for definition: Dictionary in map.buildings:
|
||||
var p: Vector2 = definition.position
|
||||
var id: String = definition.id
|
||||
check(absf(p.x) < LuneMap.HALF_EXTENT - 2.0 and absf(p.y) < LuneMap.HALF_EXTENT - 2.0, id + " lies inside the playable square")
|
||||
check(terrain.height_at(p) > LuneTerrain.WATER_LEVEL + .25, id + " stands on dry ground")
|
||||
var flat: bool = true
|
||||
for offset: Vector2 in [Vector2(1.4, 0), Vector2(-1.4, 0), Vector2(0, 1.4), Vector2(0, -1.4)]:
|
||||
flat = flat and absf(terrain.height_at(p + offset) - terrain.height_at(p)) < .15
|
||||
check(flat, id + " stands on a flat pad")
|
||||
for other: Dictionary in map.buildings:
|
||||
if other.id != id:
|
||||
check(p.distance_to(other.position) > 3.8, "%s keeps its distance to %s" % [id, other.id])
|
||||
if str(definition.tier) == "base":
|
||||
var team: int = int(definition.team)
|
||||
check((p - cores[team]).dot(cores[1 - team] - cores[team]) > 0.0, id + " stands in front of its Ancient")
|
||||
# Grotto: on the symmetry axis, beside Mid, clear of every lane corridor, entries on the terrace.
|
||||
var grotto: Vector2 = map.grotto_center()
|
||||
check(LuneMap.mirror(grotto).distance_to(grotto) < .001, "grotto lies on the symmetry axis")
|
||||
var nearest: Array = terrain.nearest_lane(grotto + LuneMap.AXIS * 0.0)
|
||||
var clearance: float = INF
|
||||
for angle_step: int in range(24):
|
||||
var edge: Vector2 = grotto + Vector2.from_angle(TAU * angle_step / 24.0) * LuneTerrain.ARENA_RADIUS
|
||||
var lane: Array = terrain.nearest_lane(edge)
|
||||
if float(lane[0]) < INF:
|
||||
clearance = minf(clearance, float(lane[0]) - float(LuneTerrain.CORRIDOR[lane[1]]))
|
||||
facts.grotto_clearance_to_lanes = snappedf(clearance, .01)
|
||||
check(clearance > 2.0, "grotto arena stays clear of every lane corridor (%.1f m)" % clearance)
|
||||
var mid: LuneMap.LanePath = map.lanes["mid"]
|
||||
check(mid.point_at(mid.project(grotto)).distance_to(grotto) > 12.0, "grotto does not block Mid")
|
||||
for team: int in range(2):
|
||||
check(absf(terrain.walk_height(map.grotto_entry(team)) - LuneTerrain.ARENA_HEIGHT) < .05, "grotto entry of team %d lies on the terrace" % team)
|
||||
check(absf(terrain.walk_height(map.grotto_boss_point()) - LuneTerrain.ARENA_HEIGHT) < .05, "boss point lies on the terrace")
|
||||
check(terrain.is_water(grotto + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS + 1.5)), "the Kluft springs at the foot of the grotto terrace")
|
||||
check(not terrain.is_water(Vector2(-25, -25)), "no water at the Top corner")
|
||||
check(terrain.is_water(Vector2(20, 20)) and terrain.is_water(Vector2(-5, -5)), "the river runs along the axis")
|
||||
# Deployment areas: dry, inside the square, in front of the own base.
|
||||
var span: Dictionary = map.deployment_span()
|
||||
for team: int in range(2):
|
||||
for lane: String in LuneMap.LANES:
|
||||
var ok: bool = true
|
||||
for progress: float in [float(span.from), float(span.to)]:
|
||||
for lateral: float in [-float(span.half_width), 0.0, float(span.half_width)]:
|
||||
var p: Vector2 = map.lane_point(team, lane, progress, lateral)
|
||||
ok = ok and absf(p.x) < LuneMap.HALF_EXTENT and absf(p.y) < LuneMap.HALF_EXTENT
|
||||
ok = ok and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .2
|
||||
ok = ok and p.distance_to(cores[team]) < p.distance_to(cores[1 - team])
|
||||
check(ok, "deployment area %d/%s is dry, inside and on the own side" % [team, lane])
|
||||
# The ground is the exact mirror image across x = z.
|
||||
var worst: float = 0.0
|
||||
for j: int in range(terrain.size):
|
||||
for i: int in range(j):
|
||||
worst = maxf(worst, absf(terrain.ground[j * terrain.size + i] - terrain.ground[i * terrain.size + j]))
|
||||
facts.terrain_mirror_error = worst
|
||||
check(worst == 0.0, "terrain is the exact mirror image across x = z")
|
||||
|
||||
# ---------------------------------------------------------------- part 2: game camera
|
||||
func key(code: Key) -> void:
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = code
|
||||
event.physical_keycode = code
|
||||
event.pressed = true
|
||||
get_viewport().push_input(event)
|
||||
await get_tree().process_frame
|
||||
var release := event.duplicate()
|
||||
release.pressed = false
|
||||
get_viewport().push_input(release)
|
||||
await get_tree().process_frame
|
||||
|
||||
func wheel(up: bool) -> void:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = MOUSE_BUTTON_WHEEL_UP if up else MOUSE_BUTTON_WHEEL_DOWN
|
||||
event.pressed = true
|
||||
event.position = Vector2(960, 560)
|
||||
get_viewport().push_input(event)
|
||||
await get_tree().process_frame
|
||||
|
||||
func settle(frames: int = 3) -> void:
|
||||
for i: int in frames:
|
||||
await get_tree().process_frame
|
||||
|
||||
func near_focus(game: Node3D, target: Vector2, tolerance: float = .6) -> bool:
|
||||
return Vector2(game.camera.focus.x, game.camera.focus.z).distance_to(target) < tolerance
|
||||
|
||||
func on_screen(game: Node3D, world: Vector3) -> bool:
|
||||
var camera: Camera3D = game.camera
|
||||
if camera.is_position_behind(world):
|
||||
return false
|
||||
var screen: Vector2 = camera.unproject_position(world)
|
||||
return get_viewport().get_visible_rect().has_point(screen)
|
||||
|
||||
func _game() -> void:
|
||||
var main: Node = load("res://scenes/luneraria.tscn").instantiate()
|
||||
add_child(main)
|
||||
var menu: MainMenu = main.menu
|
||||
while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")):
|
||||
await get_tree().process_frame
|
||||
menu.bund = "wurzelbund"
|
||||
menu.lane = "mid"
|
||||
main.start_match({"seed": 4})
|
||||
await get_tree().create_timer(.8).timeout
|
||||
var game: Node3D = main.game
|
||||
var state: LuneMatch = game.match_state
|
||||
var map: LuneMap = state.map
|
||||
var team: int = int(state.players[game.human_slot].team)
|
||||
check(game.camera.projection == Camera3D.PROJECTION_PERSPECTIVE, "the game camera is a perspective MOBA camera")
|
||||
game.camera.reduced_motion = true
|
||||
# Lane jumps F2/F3/F4 frame the own deployment side of each lane.
|
||||
for index: int in range(3):
|
||||
await key([KEY_F2, KEY_F3, KEY_F4][index])
|
||||
var lane: String = LuneMap.LANES[index]
|
||||
var target: Vector2 = map.lane_point(team, lane, map.lanes[lane].length * .2)
|
||||
check(near_focus(game, target), "F%d centres the camera on %s" % [index + 2, lane])
|
||||
check(on_screen(game, Vector3(target.x, game.world.height_at(target), target.y)), "%s is on screen after its hotkey" % lane)
|
||||
await key(KEY_B)
|
||||
check(near_focus(game, map.core_position(team)), "B centres the own Ancient")
|
||||
check(on_screen(game, game.world.to3(map.core_position(team))), "own Ancient is on screen")
|
||||
await key(KEY_G)
|
||||
check(near_focus(game, map.grotto_center()), "G centres the boss grotto")
|
||||
await key(KEY_H)
|
||||
check(game.own_hero_unit() == null, "H without a hero reports that there is none")
|
||||
# Hero jump after hiring one through the HUD command path.
|
||||
state.players[game.human_slot].draft.gold = 600
|
||||
game._on_command({"type": "hero_buy", "hero_id": "held_ringborke"})
|
||||
await settle()
|
||||
var hero: LuneUnit = game.own_hero_unit()
|
||||
check(hero != null, "the hired hero appears in the planning formation")
|
||||
if hero != null:
|
||||
await key(KEY_H)
|
||||
check(near_focus(game, hero.position), "H centres the selected hero")
|
||||
# Overview: every building and the grotto are on screen at once.
|
||||
await key(KEY_O)
|
||||
await settle(4)
|
||||
var all_visible: bool = true
|
||||
for definition: Dictionary in map.buildings:
|
||||
all_visible = all_visible and on_screen(game, game.world.to3(definition.position))
|
||||
check(all_visible, "O shows all 16 towers and both Ancients")
|
||||
check(on_screen(game, game.world.to3(map.grotto_center())), "O shows the boss grotto")
|
||||
facts.overview = {"zoom": game.camera.zoom, "pitch_deg": rad_to_deg(LuneCameraPitch.of(game.camera)), "distance": game.camera.position.distance_to(game.camera.focus)}
|
||||
# Rotation and reset.
|
||||
await key(KEY_Q)
|
||||
await settle()
|
||||
check(is_equal_approx(game.camera.yaw, -PI * .25), "Q turns the camera by 45°")
|
||||
await key(KEY_E)
|
||||
await key(KEY_E)
|
||||
check(is_equal_approx(game.camera.yaw, PI * .25), "E turns the camera back and further")
|
||||
await key(KEY_R)
|
||||
await settle()
|
||||
var home: Vector2 = map.lane_point(team, "mid", map.lanes["mid"].length * .2)
|
||||
check(is_zero_approx(game.camera.yaw) and near_focus(game, home) and is_equal_approx(game.camera.zoom, game.camera.DEFAULT_ZOOM), "R resets rotation, zoom and focus to the own lane")
|
||||
# Zoom limits with the mouse wheel.
|
||||
for i: int in 40:
|
||||
await wheel(true)
|
||||
check(is_equal_approx(game.camera.zoom, game.camera.MIN_ZOOM), "wheel zoom stops at the closest limit")
|
||||
var near_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera))
|
||||
for i: int in 60:
|
||||
await wheel(false)
|
||||
check(is_equal_approx(game.camera.zoom, game.camera.MAX_ZOOM), "wheel zoom stops at the widest limit")
|
||||
var far_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera))
|
||||
facts.pitch_range_deg = [near_pitch, far_pitch]
|
||||
check(far_pitch < near_pitch - 10.0, "the view steepens when zooming out (%.0f° → %.0f°)" % [near_pitch, far_pitch])
|
||||
var footprint: PackedVector2Array = game.camera.footprint()
|
||||
check(footprint.size() == 4 and Geometry2D.is_point_in_polygon(Vector2(game.camera.focus.x, game.camera.focus.z), footprint), "the minimap outline contains the camera focus")
|
||||
await key(KEY_R)
|
||||
# Deployment areas: the own team's three pads in planning, highlighted where units stand.
|
||||
var world: Node3D = game.world
|
||||
var own_pads: int = 0
|
||||
var other_pads: int = 0
|
||||
for key_name: String in world._pads:
|
||||
var pad: MeshInstance3D = world._pads[key_name]
|
||||
if pad.visible:
|
||||
if key_name.begins_with("%d:" % team):
|
||||
own_pads += 1
|
||||
else:
|
||||
other_pads += 1
|
||||
check(own_pads == 3 and other_pads == 0, "planning shows the own three deployment areas only")
|
||||
var mid_pad: ShaderMaterial = (world._pads["%d:mid" % team] as MeshInstance3D).material_override
|
||||
var top_pad: ShaderMaterial = (world._pads["%d:top" % team] as MeshInstance3D).material_override
|
||||
check(float(mid_pad.get_shader_parameter("highlight")) > .5 and float(top_pad.get_shader_parameter("highlight")) < .5, "the lane that holds own units is highlighted")
|
||||
# Combat: pads hide, figures stand on the ground, the lane view shows the own army.
|
||||
game.hud._starter.pressed.emit()
|
||||
await settle()
|
||||
await key(KEY_SPACE)
|
||||
await settle(2)
|
||||
check(state.phase == LuneMatch.Phase.COMBAT, "Space starts the combat")
|
||||
var hidden: bool = true
|
||||
for key_name: String in world._pads:
|
||||
hidden = hidden and not (world._pads[key_name] as MeshInstance3D).visible
|
||||
check(hidden, "deployment areas hide during combat")
|
||||
game.paused = true
|
||||
for i: int in range(240):
|
||||
state.step_combat()
|
||||
await settle(3)
|
||||
var grounded: bool = true
|
||||
var worst: float = 0.0
|
||||
for view: Node3D in game._views.values():
|
||||
var expected: float = world.height_at(Vector2(view.position.x, view.position.z)) + .03
|
||||
worst = maxf(worst, absf(view.position.y - expected))
|
||||
grounded = grounded and absf(view.position.y - expected) < .05
|
||||
facts.unit_ground_error = worst
|
||||
check(grounded and game._views.size() > 20, "all %d figures stand on the terrain (max error %.3f m)" % [game._views.size(), worst])
|
||||
await key(KEY_F3)
|
||||
await settle(3)
|
||||
var own_visible: int = 0
|
||||
for unit: LuneUnit in state.combat.units:
|
||||
if unit.alive and unit.team == team and unit.lane == "mid" and on_screen(game, game.world.to3(unit.position)):
|
||||
own_visible += 1
|
||||
check(own_visible > 0, "F3 in combat shows the own Mid army (%d on screen)" % own_visible)
|
||||
# Real-time camera motion reaches its goal.
|
||||
game.camera.reduced_motion = false
|
||||
await key(KEY_B)
|
||||
await get_tree().create_timer(1.2).timeout
|
||||
check(near_focus(game, map.core_position(team), 1.0), "a smooth camera jump arrives within 1.2 s")
|
||||
main.queue_free()
|
||||
await settle()
|
||||
|
||||
class LuneCameraPitch:
|
||||
static func of(camera: Camera3D) -> float:
|
||||
var direction: Vector3 = (camera.focus - camera.position).normalized()
|
||||
return asin(clampf(direction.y, -1.0, 1.0))
|
||||
1
tools/luneraria/verify_g1.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c36slssmwrcrh
|
||||
6
tools/luneraria/verify_g1.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/luneraria/verify_g1.gd" id="1"]
|
||||
|
||||
[node name="VerifyG1" type="Node"]
|
||||
script = ExtResource("1")
|
||||