G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das HUD im Unity-Projekt (../luneraria-unity). - HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse, Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails, Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben - Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie - Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715; verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern - Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md - HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137
docs/luneraria/G3-HUD-BESTAND.md
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# G3 · HUD-Bestand vor dem Umbau
|
||||||
|
|
||||||
|
Stand: 29. September 2026 · Grundlage `luneraria/g2` (`9f62e30`), Arbeitszweig `luneraria/g3`.
|
||||||
|
Diese Aufnahme beschreibt das HUD, wie es G2 ausliefert. Sie ist die Referenz für die Entscheidungen in [G3-HUD.md](G3-HUD.md).
|
||||||
|
|
||||||
|
## Aufnahmen
|
||||||
|
|
||||||
|
Alle Bilder stammen aus dem echten Spiel (`LuneGame` mit dem echten HUD). `tools/luneraria/g3_views.gd` setzt nur öffentlichen Partiezustand (Runde, Gold, Gebäude-HP, Boss-Claim) und bedient das HUD über dessen eigene Methoden; dasselbe Skript fotografiert später das G3-HUD.
|
||||||
|
|
||||||
|
| Auflösung | Ordner | logische Fläche |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1920×1080 | `screenshots/g3/vorher-1920x1080/` | 1706×960 (Skalierung 1,125) |
|
||||||
|
| 2560×1440 | `screenshots/g3/vorher-2560x1440/` | 1706×960 (1,5) |
|
||||||
|
| 1280×720 | `screenshots/g3/vorher-1280x720/` | 1706×960 (0,75) |
|
||||||
|
|
||||||
|
Motive je Ordner: `01-planung`, `02-held-ausgewaehlt`, `03-itemauswahl`, `04-einheit-ausgewaehlt`, `05-angriffschampion`, `06-boss-verfuegbar`, `07-boss-angekuendigt`, `08/09/10-kampf-top/mid/bottom`, `11-eigener-ancient-kritisch`, `12-gegnerischer-ancient-kritisch`, `13-turm-zerstoert`, `14-grosse-kampfansicht`.
|
||||||
|
|
||||||
|
Wichtig für das Layout: Das Projekt skaliert mit `canvas_items`/`expand` von 1600×960 aus. Jede 16:9-Auflösung ergibt dieselbe logische Fläche von 1706×960; 1280×720 zeigt also exakt dasselbe Layout wie 2560×1440, nur mit halb so vielen Pixeln. Ein 16-px-Text ist bei 720p physisch 12 px hoch. Ultrawide (21:9) verbreitert die Fläche auf etwa 2275×960, 16:10 und das Mindestfenster 1152×720 schmälern sie auf 1600×960.
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
| Datei | Zeilen | Inhalt |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `scripts/luneraria/ui/lune_hud.gd` | 484 | das gesamte HUD in einem Skript: Kopfleiste, Lane-Panel, Planungsseite, Kampfpanel, Bericht, Minimap-Rahmen, Ergebnis |
|
||||||
|
| `scripts/luneraria/ui/lune_hero_panel.gd` | 186 | Reiter „Held“: Anwerben, Stufe/EP, Einsatz-Lane, Items, Talente, Live-Porträt |
|
||||||
|
| `scripts/luneraria/ui/lune_reinforce_panel.gd` | 113 | Reiter „Verstärkung“: Champion-Buchung, Ankündigungen, Boss-Lane |
|
||||||
|
| `scripts/luneraria/ui/lune_minimap.gd` | 80 | Minimap aus dem Partiezustand |
|
||||||
|
| `scripts/luneraria/ui/lune_settings.gd` | 68 | Optionen und Steuerungstext |
|
||||||
|
| `scripts/ui/theme/saumwacht_theme.gd`, `forged_style.gd` | 247 + 69 | gemeinsames Menü-Theme (Basaltplatten, Bronzekanten) |
|
||||||
|
| `scripts/ui/components/choice_group.gd` | – | Segmentauswahl, im HUD für die drei Reiter |
|
||||||
|
|
||||||
|
Bildschirmaufteilung (logische Pixel bei 1706×960):
|
||||||
|
|
||||||
|
- **Kopfleiste** über die ganze Breite (Höhe ≈ 96): Titel, Phase, „Welle n“, zwei Ancient-Balken mit Zahl, Gold, Einkommen, Armeegröße, Identität (Team · Lane · Bund), Pause/Tempo (nur Kampf), Optionen, Menü.
|
||||||
|
- **Links** (300 breit, ≈ 400 hoch): „LANES · TÜRME“ mit drei Knöpfen `TOP Blau ●● Rot ●●` plus Besitzernamen, darunter Teamlisten mit Basistürmen und der Grottenstatus als Fließtext.
|
||||||
|
- **Rechts** (484 breit ≈ 28–30 % der Breite, 112 bis 270 über dem Boden): Reiter Truppen / Held / Verstärkung, Kaufkacheln, Armeeliste mit T/M/B/G-Umschaltern, ↑-Aufwertung, ×-Verkauf, Startaufstellung, Rückgängig, Bereit. Im Kampf ersetzt ein Textpanel mit Einheitenzahlen pro Lane die Seite.
|
||||||
|
- **Unten links** (bis 760 breit): Rundenbericht und Hinweis-/Fehlerzeile.
|
||||||
|
- **Unten rechts**: Minimap 236×236.
|
||||||
|
- **Ergebnis**: Vollbild-Abdunklung mit Sieg/Niederlage.
|
||||||
|
|
||||||
|
## Theme und Komponenten
|
||||||
|
|
||||||
|
- Das HUD nutzt das Menü-Theme `SaumwachtTheme` unverändert: Varianten `CaptionPanel`, `MenuPanel`, `SectionLabel`, `MutedLabel`, `CardTitleLabel`, `PrimaryButton`, `QuietButton`, `ChoiceButton`.
|
||||||
|
- Es gibt keine HUD-Komponenten. Knöpfe, Balken und Zeilen entstehen lokal; `_bar()` legt für jeden Balken eigene `StyleBoxFlat`-Kopien an.
|
||||||
|
- Die Menüplatten (`MenuPanel`: 30/26 px Innenabstand, Nieten, 18 px Schatten) sind für ein Spielfeld-HUD zu schwer; der rechte Block wirkt wie ein Menü über dem Feld.
|
||||||
|
|
||||||
|
## Porträts
|
||||||
|
|
||||||
|
- Kauf- und Armeekacheln zeigen `AssetLibrary.icon()`, also die 2D-Konzeptbilder aus Phase 3, nicht die Spielmodelle. Heldenmodelle, Champions, Monster und die Schlundkrone haben dort gar kein Bild.
|
||||||
|
- Nur der Heldenreiter rendert das echte Modell: eine eigene `SubViewport` mit eigener Welt, `UPDATE_WHEN_VISIBLE`, jede Frame animiert, solange der Reiter offen ist.
|
||||||
|
- Keine einheitliche Kamera, kein Cache.
|
||||||
|
|
||||||
|
## Eingabe
|
||||||
|
|
||||||
|
| Taste | Wirkung (G2) | Ort |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1–5 | Truppe kaufen | `lune_game._input` |
|
||||||
|
| Leertaste | Planung: Bereit · Kampf: Pause | `lune_game._input` |
|
||||||
|
| P | Pause | `lune_game._input` |
|
||||||
|
| Tab | nächste Lane | `lune_game._input` |
|
||||||
|
| F2 / F3 / F4 | Kamera Top / Mid / Bottom | `lune_game._input` |
|
||||||
|
| B · G · H · O · R | eigene Basis · Grotte · Held · Übersicht · Kamera zurück | `lune_game._input` |
|
||||||
|
| Q / E | Kamera drehen | `lune_game._input` |
|
||||||
|
| WASD, Pfeile, mittlere Maustaste, Mausrad | Kamera | `lune_camera` |
|
||||||
|
| F1 · M · F11 · Esc | Optionen · Ton · Vollbild · Optionen schließen | `lune_main._unhandled_input` |
|
||||||
|
|
||||||
|
Frei sind unter anderem C, V, X, Z, N, U, I, J, K, L, T, Y und F5–F10. Die Welt kennt keine Auswahl per Linksklick; Einheiten wählt man nur über die Armeeliste.
|
||||||
|
|
||||||
|
## Tooltips
|
||||||
|
|
||||||
|
Alle Hinweise sind Godot-Standard-Tooltips (`tooltip_text`): reiner Text, feste Verzögerung, Stil aus dem Menü-Theme. Einheitenwerte stehen nur im Tooltip der Kaufkachel (HP, „Schaden/s“ – tatsächlich Schaden pro Treffer bei 1 Angriff/s –, Reichweite, Fähigkeit). Item- und Talenttexte erscheinen nur als Tooltip.
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
Lautstärke, Ton stumm, Vollbild, Bewegung reduzieren, dazu ein Steuerungstext. Es gibt keine UI-Größe. „Bewegung reduzieren“ wirkt auf Kamera, Menü und Effekte; das HUD hat keine Animationen.
|
||||||
|
|
||||||
|
## Minimap
|
||||||
|
|
||||||
|
- Zeichnet Lanes, Fluss, Grotte (Farbe: frei/gesperrt/Team), alle Gebäude (voll/zerstört) und im Kampf **jede** bewegliche Einheit als Punkt. Bei 90–120 Einheiten entsteht eine Punktwolke; Helden (gold) und Boss (größer) gehen darin unter.
|
||||||
|
- Türme beider Teams unterscheiden sich nur durch die Farbe.
|
||||||
|
- Klick und Ziehen bewegen die Kamera; das Trapez des Kamerabilds ist sichtbar.
|
||||||
|
- `queue_redraw()` in jeder Frame, unabhängig von Änderungen.
|
||||||
|
|
||||||
|
## Lanes, Türme, Ancients
|
||||||
|
|
||||||
|
- Lane-Zustand nur als `●●`/`●○` für eigene und gegnerische Außen-/Innentürme; kein Schadensstand, keine Bedrohung, keine Angabe, welche Seite drückt, keine angekündigten Champions.
|
||||||
|
- Basistürme stehen als „Basistürme 2/2“ im Teamtext.
|
||||||
|
- Ancients: zwei gleich gewichtete Balken mit Zahl in der Kopfleiste; kein kritischer Zustand.
|
||||||
|
- Klick auf eine Lane springt mit der Kamera dorthin (gut, bleibt).
|
||||||
|
|
||||||
|
## Held, Items, Talente
|
||||||
|
|
||||||
|
- Heldenreiter mit Live-Porträt, Stufe, EP-Balken (Werte nur im Tooltip), Einsatz-Lane, drei Itemknöpfen (Name + ×), sechs Item-Kaufknöpfen, Talentwahl.
|
||||||
|
- Keine Grundwerte (HP, Schaden, Tempo, Reichweite, Rüstung) und keine Fähigkeitswerte.
|
||||||
|
- Item-Zustände nur über „deaktiviert“; warum ein Item nicht geht (Gold, Stufe, schon getragen, Plätze voll), steht nirgends.
|
||||||
|
- Bei 1280×720 ist der Reiter abgeschnitten: Itemkauf und Talente liegen unter „Startaufstellung/Rückgängig“ und brauchen Scrollen (`vorher-1280x720/03-itemauswahl.png`).
|
||||||
|
|
||||||
|
## Normale Einheiten
|
||||||
|
|
||||||
|
- Eine „Auswahl“ gibt es nicht; jede Armeezeile hat Porträt (2D), abgeschnittenen Namen („Korkı“, „Dorn“, „Same“), T/M/B/G, ↑ Kosten, ×.
|
||||||
|
- Werte der gekauften Einheit, Aufwertungsziel und Rückgabebetrag nur im Tooltip.
|
||||||
|
- G (Grotte) ohne Warnung, dass die Einheit ihrer Lane die ganze Kampfphase fehlt (nur im Tooltip und im Grottentext links).
|
||||||
|
|
||||||
|
## Angriffschampions, Boss, Wellen
|
||||||
|
|
||||||
|
- Champions: Textliste im Reiter „Verstärkung“ mit Ziel-Lane-Schaltern; Buchungen als „Gebucht: … Runde n“. Optisch wie normale Knöpfe, kein Unterschied zu dauerhaften Truppen.
|
||||||
|
- Ankündigungen (öffentlich) nur in diesem Reiter.
|
||||||
|
- Bossgrotte: Fließtext links („verfügbar“, „gesperrt bis Runde n“, „von Blau erobert … greift in Runde 12 über Mid an“). Kein Hinweis auf dem Spielfeld, kein Kopfgeld im Blick, keine Unterscheidung „umkämpft“.
|
||||||
|
- Wellen: „Welle n“ ohne Obergrenze (richtig). Was die nächste Welle bringt, zeigt das HUD nicht, obwohl `LuneWaves.build()` öffentlich und deterministisch ist.
|
||||||
|
- Ereignisse: nur der Rundenbericht nach dem Kampf und eine Hinweiszeile; Turmfall, Bossangriff oder kritischer Ancient erscheinen nicht im Moment des Geschehens.
|
||||||
|
|
||||||
|
## Netzwerk und Privatsphäre
|
||||||
|
|
||||||
|
- Der Gast erhält `rpc_state` mit dem **Gold aller Spieler**, deren bestätigten Armeen, Helden und Buchungen; den Entwurf (`draft`) nur für den eigenen Sitz.
|
||||||
|
- Das G2-HUD zeigt Gold nur für den eigenen Sitz (`gold_of(human_slot)`) – das bleibt Pflicht.
|
||||||
|
- Bereit-Status, Mensch/Bot und Verbindungsabbrüche zeigt das HUD nicht; Sitzungsmeldungen erscheinen nur als Hinweiszeile.
|
||||||
|
- Nach „Bereit“ lehnt der Host weitere Befehle ab („Bereits bereit gemeldet …“); das HUD zeigt dann „WARTE AUF ANDERE …“, lässt aber Kaufkacheln, Lane-Knöpfe und Tasten 1–5 aktiv.
|
||||||
|
|
||||||
|
## Leistung
|
||||||
|
|
||||||
|
- `refresh()` läuft jede Frame: alle Beschriftungen werden neu formatiert, im Kampf wird zusätzlich über alle Einheiten gezählt (O(n) je Frame), in der Planung `JSON.stringify(draft)` gebildet.
|
||||||
|
- Held- und Verstärkungsreiter bilden jede Frame eine JSON-Signatur.
|
||||||
|
- Die Minimap zeichnet jede Frame alle Einheiten.
|
||||||
|
- Die Heldenporträt-`SubViewport` rendert jede Frame, solange der Reiter offen ist.
|
||||||
|
|
||||||
|
## Konsequenzen für G3
|
||||||
|
|
||||||
|
1. HUD in Komponenten zerlegen, Theme um HUD-Varianten erweitern statt lokaler StyleBox-Kopien.
|
||||||
|
2. Ancients und Welle in eine schmale Kopfzeile; die Ancients bekommen mehr Gewicht als Einzeltürme.
|
||||||
|
3. Lane-Übersicht mit echten Turmsymbolen (Form + Schadensstand), deterministischer Bedrohungsstufe aus öffentlichem Zustand und angekündigten Gefahren.
|
||||||
|
4. Kauf, Armee und Held nach unten; rechts nur Kontext (Auswahl, nächste Welle, Boss) statt einer dauerhaften 30-%-Leiste.
|
||||||
|
5. Porträts aus den echten Modellen, einmal gerendert und zwischengespeichert, statt 2D-Skizzen und Live-Viewport.
|
||||||
|
6. Minimap mit zusammengefassten Armeen, deutlichen Helden, Champions und Boss, Teamformen statt nur Farben.
|
||||||
|
7. Ereignisleiste für Turmfall, Champions, Boss, kritische Ancients.
|
||||||
|
8. Bereit-Sperre sichtbar machen, fremdes Gold und fremde Entwürfe weiter nie anzeigen, Mensch/Bot/Verbindung pro Mitspieler zeigen.
|
||||||
|
9. Aktualisierung nach Änderungen statt jeder Frame; Minimap gedrosselt.
|
||||||
|
10. Layout für 1600–2300 logische Breite und eine UI-Größe in den Optionen; Mindestschriftgröße so, dass 720p lesbar bleibt.
|
||||||
59
docs/luneraria/G3-HUD.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# G3 · Ingame-HUD (Zwischenstand)
|
||||||
|
|
||||||
|
Stand: 29. September 2026 · Zweig `luneraria/g3` (von `luneraria/g2`, `9f62e30`).
|
||||||
|
|
||||||
|
**G3 ist nicht abgenommen.** Nach dem ersten Spieltest hat der Projektinhaber entschieden, Champions of Luneraria nach Unity zu portieren. Dieser Stand ist die Referenz für das HUD in Unity (Portphase U3). Ausgangslage vor dem Umbau: [G3-HUD-BESTAND.md](G3-HUD-BESTAND.md).
|
||||||
|
|
||||||
|
## Was steht
|
||||||
|
|
||||||
|
**Informationsarchitektur** (logische Pixel bei 1706×960, UI-Größe darauf):
|
||||||
|
|
||||||
|
| Bereich | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| oben Mitte | eigener Ancient (Emblem Mondsichel/Sonnenscheibe, HP-Balken mit Wert, zwei Basistürme, Zustand), **WELLE n** ohne Obergrenze, Phase (PLANUNG, PLANUNG · BEREIT, KAMPF mm:ss), gegnerischer Ancient |
|
||||||
|
| oben rechts | Pause, Tempo (nur im Kampf), Optionen F1, Menü |
|
||||||
|
| links | drei Lane-Zeilen: Bedrohung, Turmkette von der eigenen zur gegnerischen Basis mit HP, Spieler und Helden beider Seiten, angekündigte Champions/Boss, Einheitenzahl im Kampf; darunter das eigene Team und die Ereignisleiste |
|
||||||
|
| unten | Gold und Einkommen, Rückgängig; Kaufkarten 1–5 oder Reiter Angriffschampions; eigene Armee als Porträt-Chips mit Lane und Stufe; Heldenkarte |
|
||||||
|
| rechts | Bossgrotte, nächste Welle, ausgewählte Einheit; nie eine dauerhafte Seitenleiste über 30 % |
|
||||||
|
| unten rechts | Bereit (Planung) bzw. Kampfuhr, darunter die Minimap |
|
||||||
|
|
||||||
|
**Komponenten** (`scripts/luneraria/ui/components/`, Theme `ui/theme/lune_ui_theme.gd`): StatusBadge, HpBar, ResourceDisplay, TowerIndicator, LaneIndicator, TeamPanel, UnitCard (Champion-Variante kupfern, schraffiert, „1 WELLE“), ArmyChip, HeroCard, HeroDetail (Anwerben, Werte, Items, Talente), ItemSlot mit acht Zuständen, UnitDetail, ChampionPanel, BossIndicator, NextWave, EventFeed, TabBar, ModalPanel, ein gemeinsamer Tooltip. Symbole sind Vektorgrafiken (`LuneGlyphs`), Porträts kommen aus den echten Modellen (`LunePortraits`: eine Offscreen-Kamera, jedes Modell einmal gerendert und zwischengespeichert).
|
||||||
|
|
||||||
|
**Datenzugang und Privatsphäre:** Alles läuft über `LuneHudModel`. Gold, Entwurf und neue Buchungen gibt es nur für den eigenen Sitz; von anderen Spielern nur Name, Lane, Bereitschaft, Mensch/Bot/offline und der bestätigte Held. Die Lane-Bedrohung (`LuneLaneThreat`: RUHIG, UMKÄMPFT, UNTER DRUCK, KRITISCH) rechnet nur mit öffentlichen Daten und für Host und Gast gleich; auf einem Gast werden Positionen auf die Lane projiziert.
|
||||||
|
|
||||||
|
**Bedienung:** Klick auf Lane, Ereignis, Bossgrotte oder Minimap bewegt die Kamera; F2–F4, G, B, H wie in G2, alle in den Optionen gelistet. Linksklick auf eine eigene Figur im Feld wählt sie aus. Nach „Bereit“ sind Karten, Tasten 1–5, Rückgängig und Einsatzknöpfe gesperrt. Bereit ignoriert eine Sekunde nach Kampfende Klicks und Leertaste. Das Mausrad über HUD-Flächen zoomt die Welt nicht.
|
||||||
|
|
||||||
|
**Aktualisierung:** 10 Hz in der Planung, 5 Hz im Kampf, Minimap 10 Hz mit eigener Ebene für den Kamerarahmen; jede Komponente vergleicht eine Signatur und baut nur bei Änderungen neu.
|
||||||
|
|
||||||
|
**Team-Erkennung ohne Farbe:** Blau rund (Mondsichel), Rot eckig (Sonnenscheibe), Grotte sechseckig: in HUD, Minimap, Einheitenringen im Feld. Die entsandte Schlundkrone trägt einen zweiten Besitzring und „für Blau/Rot“ im Namen (G2-Risiko der Farbnähe).
|
||||||
|
|
||||||
|
**Legion TD 2 / Dota 2:** Die Wellenvorschau zeigt die Rüstung jedes Monsters und welcher Schadenstyp stark dagegen ist (Schadensmatrix).
|
||||||
|
|
||||||
|
## Prüfung
|
||||||
|
|
||||||
|
| Prüfung | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| `tools/luneraria/verify_g3.tscn` (Layout 1152×720 bis 2560×1440 und 2560×1080 bei 90/100/130 %, Status, Held/Items/Einheiten, Champions/Boss, Bereit-Sperre, Bots, Netzgast mit Privatsphäre, Eingabe, Ereignisse, Bewegung reduzieren, Raten, Porträt-Cache) | 715/715 |
|
||||||
|
| Regeln `test_lune_core` / `test_lune_m2` / `test_lune_net` | 91/91 · 73/73 · 43/43 |
|
||||||
|
| `verify_m1` / `verify_m2` (UI-Treiber an das neue HUD angepasst, Regelprüfungen unverändert) | 38/38 · 14/14 |
|
||||||
|
|
||||||
|
Bilder: `screenshots/g3/zwischenstand-1920x1080/`, Spieltest `screenshots/g3/spieltest/`, Ablauf `logs/g3/playtest-log.txt`.
|
||||||
|
|
||||||
|
## Spieltest (Partie über die echte Oberfläche bis Welle 8)
|
||||||
|
|
||||||
|
Werkzeug: `tools/luneraria/g3_playtest.tscn` (Mausklicks, Tasten und Mausrad an Fensterkoordinaten, Bildschirmfoto nach jedem Schritt; keine Eingriffe in den Spielzustand).
|
||||||
|
|
||||||
|
| Befund | Status |
|
||||||
|
| --- | --- |
|
||||||
|
| Pause/Tempo lagen im Kampf genau dort, wo danach „Bereit“ erscheint; ein später Klick übersprang die Planung von Welle 2 | behoben: Pause/Tempo oben rechts, Bereit 1 s gesperrt |
|
||||||
|
| Heldenkarte bot im Kampf „Held anwerben“ an | behoben: „Kein Held · Anwerben in der Planung“ |
|
||||||
|
| „1 WELLE“ auf Champion-Karten überdeckt die Köpfe der Porträts | offen: als Band am unteren Porträtrand |
|
||||||
|
| Gut lesbar: Hotkey-Kauf mit Rückmeldung, Lane-Tooltip mit Turm-HP und Gründen, gefallene Einheiten durchgestrichen, Stufenaufstieg und Turmfall in der Ereignisleiste, „TALENT!“, Item-Zustände, Champion-Ankündigung „FÜR EUCH“ in Lane und Wellenvorschau | – |
|
||||||
|
|
||||||
|
## Offen (für U3 übernehmen)
|
||||||
|
|
||||||
|
- Champion-Etikett (siehe oben).
|
||||||
|
- Schadens- und Rüstungstyp als Symbol auf Einheitenkarten, farblich passend zur Wellenvorschau (Legion-TD-Prinzip).
|
||||||
|
- Armeewert (Summe des Einsatzes) in der Armeezeile.
|
||||||
|
- Heldenporträts statt Punkten auf der Minimap (Dota-Prinzip).
|
||||||
|
- Aufnahmen 2560×1440, 1280×720, Ultrawide für den Endstand, Video, Leistungsvergleich G2 → G3.
|
||||||
47
docs/luneraria/HELDEN-KONZEPT.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Heldenkonzept · Champions of Luneraria
|
||||||
|
|
||||||
|
Stand: 29. September 2026 · Vorgabe des Projektinhabers, während G3 eingegangen.
|
||||||
|
**Noch nicht umgesetzt.** G3 schließt neue Einheiten und Regeländerungen aus; die Umsetzung gehört in eine eigene Phase (Einheiten/Helden, frühestens mit G6).
|
||||||
|
Die vier Helden sollen die drei vorläufigen Helden ablösen. Diese nutzen bisher vorhandene Modelle: Ringborke (w04), Quellgeflecht (w10) und Funkenlanze (g05); siehe `scripts/luneraria/core/lune_content.gd`.
|
||||||
|
|
||||||
|
## Die vier Helden
|
||||||
|
|
||||||
|
| | Lucy | Ghaliga | Siegfried | Feena |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Volk | Dunkelelfe | Zwerg | Mensch | Waldelfe |
|
||||||
|
| Rolle | Kontrolle, Schwächung, Wellen stören | Tank, Bruiser, Frontkämpfer | Anführer, Paladin, Team-Buff, Front | Fernkampf-DPS, Boss- und Elitekiller |
|
||||||
|
| Waffe | Bogen | Axt | Schwert (gold-blau) | Bogen |
|
||||||
|
| Fähigkeit | Frostpfeile verlangsamen Gegner | Wirbelhieb: Flächenangriff um ihn herum | Schlachtruf stärkt nahe Verbündete | Giftpfeile: Schaden über Zeit |
|
||||||
|
| Ultimate (Command-Menü) | Multischuss: mehrere Pfeile auf mehrere Ziele, Kontrolle und Flächendruck statt Burst | Versteinerung: absorbiert massiv Schaden, extrem zäh, optional Bindung/Aggro | Excalibur: heiliger Schlag mit hohem Schaden und kurzer Betäubung mehrerer Gegner | Turbopfeile: kurz stark erhöhte Angriffsgeschwindigkeit |
|
||||||
|
| Spielziel | gefährliche Wellen verlangsamen, Formationen stören | Front halten, Gegner binden, Wellen abfangen | Verbündete stärken, kritische Kämpfe drehen | hoher anhaltender Fernkampfschaden, besonders gegen starke Einzelziele |
|
||||||
|
| Optik | lange weiße Haare, grüne Augen, dunkle Haut, dunkle Lederkleidung mit dunkelblauen Akzenten, frostige Nachtästhetik | rote Haare, langer Bart, blaue Augen, goldene Zwergenrüstung, schwere robuste Silhouette | kurze blonde Haare, blaue Augen, goldene Paladinrüstung | lange rote Haare, grüne Augen, grün-braune Lederrüstung, flinke naturverbundene Schützin |
|
||||||
|
|
||||||
|
## Was die Umsetzung berührt (Notizen für die spätere Phase)
|
||||||
|
|
||||||
|
**Neue Mechanik: Ultimates über ein Command-Menü.** Bisher laufen Kämpfe automatisch, Helden handeln allein und Spieler geben nur in der Planung Befehle. Ein Ultimate im Kampf braucht deshalb:
|
||||||
|
- Regeln: Auslösen, Abklingzeit oder Ladung, Zielwahl (Spieler oder automatisch), Dauer, Reihenfolge im Simulationsschritt, Determinismus.
|
||||||
|
- Netzwerk: einen Kampfbefehl vom Gast an den Host; heute gibt es nur Planungsbefehle.
|
||||||
|
- Bots: wann sie auslösen.
|
||||||
|
- HUD: ein Command-Menü. G3 hat bewusst keine MOBA-Aktionsleiste; Heldenkarte und Heldenfenster haben aber Platz für Fähigkeit und Ultimate.
|
||||||
|
|
||||||
|
**Effekte gegenüber dem heutigen Regelwerk:**
|
||||||
|
|
||||||
|
| Effekt | Stand heute |
|
||||||
|
| --- | --- |
|
||||||
|
| Verlangsamung (Frost) | vorhanden: `movement_slow`, `attack_slow` |
|
||||||
|
| Gift über Zeit | vorhanden: DoTs wie `seed_burn`, `burn` |
|
||||||
|
| Fläche um den Helden (Wirbelhieb) | vorhanden: Splash-Logik |
|
||||||
|
| Buff naher Verbündeter (Schlachtruf) | vorhanden: `damage`-Effekt |
|
||||||
|
| Angriffstempo (Turbopfeile) | vorhanden: `attack_speed`-Effekt |
|
||||||
|
| Schadensabsorption / Zähigkeit (Versteinerung) | vorhanden: Schild und Schadensminderung (Deckel 35 %) |
|
||||||
|
| Betäubung (Excalibur) | **neu** |
|
||||||
|
| Bindung / Aggro (Versteinerung) | **neu**: Zielwahl-Regel |
|
||||||
|
|
||||||
|
**Assets:** vier neue skelettierte Modelle (Blender) mit mindestens idle, walk, attack, ability, ultimate, hit und death, dazu die Waffen (zwei Bögen, Axt, Schwert). Porträts entstehen automatisch über den G3-Porträt-Cache aus den Modellen.
|
||||||
|
|
||||||
|
**Offene Fragen:**
|
||||||
|
- Bleibt es bei einem Helden pro Spieler?
|
||||||
|
- Hängen Helden an Bünden (Glutpakt/Wurzelbund) oder sind sie frei wählbar?
|
||||||
|
- Ersetzen die vier die drei vorläufigen Helden vollständig?
|
||||||
|
- Wie oft gibt es das Ultimate (je Welle, Abklingzeit in Sekunden, Ladung über Stufe)?
|
||||||
|
- Wie werden Talente und Items auf die neuen Rollen abgestimmt?
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# STATUS · Champions of Luneraria
|
# STATUS · Champions of Luneraria
|
||||||
|
|
||||||
Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1`, `luneraria/g2` · Luneraria-Demo 0.17.0-g2 (macOS, Windows x64/ARM64, Linux).
|
Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1`, `luneraria/g2`, `luneraria/g3` (Zwischenstand) · Luneraria-Demo 0.17.0-g2 (macOS, Windows x64/ARM64, Linux).
|
||||||
|
|
||||||
|
**Engine-Wechsel (29. September 2026):** Der Projektinhaber hat entschieden, Champions of Luneraria nach Unity 6 zu portieren, um eine höhere Grafikqualität zu erreichen. Das Unity-Projekt liegt in `../luneraria-unity`. Dieses Godot-Projekt bleibt als Referenz für Regeln, Tests, Assets und HUD-Design erhalten; neue Arbeit findet im Unity-Projekt statt.
|
||||||
|
|
||||||
Arbeitsrechner seit G2: Mac (Apple M5, macOS 27) mit Godot 4.7.2 und Blender 5.2.2 LTS; bis G1 Fedora 44 (RTX 5060 Ti). Werkzeugpfade und Plattformhinweise: [ANLEITUNG.md](ANLEITUNG.md).
|
Arbeitsrechner seit G2: Mac (Apple M5, macOS 27) mit Godot 4.7.2 und Blender 5.2.2 LTS; bis G1 Fedora 44 (RTX 5060 Ti). Werkzeugpfade und Plattformhinweise: [ANLEITUNG.md](ANLEITUNG.md).
|
||||||
|
|
||||||
|
|
@ -13,7 +15,8 @@ Arbeitsrechner seit G2: Mac (Apple M5, macOS 27) mit Godot 4.7.2 und Blender 5.2
|
||||||
| M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: Windows auf x64-Hardware, Netz über mehrere Rechner) |
|
| M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: Windows auf x64-Hardware, Netz über mehrere Rechner) |
|
||||||
| G1 · Map-Blockout und Drei-Lane-Layout | **abgeschlossen** ([G1-KARTE.md](G1-KARTE.md)) |
|
| G1 · Map-Blockout und Drei-Lane-Layout | **abgeschlossen** ([G1-KARTE.md](G1-KARTE.md)) |
|
||||||
| G2 · Ancients, Türme und Bossgrotte | **abgeschlossen** ([G2-STRATEGISCHE-BAUWERKE.md](G2-STRATEGISCHE-BAUWERKE.md), [G2-ASSETS.md](G2-ASSETS.md)) |
|
| G2 · Ancients, Türme und Bossgrotte | **abgeschlossen** ([G2-STRATEGISCHE-BAUWERKE.md](G2-STRATEGISCHE-BAUWERKE.md), [G2-ASSETS.md](G2-ASSETS.md)) |
|
||||||
| G3–G6 · HUD, Umgebung, Licht, Einheiten | offen |
|
| G3 · Ingame-HUD | **Zwischenstand, nicht abgenommen** ([G3-HUD.md](G3-HUD.md)); abgebrochen zugunsten des Unity-Ports |
|
||||||
|
| G4–G6 · Umgebung, Licht, Einheiten | in Godot nicht mehr geplant; werden nach dem Port in Unity fortgesetzt |
|
||||||
|
|
||||||
## M1 · abgeschlossen
|
## M1 · abgeschlossen
|
||||||
|
|
||||||
|
|
|
||||||
94
docs/luneraria/logs/g3/playtest-log.txt
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
6.4 s #1 [menu] wait 1.5
|
||||||
|
7.9 s #1 [menu] click 290 853
|
||||||
|
7.9 s #1 [menu] wait 1.2
|
||||||
|
9.1 s #1 [menu] click 247 320
|
||||||
|
9.1 s #1 [menu] wait 0.4
|
||||||
|
9.5 s #1 [menu] click 415 945
|
||||||
|
9.8 s #1 [round=1 phase=PLANUNG] wait 2.5
|
||||||
|
32.2 s #2 [round=1 phase=PLANUNG] click 988 916
|
||||||
|
32.3 s #2 [round=1 phase=PLANUNG] wait 0.5
|
||||||
|
32.7 s #2 [round=1 phase=PLANUNG] move 1000 400
|
||||||
|
32.7 s #2 [round=1 phase=PLANUNG] key SPACE
|
||||||
|
33.8 s #2 [round=1 phase=KAMPF] wait 1.5
|
||||||
|
35.1 s #2 [round=1 phase=KAMPF] shot r1-kampfbeginn
|
||||||
|
35.5 s #2 [round=1 phase=KAMPF] click 1812 33
|
||||||
|
35.5 s #2 [round=1 phase=KAMPF] key F3
|
||||||
|
35.5 s #2 [round=1 phase=KAMPF] wait 5
|
||||||
|
65.2 s #3 [round=1 phase=KAMPF] key ESCAPE
|
||||||
|
65.2 s #3 [round=1 phase=KAMPF] wait 0.3
|
||||||
|
65.5 s #3 [round=1 phase=KAMPF] click 1741 33
|
||||||
|
65.5 s #3 [round=1 phase=KAMPF] key F3
|
||||||
|
65.5 s #3 [round=1 phase=KAMPF] wait 5
|
||||||
|
96.1 s #4 [round=1 phase=KAMPF] key P
|
||||||
|
96.1 s #4 [round=1 phase=KAMPF] wait 7
|
||||||
|
118.5 s #5 [round=1 phase=KAMPF] wait 12
|
||||||
|
153.0 s #6 [round=2 phase=PLANUNG] move 1000 450
|
||||||
|
153.0 s #6 [round=2 phase=PLANUNG] key 4
|
||||||
|
153.0 s #6 [round=2 phase=PLANUNG] wait 0.4
|
||||||
|
153.4 s #6 [round=2 phase=PLANUNG] move 180 205
|
||||||
|
153.4 s #6 [round=2 phase=PLANUNG] wait 0.8
|
||||||
|
172.5 s #7 [round=2 phase=PLANUNG] move 1000 450
|
||||||
|
172.6 s #7 [round=2 phase=PLANUNG] key SPACE
|
||||||
|
172.8 s #7 [round=2 phase=KAMPF] wait 0.6
|
||||||
|
173.3 s #7 [round=2 phase=KAMPF] click 1741 33
|
||||||
|
173.3 s #7 [round=2 phase=KAMPF] key F3
|
||||||
|
173.3 s #7 [round=2 phase=KAMPF] wait 8
|
||||||
|
202.8 s #8 [round=3 phase=PLANUNG] wait 10
|
||||||
|
233.1 s #9 [round=3 phase=PLANUNG] move 1000 450
|
||||||
|
233.1 s #9 [round=3 phase=PLANUNG] key 3
|
||||||
|
233.1 s #9 [round=3 phase=PLANUNG] wait 0.4
|
||||||
|
233.5 s #9 [round=3 phase=PLANUNG] click 1165 925
|
||||||
|
233.5 s #9 [round=3 phase=PLANUNG] wait 0.6
|
||||||
|
252.1 s #10 [round=3 phase=PLANUNG] key ESCAPE
|
||||||
|
252.1 s #10 [round=3 phase=PLANUNG] move 1000 450
|
||||||
|
252.1 s #10 [round=3 phase=PLANUNG] key SPACE
|
||||||
|
252.7 s #10 [round=3 phase=KAMPF] wait 20
|
||||||
|
291.3 s #11 [round=4 phase=PLANUNG] move 1000 450
|
||||||
|
291.3 s #11 [round=4 phase=PLANUNG] key SPACE
|
||||||
|
291.4 s #11 [round=4 phase=KAMPF] wait 22
|
||||||
|
332.1 s #12 [round=5 phase=PLANUNG] click 1430 960
|
||||||
|
332.1 s #12 [round=5 phase=PLANUNG] wait 1.0
|
||||||
|
349.0 s #13 [round=5 phase=PLANUNG] click 615 838
|
||||||
|
349.0 s #13 [round=5 phase=PLANUNG] wait 1.0
|
||||||
|
367.2 s #14 [round=5 phase=PLANUNG] key ESCAPE
|
||||||
|
367.2 s #14 [round=5 phase=PLANUNG] move 1000 450
|
||||||
|
367.2 s #14 [round=5 phase=PLANUNG] wait 0.3
|
||||||
|
367.5 s #14 [round=5 phase=PLANUNG] key SPACE
|
||||||
|
368.1 s #14 [round=5 phase=KAMPF] wait 9
|
||||||
|
398.4 s #15 [round=6 phase=PLANUNG] wait 16
|
||||||
|
440.3 s #16 [round=6 phase=PLANUNG] move 1000 450
|
||||||
|
440.3 s #16 [round=6 phase=PLANUNG] key 2
|
||||||
|
440.3 s #16 [round=6 phase=PLANUNG] key 1
|
||||||
|
440.3 s #16 [round=6 phase=PLANUNG] wait 0.5
|
||||||
|
440.8 s #16 [round=6 phase=PLANUNG] shot r6-gekauft
|
||||||
|
458.6 s #17 [round=6 phase=PLANUNG] click 996 995
|
||||||
|
458.6 s #17 [round=6 phase=PLANUNG] wait 0.5
|
||||||
|
475.8 s #18 [round=6 phase=PLANUNG] click 1752 470
|
||||||
|
475.9 s #18 [round=6 phase=PLANUNG] wait 0.5
|
||||||
|
476.4 s #18 [round=6 phase=PLANUNG] key F4
|
||||||
|
476.4 s #18 [round=6 phase=PLANUNG] wait 0.8
|
||||||
|
499.9 s #19 [round=6 phase=PLANUNG] key ESCAPE
|
||||||
|
499.9 s #19 [round=6 phase=PLANUNG] move 1000 450
|
||||||
|
499.9 s #19 [round=6 phase=PLANUNG] key SPACE
|
||||||
|
500.0 s #19 [round=6 phase=KAMPF] wait 1
|
||||||
|
501.0 s #19 [round=6 phase=KAMPF] key F4
|
||||||
|
501.0 s #19 [round=6 phase=KAMPF] wait 22
|
||||||
|
543.9 s #20 [round=7 phase=PLANUNG] click 1430 960
|
||||||
|
544.0 s #20 [round=7 phase=PLANUNG] wait 0.6
|
||||||
|
544.5 s #20 [round=7 phase=PLANUNG] click 1133 258
|
||||||
|
544.5 s #20 [round=7 phase=PLANUNG] wait 0.6
|
||||||
|
565.5 s #21 [round=7 phase=PLANUNG] click 1463 365
|
||||||
|
565.6 s #21 [round=7 phase=PLANUNG] wait 0.5
|
||||||
|
566.1 s #21 [round=7 phase=PLANUNG] click 1018 258
|
||||||
|
566.1 s #21 [round=7 phase=PLANUNG] wait 0.6
|
||||||
|
639.6 s #22 [round=7 phase=PLANUNG] key ESCAPE
|
||||||
|
639.6 s #22 [round=7 phase=PLANUNG] wait 0.3
|
||||||
|
639.9 s #22 [round=7 phase=PLANUNG] click 494 885
|
||||||
|
640.0 s #22 [round=7 phase=PLANUNG] wait 0.6
|
||||||
|
661.1 s #23 [round=7 phase=PLANUNG] click 393 985
|
||||||
|
661.1 s #23 [round=7 phase=PLANUNG] wait 0.6
|
||||||
|
677.6 s #24 [round=7 phase=PLANUNG] click 272 885
|
||||||
|
677.6 s #24 [round=7 phase=PLANUNG] move 1000 450
|
||||||
|
677.6 s #24 [round=7 phase=PLANUNG] key SPACE
|
||||||
|
677.6 s #24 [round=7 phase=KAMPF] wait 24
|
||||||
|
1155.4 s #25 [round=8 phase=PLANUNG] quit
|
||||||
133
docs/luneraria/logs/g3/verify-g3.json
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
{
|
||||||
|
"checks": 715,
|
||||||
|
"facts": {
|
||||||
|
"boss_badges": [
|
||||||
|
"BOSS VERFÜGBAR",
|
||||||
|
"COOLDOWN – 2 RUNDEN",
|
||||||
|
"EINSATZ NÄCHSTE WELLE",
|
||||||
|
"BOSS GREIFT AN · MID",
|
||||||
|
"BOSS UMKÄMPFT"
|
||||||
|
],
|
||||||
|
"checks_per_part": {
|
||||||
|
"A layout": 593,
|
||||||
|
"B status": 27,
|
||||||
|
"C hero and units": 24,
|
||||||
|
"D champions and boss": 19,
|
||||||
|
"E ready, bots, network": 16,
|
||||||
|
"F input": 20,
|
||||||
|
"G events and motion": 8,
|
||||||
|
"H rates and portraits": 8
|
||||||
|
},
|
||||||
|
"layouts": [
|
||||||
|
[
|
||||||
|
"1280x720 @90%",
|
||||||
|
"(1280, 720)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1280x720 @100%",
|
||||||
|
"(1280, 720)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1280x720 @130%",
|
||||||
|
"(1280, 720)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.09090909090909
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1920x1080 @90%",
|
||||||
|
"(1920, 1080)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1920x1080 @100%",
|
||||||
|
"(1920, 1080)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1920x1080 @130%",
|
||||||
|
"(1920, 1080)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.09090909090909
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1440 @90%",
|
||||||
|
"(2560, 1440)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1440 @100%",
|
||||||
|
"(2560, 1440)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1440 @130%",
|
||||||
|
"(2560, 1440)",
|
||||||
|
"(1706.0, 960.0)",
|
||||||
|
1.09090909090909
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1080 @90%",
|
||||||
|
"(2560, 1080)",
|
||||||
|
"(2275.0, 960.0)",
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1080 @100%",
|
||||||
|
"(2560, 1080)",
|
||||||
|
"(2275.0, 960.0)",
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2560x1080 @130%",
|
||||||
|
"(2560, 1080)",
|
||||||
|
"(2275.0, 960.0)",
|
||||||
|
1.09090909090909
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1152x720 @90%",
|
||||||
|
"(1152, 720)",
|
||||||
|
"(1600.0, 1000.0)",
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1152x720 @100%",
|
||||||
|
"(1152, 720)",
|
||||||
|
"(1600.0, 1000.0)",
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1152x720 @130%",
|
||||||
|
"(1152, 720)",
|
||||||
|
"(1600.0, 1000.0)",
|
||||||
|
1.02564102564103
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"portraits": {
|
||||||
|
"cached": 24,
|
||||||
|
"rendered": 24
|
||||||
|
},
|
||||||
|
"rates": {
|
||||||
|
"combat_syncs_per_s": 4,
|
||||||
|
"hud_sync_usec": 1936.70488656746,
|
||||||
|
"minimap_markers": {
|
||||||
|
"boss": 0,
|
||||||
|
"champions": 0,
|
||||||
|
"clusters": 18,
|
||||||
|
"heroes": 6,
|
||||||
|
"units": 114
|
||||||
|
},
|
||||||
|
"minimap_redraws_per_s": 10,
|
||||||
|
"planning_syncs_per_s": 10
|
||||||
|
},
|
||||||
|
"seconds": 17.894
|
||||||
|
},
|
||||||
|
"failures": []
|
||||||
|
}
|
||||||
0
docs/luneraria/screenshots/g3/.gdignore
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/01-planung-welle1.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/02-kampfbeginn.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/03-lane-tooltip.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/04-einheit-details.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/05-held-anwerben.png
Normal file
|
After Width: | Height: | Size: 1,015 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/07-einsatz-bottom.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/luneraria/screenshots/g3/spieltest/08-talentwahl.png
Normal file
|
After Width: | Height: | Size: 876 KiB |
BIN
docs/luneraria/screenshots/g3/spieltest/09-items-zu-teuer.png
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
docs/luneraria/screenshots/g3/spieltest/10-champion-gebucht.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-1280x720/01-planung.png
Normal file
|
After Width: | Height: | Size: 858 KiB |
|
After Width: | Height: | Size: 858 KiB |
BIN
docs/luneraria/screenshots/g3/vorher-1280x720/03-itemauswahl.png
Normal file
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 883 KiB |
|
After Width: | Height: | Size: 891 KiB |
|
After Width: | Height: | Size: 849 KiB |
BIN
docs/luneraria/screenshots/g3/vorher-1280x720/08-kampf-top.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-1280x720/09-kampf-mid.png
Normal file
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 831 KiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-1920x1080/01-planung.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-1920x1080/08-kampf-top.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-1920x1080/09-kampf-mid.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-2560x1440/01-planung.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
|
After Width: | Height: | Size: 3 MiB |
|
After Width: | Height: | Size: 3 MiB |
|
After Width: | Height: | Size: 3 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-2560x1440/08-kampf-top.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
docs/luneraria/screenshots/g3/vorher-2560x1440/09-kampf-mid.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
|
@ -8,6 +8,8 @@ var follow_camera: bool = true
|
||||||
var fullscreen: bool = false
|
var fullscreen: bool = false
|
||||||
## Menu camera travel, idle drift, particles and fades are skipped when set.
|
## Menu camera travel, idle drift, particles and fades are skipped when set.
|
||||||
var reduced_motion: bool = false
|
var reduced_motion: bool = false
|
||||||
|
## Size of the in-match HUD (Luneraria); the HUD caps it so its layout always fits.
|
||||||
|
var ui_scale: float = 1.0
|
||||||
var storage_path: String = "user://preferences.cfg"
|
var storage_path: String = "user://preferences.cfg"
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -37,6 +39,7 @@ func read_settings() -> void:
|
||||||
follow_camera = bool(config.get_value("camera", "follow", true))
|
follow_camera = bool(config.get_value("camera", "follow", true))
|
||||||
fullscreen = bool(config.get_value("display", "fullscreen", false))
|
fullscreen = bool(config.get_value("display", "fullscreen", false))
|
||||||
reduced_motion = bool(config.get_value("display", "reduced_motion", false))
|
reduced_motion = bool(config.get_value("display", "reduced_motion", false))
|
||||||
|
ui_scale = clampf(float(config.get_value("display", "ui_scale", 1.0)), .75, 1.5)
|
||||||
|
|
||||||
func apply() -> void:
|
func apply() -> void:
|
||||||
AudioServer.set_bus_volume_db(0, linear_to_db(maxf(.001, volume)))
|
AudioServer.set_bus_volume_db(0, linear_to_db(maxf(.001, volume)))
|
||||||
|
|
@ -52,6 +55,7 @@ func save_settings() -> Error:
|
||||||
config.set_value("camera", "follow", follow_camera)
|
config.set_value("camera", "follow", follow_camera)
|
||||||
config.set_value("display", "fullscreen", fullscreen)
|
config.set_value("display", "fullscreen", fullscreen)
|
||||||
config.set_value("display", "reduced_motion", reduced_motion)
|
config.set_value("display", "reduced_motion", reduced_motion)
|
||||||
|
config.set_value("display", "ui_scale", ui_scale)
|
||||||
return config.save(storage_path)
|
return config.save(storage_path)
|
||||||
|
|
||||||
func commit() -> void:
|
func commit() -> void:
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ func start(setup_options: Dictionary) -> void:
|
||||||
hud = Hud.new()
|
hud = Hud.new()
|
||||||
hud.match_state = match_state
|
hud.match_state = match_state
|
||||||
hud.human_slot = maxi(0, human_slot)
|
hud.human_slot = maxi(0, human_slot)
|
||||||
|
hud.session = session if _net() else null
|
||||||
layer.add_child(hud)
|
layer.add_child(hud)
|
||||||
hud.command_requested.connect(_on_command)
|
hud.command_requested.connect(_on_command)
|
||||||
hud.undo_requested.connect(func() -> void: _feedback(_send({"type": "undo"}) if _net() else match_state.undo(human_slot)))
|
hud.undo_requested.connect(func() -> void: _feedback(_send({"type": "undo"}) if _net() else match_state.undo(human_slot)))
|
||||||
|
|
@ -64,11 +65,14 @@ func start(setup_options: Dictionary) -> void:
|
||||||
hud.pause_requested.connect(_toggle_pause)
|
hud.pause_requested.connect(_toggle_pause)
|
||||||
hud.lane_focus_requested.connect(focus_lane)
|
hud.lane_focus_requested.connect(focus_lane)
|
||||||
hud.map_focus_requested.connect(func(point: Vector3) -> void: camera.look_at_point(point))
|
hud.map_focus_requested.connect(func(point: Vector3) -> void: camera.look_at_point(point))
|
||||||
|
hud.camera_requested.connect(focus_target)
|
||||||
hud.menu_requested.connect(func() -> void: exit_requested.emit())
|
hud.menu_requested.connect(func() -> void: exit_requested.emit())
|
||||||
hud.rematch_requested.connect(func() -> void: rematch_requested.emit())
|
hud.rematch_requested.connect(func() -> void: rematch_requested.emit())
|
||||||
if _net():
|
if _net():
|
||||||
session.session_message.connect(func(text: String, is_error: bool) -> void: hud.notify(text, is_error))
|
session.session_message.connect(func(text: String, is_error: bool) -> void: hud.notify(text, is_error))
|
||||||
session.state_received.connect(_rebuild_preview)
|
session.state_received.connect(_rebuild_preview)
|
||||||
|
session.state_received.connect(hud.mark_dirty)
|
||||||
|
session.lobby_changed.connect(hud.mark_dirty)
|
||||||
if human_slot >= 0:
|
if human_slot >= 0:
|
||||||
focus_lane(match_state.players[human_slot].lane, true)
|
focus_lane(match_state.players[human_slot].lane, true)
|
||||||
else:
|
else:
|
||||||
|
|
@ -104,6 +108,21 @@ func focus_grotto(instant: bool = false) -> void:
|
||||||
var center: Vector2 = match_state.map.grotto_center()
|
var center: Vector2 = match_state.map.grotto_center()
|
||||||
camera.look_at_point(Vector3(center.x, 0, center.y), 22.0, instant)
|
camera.look_at_point(Vector3(center.x, 0, center.y), 22.0, instant)
|
||||||
|
|
||||||
|
func focus_enemy_base(instant: bool = false) -> void:
|
||||||
|
var core: Vector2 = match_state.map.core_position(1 - _own_team())
|
||||||
|
camera.look_at_point(Vector3(core.x, 0, core.y), 26.0, instant)
|
||||||
|
|
||||||
|
## Camera jumps requested by the HUD (boss card, event feed, banners).
|
||||||
|
func focus_target(target: String) -> void:
|
||||||
|
match target:
|
||||||
|
"grotto": focus_grotto()
|
||||||
|
"base": focus_base()
|
||||||
|
"enemy_base": focus_enemy_base()
|
||||||
|
"hero": focus_hero()
|
||||||
|
_:
|
||||||
|
if LuneMap.LANES.has(target):
|
||||||
|
focus_lane(target)
|
||||||
|
|
||||||
## The own hero: its planning preview or, in combat, the live unit.
|
## The own hero: its planning preview or, in combat, the live unit.
|
||||||
func own_hero_unit() -> LuneUnit:
|
func own_hero_unit() -> LuneUnit:
|
||||||
if human_slot < 0:
|
if human_slot < 0:
|
||||||
|
|
@ -304,8 +323,39 @@ func _after_combat() -> void:
|
||||||
var input_blocked: Callable = func() -> bool: return false
|
var input_blocked: Callable = func() -> bool: return false
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not input_blocked.call() and camera.handle_world_input(event):
|
if input_blocked.call():
|
||||||
|
return
|
||||||
|
if camera.handle_world_input(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
# Left click in the field selects an own unit (details in the HUD); it never orders anything.
|
||||||
|
var picked: LuneUnit = pick_own_unit(event.position)
|
||||||
|
if picked != null and picked.kind == "hero":
|
||||||
|
hud.open_hero()
|
||||||
|
elif picked != null:
|
||||||
|
hud.select_unit(picked.persistent_uid)
|
||||||
|
elif not hud.selected_uid.is_empty():
|
||||||
|
hud.clear_selection()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
## The own troop or hero whose figure is nearest to a screen point (within 34 px), or null.
|
||||||
|
func pick_own_unit(screen: Vector2) -> LuneUnit:
|
||||||
|
if human_slot < 0:
|
||||||
|
return null
|
||||||
|
var best: LuneUnit = null
|
||||||
|
var best_distance: float = 34.0
|
||||||
|
for view: Node3D in _views.values():
|
||||||
|
var unit: LuneUnit = view.unit
|
||||||
|
if unit == null or not unit.alive or unit.owner_slot != human_slot or not (unit.kind in ["troop", "hero"]) or not view.visible:
|
||||||
|
continue
|
||||||
|
var at: Vector3 = view.aim_point()
|
||||||
|
if camera.is_position_behind(at):
|
||||||
|
continue
|
||||||
|
var distance: float = camera.unproject_position(at).distance_to(screen)
|
||||||
|
if distance < best_distance:
|
||||||
|
best_distance = distance
|
||||||
|
best = unit
|
||||||
|
return best
|
||||||
|
|
||||||
## Game keys run before the GUI so Space/Tab keep their meaning even when a button has focus.
|
## Game keys run before the GUI so Space/Tab keep their meaning even when a button has focus.
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
|
|
@ -315,7 +365,9 @@ func _input(event: InputEvent) -> void:
|
||||||
match event.keycode:
|
match event.keycode:
|
||||||
KEY_SPACE:
|
KEY_SPACE:
|
||||||
if match_state.phase == LuneMatch.Phase.PLANNING:
|
if match_state.phase == LuneMatch.Phase.PLANNING:
|
||||||
_on_ready()
|
# A Space meant as pause at the very end of a combat must not skip the planning.
|
||||||
|
if not hud.ready_blocked():
|
||||||
|
_on_ready()
|
||||||
else:
|
else:
|
||||||
_toggle_pause()
|
_toggle_pause()
|
||||||
KEY_P:
|
KEY_P:
|
||||||
|
|
@ -343,8 +395,12 @@ func _input(event: InputEvent) -> void:
|
||||||
camera.overview()
|
camera.overview()
|
||||||
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5:
|
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5:
|
||||||
if match_state.phase == LuneMatch.Phase.PLANNING and human_slot >= 0:
|
if match_state.phase == LuneMatch.Phase.PLANNING and human_slot >= 0:
|
||||||
var ids: Array[String] = match_state.base_ids(match_state.players[human_slot].bund)
|
if bool(match_state.players[human_slot].ready):
|
||||||
_on_command({"type": "buy", "type_id": ids[event.keycode - KEY_1]})
|
# After Ready the plan is binding (the host refuses); the keys do nothing.
|
||||||
|
hud.notify("Bereit gemeldet – die Planung ist verbindlich.", true)
|
||||||
|
else:
|
||||||
|
var ids: Array[String] = match_state.base_ids(match_state.players[human_slot].bund)
|
||||||
|
_on_command({"type": "buy", "type_id": ids[event.keycode - KEY_1]})
|
||||||
_:
|
_:
|
||||||
handled = false
|
handled = false
|
||||||
if handled:
|
if handled:
|
||||||
|
|
|
||||||
51
scripts/luneraria/ui/components/army_chip.gd
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
class_name LuneArmyChip
|
||||||
|
extends Button
|
||||||
|
## One own troop in the army strip: real-model portrait, a band with its lane (T, M, B or G)
|
||||||
|
## and stage (II for upgraded troops). Selected = pressed style. In combat a fallen troop is
|
||||||
|
## dimmed and crossed out. Size adapts to how many troops the strip must hold.
|
||||||
|
|
||||||
|
const BAND: float = 15.0
|
||||||
|
|
||||||
|
var uid: String = ""
|
||||||
|
var type_id: String = ""
|
||||||
|
var lane: String = "mid"
|
||||||
|
var fallen: bool = false:
|
||||||
|
set(value):
|
||||||
|
if value != fallen:
|
||||||
|
fallen = value
|
||||||
|
portrait.dimmed = value
|
||||||
|
portrait.crossed = value
|
||||||
|
var portrait: LunePortraitRect
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
theme_type_variation = "HudChip"
|
||||||
|
toggle_mode = true
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
portrait = LunePortraitRect.new()
|
||||||
|
add_child(portrait)
|
||||||
|
resized.connect(_place)
|
||||||
|
|
||||||
|
func setup(entry: Dictionary, edge: float) -> void:
|
||||||
|
uid = str(entry.uid)
|
||||||
|
type_id = str(entry.type_id)
|
||||||
|
lane = str(entry.lane)
|
||||||
|
custom_minimum_size = Vector2(edge, edge + BAND)
|
||||||
|
portrait.show_model(type_id)
|
||||||
|
_place()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _place() -> void:
|
||||||
|
portrait.position = Vector2(2, 2)
|
||||||
|
portrait.size = Vector2(size.x - 4.0, size.y - BAND - 2.0)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var band := Rect2(Vector2(2, size.y - BAND - 1.0), Vector2(size.x - 4.0, BAND))
|
||||||
|
draw_rect(band, Color(0, 0, 0, .55))
|
||||||
|
var font: Font = get_theme_font("font", "HudCaps")
|
||||||
|
var grotto: bool = lane == LuneMatch.GROTTO
|
||||||
|
var letter: String = "G" if grotto else str(LuneMap.LANE_NAMES.get(lane, "?")).left(1)
|
||||||
|
var size_px: int = 13
|
||||||
|
draw_string(font, Vector2(5, size.y - 5.0), letter, HORIZONTAL_ALIGNMENT_LEFT, -1, size_px, LuneUiTheme.TEAM[2] if grotto else LuneUiTheme.BRONZE_LIGHT)
|
||||||
|
if LuneHudModel.stage_of(type_id) == 2:
|
||||||
|
draw_string(font, Vector2(size.x - 20.0, size.y - 5.0), "II", HORIZONTAL_ALIGNMENT_LEFT, -1, size_px, LuneUiTheme.GOLD)
|
||||||
1
scripts/luneraria/ui/components/army_chip.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bug8bsj664xie
|
||||||
166
scripts/luneraria/ui/components/boss_indicator.gd
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
class_name LuneBossIndicator
|
||||||
|
extends PanelContainer
|
||||||
|
## The boss grotto and the Schlundkrone at a glance: BOSS VERFÜGBAR, BOSS UMKÄMPFT, BOSS
|
||||||
|
## EROBERT, EINSATZ NÄCHSTE WELLE, BOSS GREIFT AN, COOLDOWN – X RUNDEN. When a team owns the
|
||||||
|
## boss the card names target lane, deployment wave and the bounty the defenders earn; the
|
||||||
|
## player who steers the boss chooses its lane here. A click on the card moves the camera.
|
||||||
|
|
||||||
|
signal focus_requested
|
||||||
|
signal lane_chosen(lane: String)
|
||||||
|
|
||||||
|
var data: Dictionary = {}
|
||||||
|
var portrait: LunePortraitRect
|
||||||
|
var _badge: LuneStatusBadge
|
||||||
|
var _line: Label
|
||||||
|
var _risk: Label
|
||||||
|
var _bar: LuneHpBar
|
||||||
|
var _choice: HBoxContainer
|
||||||
|
var _lane_buttons: Dictionary = {}
|
||||||
|
var _owner: LuneGlyph
|
||||||
|
var _signature: String = ""
|
||||||
|
var compact: bool = false
|
||||||
|
var _own_team: int = 0
|
||||||
|
var _round: int = 0
|
||||||
|
var _planning: bool = true
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
theme_type_variation = "HudPanelTight"
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(row)
|
||||||
|
portrait = LunePortraitRect.new()
|
||||||
|
portrait.custom_minimum_size = Vector2(64, 64)
|
||||||
|
portrait.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
row.add_child(portrait)
|
||||||
|
_owner = LuneGlyph.make("emblem_2", LuneUiTheme.TEAM[2], 18.0, portrait)
|
||||||
|
_owner.position = Vector2(2, 44)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 3)
|
||||||
|
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(column)
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
head.add_theme_constant_override("separation", 6)
|
||||||
|
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
column.add_child(head)
|
||||||
|
LuneGlyph.make("grotto", LuneUiTheme.TEAM[2], 15.0, head)
|
||||||
|
LuneUiTheme.label("BOSSGROTTE · G", "HudCaps", head)
|
||||||
|
_badge = LuneStatusBadge.new()
|
||||||
|
column.add_child(_badge)
|
||||||
|
_badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
_line = LuneUiTheme.label("", "HudText", column)
|
||||||
|
_line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_risk = LuneUiTheme.label("", "HudMuted", column)
|
||||||
|
_risk.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_bar = LuneHpBar.new()
|
||||||
|
_bar.custom_minimum_size = Vector2(0, 5)
|
||||||
|
_bar.visible = false
|
||||||
|
column.add_child(_bar)
|
||||||
|
_choice = HBoxContainer.new()
|
||||||
|
_choice.add_theme_constant_override("separation", 4)
|
||||||
|
_choice.visible = false
|
||||||
|
column.add_child(_choice)
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
var button := LuneUiTheme.button(LuneMap.LANE_NAMES[lane].to_upper(), "HudToggle", _choice)
|
||||||
|
button.toggle_mode = true
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.pressed.connect(func() -> void: lane_chosen.emit(lane))
|
||||||
|
_lane_buttons[lane] = button
|
||||||
|
LuneTooltip.attach(self, _tip)
|
||||||
|
|
||||||
|
## Short form while the right column shows a selected unit: badge only.
|
||||||
|
func set_compact(value: bool) -> void:
|
||||||
|
if value == compact:
|
||||||
|
return
|
||||||
|
compact = value
|
||||||
|
portrait.custom_minimum_size = Vector2(40, 40) if value else Vector2(64, 64)
|
||||||
|
_signature = ""
|
||||||
|
if not data.is_empty():
|
||||||
|
apply(data, _own_team, _round, _planning)
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
focus_requested.emit()
|
||||||
|
accept_event()
|
||||||
|
|
||||||
|
func apply(boss: Dictionary, own_team: int, round_number: int, planning: bool) -> void:
|
||||||
|
var signature: String = "%s|%d|%d|%s" % [str(boss), own_team, round_number, planning]
|
||||||
|
if signature == _signature:
|
||||||
|
return
|
||||||
|
_signature = signature
|
||||||
|
data = boss
|
||||||
|
_own_team = own_team
|
||||||
|
_round = round_number
|
||||||
|
_planning = planning
|
||||||
|
portrait.show_model("schlundkrone")
|
||||||
|
var owner: int = int(boss.team)
|
||||||
|
var own: bool = owner == own_team
|
||||||
|
var lane_text: String = LuneMap.LANE_NAMES.get(str(boss.lane), "?").to_upper()
|
||||||
|
var side: String = LuneUiTheme.team_name(owner)
|
||||||
|
_owner.visible = owner >= 0
|
||||||
|
_owner.glyph = "emblem_%d" % clampi(owner, 0, 2)
|
||||||
|
_owner.color = LuneUiTheme.team_color(owner)
|
||||||
|
_bar.visible = false
|
||||||
|
_choice.visible = false
|
||||||
|
_risk.text = ""
|
||||||
|
match str(boss.state):
|
||||||
|
"available":
|
||||||
|
_badge.set_state("BOSS VERFÜGBAR", LuneUiTheme.TEAM[2], "crown")
|
||||||
|
_line.text = "Einheiten mit „Grotte“ entsenden. Wer den Boss besiegt, schickt ihn in der Folgewelle gegen den Gegner."
|
||||||
|
"contested":
|
||||||
|
_badge.set_state("BOSS UMKÄMPFT", LuneUiTheme.WARN, "melee")
|
||||||
|
_line.text = "Expeditionen kämpfen in der Grotte; sie fehlen ihren Lanes bis Kampfende."
|
||||||
|
"conquered":
|
||||||
|
_badge.set_state("BOSS EROBERT · %s" % side.to_upper(), LuneUiTheme.team_color(owner), "crown", true)
|
||||||
|
_line.text = "Einsatz in Welle %d für %s." % [int(boss.deploy_round), "dein Team" if own else side]
|
||||||
|
"failed":
|
||||||
|
_badge.set_state("EXPEDITION GESCHEITERT", LuneUiTheme.MUTED, "close")
|
||||||
|
_line.text = "Der Boss hat überlebt; niemand erhält ihn."
|
||||||
|
"deploy_next":
|
||||||
|
_badge.set_state("EINSATZ NÄCHSTE WELLE", LuneUiTheme.team_color(owner) if own else LuneUiTheme.DANGER, "crown", true)
|
||||||
|
if own:
|
||||||
|
_line.text = "Deine Schlundkrone greift in Welle %d auf %s an." % [int(boss.deploy_round), lane_text]
|
||||||
|
_risk.text = "Fällt sie, erhält %s %s Gold Kopfgeld." % [LuneUiTheme.team_name(1 - own_team), LuneUiTheme.number(boss.bounty)]
|
||||||
|
else:
|
||||||
|
_line.text = "%s: Schlundkrone greift in Welle %d auf %s an." % [side, int(boss.deploy_round), lane_text]
|
||||||
|
_risk.text = "Besiegt ihr sie, erhält dein Team %s Gold Kopfgeld." % LuneUiTheme.number(boss.bounty)
|
||||||
|
if bool(boss.chooser) and planning:
|
||||||
|
_choice.visible = true
|
||||||
|
for lane: String in _lane_buttons:
|
||||||
|
(_lane_buttons[lane] as Button).set_pressed_no_signal(lane == str(boss.lane))
|
||||||
|
_risk.text += " Du wählst die Lane:"
|
||||||
|
"deploying":
|
||||||
|
_badge.set_state("BOSS GREIFT AN · %s" % lane_text, LuneUiTheme.team_color(owner) if own else LuneUiTheme.DANGER, "warning", true)
|
||||||
|
_line.text = ("Deine Schlundkrone greift %s an." if own else "Gegnerische Schlundkrone auf %s!") % lane_text
|
||||||
|
_risk.text = "Kopfgeld für die Verteidiger: %s Gold." % LuneUiTheme.number(boss.bounty)
|
||||||
|
if boss.has("hp"):
|
||||||
|
_bar.visible = true
|
||||||
|
_bar.fraction = float(boss.hp) / maxf(1.0, float(boss.max_hp))
|
||||||
|
_bar.fill = LuneUiTheme.team_color(owner)
|
||||||
|
"defeated":
|
||||||
|
_badge.set_state("SCHLUNDKRONE BESIEGT", LuneUiTheme.GOOD if not own else LuneUiTheme.MUTED, "check")
|
||||||
|
_line.text = "Das Kopfgeld von %s Gold geht an %s." % [LuneUiTheme.number(boss.bounty), "den Gegner" if own else "dein Team"]
|
||||||
|
_:
|
||||||
|
var left: int = int(boss.rounds_left)
|
||||||
|
_badge.set_state("COOLDOWN – %d %s" % [left, "RUNDE" if left == 1 else "RUNDEN"], LuneUiTheme.MUTED, "clock")
|
||||||
|
_line.text = "Grotte wieder offen ab Welle %d." % int(boss.next_round)
|
||||||
|
_risk.visible = not _risk.text.is_empty() and not compact
|
||||||
|
_line.visible = not compact
|
||||||
|
_owner.visible = _owner.visible and not compact
|
||||||
|
if compact:
|
||||||
|
_choice.visible = false
|
||||||
|
if LuneTooltip.current != null:
|
||||||
|
LuneTooltip.current.refresh(self)
|
||||||
|
|
||||||
|
## Live boss HP during its attack, without rebuilding the texts.
|
||||||
|
func set_boss_hp(fraction: float) -> void:
|
||||||
|
if _bar.visible:
|
||||||
|
_bar.fraction = fraction
|
||||||
|
|
||||||
|
func _tip() -> Dictionary:
|
||||||
|
return {"title": "Bossgrotte · Schlundkrone", "glyph": "crown", "glyph_color": LuneUiTheme.TEAM[2],
|
||||||
|
"text": "Expeditionen kämpfen zuerst gegeneinander; der Boss erwacht, wenn höchstens ein Team übrig ist. Sieg: Der Boss greift in der Folgewelle für die Sieger an, die Verteidiger erhalten bei seinem Fall ein hohes Kopfgeld. Danach ist die Grotte %d Runden gesperrt." % LuneContent.BOSS_LOCK_ROUNDS,
|
||||||
|
"note": "Klick oder G: Kamera zur Grotte"}
|
||||||
1
scripts/luneraria/ui/components/boss_indicator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dgdrwfx21xaxc
|
||||||
119
scripts/luneraria/ui/components/champion_panel.gd
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
class_name LuneChampionPanel
|
||||||
|
extends HBoxContainer
|
||||||
|
## Attack champions: target lane, the three champion cards (copper, hatched, "1 WELLE") and
|
||||||
|
## the own bookings ("Einsatz in Welle 15 – MID"). Champions are bought in planning n, fight
|
||||||
|
## once in wave n+1 and then vanish; they never join the permanent army.
|
||||||
|
|
||||||
|
signal command_requested(command: Dictionary)
|
||||||
|
|
||||||
|
var target_lane: String = "mid"
|
||||||
|
var cards: Dictionary = {}
|
||||||
|
var _lane_buttons: Dictionary = {}
|
||||||
|
var _bookings: VBoxContainer
|
||||||
|
var _count: Label
|
||||||
|
var _signature: String = ""
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
add_theme_constant_override("separation", 10)
|
||||||
|
var lanes := VBoxContainer.new()
|
||||||
|
lanes.add_theme_constant_override("separation", 4)
|
||||||
|
add_child(lanes)
|
||||||
|
LuneUiTheme.label("ZIEL", "HudCaps", lanes)
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
var button := LuneUiTheme.button(LuneMap.LANE_NAMES[lane].to_upper(), "HudToggle", lanes)
|
||||||
|
button.toggle_mode = true
|
||||||
|
button.custom_minimum_size = Vector2(86, 0)
|
||||||
|
button.set_pressed_no_signal(lane == target_lane)
|
||||||
|
button.pressed.connect(func() -> void:
|
||||||
|
target_lane = lane
|
||||||
|
_signature = ""
|
||||||
|
for other: String in _lane_buttons:
|
||||||
|
(_lane_buttons[other] as Button).set_pressed_no_signal(other == lane))
|
||||||
|
_lane_buttons[lane] = button
|
||||||
|
LuneTooltip.attach(button, func() -> Dictionary: return {"title": "Ziel-Lane %s" % LuneMap.LANE_NAMES[lane], "text": "Gebuchte Champions greifen in der nächsten Welle auf dieser Lane an."})
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 6)
|
||||||
|
add_child(row)
|
||||||
|
for champion_id: String in LuneContent.CHAMPIONS:
|
||||||
|
var spec: Dictionary = LuneContent.CHAMPIONS[champion_id]
|
||||||
|
var card := LuneUnitCard.new()
|
||||||
|
row.add_child(card)
|
||||||
|
var role_key: String = {"ch_schutz": "schutz", "ch_druck": "druck", "ch_belagerung": "siege"}.get(champion_id, "melee")
|
||||||
|
card.setup(champion_id, str(spec.model), str(spec.name), role_key, int(spec.cost), "", true)
|
||||||
|
card.pressed.connect(func() -> void: command_requested.emit({"type": "champion_book", "champion_id": champion_id, "lane": target_lane}))
|
||||||
|
cards[champion_id] = card
|
||||||
|
LuneTooltip.attach(card, _tip.bind(champion_id))
|
||||||
|
var right := VBoxContainer.new()
|
||||||
|
right.add_theme_constant_override("separation", 4)
|
||||||
|
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
right.custom_minimum_size.x = 200
|
||||||
|
add_child(right)
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
right.add_child(head)
|
||||||
|
LuneUiTheme.label("EINSÄTZE", "HudCaps", head).size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_count = LuneUiTheme.label("", "HudMuted", head)
|
||||||
|
_bookings = VBoxContainer.new()
|
||||||
|
_bookings.add_theme_constant_override("separation", 3)
|
||||||
|
right.add_child(_bookings)
|
||||||
|
|
||||||
|
var _model: LuneHudModel
|
||||||
|
|
||||||
|
func apply(model: LuneHudModel) -> void:
|
||||||
|
_model = model
|
||||||
|
var bookings: Array = model.own_bookings()
|
||||||
|
var fresh: int = (model.me().draft.new_bookings as Array).size() if model.planning() else 0
|
||||||
|
var gold: int = model.own_gold()
|
||||||
|
var signature: String = "%s|%d|%s|%s|%s|%d" % [str(bookings), gold, model.planning(), model.locked(), target_lane, model.state.round_number]
|
||||||
|
if signature == _signature:
|
||||||
|
return
|
||||||
|
_signature = signature
|
||||||
|
var can_act: bool = model.planning() and not model.locked()
|
||||||
|
for champion_id: String in cards:
|
||||||
|
var card: LuneUnitCard = cards[champion_id]
|
||||||
|
var cost: int = int(LuneContent.CHAMPIONS[champion_id].cost)
|
||||||
|
var why: String = ""
|
||||||
|
if not model.planning():
|
||||||
|
why = "Buchungen nur in der Planung."
|
||||||
|
elif model.locked():
|
||||||
|
why = "Bereit gemeldet – die Planung ist verbindlich."
|
||||||
|
elif fresh >= LuneContent.MAX_BOOKINGS:
|
||||||
|
why = "Höchstens %d Champions pro Runde." % LuneContent.MAX_BOOKINGS
|
||||||
|
elif gold < cost:
|
||||||
|
why = "Es fehlen %d Gold." % (cost - gold)
|
||||||
|
card.set_available(why.is_empty(), why, not can_act, gold < cost)
|
||||||
|
_count.text = "%d/%d diese Runde" % [fresh, LuneContent.MAX_BOOKINGS]
|
||||||
|
for child: Node in _bookings.get_children():
|
||||||
|
child.free()
|
||||||
|
if bookings.is_empty():
|
||||||
|
var hint := LuneUiTheme.label("Keine Champions gebucht. Kauf jetzt – Einsatz in Welle %d." % (model.state.round_number + (1 if model.planning() else 2)), "HudMuted", _bookings)
|
||||||
|
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
return
|
||||||
|
for booking: Dictionary in bookings:
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 6)
|
||||||
|
_bookings.add_child(line)
|
||||||
|
LuneGlyph.make("hourglass", LuneUiTheme.CHAMPION, 14.0, line)
|
||||||
|
var running: bool = int(booking.deploy_round) == model.state.round_number
|
||||||
|
var texts := VBoxContainer.new()
|
||||||
|
texts.add_theme_constant_override("separation", -2)
|
||||||
|
texts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
line.add_child(texts)
|
||||||
|
LuneUiTheme.label(str(LuneContent.CHAMPIONS[booking.champion_id].name), "HudText", texts)
|
||||||
|
LuneUiTheme.label("Einsatz in Welle %d – %s%s" % [int(booking.deploy_round), LuneMap.LANE_NAMES[booking.lane].to_upper(), " (jetzt)" if running else ""], "HudMuted", texts)
|
||||||
|
if bool(booking.get("fresh", false)):
|
||||||
|
var cancel := LuneUiTheme.button("×", "HudIconButton", line)
|
||||||
|
cancel.disabled = not can_act
|
||||||
|
var uid: String = str(booking.uid)
|
||||||
|
cancel.pressed.connect(func() -> void: command_requested.emit({"type": "champion_cancel", "uid": uid}))
|
||||||
|
LuneTooltip.attach(cancel, func() -> Dictionary: return {"title": "Buchung zurücknehmen", "text": "Volle Erstattung, solange die Planung läuft."})
|
||||||
|
|
||||||
|
func _tip(champion_id: String) -> Dictionary:
|
||||||
|
var spec: Dictionary = LuneContent.CHAMPIONS[champion_id]
|
||||||
|
var round_number: int = _model.state.round_number + 1 if _model != null else 2
|
||||||
|
var definition: Dictionary = LuneContent.champion_definition(champion_id, round_number, _model.state.balance if _model != null else LuneBalance.default_balance())
|
||||||
|
var card: LuneUnitCard = cards[champion_id]
|
||||||
|
return {"title": "%s · Angriffschampion" % spec.name, "glyph": "champion", "glyph_color": LuneUiTheme.CHAMPION,
|
||||||
|
"subtitle": "%s · %d Gold · Einsatz in Welle %d – %s" % [spec.role, int(spec.cost), round_number, LuneMap.LANE_NAMES[target_lane].to_upper()],
|
||||||
|
"stats": LuneStats.rows(definition), "text": str(spec.text),
|
||||||
|
"note": "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner %d Gold." % int(definition.bounty),
|
||||||
|
"warn": card.reason}
|
||||||
1
scripts/luneraria/ui/components/champion_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://8h25mipeosry
|
||||||
72
scripts/luneraria/ui/components/event_feed.gd
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
class_name LuneEventFeed
|
||||||
|
extends VBoxContainer
|
||||||
|
## Short event list: tower falls, champions, boss, critical Ancients, round results. At most
|
||||||
|
## five entries, newest on top; each fades after a while. A click on an entry with a place
|
||||||
|
## moves the camera there. It never lists every kill.
|
||||||
|
|
||||||
|
signal focus_requested(target: Dictionary)
|
||||||
|
|
||||||
|
const LIMIT: int = 5
|
||||||
|
const LIFETIME: float = 14.0
|
||||||
|
const LEVEL_COLORS: Dictionary = {"info": LuneUiTheme.INFO, "good": LuneUiTheme.GOOD, "warn": LuneUiTheme.WARN, "danger": LuneUiTheme.DANGER}
|
||||||
|
|
||||||
|
var entries: Array[Dictionary] = []
|
||||||
|
var _clock: float = 0.0
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
add_theme_constant_override("separation", 3)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
## level: info, good, warn, danger. target: {} or {"lane": …} / {"point": Vector2} / {"camera": name}.
|
||||||
|
func push(text_value: String, level: String = "info", glyph: String = "", target: Dictionary = {}) -> void:
|
||||||
|
for entry: Dictionary in entries:
|
||||||
|
if entry.text == text_value and _clock - float(entry.born) < 4.0:
|
||||||
|
return
|
||||||
|
var button := Button.new()
|
||||||
|
button.theme_type_variation = "HudRow"
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if not target.is_empty() else Control.CURSOR_ARROW
|
||||||
|
button.custom_minimum_size = Vector2(0, 30)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
row.offset_left = 8
|
||||||
|
row.offset_right = -8
|
||||||
|
row.add_theme_constant_override("separation", 7)
|
||||||
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
button.add_child(row)
|
||||||
|
var tint: Color = LEVEL_COLORS.get(level, LuneUiTheme.INFO)
|
||||||
|
LuneGlyph.make(glyph if not glyph.is_empty() else "star", tint, 15.0, row)
|
||||||
|
var label := LuneUiTheme.label(text_value, "HudText", row)
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
label.clip_text = true
|
||||||
|
label.add_theme_color_override("font_color", tint.lerp(LuneUiTheme.TEXT, .45))
|
||||||
|
button.pressed.connect(func() -> void:
|
||||||
|
if not target.is_empty():
|
||||||
|
focus_requested.emit(target))
|
||||||
|
LuneTooltip.attach(button, func() -> Dictionary: return {"title": text_value, "note": "Klick: Kamera dorthin" if not target.is_empty() else ""})
|
||||||
|
add_child(button)
|
||||||
|
move_child(button, 0)
|
||||||
|
entries.push_front({"text": text_value, "level": level, "born": _clock, "node": button})
|
||||||
|
while entries.size() > LIMIT:
|
||||||
|
var old: Dictionary = entries.pop_back()
|
||||||
|
(old.node as Node).queue_free()
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
func clear_all() -> void:
|
||||||
|
for entry: Dictionary in entries:
|
||||||
|
(entry.node as Node).queue_free()
|
||||||
|
entries.clear()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_clock += delta
|
||||||
|
for index: int in range(entries.size() - 1, -1, -1):
|
||||||
|
var entry: Dictionary = entries[index]
|
||||||
|
var age: float = _clock - float(entry.born)
|
||||||
|
var node: Control = entry.node
|
||||||
|
if age > LIFETIME:
|
||||||
|
node.queue_free()
|
||||||
|
entries.remove_at(index)
|
||||||
|
elif age > LIFETIME - 2.0:
|
||||||
|
node.modulate.a = clampf((LIFETIME - age) / 2.0, 0.0, 1.0)
|
||||||
|
visible = not entries.is_empty()
|
||||||
1
scripts/luneraria/ui/components/event_feed.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://rswjv3w8uesi
|
||||||
207
scripts/luneraria/ui/components/hero_card.gd
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
class_name LuneHeroCard
|
||||||
|
extends Button
|
||||||
|
## The hero in the bottom bar: portrait, name, level, experience progress, HP, role, the
|
||||||
|
## three item slots and the talent state. A click opens the hero window (details, items,
|
||||||
|
## talents) or, without a hero, the recruiting window. No MOBA action bar: heroes act alone.
|
||||||
|
|
||||||
|
var hero_id: String = ""
|
||||||
|
var _portrait: LunePortraitRect
|
||||||
|
var _name: Label
|
||||||
|
var _level: Label
|
||||||
|
var _role_glyph: LuneGlyph
|
||||||
|
var _role: Label
|
||||||
|
var _xp: LuneHpBar
|
||||||
|
var _xp_text: Label
|
||||||
|
var _hp_glyph: LuneGlyph
|
||||||
|
var _hp: Label
|
||||||
|
var _hp_bar: LuneHpBar
|
||||||
|
var _slots: Array[LuneGlyph] = []
|
||||||
|
var _slot_boxes: Array[PanelContainer] = []
|
||||||
|
var _talent: LuneStatusBadge
|
||||||
|
var _empty: VBoxContainer
|
||||||
|
var _filled: VBoxContainer
|
||||||
|
var _faces: Array[LunePortraitRect] = []
|
||||||
|
var data: Dictionary = {}
|
||||||
|
var _signature: String = ""
|
||||||
|
var _empty_title: Label
|
||||||
|
var _empty_price: Label
|
||||||
|
var _empty_coin: LuneGlyph
|
||||||
|
var _faces_row: HBoxContainer
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
theme_type_variation = "HudCard"
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
custom_minimum_size = Vector2(320, 132)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
row.offset_left = 8
|
||||||
|
row.offset_right = -8
|
||||||
|
row.offset_top = 8
|
||||||
|
row.offset_bottom = -8
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(row)
|
||||||
|
_portrait = LunePortraitRect.new()
|
||||||
|
_portrait.custom_minimum_size = Vector2(88, 0)
|
||||||
|
row.add_child(_portrait)
|
||||||
|
_filled = VBoxContainer.new()
|
||||||
|
_filled.add_theme_constant_override("separation", 2)
|
||||||
|
_filled.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_filled.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(_filled)
|
||||||
|
_name = LuneUiTheme.label("", "HudName", _filled)
|
||||||
|
_name.clip_text = true
|
||||||
|
_name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
var role_row := HBoxContainer.new()
|
||||||
|
role_row.add_theme_constant_override("separation", 5)
|
||||||
|
role_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_filled.add_child(role_row)
|
||||||
|
LuneGlyph.make("star", LuneUiTheme.HERO, 13.0, role_row)
|
||||||
|
_level = LuneUiTheme.label("", "HudValue", role_row)
|
||||||
|
_role_glyph = LuneGlyph.make("tank", LuneUiTheme.MUTED, 14.0, role_row)
|
||||||
|
_role = LuneUiTheme.label("", "HudMuted", role_row)
|
||||||
|
var xp_row := HBoxContainer.new()
|
||||||
|
xp_row.add_theme_constant_override("separation", 6)
|
||||||
|
xp_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_filled.add_child(xp_row)
|
||||||
|
_xp = LuneHpBar.new()
|
||||||
|
_xp.custom_minimum_size = Vector2(60, 6)
|
||||||
|
_xp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_xp.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
_xp.fill = LuneUiTheme.BRONZE_LIGHT
|
||||||
|
xp_row.add_child(_xp)
|
||||||
|
_xp_text = LuneUiTheme.label("", "HudMuted", xp_row)
|
||||||
|
var hp_row := HBoxContainer.new()
|
||||||
|
hp_row.add_theme_constant_override("separation", 5)
|
||||||
|
hp_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_filled.add_child(hp_row)
|
||||||
|
_hp_glyph = LuneGlyph.make("heart", LuneUiTheme.DANGER.lightened(.2), 13.0, hp_row)
|
||||||
|
_hp = LuneUiTheme.label("", "HudText", hp_row)
|
||||||
|
_hp_bar = LuneHpBar.new()
|
||||||
|
_hp_bar.custom_minimum_size = Vector2(50, 5)
|
||||||
|
_hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
hp_row.add_child(_hp_bar)
|
||||||
|
var item_row := HBoxContainer.new()
|
||||||
|
item_row.add_theme_constant_override("separation", 4)
|
||||||
|
item_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_filled.add_child(item_row)
|
||||||
|
for index: int in range(LuneContent.ITEM_SLOTS):
|
||||||
|
var box := PanelContainer.new()
|
||||||
|
box.add_theme_stylebox_override("panel", LuneUiTheme.slot_style())
|
||||||
|
box.custom_minimum_size = Vector2(26, 26)
|
||||||
|
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
item_row.add_child(box)
|
||||||
|
var glyph := LuneGlyph.new()
|
||||||
|
glyph.custom_minimum_size = Vector2(18, 18)
|
||||||
|
box.add_child(glyph)
|
||||||
|
_slots.append(glyph)
|
||||||
|
_slot_boxes.append(box)
|
||||||
|
_talent = LuneStatusBadge.new()
|
||||||
|
item_row.add_child(_talent)
|
||||||
|
_empty = VBoxContainer.new()
|
||||||
|
_empty.add_theme_constant_override("separation", 3)
|
||||||
|
_empty.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_empty.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
_empty.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(_empty)
|
||||||
|
_empty_title = LuneUiTheme.label("HELD ANWERBEN", "HudName", _empty)
|
||||||
|
var faces := HBoxContainer.new()
|
||||||
|
_faces_row = faces
|
||||||
|
faces.add_theme_constant_override("separation", 4)
|
||||||
|
faces.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_empty.add_child(faces)
|
||||||
|
for hero: String in LuneContent.HEROES:
|
||||||
|
var face := LunePortraitRect.new()
|
||||||
|
face.custom_minimum_size = Vector2(38, 38)
|
||||||
|
faces.add_child(face)
|
||||||
|
_faces.append(face)
|
||||||
|
var price := HBoxContainer.new()
|
||||||
|
price.add_theme_constant_override("separation", 4)
|
||||||
|
price.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_empty.add_child(price)
|
||||||
|
_empty_coin = LuneGlyph.make("coin", LuneUiTheme.GOLD, 14.0, price)
|
||||||
|
_empty_price = LuneUiTheme.label("%d · ein Held pro Spieler" % int(LuneContent.HEROES.values()[0].cost), "HudMuted", price)
|
||||||
|
LuneTooltip.attach(self, _tip)
|
||||||
|
|
||||||
|
## hero: own hero dictionary (draft in planning) or {}; live: the combat unit or null.
|
||||||
|
func apply(hero: Dictionary, model: LuneHudModel, live: LuneUnit) -> void:
|
||||||
|
var has_hero: bool = not hero.is_empty()
|
||||||
|
if live != null and has_hero:
|
||||||
|
_hp.text = ("%s / %s" % [LuneUiTheme.number(live.hp), LuneUiTheme.number(live.max_hp)]) if live.alive else "gefallen"
|
||||||
|
_hp_bar.fraction = live.hp / maxf(1.0, live.max_hp) if live.alive else 0.0
|
||||||
|
_hp_bar.fill = LuneHpBar.health_color(_hp_bar.fraction, LuneUiTheme.GOOD)
|
||||||
|
_portrait.crossed = not live.alive
|
||||||
|
var signature: String = "%s|%s|%d|%s" % [str(hero), live != null, model.own_gold(), model.planning()]
|
||||||
|
if signature == _signature:
|
||||||
|
return
|
||||||
|
_signature = signature
|
||||||
|
_filled.visible = has_hero
|
||||||
|
_empty.visible = not has_hero
|
||||||
|
if not has_hero:
|
||||||
|
hero_id = ""
|
||||||
|
data = {}
|
||||||
|
_portrait.show_model("w04")
|
||||||
|
_portrait.dimmed = true
|
||||||
|
var index: int = 0
|
||||||
|
for id: String in LuneContent.HEROES:
|
||||||
|
_faces[index].show_model(str(LuneContent.HEROES[id].model))
|
||||||
|
index += 1
|
||||||
|
# In combat nobody can recruit: say so instead of offering it.
|
||||||
|
var planning: bool = model.planning()
|
||||||
|
_empty_title.text = "HELD ANWERBEN" if planning else "KEIN HELD"
|
||||||
|
_empty_price.text = ("%d · ein Held pro Spieler" % int(LuneContent.HEROES.values()[0].cost)) if planning else "Anwerben in der Planung"
|
||||||
|
_empty_coin.visible = planning
|
||||||
|
_faces_row.visible = planning
|
||||||
|
return
|
||||||
|
hero_id = str(hero.hero_id)
|
||||||
|
var spec: Dictionary = LuneContent.HEROES[hero_id]
|
||||||
|
var definition: Dictionary = model.hero_definition(hero)
|
||||||
|
data = {"hero": hero, "definition": definition}
|
||||||
|
_portrait.dimmed = false
|
||||||
|
_portrait.crossed = live != null and not live.alive
|
||||||
|
_portrait.show_model(str(spec.model))
|
||||||
|
_name.text = str(spec.name)
|
||||||
|
var xp: int = int(hero.xp)
|
||||||
|
var level: int = LuneContent.level_for(xp)
|
||||||
|
_level.text = "STUFE %d" % level
|
||||||
|
_role_glyph.glyph = LuneStats.role_glyph(str(definition.role))
|
||||||
|
_role.text = LuneStats.role_name(str(definition.role))
|
||||||
|
var low: int = LuneContent.LEVEL_XP[level - 1]
|
||||||
|
var top: bool = level >= LuneContent.LEVEL_XP.size()
|
||||||
|
var high: int = LuneContent.LEVEL_XP[mini(level, LuneContent.LEVEL_XP.size() - 1)]
|
||||||
|
_xp.fraction = 1.0 if top else float(xp - low) / maxf(1.0, float(high - low))
|
||||||
|
_xp_text.text = "EP %d / %d" % [xp, high] if not top else "EP %d · max" % xp
|
||||||
|
_hp_bar.visible = live != null
|
||||||
|
if live == null:
|
||||||
|
_hp.text = "%s Leben" % LuneUiTheme.number(definition.max_hp)
|
||||||
|
var items: Array = hero.items
|
||||||
|
for index: int in range(_slots.size()):
|
||||||
|
var item_id: String = str(items[index]) if index < items.size() else ""
|
||||||
|
_slots[index].glyph = item_id
|
||||||
|
_slots[index].color = LuneItemSlot.color_of(item_id)
|
||||||
|
var talents: Array = hero.talents
|
||||||
|
var open_tier: int = talents.size()
|
||||||
|
if open_tier < LuneContent.TALENT_LEVELS.size() and level >= LuneContent.TALENT_LEVELS[open_tier]:
|
||||||
|
_talent.set_state("TALENT!", LuneUiTheme.HERO, "star", true)
|
||||||
|
_talent.visible = model.planning()
|
||||||
|
elif open_tier < LuneContent.TALENT_LEVELS.size():
|
||||||
|
_talent.set_state("TALENT AB %d" % LuneContent.TALENT_LEVELS[open_tier], LuneUiTheme.MUTED)
|
||||||
|
_talent.visible = true
|
||||||
|
else:
|
||||||
|
_talent.visible = false
|
||||||
|
|
||||||
|
func _tip() -> Dictionary:
|
||||||
|
if data.is_empty():
|
||||||
|
return {"title": "Held anwerben", "glyph": "crown", "glyph_color": LuneUiTheme.HERO,
|
||||||
|
"text": "Drei Helden stehen zur Wahl; jeder Spieler führt einen. Helden sammeln Erfahrung, steigen bis Stufe 10 auf, tragen drei Items und wählen zwei Talente.",
|
||||||
|
"note": "Klick: Heldenauswahl öffnen"}
|
||||||
|
var hero: Dictionary = data.hero
|
||||||
|
var spec: Dictionary = LuneContent.HEROES[hero.hero_id]
|
||||||
|
var names: PackedStringArray = []
|
||||||
|
for item_id: String in hero.items:
|
||||||
|
names.append(LuneContent.ITEMS[item_id].name)
|
||||||
|
return {"title": "%s · Stufe %d" % [spec.name, LuneContent.level_for(int(hero.xp))], "glyph": "crown", "glyph_color": LuneUiTheme.HERO,
|
||||||
|
"subtitle": str(spec.title), "stats": LuneStats.rows(data.definition),
|
||||||
|
"text": "Items: " + (", ".join(names) if not names.is_empty() else "keine"), "note": "Klick: Werte, Items und Talente · H: Kamera zum Helden"}
|
||||||
1
scripts/luneraria/ui/components/hero_card.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cfy1k2rm6vc7j
|
||||||
347
scripts/luneraria/ui/components/hero_detail.gd
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
class_name LuneHeroDetail
|
||||||
|
extends VBoxContainer
|
||||||
|
## Hero window content. Without a hero: the three heroes with portrait, role, real stats and
|
||||||
|
## price to recruit one. With a hero: portrait, level and experience, lane order, and tabs for
|
||||||
|
## values and ability (from the rules' definitions, including level, items and talents), the
|
||||||
|
## three item slots with the item shop, and the two talent tiers. Only shows data; every
|
||||||
|
## change is a command through the game.
|
||||||
|
|
||||||
|
signal command_requested(command: Dictionary)
|
||||||
|
|
||||||
|
const TAB_VALUES: int = 0
|
||||||
|
const TAB_ITEMS: int = 1
|
||||||
|
const TAB_TALENTS: int = 2
|
||||||
|
|
||||||
|
var tab: int = TAB_VALUES
|
||||||
|
var _model: LuneHudModel
|
||||||
|
var _signature: String = ""
|
||||||
|
var item_cards: Dictionary = {}
|
||||||
|
var recruit_buttons: Dictionary = {}
|
||||||
|
var lane_buttons: Dictionary = {}
|
||||||
|
var tabs: LuneTabBar
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
add_theme_constant_override("separation", 10)
|
||||||
|
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
func show_tab(index: int) -> void:
|
||||||
|
tab = index
|
||||||
|
_signature = ""
|
||||||
|
if _model != null:
|
||||||
|
apply(_model)
|
||||||
|
|
||||||
|
func title_text(model: LuneHudModel) -> String:
|
||||||
|
var hero: Dictionary = model.own_hero()
|
||||||
|
if hero.is_empty():
|
||||||
|
return "HELD ANWERBEN"
|
||||||
|
return "HELD · %s" % str(LuneContent.HEROES[hero.hero_id].name).to_upper()
|
||||||
|
|
||||||
|
func apply(model: LuneHudModel) -> void:
|
||||||
|
_model = model
|
||||||
|
var hero: Dictionary = model.own_hero()
|
||||||
|
var signature: String = "%s|%d|%s|%s|%d|%s|%d" % [str(hero), model.own_gold(), model.planning(), model.locked(), tab, model.state.grotto_available(), model.state.round_number]
|
||||||
|
if signature == _signature:
|
||||||
|
return
|
||||||
|
_signature = signature
|
||||||
|
for child: Node in get_children():
|
||||||
|
child.free()
|
||||||
|
item_cards.clear()
|
||||||
|
recruit_buttons.clear()
|
||||||
|
lane_buttons.clear()
|
||||||
|
if hero.is_empty():
|
||||||
|
_recruit(model)
|
||||||
|
else:
|
||||||
|
_hero(model, hero)
|
||||||
|
|
||||||
|
func _can_act(model: LuneHudModel) -> bool:
|
||||||
|
return model.planning() and not model.locked()
|
||||||
|
|
||||||
|
func _why_not(model: LuneHudModel) -> String:
|
||||||
|
if not model.planning():
|
||||||
|
return "Nur in der Planung."
|
||||||
|
if model.locked():
|
||||||
|
return "Bereit gemeldet – die Planung ist verbindlich."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- recruiting
|
||||||
|
func _recruit(model: LuneHudModel) -> void:
|
||||||
|
LuneUiTheme.label("Jeder Spieler führt einen Helden. Er kämpft auf seiner Lane mit, sammelt Erfahrung aus Gefechten in seiner Nähe und aus jeder Runde, trägt drei Items und wählt ab Stufe 3 und 6 je ein Talent.", "HudMuted", self).autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 12)
|
||||||
|
row.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
add_child(row)
|
||||||
|
var gold: int = model.own_gold()
|
||||||
|
for hero_id: String in LuneContent.HEROES:
|
||||||
|
var spec: Dictionary = LuneContent.HEROES[hero_id]
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.theme_type_variation = "HudPanelTight"
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(panel)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 5)
|
||||||
|
panel.add_child(column)
|
||||||
|
var portrait := LunePortraitRect.new()
|
||||||
|
portrait.custom_minimum_size = Vector2(0, 170)
|
||||||
|
column.add_child(portrait)
|
||||||
|
portrait.show_model(str(spec.model), true)
|
||||||
|
LuneUiTheme.label(str(spec.name), "HudTitle", column)
|
||||||
|
LuneUiTheme.label(str(spec.title), "HudMuted", column)
|
||||||
|
var role := HBoxContainer.new()
|
||||||
|
role.add_theme_constant_override("separation", 5)
|
||||||
|
column.add_child(role)
|
||||||
|
var definition: Dictionary = model.hero_definition({"hero_id": hero_id, "xp": 0, "items": [], "talents": []})
|
||||||
|
LuneGlyph.make(LuneStats.role_glyph(str(definition.role)), LuneUiTheme.BRONZE_LIGHT, 15.0, role)
|
||||||
|
LuneUiTheme.label(str(spec.role), "HudText", role)
|
||||||
|
var grid := GridContainer.new()
|
||||||
|
grid.columns = 3
|
||||||
|
grid.add_theme_constant_override("h_separation", 6)
|
||||||
|
column.add_child(grid)
|
||||||
|
var rows: Array = LuneStats.rows(definition)
|
||||||
|
for entry: Array in rows.slice(0, 5):
|
||||||
|
LuneGlyph.make(str(entry[0]), LuneUiTheme.MUTED, 13.0, grid)
|
||||||
|
LuneUiTheme.label(str(entry[1]), "HudMuted", grid)
|
||||||
|
LuneUiTheme.label(str(entry[2]), "HudText", grid)
|
||||||
|
var summary := LuneUiTheme.label(str(spec.summary), "HudText", column)
|
||||||
|
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
summary.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
var cost: int = int(spec.cost)
|
||||||
|
var buy := LuneUiTheme.button("ANWERBEN · %d G" % cost, "HudPrimary", column)
|
||||||
|
var why: String = _why_not(model)
|
||||||
|
if why.is_empty() and gold < cost:
|
||||||
|
why = "Es fehlen %d Gold." % (cost - gold)
|
||||||
|
buy.disabled = not why.is_empty()
|
||||||
|
buy.pressed.connect(func() -> void: command_requested.emit({"type": "hero_buy", "hero_id": hero_id}))
|
||||||
|
recruit_buttons[hero_id] = buy
|
||||||
|
if not why.is_empty():
|
||||||
|
LuneUiTheme.label(why, "HudMuted", column).add_theme_color_override("font_color", LuneUiTheme.DANGER if gold < cost else LuneUiTheme.MUTED)
|
||||||
|
LuneUiTheme.label("Vorläufige Rolle des vorhandenen Modells.", "HudMuted", column)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- hero
|
||||||
|
func _hero(model: LuneHudModel, hero: Dictionary) -> void:
|
||||||
|
var spec: Dictionary = LuneContent.HEROES[hero.hero_id]
|
||||||
|
var definition: Dictionary = model.hero_definition(hero)
|
||||||
|
var can_act: bool = _can_act(model)
|
||||||
|
var body := HBoxContainer.new()
|
||||||
|
body.add_theme_constant_override("separation", 18)
|
||||||
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
add_child(body)
|
||||||
|
var left := VBoxContainer.new()
|
||||||
|
left.add_theme_constant_override("separation", 6)
|
||||||
|
left.custom_minimum_size.x = 290
|
||||||
|
body.add_child(left)
|
||||||
|
var portrait := LunePortraitRect.new()
|
||||||
|
portrait.custom_minimum_size = Vector2(290, 230)
|
||||||
|
left.add_child(portrait)
|
||||||
|
portrait.show_model(str(spec.model), true)
|
||||||
|
LuneUiTheme.label(str(spec.title), "HudMuted", left)
|
||||||
|
var role := HBoxContainer.new()
|
||||||
|
role.add_theme_constant_override("separation", 6)
|
||||||
|
left.add_child(role)
|
||||||
|
LuneGlyph.make(LuneStats.role_glyph(str(definition.role)), LuneUiTheme.BRONZE_LIGHT, 16.0, role)
|
||||||
|
LuneUiTheme.label(str(spec.role), "HudText", role)
|
||||||
|
var xp: int = int(hero.xp)
|
||||||
|
var level: int = LuneContent.level_for(xp)
|
||||||
|
var level_row := HBoxContainer.new()
|
||||||
|
level_row.add_theme_constant_override("separation", 6)
|
||||||
|
left.add_child(level_row)
|
||||||
|
LuneGlyph.make("star", LuneUiTheme.HERO, 16.0, level_row)
|
||||||
|
LuneUiTheme.label("STUFE %d / %d" % [level, LuneContent.LEVEL_XP.size()], "HudValue", level_row)
|
||||||
|
var bar := LuneHpBar.new()
|
||||||
|
bar.custom_minimum_size = Vector2(0, 8)
|
||||||
|
bar.fill = LuneUiTheme.BRONZE_LIGHT
|
||||||
|
left.add_child(bar)
|
||||||
|
var top: bool = level >= LuneContent.LEVEL_XP.size()
|
||||||
|
var low: int = LuneContent.LEVEL_XP[level - 1]
|
||||||
|
var high: int = LuneContent.LEVEL_XP[mini(level, LuneContent.LEVEL_XP.size() - 1)]
|
||||||
|
bar.fraction = 1.0 if top else float(xp - low) / maxf(1.0, float(high - low))
|
||||||
|
LuneUiTheme.label(("%d / %d EP · noch %d bis Stufe %d" % [xp, high, high - xp, level + 1]) if not top else "%d EP · Höchststufe" % xp, "HudMuted", left)
|
||||||
|
LuneUiTheme.label("EINSATZ", "HudCaps", left)
|
||||||
|
var lanes := HBoxContainer.new()
|
||||||
|
lanes.add_theme_constant_override("separation", 4)
|
||||||
|
left.add_child(lanes)
|
||||||
|
for lane: String in LuneMap.LANES + [LuneMatch.GROTTO]:
|
||||||
|
var title: String = "GROTTE" if lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[lane].to_upper()
|
||||||
|
var button := LuneUiTheme.button(title, "HudToggle", lanes)
|
||||||
|
button.toggle_mode = true
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.set_pressed_no_signal(str(hero.lane) == lane)
|
||||||
|
button.disabled = not can_act or (lane == LuneMatch.GROTTO and not model.state.grotto_available())
|
||||||
|
var uid: String = str(hero.uid)
|
||||||
|
button.pressed.connect(func() -> void: command_requested.emit({"type": "assign", "uid": uid, "lane": lane}))
|
||||||
|
lane_buttons[lane] = button
|
||||||
|
if str(hero.lane) == LuneMatch.GROTTO:
|
||||||
|
var warning := LuneUiTheme.label("Der Held fehlt seiner Lane in der nächsten Kampfphase.", "HudText", left)
|
||||||
|
warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
warning.add_theme_color_override("font_color", LuneUiTheme.WARN)
|
||||||
|
if bool(hero.get("fresh", false)) and model.planning():
|
||||||
|
var refund: int = int(hero.invested)
|
||||||
|
for item_id: String in hero.items:
|
||||||
|
refund += int(LuneContent.ITEMS[item_id].cost)
|
||||||
|
var cancel := LuneUiTheme.button("Held zurückgeben +%d" % refund, "HudButton", left)
|
||||||
|
cancel.disabled = not can_act
|
||||||
|
cancel.pressed.connect(func() -> void: command_requested.emit({"type": "hero_cancel"}))
|
||||||
|
var right := VBoxContainer.new()
|
||||||
|
right.add_theme_constant_override("separation", 8)
|
||||||
|
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
body.add_child(right)
|
||||||
|
tabs = LuneTabBar.new()
|
||||||
|
right.add_child(tabs)
|
||||||
|
tabs.add_tab("WERTE & FÄHIGKEIT")
|
||||||
|
tabs.add_tab("ITEMS %d/%d" % [(hero.items as Array).size(), LuneContent.ITEM_SLOTS])
|
||||||
|
var open_tier: int = (hero.talents as Array).size()
|
||||||
|
var talent_ready: bool = open_tier < LuneContent.TALENT_LEVELS.size() and level >= LuneContent.TALENT_LEVELS[open_tier]
|
||||||
|
tabs.add_tab("TALENTE" + (" ★" if talent_ready else ""))
|
||||||
|
tabs.select(tab)
|
||||||
|
tabs.tab_changed.connect(show_tab)
|
||||||
|
var page := VBoxContainer.new()
|
||||||
|
page.add_theme_constant_override("separation", 8)
|
||||||
|
page.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
right.add_child(page)
|
||||||
|
match tab:
|
||||||
|
TAB_ITEMS:
|
||||||
|
_items(model, hero, level, page)
|
||||||
|
TAB_TALENTS:
|
||||||
|
_talents(model, hero, level, page)
|
||||||
|
_:
|
||||||
|
_values(hero, definition, spec, page)
|
||||||
|
|
||||||
|
func _values(hero: Dictionary, definition: Dictionary, spec: Dictionary, page: VBoxContainer) -> void:
|
||||||
|
var grid := GridContainer.new()
|
||||||
|
grid.columns = 3
|
||||||
|
grid.add_theme_constant_override("h_separation", 10)
|
||||||
|
grid.add_theme_constant_override("v_separation", 3)
|
||||||
|
page.add_child(grid)
|
||||||
|
var rows: Array = LuneStats.rows(definition, true)
|
||||||
|
rows.append(["sword", "Schaden/Sekunde", "≈ %s (ohne Fähigkeit)" % LuneUiTheme.decimal(LuneStats.dps(definition))])
|
||||||
|
for entry: Array in rows:
|
||||||
|
LuneGlyph.make(str(entry[0]), LuneUiTheme.MUTED, 15.0, grid)
|
||||||
|
LuneUiTheme.label(str(entry[1]), "HudMuted", grid)
|
||||||
|
LuneUiTheme.label(str(entry[2]), "HudText", grid)
|
||||||
|
var level: int = int(definition.level)
|
||||||
|
var base: Dictionary = spec.def
|
||||||
|
var growth := LuneUiTheme.label("Grundwerte %s Leben, %s Schaden; je Stufe +%d %% Leben und +%d %% Schaden (jetzt Stufe %d)." % [
|
||||||
|
LuneUiTheme.number(base.max_hp), LuneUiTheme.decimal(base.damage), int(LuneContent.HP_PER_LEVEL * 100.0), int(LuneContent.DAMAGE_PER_LEVEL * 100.0), level], "HudMuted", page)
|
||||||
|
growth.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
LuneUiTheme.label("FÄHIGKEIT", "HudCaps", page)
|
||||||
|
var ability := LuneUiTheme.label(str(DataCatalog.get_unit(str(spec.model)).get("ability_description", "")), "HudText", page)
|
||||||
|
ability.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
var chosen: PackedStringArray = []
|
||||||
|
for tier: int in range((hero.talents as Array).size()):
|
||||||
|
for option: Dictionary in LuneContent.TALENTS[hero.hero_id][tier]:
|
||||||
|
if option.id == hero.talents[tier]:
|
||||||
|
chosen.append("%s: %s" % [option.name, option.text])
|
||||||
|
if not chosen.is_empty():
|
||||||
|
LuneUiTheme.label("TALENTE", "HudCaps", page)
|
||||||
|
var talents := LuneUiTheme.label("\n".join(chosen), "HudText", page)
|
||||||
|
talents.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
var xp := LuneUiTheme.label("Erfahrung: Tode von Gegnern in %d m (Monster 12 + Welle, Champion 30 + 2 × Welle, Gebäude %d, Boss %d) und je Kampf %d + %d × Welle." % [
|
||||||
|
int(LuneContent.XP_RADIUS), LuneContent.BUILDING_XP, LuneContent.BOSS_XP, LuneContent.XP_PARTICIPATION, LuneContent.XP_PER_ROUND], "HudMuted", page)
|
||||||
|
xp.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
|
||||||
|
func _items(model: LuneHudModel, hero: Dictionary, level: int, page: VBoxContainer) -> void:
|
||||||
|
var can_act: bool = _can_act(model)
|
||||||
|
var items: Array = hero.items
|
||||||
|
var slots := HBoxContainer.new()
|
||||||
|
slots.add_theme_constant_override("separation", 10)
|
||||||
|
page.add_child(slots)
|
||||||
|
for index: int in range(LuneContent.ITEM_SLOTS):
|
||||||
|
var box := VBoxContainer.new()
|
||||||
|
box.add_theme_constant_override("separation", 3)
|
||||||
|
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
slots.add_child(box)
|
||||||
|
var slot := LuneItemSlot.new(true)
|
||||||
|
slot.custom_minimum_size = Vector2(46, 46)
|
||||||
|
slot.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
box.add_child(slot)
|
||||||
|
if index < items.size():
|
||||||
|
var item_id: String = items[index]
|
||||||
|
var item: Dictionary = LuneContent.ITEMS[item_id]
|
||||||
|
slot.set_item(item_id, LuneItemSlot.State.EQUIPPED)
|
||||||
|
LuneTooltip.attach(slot, func() -> Dictionary: return {"title": str(item.name), "glyph": item_id, "glyph_color": LuneItemSlot.color_of(item_id), "text": str(item.text)})
|
||||||
|
LuneUiTheme.label(str(item.name), "HudText", box)
|
||||||
|
var fresh: bool = (hero.get("fresh_items", []) as Array).has(item_id)
|
||||||
|
var refund: int = int(item.cost) if fresh else floori(float(item.cost) * LuneContent.ITEM_REFUND)
|
||||||
|
var sell := LuneUiTheme.button("Verkaufen +%d" % refund, "HudButton", box)
|
||||||
|
sell.disabled = not can_act
|
||||||
|
sell.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
sell.pressed.connect(func() -> void: command_requested.emit({"type": "item_sell", "item_id": item_id}))
|
||||||
|
else:
|
||||||
|
slot.set_item("", LuneItemSlot.State.EMPTY)
|
||||||
|
LuneUiTheme.label("Platz %d frei" % (index + 1), "HudMuted", box)
|
||||||
|
LuneUiTheme.label("ITEMS · einzigartig je Held, bleiben dauerhaft, Rückgabe %d %%" % int(LuneContent.ITEM_REFUND * 100.0), "HudCaps", page)
|
||||||
|
var grid := GridContainer.new()
|
||||||
|
grid.columns = 2
|
||||||
|
grid.add_theme_constant_override("h_separation", 8)
|
||||||
|
grid.add_theme_constant_override("v_separation", 8)
|
||||||
|
page.add_child(grid)
|
||||||
|
var gold: int = model.own_gold()
|
||||||
|
for item_id: String in LuneContent.ITEMS:
|
||||||
|
var item: Dictionary = LuneContent.ITEMS[item_id]
|
||||||
|
var card := LuneItemSlot.new(false)
|
||||||
|
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
grid.add_child(card)
|
||||||
|
var state: int = LuneItemSlot.State.AVAILABLE
|
||||||
|
if items.has(item_id):
|
||||||
|
state = LuneItemSlot.State.UNIQUE
|
||||||
|
elif not can_act:
|
||||||
|
state = LuneItemSlot.State.LOCKED
|
||||||
|
elif level < int(item.get("min_level", 1)):
|
||||||
|
state = LuneItemSlot.State.INCOMPATIBLE
|
||||||
|
elif items.size() >= LuneContent.ITEM_SLOTS:
|
||||||
|
state = LuneItemSlot.State.FULL
|
||||||
|
elif gold < int(item.cost):
|
||||||
|
state = LuneItemSlot.State.UNAFFORDABLE
|
||||||
|
card.set_item(item_id, state)
|
||||||
|
item_cards[item_id] = card
|
||||||
|
var uid: String = str(hero.uid)
|
||||||
|
card.pressed.connect(func() -> void:
|
||||||
|
if card.state == LuneItemSlot.State.AVAILABLE:
|
||||||
|
command_requested.emit({"type": "item_buy", "item_id": item_id, "uid": uid}))
|
||||||
|
var reasons: Dictionary = {LuneItemSlot.State.UNIQUE: "Dein Held trägt es bereits; Items sind einzigartig.",
|
||||||
|
LuneItemSlot.State.LOCKED: _why_not(model), LuneItemSlot.State.INCOMPATIBLE: "Erfordert Heldenstufe %d." % int(item.get("min_level", 1)),
|
||||||
|
LuneItemSlot.State.FULL: "Alle %d Plätze belegt; verkaufe zuerst ein Item." % LuneContent.ITEM_SLOTS,
|
||||||
|
LuneItemSlot.State.UNAFFORDABLE: "Es fehlen %d Gold." % (int(item.cost) - gold), LuneItemSlot.State.AVAILABLE: "Klick: kaufen und ausrüsten."}
|
||||||
|
var why: String = str(reasons.get(state, ""))
|
||||||
|
LuneTooltip.attach(card, func() -> Dictionary: return {"title": "%s · %d Gold" % [item.name, int(item.cost)], "glyph": item_id,
|
||||||
|
"glyph_color": LuneItemSlot.color_of(item_id), "text": str(item.text), "note": why})
|
||||||
|
|
||||||
|
func _talents(model: LuneHudModel, hero: Dictionary, level: int, page: VBoxContainer) -> void:
|
||||||
|
var can_act: bool = _can_act(model)
|
||||||
|
var talents: Array = hero.talents
|
||||||
|
for tier: int in range(LuneContent.TALENT_LEVELS.size()):
|
||||||
|
var required: int = LuneContent.TALENT_LEVELS[tier]
|
||||||
|
var state_text: String = "gewählt" if tier < talents.size() else ("jetzt wählbar" if level >= required and tier == talents.size() else "ab Stufe %d" % required)
|
||||||
|
LuneUiTheme.label("TALENT %d · STUFE %d · %s" % [tier + 1, required, state_text.to_upper()], "HudCaps", page)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
page.add_child(row)
|
||||||
|
var options: Array = LuneContent.TALENTS[hero.hero_id][tier]
|
||||||
|
for choice: int in range(options.size()):
|
||||||
|
var option: Dictionary = options[choice]
|
||||||
|
var card := Button.new()
|
||||||
|
card.theme_type_variation = "HudCard"
|
||||||
|
card.toggle_mode = true
|
||||||
|
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
card.custom_minimum_size = Vector2(0, 96)
|
||||||
|
card.focus_mode = Control.FOCUS_ALL
|
||||||
|
row.add_child(card)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
column.offset_left = 10
|
||||||
|
column.offset_right = -10
|
||||||
|
column.offset_top = 8
|
||||||
|
column.offset_bottom = -8
|
||||||
|
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
card.add_child(column)
|
||||||
|
LuneUiTheme.label(str(option.name), "HudValue", column)
|
||||||
|
var text := LuneUiTheme.label(str(option.text), "HudMuted", column)
|
||||||
|
text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
var chosen: bool = tier < talents.size() and talents[tier] == option.id
|
||||||
|
card.set_pressed_no_signal(chosen)
|
||||||
|
card.disabled = not (can_act and tier == talents.size() and level >= required) and not chosen
|
||||||
|
var command: Dictionary = {"type": "talent", "tier": tier, "choice": choice}
|
||||||
|
card.pressed.connect(func() -> void:
|
||||||
|
card.set_pressed_no_signal(chosen)
|
||||||
|
if not chosen:
|
||||||
|
command_requested.emit(command))
|
||||||
|
LuneUiTheme.label("Die Wahl ist endgültig; jede Stufe bietet zwei Richtungen (Einzelziel oder Fläche, Schutz oder Druck).", "HudMuted", page).autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
1
scripts/luneraria/ui/components/hero_detail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bfrq4nh62rqic
|
||||||
41
scripts/luneraria/ui/components/hp_bar.gd
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
class_name LuneHpBar
|
||||||
|
extends Control
|
||||||
|
## Thin value bar drawn directly: dark track, fill, a top highlight and optional ticks.
|
||||||
|
|
||||||
|
var fraction: float = 1.0:
|
||||||
|
set(value):
|
||||||
|
value = clampf(value, 0.0, 1.0)
|
||||||
|
if absf(value - fraction) > .0005:
|
||||||
|
fraction = value
|
||||||
|
queue_redraw()
|
||||||
|
var fill: Color = LuneUiTheme.GOOD:
|
||||||
|
set(value):
|
||||||
|
if value != fill:
|
||||||
|
fill = value
|
||||||
|
queue_redraw()
|
||||||
|
var ticks: int = 0
|
||||||
|
var track: Color = Color(0, 0, 0, .55)
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
custom_minimum_size = Vector2(60, 6)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
|
draw_rect(rect, track)
|
||||||
|
if fraction > 0.0:
|
||||||
|
var filled := Rect2(Vector2.ZERO, Vector2(maxf(1.0, size.x * fraction), size.y))
|
||||||
|
draw_rect(filled, fill)
|
||||||
|
draw_line(Vector2(0, .5), Vector2(filled.size.x, .5), Color(1, 1, 1, .28), 1.0)
|
||||||
|
for index: int in range(1, ticks):
|
||||||
|
var x: float = roundf(size.x * float(index) / float(ticks))
|
||||||
|
draw_line(Vector2(x, 0), Vector2(x, size.y), Color(0, 0, 0, .45), 1.0)
|
||||||
|
draw_rect(rect, Color(1, 1, 1, .12), false, 1.0)
|
||||||
|
|
||||||
|
## Green above 60 %, amber above 30 %, red below; the team colour when given.
|
||||||
|
static func health_color(value: float, base: Color = LuneUiTheme.GOOD) -> Color:
|
||||||
|
if value > .6:
|
||||||
|
return base
|
||||||
|
if value > .3:
|
||||||
|
return LuneUiTheme.WARN
|
||||||
|
return LuneUiTheme.DANGER
|
||||||
1
scripts/luneraria/ui/components/hp_bar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://p0d75xwrg6g0
|
||||||
93
scripts/luneraria/ui/components/item_slot.gd
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
class_name LuneItemSlot
|
||||||
|
extends Button
|
||||||
|
## A hero item: either one of the three slots (compact: symbol only) or a card in the item
|
||||||
|
## shop (symbol, name, cost, main effect, state). States say why something cannot be done:
|
||||||
|
## equipped, available, unaffordable, incompatible (hero level), unique (already carried),
|
||||||
|
## slots full, locked (plan committed) or an empty slot.
|
||||||
|
|
||||||
|
enum State { EMPTY, EQUIPPED, AVAILABLE, UNAFFORDABLE, INCOMPATIBLE, UNIQUE, FULL, LOCKED }
|
||||||
|
const STATE_TEXT: Dictionary = {State.EQUIPPED: "AUSGERÜSTET", State.AVAILABLE: "KAUFBAR", State.UNAFFORDABLE: "ZU TEUER",
|
||||||
|
State.INCOMPATIBLE: "STUFE FEHLT", State.UNIQUE: "GETRAGEN", State.FULL: "PLÄTZE VOLL", State.LOCKED: "GESPERRT", State.EMPTY: "LEER"}
|
||||||
|
const ITEM_COLORS: Dictionary = {"basaltschild": Color("9fb2c8"), "glutklinge": Color("ff9a5a"), "rankenmantel": Color("8fd08a"),
|
||||||
|
"funkenfokus": Color("f5d76a"), "quellamulett": Color("7ad6e6"), "kristallsplitter": Color("c7a2ff")}
|
||||||
|
|
||||||
|
var item_id: String = ""
|
||||||
|
var state: int = State.EMPTY
|
||||||
|
var compact: bool = true
|
||||||
|
var _glyph: LuneGlyph
|
||||||
|
var _name: Label
|
||||||
|
var _effect: Label
|
||||||
|
var _cost: Label
|
||||||
|
var _badge: LuneStatusBadge
|
||||||
|
|
||||||
|
static func color_of(id: String) -> Color:
|
||||||
|
return ITEM_COLORS.get(id, LuneUiTheme.MUTED)
|
||||||
|
|
||||||
|
func _init(is_compact: bool = true) -> void:
|
||||||
|
compact = is_compact
|
||||||
|
theme_type_variation = "HudChip" if compact else "HudCard"
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
if compact:
|
||||||
|
custom_minimum_size = Vector2(30, 30)
|
||||||
|
_glyph = LuneGlyph.new()
|
||||||
|
_glyph.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_glyph.offset_left = 5
|
||||||
|
_glyph.offset_top = 5
|
||||||
|
_glyph.offset_right = -5
|
||||||
|
_glyph.offset_bottom = -5
|
||||||
|
add_child(_glyph)
|
||||||
|
return
|
||||||
|
custom_minimum_size = Vector2(0, 74)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
row.offset_left = 10
|
||||||
|
row.offset_right = -8
|
||||||
|
row.offset_top = 6
|
||||||
|
row.offset_bottom = -6
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(row)
|
||||||
|
_glyph = LuneGlyph.make("", Color.WHITE, 34.0, row)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 0)
|
||||||
|
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
column.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(column)
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
head.add_theme_constant_override("separation", 6)
|
||||||
|
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
column.add_child(head)
|
||||||
|
_name = LuneUiTheme.label("", "HudValue", head)
|
||||||
|
_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
LuneGlyph.make("coin", LuneUiTheme.GOLD, 14.0, head)
|
||||||
|
_cost = LuneUiTheme.label("", "HudValue", head)
|
||||||
|
_effect = LuneUiTheme.label("", "HudMuted", column)
|
||||||
|
_effect.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
_effect.clip_text = true
|
||||||
|
_badge = LuneStatusBadge.new()
|
||||||
|
column.add_child(_badge)
|
||||||
|
_badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
|
func set_item(id: String, new_state: int) -> void:
|
||||||
|
item_id = id
|
||||||
|
state = new_state
|
||||||
|
var item: Dictionary = LuneContent.ITEMS.get(id, {})
|
||||||
|
var tint: Color = color_of(id)
|
||||||
|
var usable: bool = new_state in [State.AVAILABLE, State.EQUIPPED]
|
||||||
|
_glyph.glyph = id if not id.is_empty() else ""
|
||||||
|
_glyph.color = tint if usable or compact else Color(tint, .45)
|
||||||
|
if compact:
|
||||||
|
disabled = id.is_empty()
|
||||||
|
return
|
||||||
|
_name.text = str(item.get("name", ""))
|
||||||
|
_cost.text = str(int(item.get("cost", 0)))
|
||||||
|
_cost.add_theme_color_override("font_color", LuneUiTheme.DANGER if new_state == State.UNAFFORDABLE else LuneUiTheme.BRIGHT)
|
||||||
|
_effect.text = str(item.get("text", ""))
|
||||||
|
var badge_color: Color = LuneUiTheme.GOOD if new_state == State.EQUIPPED else (LuneUiTheme.BRONZE_LIGHT if new_state == State.AVAILABLE else LuneUiTheme.MUTED)
|
||||||
|
if new_state == State.UNAFFORDABLE:
|
||||||
|
badge_color = LuneUiTheme.DANGER
|
||||||
|
var glyphs: Dictionary = {State.EQUIPPED: "check", State.INCOMPATIBLE: "lock", State.LOCKED: "lock", State.UNIQUE: "check"}
|
||||||
|
_badge.set_state(STATE_TEXT.get(new_state, ""), badge_color, glyphs.get(new_state, ""), new_state == State.EQUIPPED)
|
||||||
|
disabled = not (new_state == State.AVAILABLE or new_state == State.EQUIPPED)
|
||||||
1
scripts/luneraria/ui/components/item_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://xas3b6sqx7ry
|
||||||
202
scripts/luneraria/ui/components/lane_indicator.gd
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
class_name LuneLaneIndicator
|
||||||
|
extends Button
|
||||||
|
## One lane in the lane overview: name and hotkey, the threat level (text, symbol and colour),
|
||||||
|
## the tower chain from the own base to the enemy base, the two lane players, announced
|
||||||
|
## champions or the boss, and in combat the unit count of both sides. A click moves the camera.
|
||||||
|
## The threat rises at once but only falls after a short hold, so it does not flicker.
|
||||||
|
|
||||||
|
const HOLD: float = 2.5
|
||||||
|
|
||||||
|
var lane: String = "top"
|
||||||
|
var hotkey: String = ""
|
||||||
|
var level: int = 0
|
||||||
|
var data: Dictionary = {}
|
||||||
|
var towers: Array[LuneTowerIndicator] = []
|
||||||
|
var _title: Label
|
||||||
|
var _badge: LuneStatusBadge
|
||||||
|
var _own: Label
|
||||||
|
var _enemy: Label
|
||||||
|
var _own_hero: LunePortraitRect
|
||||||
|
var _enemy_hero: LunePortraitRect
|
||||||
|
var _army: Label
|
||||||
|
var _dangers: HBoxContainer
|
||||||
|
var _mine: LuneGlyph
|
||||||
|
var _own_shape: LuneGlyph
|
||||||
|
var _enemy_shape: LuneGlyph
|
||||||
|
var _lower_hold: float = 0.0
|
||||||
|
var _pulse: float = 0.0
|
||||||
|
var _signature: String = ""
|
||||||
|
|
||||||
|
func _init(lane_name: String = "top", key: String = "") -> void:
|
||||||
|
lane = lane_name
|
||||||
|
hotkey = key
|
||||||
|
theme_type_variation = "HudRow"
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
custom_minimum_size = Vector2(0, 92)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
column.offset_left = 10
|
||||||
|
column.offset_right = -9
|
||||||
|
column.offset_top = 6
|
||||||
|
column.offset_bottom = -6
|
||||||
|
column.add_theme_constant_override("separation", 3)
|
||||||
|
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(column)
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
head.add_theme_constant_override("separation", 6)
|
||||||
|
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
column.add_child(head)
|
||||||
|
LuneGlyph.make("lane_" + lane, LuneUiTheme.BRONZE_LIGHT, 17.0, head)
|
||||||
|
_title = LuneUiTheme.label(LuneMap.LANE_NAMES[lane].to_upper(), "HudLane", head)
|
||||||
|
_mine = LuneGlyph.make("star", LuneUiTheme.BRONZE_LIGHT, 12.0, head)
|
||||||
|
_mine.visible = false
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
head.add_child(spacer)
|
||||||
|
_badge = LuneStatusBadge.new()
|
||||||
|
head.add_child(_badge)
|
||||||
|
var chain := HBoxContainer.new()
|
||||||
|
chain.add_theme_constant_override("separation", 3)
|
||||||
|
chain.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
column.add_child(chain)
|
||||||
|
_own_shape = LuneGlyph.make("emblem_0", LuneUiTheme.TEAM[0], 13.0, chain)
|
||||||
|
for index: int in range(4):
|
||||||
|
var tower := LuneTowerIndicator.new()
|
||||||
|
chain.add_child(tower)
|
||||||
|
towers.append(tower)
|
||||||
|
LuneTooltip.attach(tower, _tower_tip.bind(index))
|
||||||
|
if index == 1:
|
||||||
|
var middle := HBoxContainer.new()
|
||||||
|
middle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
middle.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
middle.add_theme_constant_override("separation", 4)
|
||||||
|
middle.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
chain.add_child(middle)
|
||||||
|
_army = LuneUiTheme.label("", "HudValue", middle)
|
||||||
|
_dangers = HBoxContainer.new()
|
||||||
|
_dangers.add_theme_constant_override("separation", 2)
|
||||||
|
_dangers.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
middle.add_child(_dangers)
|
||||||
|
_enemy_shape = LuneGlyph.make("emblem_1", LuneUiTheme.TEAM[1], 13.0, chain)
|
||||||
|
var names := HBoxContainer.new()
|
||||||
|
names.add_theme_constant_override("separation", 5)
|
||||||
|
names.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
column.add_child(names)
|
||||||
|
# Heroes of both lane players (Dota-style "who stands where"), committed heroes only.
|
||||||
|
_own_hero = LunePortraitRect.new()
|
||||||
|
_own_hero.custom_minimum_size = Vector2(22, 22)
|
||||||
|
names.add_child(_own_hero)
|
||||||
|
_own = LuneUiTheme.label("", "HudMuted", names)
|
||||||
|
_own.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_own.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
_own.clip_text = true
|
||||||
|
_enemy = LuneUiTheme.label("", "HudMuted", names)
|
||||||
|
_enemy.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_enemy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_enemy.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
_enemy.clip_text = true
|
||||||
|
_enemy_hero = LunePortraitRect.new()
|
||||||
|
_enemy_hero.custom_minimum_size = Vector2(22, 22)
|
||||||
|
names.add_child(_enemy_hero)
|
||||||
|
LuneTooltip.attach(self, _tip)
|
||||||
|
|
||||||
|
## Applies a lane dictionary from LuneHudModel.lane(); returns true if something changed.
|
||||||
|
func apply(entry: Dictionary, own_team: int, delta: float, reduced_motion: bool) -> void:
|
||||||
|
data = entry
|
||||||
|
var threat: int = int(entry.threat.level)
|
||||||
|
if threat >= level:
|
||||||
|
if threat == LuneLaneThreat.Level.CRITICAL and level != threat and not reduced_motion:
|
||||||
|
_pulse = 1.6
|
||||||
|
level = threat
|
||||||
|
_lower_hold = HOLD
|
||||||
|
else:
|
||||||
|
_lower_hold -= delta
|
||||||
|
if _lower_hold <= 0.0:
|
||||||
|
level = threat
|
||||||
|
_lower_hold = HOLD
|
||||||
|
var tint: Color = LuneUiTheme.THREAT[level]
|
||||||
|
_badge.set_state(LuneLaneThreat.NAMES[level], tint, LuneLaneThreat.GLYPHS[level] if level >= 2 else "", level == LuneLaneThreat.Level.CRITICAL)
|
||||||
|
_own_shape.glyph = "emblem_%d" % own_team
|
||||||
|
_own_shape.color = LuneUiTheme.team_color(own_team)
|
||||||
|
_enemy_shape.glyph = "emblem_%d" % (1 - own_team)
|
||||||
|
_enemy_shape.color = LuneUiTheme.team_color(1 - own_team)
|
||||||
|
var list: Array = entry.towers
|
||||||
|
for index: int in range(4):
|
||||||
|
var tower: Dictionary = list[index]
|
||||||
|
towers[index].team = int(tower.team)
|
||||||
|
towers[index].tier = str(tower.tier)
|
||||||
|
towers[index].set_state(bool(tower.alive), float(tower.fraction), bool(tower.attackable))
|
||||||
|
_own.text = str(entry.own_player)
|
||||||
|
_own.add_theme_color_override("font_color", LuneUiTheme.BRONZE_LIGHT if entry.own_is_me else LuneUiTheme.MUTED)
|
||||||
|
_enemy.text = str(entry.enemy_player)
|
||||||
|
for pair: Array in [[_own_hero, entry.get("own_hero", {})], [_enemy_hero, entry.get("enemy_hero", {})]]:
|
||||||
|
var portrait: LunePortraitRect = pair[0]
|
||||||
|
var hero: Dictionary = pair[1]
|
||||||
|
portrait.visible = not hero.is_empty()
|
||||||
|
if not hero.is_empty():
|
||||||
|
portrait.show_model(str(hero.model))
|
||||||
|
_mine.visible = bool(entry.own_is_me)
|
||||||
|
_army.text = "%d : %d" % [int(entry.own_count), int(entry.enemy_count)] if entry.has("own_count") else ""
|
||||||
|
var signature: String = str(entry.dangers)
|
||||||
|
if signature != _signature:
|
||||||
|
_signature = signature
|
||||||
|
for child: Node in _dangers.get_children():
|
||||||
|
child.free()
|
||||||
|
for danger: Dictionary in entry.dangers:
|
||||||
|
var hostile: bool = int(danger.team) != own_team
|
||||||
|
var glyph: String = "crown" if danger.kind == "boss" else "champion"
|
||||||
|
LuneGlyph.make(glyph, LuneUiTheme.DANGER if hostile else LuneUiTheme.team_color(int(danger.team)), 16.0, _dangers)
|
||||||
|
if _pulse > 0.0:
|
||||||
|
_pulse = maxf(0.0, _pulse - delta)
|
||||||
|
_badge.modulate = Color(1, 1, 1, .55 + .45 * absf(cos(_pulse * PI * 2.0)))
|
||||||
|
if _pulse <= 0.0:
|
||||||
|
_badge.modulate = Color.WHITE
|
||||||
|
elif reduced_motion and _badge.modulate != Color.WHITE:
|
||||||
|
_badge.modulate = Color.WHITE
|
||||||
|
if LuneTooltip.current != null:
|
||||||
|
LuneTooltip.current.refresh(self)
|
||||||
|
|
||||||
|
func _tip() -> Dictionary:
|
||||||
|
if data.is_empty():
|
||||||
|
return {}
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for reason: String in data.threat.reasons:
|
||||||
|
lines.append("• " + reason)
|
||||||
|
if lines.is_empty():
|
||||||
|
lines.append("Keine Bedrohung aus Türmen, Kampflage oder Ankündigungen.")
|
||||||
|
var stats: Array = []
|
||||||
|
var list: Array = data.towers
|
||||||
|
for index: int in [0, 1]:
|
||||||
|
var tower: Dictionary = list[index]
|
||||||
|
stats.append(["tower", "Eigene " + LuneHudModel.building_title(tower.id), _tower_value(tower)])
|
||||||
|
for index: int in [2, 3]:
|
||||||
|
var tower: Dictionary = list[index]
|
||||||
|
stats.append(["tower", "Gegner " + LuneHudModel.building_title(tower.id), _tower_value(tower)])
|
||||||
|
if data.has("own_count"):
|
||||||
|
stats.append(["melee", "Einheiten", "%d eigene · %d gegnerische" % [int(data.own_count), int(data.enemy_count)]])
|
||||||
|
var notes: PackedStringArray = []
|
||||||
|
for danger: Dictionary in data.dangers:
|
||||||
|
var side: String = LuneUiTheme.team_name(int(danger.team))
|
||||||
|
notes.append(("Schlundkrone (%s) greift an" % side) if danger.kind == "boss" else ("%s (%s) angekündigt" % [LuneContent.CHAMPIONS[danger.champion_id].name, side]))
|
||||||
|
return {"title": "%s-LANE · %s" % [data.title, LuneLaneThreat.NAMES[level]], "glyph": "lane_" + lane,
|
||||||
|
"subtitle": "%s gegen %s" % [_player_text(data.own_player, data.get("own_hero", {})), _player_text(data.enemy_player, data.get("enemy_hero", {}))], "stats": stats, "text": "\n".join(lines),
|
||||||
|
"note": ("; ".join(notes) + "\n" if not notes.is_empty() else "") + "Klick: Kamera auf die Lane" + (" (%s)" % hotkey if not hotkey.is_empty() else "")}
|
||||||
|
|
||||||
|
static func _player_text(player: String, hero: Dictionary) -> String:
|
||||||
|
return player if hero.is_empty() else "%s (%s, Stufe %d)" % [player, hero.name, int(hero.level)]
|
||||||
|
|
||||||
|
func _tower_tip(index: int) -> Dictionary:
|
||||||
|
if data.is_empty():
|
||||||
|
return {}
|
||||||
|
var tower: Dictionary = data.towers[index]
|
||||||
|
var title: String = "%s · %s" % [LuneHudModel.building_title(tower.id), LuneUiTheme.team_name(int(tower.team))]
|
||||||
|
var text: String = "Zerstört – die Ruine blockiert nicht." if not tower.alive else ("Angreifbar." if tower.attackable else "Noch geschützt: erst fällt der Turm davor.")
|
||||||
|
return {"title": title, "glyph": "tower", "glyph_color": LuneUiTheme.team_color(int(tower.team)),
|
||||||
|
"stats": [["heart", "Leben", _tower_value(tower)]], "text": text}
|
||||||
|
|
||||||
|
static func _tower_value(tower: Dictionary) -> String:
|
||||||
|
if not tower.alive:
|
||||||
|
return "zerstört"
|
||||||
|
return "%s / %s (%d %%)" % [LuneUiTheme.number(tower.hp), LuneUiTheme.number(tower.max_hp), int(round(float(tower.fraction) * 100.0))]
|
||||||
1
scripts/luneraria/ui/components/lane_indicator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d2rwsckejynpm
|
||||||
32
scripts/luneraria/ui/components/lune_glyph.gd
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
class_name LuneGlyph
|
||||||
|
extends Control
|
||||||
|
## A LuneGlyphs symbol as a control (icons in cards, badges and rows). Ignores the mouse.
|
||||||
|
|
||||||
|
var glyph: String = "":
|
||||||
|
set(value):
|
||||||
|
if value != glyph:
|
||||||
|
glyph = value
|
||||||
|
queue_redraw()
|
||||||
|
var color: Color = LuneUiTheme.TEXT:
|
||||||
|
set(value):
|
||||||
|
if value != color:
|
||||||
|
color = value
|
||||||
|
queue_redraw()
|
||||||
|
var accent: Color = Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
static func make(name_value: String, tint: Color, edge: float = 18.0, parent: Node = null) -> LuneGlyph:
|
||||||
|
var result := LuneGlyph.new()
|
||||||
|
result.glyph = name_value
|
||||||
|
result.color = tint
|
||||||
|
result.custom_minimum_size = Vector2(edge, edge)
|
||||||
|
result.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
if parent != null:
|
||||||
|
parent.add_child(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if not glyph.is_empty():
|
||||||
|
LuneGlyphs.draw(self, glyph, Rect2(Vector2.ZERO, size), color, accent)
|
||||||
1
scripts/luneraria/ui/components/lune_glyph.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://7ltitpqbfktb
|
||||||
141
scripts/luneraria/ui/components/lune_tooltip.gd
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
class_name LuneTooltip
|
||||||
|
extends PanelContainer
|
||||||
|
## The one tooltip of the HUD. A single panel is reused for every hover: it appears after a
|
||||||
|
## short delay next to the hovered control, stays inside the window, never takes mouse input
|
||||||
|
## and hides as soon as the control is left, hidden, disabled for input, freed or clicked.
|
||||||
|
## Content is a small dictionary: title, subtitle, stats ([glyph, name, value] rows), text,
|
||||||
|
## note and warn. Components register with `LuneTooltip.attach(control, builder)`.
|
||||||
|
|
||||||
|
const DELAY: float = .28
|
||||||
|
const QUICK: float = .05
|
||||||
|
const WIDTH: float = 300.0
|
||||||
|
|
||||||
|
static var current: LuneTooltip
|
||||||
|
|
||||||
|
var _owner: Control
|
||||||
|
var _builder: Callable
|
||||||
|
var _wait: float = 0.0
|
||||||
|
var _last_hide: float = -10.0
|
||||||
|
var _clock: float = 0.0
|
||||||
|
var _column: VBoxContainer
|
||||||
|
## Last pointer position from input events (canvas coordinates); real and synthetic motion.
|
||||||
|
var _pointer: Vector2 = Vector2(-1, -1)
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
theme_type_variation = "HudTooltip"
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
visible = false
|
||||||
|
z_index = 100
|
||||||
|
_column = VBoxContainer.new()
|
||||||
|
_column.add_theme_constant_override("separation", 4)
|
||||||
|
_column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_column)
|
||||||
|
current = self
|
||||||
|
|
||||||
|
static func attach(control: Control, builder: Callable) -> void:
|
||||||
|
control.mouse_entered.connect(func() -> void:
|
||||||
|
if current != null and is_instance_valid(current):
|
||||||
|
current.hover(control, builder))
|
||||||
|
control.mouse_exited.connect(func() -> void:
|
||||||
|
if current != null and is_instance_valid(current):
|
||||||
|
current.leave(control))
|
||||||
|
control.gui_input.connect(func(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and current != null and is_instance_valid(current):
|
||||||
|
current.leave(control))
|
||||||
|
|
||||||
|
func hover(control: Control, builder: Callable) -> void:
|
||||||
|
if _pointer.x < 0.0:
|
||||||
|
_pointer = control.get_global_mouse_position()
|
||||||
|
_owner = control
|
||||||
|
_builder = builder
|
||||||
|
_wait = QUICK if _clock - _last_hide < .6 else DELAY
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
func leave(control: Control) -> void:
|
||||||
|
if control == _owner:
|
||||||
|
_hide()
|
||||||
|
|
||||||
|
func _hide() -> void:
|
||||||
|
if visible:
|
||||||
|
_last_hide = _clock
|
||||||
|
_owner = null
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
## Refreshes the text of an open tooltip (values changed while hovering).
|
||||||
|
func refresh(control: Control) -> void:
|
||||||
|
if control == _owner and visible:
|
||||||
|
_show()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouse:
|
||||||
|
_pointer = (event as InputEventMouse).position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_clock += delta
|
||||||
|
if _owner == null:
|
||||||
|
return
|
||||||
|
if not is_instance_valid(_owner) or not _owner.is_visible_in_tree() or not _owner.get_global_rect().has_point(_pointer):
|
||||||
|
_hide()
|
||||||
|
return
|
||||||
|
if not visible:
|
||||||
|
_wait -= delta
|
||||||
|
if _wait <= 0.0:
|
||||||
|
_show()
|
||||||
|
|
||||||
|
func _show() -> void:
|
||||||
|
var content: Dictionary = _builder.call() if _builder.is_valid() else {}
|
||||||
|
if content.is_empty():
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
for child: Node in _column.get_children():
|
||||||
|
child.free()
|
||||||
|
if content.has("title"):
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
head.add_theme_constant_override("separation", 6)
|
||||||
|
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_column.add_child(head)
|
||||||
|
if content.has("glyph"):
|
||||||
|
LuneGlyph.make(str(content.glyph), content.get("glyph_color", LuneUiTheme.BRONZE_LIGHT), 18.0, head)
|
||||||
|
LuneUiTheme.label(str(content.title), "HudValue", head)
|
||||||
|
if content.has("subtitle"):
|
||||||
|
LuneUiTheme.label(str(content.subtitle), "HudMuted", _column)
|
||||||
|
var stats: Array = content.get("stats", [])
|
||||||
|
if not stats.is_empty():
|
||||||
|
var grid := GridContainer.new()
|
||||||
|
grid.columns = 3
|
||||||
|
grid.add_theme_constant_override("h_separation", 8)
|
||||||
|
grid.add_theme_constant_override("v_separation", 2)
|
||||||
|
grid.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_column.add_child(grid)
|
||||||
|
for row: Array in stats:
|
||||||
|
LuneGlyph.make(str(row[0]), LuneUiTheme.MUTED, 14.0, grid)
|
||||||
|
LuneUiTheme.label(str(row[1]), "HudMuted", grid)
|
||||||
|
var value := LuneUiTheme.label(str(row[2]), "HudText", grid)
|
||||||
|
value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
for key: String in ["text", "note", "warn"]:
|
||||||
|
if content.has(key) and not str(content[key]).is_empty():
|
||||||
|
var line := LuneUiTheme.label(str(content[key]), "HudText" if key == "text" else "HudMuted", _column)
|
||||||
|
line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
line.custom_minimum_size.x = WIDTH - 24.0
|
||||||
|
if key == "warn":
|
||||||
|
line.add_theme_color_override("font_color", LuneUiTheme.DANGER)
|
||||||
|
visible = true
|
||||||
|
reset_size()
|
||||||
|
custom_minimum_size.x = 0.0
|
||||||
|
await get_tree().process_frame
|
||||||
|
if not visible or _owner == null or not is_instance_valid(_owner):
|
||||||
|
return
|
||||||
|
_place()
|
||||||
|
|
||||||
|
func _place() -> void:
|
||||||
|
reset_size()
|
||||||
|
var parent_rect: Rect2 = (get_parent() as Control).get_global_rect() if get_parent() is Control else get_viewport_rect()
|
||||||
|
var anchor: Rect2 = _owner.get_global_rect()
|
||||||
|
var scale_factor: float = get_global_transform().get_scale().x
|
||||||
|
var extent: Vector2 = size * scale_factor
|
||||||
|
var at := Vector2(anchor.get_center().x - extent.x * .5, anchor.position.y - extent.y - 8.0)
|
||||||
|
if at.y < parent_rect.position.y + 6.0:
|
||||||
|
at.y = anchor.end.y + 8.0
|
||||||
|
at.x = clampf(at.x, parent_rect.position.x + 6.0, parent_rect.end.x - extent.x - 6.0)
|
||||||
|
at.y = clampf(at.y, parent_rect.position.y + 6.0, parent_rect.end.y - extent.y - 6.0)
|
||||||
|
global_position = at
|
||||||
1
scripts/luneraria/ui/components/lune_tooltip.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://comnp3s0ir8hg
|
||||||
74
scripts/luneraria/ui/components/modal_panel.gd
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
class_name LuneModalPanel
|
||||||
|
extends Control
|
||||||
|
## A window over the field for detail work (hero, recruiting). A light backdrop keeps clicks
|
||||||
|
## away from the world and closes the window; Esc and the × close it too. The window never
|
||||||
|
## covers the whole screen, and it only opens on request.
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
var panel: PanelContainer
|
||||||
|
var body: VBoxContainer
|
||||||
|
var title: Label
|
||||||
|
var _close: Button
|
||||||
|
var _box: Vector2
|
||||||
|
|
||||||
|
func _init(window_size: Vector2 = Vector2(980, 600)) -> void:
|
||||||
|
_box = window_size
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
visible = false
|
||||||
|
var backdrop := ColorRect.new()
|
||||||
|
backdrop.color = Color(.01, .015, .025, .38)
|
||||||
|
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
backdrop.gui_input.connect(func(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
close())
|
||||||
|
add_child(backdrop)
|
||||||
|
panel = PanelContainer.new()
|
||||||
|
panel.theme_type_variation = "HudModal"
|
||||||
|
panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(panel)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 12)
|
||||||
|
panel.add_child(column)
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
column.add_child(head)
|
||||||
|
title = LuneUiTheme.label("", "HudTitle", head)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_close = LuneUiTheme.button("", "HudIconButton", head)
|
||||||
|
_close.custom_minimum_size = Vector2(36, 34)
|
||||||
|
var cross := LuneGlyph.make("close", LuneUiTheme.MUTED, 14.0, _close)
|
||||||
|
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||||
|
_close.pressed.connect(close)
|
||||||
|
LuneTooltip.attach(_close, func() -> Dictionary: return {"title": "Schließen", "text": "Esc oder Klick neben das Fenster."})
|
||||||
|
body = VBoxContainer.new()
|
||||||
|
body.add_theme_constant_override("separation", 10)
|
||||||
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
column.add_child(body)
|
||||||
|
resized.connect(_layout)
|
||||||
|
|
||||||
|
func open(heading: String) -> void:
|
||||||
|
title.text = heading
|
||||||
|
visible = true
|
||||||
|
_layout()
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
if visible:
|
||||||
|
visible = false
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
func _layout() -> void:
|
||||||
|
var edge: Vector2 = Vector2(minf(_box.x, size.x - 40.0), minf(_box.y, size.y - 40.0))
|
||||||
|
panel.custom_minimum_size = edge
|
||||||
|
panel.size = edge
|
||||||
|
panel.position = ((size - edge) * .5).floor()
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
accept_event()
|
||||||
|
|
||||||
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
|
if visible and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
close()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||