update
This commit is contained in:
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Dinger
|
# Dinger
|
||||||
|
|
||||||
Kartenspiel in 2D (Godot 4.7): du und 1 bis 5 KI-Magier, jeder gegen jeden, alle mit denselben Regeln.
|
Kartenspiel in 2D (Godot 4.7): ein Duell gegen einen KI-Magier, beide mit denselben Regeln.
|
||||||
|
|
||||||
## Starten
|
## Starten
|
||||||
|
|
||||||
@@ -24,20 +24,20 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- Kein Deck und keine Starthand: jeder hat einen eigenen Laden mit fünf Angeboten. Kaufen kostet Mana, Ausspielen nichts, neu würfeln kostet 1 Mana
|
- Kein Deck und keine Starthand: jeder hat einen eigenen Laden mit fünf Angeboten. Kaufen kostet Mana, Ausspielen nichts, neu würfeln kostet 1 Mana
|
||||||
- Mana wächst um 1 je Zug (bis 10) und bleibt liegen, wenn du es nicht ausgibst — sparen ist eine echte Entscheidung
|
- Mana wächst um 1 je Zug (bis 10) und bleibt liegen, wenn du es nicht ausgibst — sparen ist eine echte Entscheidung
|
||||||
- Der Laden zeigt nur Karten, die höchstens dein Einkommen plus eins kosten: teure Wesen tauchen erst später auf
|
- Der Laden zeigt nur Karten, die höchstens dein Einkommen plus eins kosten: teure Wesen tauchen erst später auf
|
||||||
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik: sie ändert weder Werte noch Text
|
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Holo-Karten: selten farbiger Rahmen, Schein und Edelstein, episch ein wandernder Lichtstreifen, legendär Funkeln, mythisch schillernde Farben; auch auf dem Feld für alle sichtbar, Werte und Text bleiben gleich
|
||||||
- Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
- Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
||||||
- Resonanz: gleiche Elemente auf dem Feld verstärken sich (+1, +2, +3, +3)
|
- Resonanz: gleiche Elemente auf dem Feld verstärken sich (+1, +2, +3, +3); der Bonus steht als „+n“ am Angriff, verbundene Wesen sind unter der Reihe verlinkt
|
||||||
- Feldkombos: ein neues Wesen neben einem Wesen mit anderem Element löst einen Effekt aus
|
- Feldkombos: ein neues Wesen neben einem Wesen mit anderem Element löst einen Effekt aus
|
||||||
- Karteneffekte lösen beim Ausspielen aus (Schaden, Einfrieren, Heilen, Zurückwerfen, Verstärken, Schild, Mana); Kartenvorteil ist hier Manavorteil
|
- Karteneffekte lösen beim Ausspielen aus (Schaden, Einfrieren, Heilen, Zurückwerfen, Verstärken, Schild, Mana); Kartenvorteil ist hier Manavorteil
|
||||||
- Elementvorteil im Kreis (Wasser > Feuer > Luft > Erde > Wasser): 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, auch für Karteneffekte gegen Wesen
|
- Elementvorteil im Kreis (Wasser > Feuer > Luft > Erde > Wasser): 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, auch für Karteneffekte gegen Wesen
|
||||||
- Gemeinsames Feld: vier gespielte Karten eines Elements kippen es (Vulkan, Flut, Fels, Wirbel), Wesen dieses Elements bekommen +1 Angriff
|
- Gemeinsames Feld: vier gespielte Karten eines Elements kippen es (Vulkan, Flut, Fels, Wirbel), Wesen dieses Elements bekommen +1 Angriff; die Feldzeile zeigt je Element die Ladung als vier Punkte
|
||||||
|
- Vorschau: Hand- und Ladenkarten zeigen, welchen Bonus und welche Feldkombo das Ausspielen jetzt brächte; beim Zeigen ist der Kombo-Partner markiert, beim Angreifen steht der Elementvorteil (+1/−1) am Ziel
|
||||||
- Verschmelzen: zwei eigene Wesen werden eins (Elemente und Schlüsselwörter beider, Werte Durchschnitt plus 1, Kosten der Durchschnitt der Karten); drei oder mehr Elemente ergeben eine Chimäre
|
- Verschmelzen: zwei eigene Wesen werden eins (Elemente und Schlüsselwörter beider, Werte Durchschnitt plus 1, Kosten der Durchschnitt der Karten); drei oder mehr Elemente ergeben eine Chimäre
|
||||||
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner; die Hand fasst 8 Karten, das Feld 5 Wesen
|
- Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 8 Karten, das Feld 5 Wesen
|
||||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||||
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
||||||
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
|
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
|
||||||
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
|
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
|
||||||
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
|
||||||
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
||||||
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
||||||
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen
|
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen
|
||||||
@@ -56,7 +56,7 @@ DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fe
|
|||||||
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
||||||
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
|
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
|
||||||
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Ladenangebot, Musik
|
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Ladenangebot, Musik
|
||||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
||||||
```
|
```
|
||||||
|
|
||||||
## Export
|
## Export
|
||||||
@@ -79,4 +79,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
|||||||
- `scripts/campaign.gd` – Kampagnenstufen
|
- `scripts/campaign.gd` – Kampagnenstufen
|
||||||
- `scenes/main.tscn` – Startszene
|
- `scenes/main.tscn` – Startszene
|
||||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||||
|
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `field_view.gd` die Feldzeile, `battle_layout.gd` die Aufteilung des Kampfes
|
||||||
- `translations/ui.csv` – englische Oberflächentexte
|
- `translations/ui.csv` – englische Oberflächentexte
|
||||||
|
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
||||||
|
|||||||
@@ -5,15 +5,14 @@
|
|||||||
## Grundidee
|
## Grundidee
|
||||||
- Kartenspiel wie zum Beispiel Magic
|
- Kartenspiel wie zum Beispiel Magic
|
||||||
- PC-Spiel
|
- PC-Spiel
|
||||||
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
- Duell: Spieler gegen einen KI-Gegner (oder einen Mitspieler im Netz), beide mit gleichen Regeln
|
||||||
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
|
||||||
- Kampagne gegen benannte Magier als Fortschritt
|
- Kampagne gegen benannte Magier als Fortschritt
|
||||||
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
|
- Im lokalen Netz zu zweit spielbar, weitere als Zuschauer
|
||||||
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
|
||||||
|
|
||||||
## Sammeln
|
## Sammeln
|
||||||
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
|
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
|
||||||
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik (auch im Netzwerkspiel sichtbar)
|
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik wie bei Holo-Karten: je höher die Stufe, desto mehr Glanz (Rahmen, Schein, Lichtstreifen, Funkeln, Schillern), auch im Netzwerkspiel sichtbar
|
||||||
- Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub
|
- Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub
|
||||||
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
||||||
- Kein Deckbau und kein Nachziehstapel: fünf Angebote im Laden, Kaufen kostet Mana, Ausspielen nichts
|
- Kein Deckbau und kein Nachziehstapel: fünf Angebote im Laden, Kaufen kostet Mana, Ausspielen nichts
|
||||||
@@ -23,8 +22,8 @@
|
|||||||
## Elemente
|
## Elemente
|
||||||
- Jede Karte gehört zu genau einem Element
|
- Jede Karte gehört zu genau einem Element
|
||||||
- Kombos entstehen erst im Spiel: beim Verschmelzen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) und als Feldkombo
|
- Kombos entstehen erst im Spiel: beim Verschmelzen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) und als Feldkombo
|
||||||
- Elemente wirken auf dem Feld zusammen: gleiche verstärken sich (Resonanz), verschiedene lösen Feldkombos aus
|
- Elemente wirken auf dem Feld zusammen: gleiche verstärken sich (Resonanz), verschiedene lösen Feldkombos aus; beides ist vor dem Ausspielen auf der Karte zu sehen
|
||||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (1 Schaden mehr im Vorteil, 1 weniger im Nachteil; Mehrfachelemente nehmen den für sie besseren Wert)
|
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (1 Schaden mehr im Vorteil, 1 weniger im Nachteil; Mehrfachelemente nehmen den für sie besseren Wert)
|
||||||
- Das Feld gehört allen am Tisch: vier gespielte Karten eines Elements kippen es, Wesen dieses Elements bekommen +1 Angriff
|
- Das Feld gehört beiden: vier gespielte Karten eines Elements kippen es, Wesen dieses Elements bekommen +1 Angriff
|
||||||
- Zwei eigene Wesen lassen sich für Energie verschmelzen: das neue erbt Elemente und Fähigkeiten beider, seine Werte sind der Durchschnitt plus 1
|
- Zwei eigene Wesen lassen sich für Energie verschmelzen: das neue erbt Elemente und Fähigkeiten beider, seine Werte sind der Durchschnitt plus 1
|
||||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||||
|
|||||||
92
design.md
Normal file
92
design.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Design von Dinger
|
||||||
|
|
||||||
|
Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entwurf.
|
||||||
|
|
||||||
|
## Woran wir uns orientieren
|
||||||
|
|
||||||
|
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
|
||||||
|
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
||||||
|
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, Verlauf links, Zug beenden rechts, Wesen
|
||||||
|
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
||||||
|
- **Teamfight Tactics** – der Laden ist eine Reihe von fünf großen Karten unten, Neuwürfeln steht
|
||||||
|
mit seinem Preis direkt daneben, die Bank (bei uns die Hand) liegt als schmale Reihe darüber.
|
||||||
|
Der Laden zeigt an, was zum eigenen Brett passt (bei uns: „+n“ und die Feldkombo).
|
||||||
|
- **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole
|
||||||
|
statt Sätze.
|
||||||
|
- **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen,
|
||||||
|
Lesefluss von links (Vergangenheit) über die Mitte (Spiel) nach rechts (Zukunft), die eigene
|
||||||
|
Seite größer als die der Gegner, Zahlen groß, Text klein, Symbole statt Wörter, und Rückmeldung
|
||||||
|
für jede Aktion (Bewegung, Ton, kurzes Zittern).
|
||||||
|
|
||||||
|
## Karte
|
||||||
|
|
||||||
|
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
|
||||||
|
Schlüsselwörter als Symbole (Mauer = Wächter, Schild, Doppelpfeil = Flink, gekreuzte Klingen =
|
||||||
|
Doppelschlag), dann der Effekttext ohne Einleitung, unten Angriff (gelb) und Leben (rot) als
|
||||||
|
Kreise, dazwischen der Edelstein der Stufe.
|
||||||
|
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol und die
|
||||||
|
Überschrift „Beim Ausspielen“ über den Effekt. Kleine Karten zeigen nur Symbole.
|
||||||
|
- Handkacheln (96×62) sind flach: Kosten, Name, Werte, Symbole oder das Kombo-Schild. Details
|
||||||
|
kommen aus der schwebenden Vorschau.
|
||||||
|
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen;
|
||||||
|
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben.
|
||||||
|
- Der Kartentext in Daten und Sammlungssuche bleibt vollständig (`Card.text()`), die Karte
|
||||||
|
zeichnet nur `effect_text()`.
|
||||||
|
|
||||||
|
## Kampfbildschirm (1280×800)
|
||||||
|
|
||||||
|
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||||
|
|
||||||
|
- **Laden** als fünf große Karten, links daneben „Neu würfeln“ mit Preis, rechts „Zug beenden“.
|
||||||
|
- **Hand** als flache Kacheln (die Bank), für andere verdeckt.
|
||||||
|
- **Eigenes Brett** mit fünf sichtbaren Plätzen, färbt sich mit dem aktiven Feld; links daneben
|
||||||
|
die eigene Leiste (Leben, Mana, Einkommen, Zug), rechts „Verschmelzen“ und „Rückgängig“, nur
|
||||||
|
wenn sie gerade möglich sind.
|
||||||
|
- **Feldband** mit vier Ladepunkten je Element.
|
||||||
|
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie
|
||||||
|
Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
|
||||||
|
macht Ziele, Texte und Balance eindeutig.
|
||||||
|
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
|
||||||
|
und Feldkombo darunter.
|
||||||
|
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
|
||||||
|
- Hand- und Ladenkarten tragen „+n“ (Bonus auf dem Brett) und das Kombo-Schild; beim Zeigen ist
|
||||||
|
der Partner auf dem Brett markiert; beim Angreifen steht der Elementkonter (+1/−1) am Ziel.
|
||||||
|
- Wesen mit gleichem Element sind unter der Reihe verbunden (Resonanz).
|
||||||
|
|
||||||
|
## Bewegung und Ton
|
||||||
|
|
||||||
|
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Laden wird ausgeteilt, Ton.
|
||||||
|
- Kaufen: die Karte fliegt aus dem Laden auf die Hand, Münzton.
|
||||||
|
- Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein.
|
||||||
|
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
|
||||||
|
- Feldwechsel: Band in Elementfarbe, das Brett färbt sich.
|
||||||
|
- Karten heben sich beim Zeigen an, Knöpfe federn beim Drücken, Bildschirme blenden ein.
|
||||||
|
- Booster: Karten drehen sich auf, seltene Stufen blitzen in ihrer Farbe auf.
|
||||||
|
- Alles hängt an der Einstellung „Animationen“.
|
||||||
|
|
||||||
|
## Menüs
|
||||||
|
|
||||||
|
- Hauptmenü: Titel und drei Spielarten links, darunter Booster, Sammlung, Einstellungen,
|
||||||
|
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
|
||||||
|
Staub oben rechts.
|
||||||
|
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
|
||||||
|
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte
|
||||||
|
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale
|
||||||
|
stärker, nicht über Begleiter.
|
||||||
|
- Sammlung: sechs Spalten, aufgewertete Karten zuerst, rechts die große Vorschau.
|
||||||
|
|
||||||
|
## Offen
|
||||||
|
|
||||||
|
- Bilder statt Elementsymbolen, wenn es einmal Kartenkunst gibt.
|
||||||
|
- Hero-Porträts für Kampagnenbosse.
|
||||||
|
- Ein Tisch mit mehreren Spielern ließe sich als TFT-Rotation obendrauf setzen: jede Runde ein
|
||||||
|
anderer Gegner, das Brett bleibt.
|
||||||
|
|
||||||
|
## Quellen
|
||||||
|
|
||||||
|
- https://hearthstone.wiki.gg/wiki/Battlefield
|
||||||
|
- https://hearthstone.fandom.com/wiki/Card
|
||||||
|
- https://moirao.me/home/tft-ui
|
||||||
|
- https://gdkeys.com/the-card-games-ui-design-of-fairtravel-battle/
|
||||||
|
- https://medium.com/@acbassettone/5-ux-ui-lessons-from-designing-a-card-game
|
||||||
|
- https://hackread.com/the-juice-factor-designing-game-feel/
|
||||||
@@ -39,8 +39,8 @@ static func hand_value(p: Game.Player) -> float:
|
|||||||
|
|
||||||
|
|
||||||
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv,
|
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv,
|
||||||
## die der Gegner negativ, der stärkste Gegner etwas mehr (jeder gegen jeden: den Führenden
|
## die des Gegners negativ. Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung,
|
||||||
## bremsen). Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung, tödliche besonders.
|
## tödliche besonders.
|
||||||
static func evaluate(g: Game, p: Game.Player) -> float:
|
static func evaluate(g: Game, p: Game.Player) -> float:
|
||||||
if not p.alive():
|
if not p.alive():
|
||||||
return -10000.0
|
return -10000.0
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
class_name Campaign
|
class_name Campaign
|
||||||
## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Gegnern am Tisch.
|
## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Leben und Merkmalen.
|
||||||
|
|
||||||
const STAGES := [
|
const STAGES := [
|
||||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"},
|
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "life": 30, "reward": 100, "perk": "feuer_angriff"},
|
||||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "extra": 0, "reward": 100, "perk": "leben_35"},
|
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "life": 30, "reward": 100, "perk": "leben_35"},
|
||||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "extra": 1, "reward": 150, "perk": "waechter_leben"},
|
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "life": 31, "reward": 150, "perk": "waechter_leben"},
|
||||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "extra": 1, "reward": 150, "perk": "flink_angriff"},
|
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "life": 31, "reward": 150, "perk": "flink_angriff"},
|
||||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "extra": 2, "reward": 200, "perk": "wesen_leben"},
|
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "life": 32, "reward": 200, "perk": "wesen_leben"},
|
||||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "extra": 2, "reward": 200, "perk": "extra_mana"},
|
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 200, "perk": "extra_mana"},
|
||||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "extra": 3, "reward": 250, "perk": "resonanz_doppelt"},
|
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "life": 33, "reward": 250, "perk": "resonanz_doppelt"},
|
||||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "extra": 3, "reward": 250, "perk": "schild_wesen"},
|
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "life": 33, "reward": 250, "perk": "schild_wesen"},
|
||||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 300, "perk": "alle_flink"},
|
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 300, "perk": "alle_flink"},
|
||||||
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 500, "perk": "start_energie"},
|
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 500, "perk": "start_energie"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Gefolge zurück.
|
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
|
||||||
const STAGES2 := [
|
const STAGES2 := [
|
||||||
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "extra": 1, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "life": 30, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||||
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "extra": 1, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "life": 30, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
||||||
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "extra": 2, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "life": 31, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
||||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "extra": 2, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "life": 31, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
||||||
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "extra": 3, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "life": 32, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
||||||
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "extra": 3, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
|
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
|
||||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "extra": 4, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]},
|
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "life": 33, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]},
|
||||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "extra": 4, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "life": 33, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
||||||
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]},
|
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
## Kapitel 3: drei Merkmale und volle Tische.
|
## Kapitel 3: drei Merkmale und volle Tische.
|
||||||
const STAGES3 := [
|
const STAGES3 := [
|
||||||
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "extra": 2, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "life": 32, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
||||||
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "extra": 2, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "life": 32, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
||||||
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "extra": 3, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
|
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "life": 33, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
|
||||||
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "extra": 3, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]},
|
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "life": 33, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]},
|
||||||
{"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "extra": 4, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "waechter_leben"]},
|
{"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "life": 34, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "waechter_leben"]},
|
||||||
{"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "extra": 4, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]},
|
{"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "life": 34, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]},
|
||||||
{"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "extra": 4, "reward": 900, "perks": ["resonanz_doppelt", "wesen_leben", "start_energie"]},
|
{"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "life": 35, "reward": 900, "perks": ["resonanz_doppelt", "wesen_leben", "start_energie"]},
|
||||||
{"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "extra": 4, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
|
{"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "life": 35, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
|
||||||
{"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
|
{"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "life": 36, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
|
||||||
{"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "extra": 4, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]},
|
{"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "life": 36, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
||||||
@@ -63,14 +63,9 @@ static func stage(i: int) -> Dictionary:
|
|||||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||||
|
|
||||||
|
|
||||||
## Gegner einer Stufe: der Boss handelt nur mit seinen Elementen, Begleiter mit allen.
|
## Boss einer Stufe: handelt nur mit seinen Elementen, mit dem Leben und den Merkmalen der Stufe.
|
||||||
static func opponents(stage_index: int) -> Array:
|
static func opponent(stage_index: int) -> Game.Player:
|
||||||
var st := stage(stage_index)
|
var st := stage(stage_index)
|
||||||
var life: int = 35 if "leben_35" in st.perks else Game.START_LIFE
|
var boss := Game.Player.new(st.name, false, int(st.life), st.title, st.elements)
|
||||||
var boss := Game.Player.new(st.name, false, life, st.title, st.elements)
|
|
||||||
boss.perks = Array(st.perks)
|
boss.perks = Array(st.perks)
|
||||||
var out := [boss]
|
return boss
|
||||||
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
|
||||||
for k in extra:
|
|
||||||
out.append(DeckGen.random_opponent(k + 1))
|
|
||||||
return out
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const COMBOS := {
|
|||||||
const FIELD_COMBO_TEXT := {
|
const FIELD_COMBO_TEXT := {
|
||||||
"Dampf": "Friere das stärkste gegnerische Wesen ein",
|
"Dampf": "Friere das stärkste gegnerische Wesen ein",
|
||||||
"Lava": "1 Schaden an das stärkste gegnerische Wesen",
|
"Lava": "1 Schaden an das stärkste gegnerische Wesen",
|
||||||
"Blitz": "2 Schaden an den schwächsten Gegner",
|
"Blitz": "2 Schaden an den Gegner",
|
||||||
"Schlamm": "Heile dich um 3",
|
"Schlamm": "Heile dich um 3",
|
||||||
"Sturm": "Erhalte 1 Mana",
|
"Sturm": "Erhalte 1 Mana",
|
||||||
"Sand": "Das neue Wesen erhält +1/+1",
|
"Sand": "Das neue Wesen erhält +1/+1",
|
||||||
@@ -165,9 +165,9 @@ class Effect extends RefCounted:
|
|||||||
"all_enemy_creatures":
|
"all_enemy_creatures":
|
||||||
return tr("%s Schaden an alle gegnerischen Wesen") % n
|
return tr("%s Schaden an alle gegnerischen Wesen") % n
|
||||||
"all_enemy_heroes":
|
"all_enemy_heroes":
|
||||||
return tr("%s Schaden an alle Gegner") % n
|
return tr("%s Schaden an den Gegner") % n
|
||||||
"all_enemies":
|
"all_enemies":
|
||||||
return tr("%s Schaden an alle Gegner und ihre Wesen") % n
|
return tr("%s Schaden an den Gegner und seine Wesen") % n
|
||||||
"heal":
|
"heal":
|
||||||
return tr("Heile dich um %s") % n
|
return tr("Heile dich um %s") % n
|
||||||
"mana":
|
"mana":
|
||||||
@@ -250,6 +250,15 @@ class Card extends RefCounted:
|
|||||||
return fx.target
|
return fx.target
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
## Nur die Effekte (die Karte zeigt Schlüsselwörter als Symbole).
|
||||||
|
func effect_text() -> String:
|
||||||
|
if effects.is_empty():
|
||||||
|
return ""
|
||||||
|
var fx: Array[String] = []
|
||||||
|
for f in effects:
|
||||||
|
fx.append(f.text())
|
||||||
|
return ", ".join(fx) + "."
|
||||||
|
|
||||||
func text() -> String:
|
func text() -> String:
|
||||||
var parts: Array[String] = []
|
var parts: Array[String] = []
|
||||||
if not keywords.is_empty():
|
if not keywords.is_empty():
|
||||||
|
|||||||
@@ -31,5 +31,5 @@ static func shop_cards(r: RandomNumberGenerator, income: int, elements: Array =
|
|||||||
|
|
||||||
|
|
||||||
## Gegner mit eigenem Laden über alle Elemente.
|
## Gegner mit eigenem Laden über alle Elemente.
|
||||||
static func random_opponent(index: int, life: int = Game.START_LIFE) -> Game.Player:
|
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
|
||||||
return Game.Player.new("Gegner %d" % index, false, life, "Magier")
|
return Game.Player.new("Gegner", false, life)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const SHOP_SIZE := 5 # so viele Karten liegen im Laden
|
|||||||
const REROLL_COST := 1
|
const REROLL_COST := 1
|
||||||
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
|
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
|
||||||
const START_LIFE := 30
|
const START_LIFE := 30
|
||||||
const MAX_OPPONENTS := 5
|
|
||||||
|
|
||||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||||
const PERKS := {
|
const PERKS := {
|
||||||
@@ -18,7 +17,7 @@ const PERKS := {
|
|||||||
"waechter_leben": "Wächter kommen mit +2 Leben",
|
"waechter_leben": "Wächter kommen mit +2 Leben",
|
||||||
"wesen_leben": "Wesen kommen mit +1 Leben",
|
"wesen_leben": "Wesen kommen mit +1 Leben",
|
||||||
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
||||||
"leben_35": "Beginnt mit 35 Leben",
|
"leben_35": "Beginnt mit 5 Leben mehr",
|
||||||
"schild_wesen": "Wesen kommen mit Schild",
|
"schild_wesen": "Wesen kommen mit Schild",
|
||||||
"alle_flink": "Alle Wesen sind flink",
|
"alle_flink": "Alle Wesen sind flink",
|
||||||
"start_energie": "Beginnt mit 3 Mana",
|
"start_energie": "Beginnt mit 3 Mana",
|
||||||
@@ -151,8 +150,8 @@ class Player extends RefCounted:
|
|||||||
|
|
||||||
|
|
||||||
var human: Player # der (erste) menschliche Spieler
|
var human: Player # der (erste) menschliche Spieler
|
||||||
var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere)
|
var humans: Array = [] # alle menschlichen Spieler (im Netz zwei)
|
||||||
var opponents: Array = [] # alle außer dem ersten Menschen
|
var opponent: Player # der andere am Tisch
|
||||||
var players: Array = [] # in Zugreihenfolge
|
var players: Array = [] # in Zugreihenfolge
|
||||||
var winner_player: Player = null
|
var winner_player: Player = null
|
||||||
var rng: RandomNumberGenerator
|
var rng: RandomNumberGenerator
|
||||||
@@ -167,10 +166,10 @@ var field_element := "" # gemeinsames Feld aller am Tisch, "" = neutral
|
|||||||
var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel
|
var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel
|
||||||
|
|
||||||
|
|
||||||
func _init(h: Player, opps: Array, r: RandomNumberGenerator = null) -> void:
|
func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
||||||
human = h
|
human = h
|
||||||
opponents = opps
|
opponent = o
|
||||||
players = [h] + opps
|
players = [h, o]
|
||||||
humans = players.filter(func(p): return p.is_human)
|
humans = players.filter(func(p): return p.is_human)
|
||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
@@ -199,11 +198,15 @@ func ref_of(target) -> Dictionary:
|
|||||||
return {"kind": "player", "index": players.find(target)}
|
return {"kind": "player", "index": players.find(target)}
|
||||||
|
|
||||||
|
|
||||||
## Jeder gegen jeden: alle anderen lebenden Spieler sind Gegner.
|
## Der Gegner von p, solange er lebt (als Liste, damit Effekte einheitlich bleiben).
|
||||||
func enemies_of(p: Player) -> Array:
|
func enemies_of(p: Player) -> Array:
|
||||||
return players.filter(func(o): return o != p and o.alive())
|
return players.filter(func(o): return o != p and o.alive())
|
||||||
|
|
||||||
|
|
||||||
|
func enemy_of(p: Player) -> Player:
|
||||||
|
return opponent if p == human else human
|
||||||
|
|
||||||
|
|
||||||
func find_creature(uid: int) -> Creature:
|
func find_creature(uid: int) -> Creature:
|
||||||
for p in players:
|
for p in players:
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
@@ -218,7 +221,7 @@ func clone() -> Game:
|
|||||||
for p in players:
|
for p in players:
|
||||||
copies.append(p.clone())
|
copies.append(p.clone())
|
||||||
var idx := players.find(human)
|
var idx := players.find(human)
|
||||||
var g := Game.new(copies[idx], copies.slice(0, idx) + copies.slice(idx + 1), RandomNumberGenerator.new())
|
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
||||||
g.players = copies # gleiche Zugreihenfolge wie das Original
|
g.players = copies # gleiche Zugreihenfolge wie das Original
|
||||||
g.turn = turn
|
g.turn = turn
|
||||||
g.current_idx = current_idx
|
g.current_idx = current_idx
|
||||||
@@ -483,6 +486,26 @@ static func resonance_for(partners: int) -> int:
|
|||||||
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
|
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
## Vorschau fürs Ausspielen: Angriffsbonus aus Resonanz und Feld sowie die Feldkombo,
|
||||||
|
## die die Karte mit dem ersten eigenen Wesen aus anderem Element auslösen würde.
|
||||||
|
func forecast(p: Player, card: Cards.Card) -> Dictionary:
|
||||||
|
var partners := 0
|
||||||
|
var combo := ""
|
||||||
|
var partner: Creature = null
|
||||||
|
for o in p.board:
|
||||||
|
if o.card.elements.any(func(e): return e in card.elements):
|
||||||
|
partners += 1
|
||||||
|
if combo == "":
|
||||||
|
for e1 in card.elements:
|
||||||
|
for e2 in o.card.elements:
|
||||||
|
if e1 != e2 and combo == "":
|
||||||
|
combo = Cards.combo_name([e1, e2])
|
||||||
|
partner = o
|
||||||
|
var resonance := resonance_for(partners) * (2 if "resonanz_doppelt" in p.perks else 1)
|
||||||
|
var field := 1 if field_element != "" and field_element in card.elements else 0
|
||||||
|
return {"resonance": resonance, "field": field, "combo": combo, "partner": partner}
|
||||||
|
|
||||||
|
|
||||||
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
|
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
|
||||||
func update_resonance() -> void:
|
func update_resonance() -> void:
|
||||||
for p in players:
|
for p in players:
|
||||||
@@ -642,20 +665,19 @@ func cleanup() -> void:
|
|||||||
update_resonance()
|
update_resonance()
|
||||||
|
|
||||||
|
|
||||||
## Ende: nur noch ein Spieler übrig, oder kein Mensch mehr am Tisch.
|
## Ende: einer von beiden hat kein Leben mehr.
|
||||||
func check_end() -> void:
|
func check_end() -> void:
|
||||||
if winner != "":
|
if winner != "":
|
||||||
return
|
return
|
||||||
var alive := players.filter(func(p): return p.alive())
|
var alive := players.filter(func(p): return p.alive())
|
||||||
var humans_alive := alive.filter(func(p): return p.is_human)
|
if alive.size() != 1:
|
||||||
if humans_alive.is_empty():
|
return
|
||||||
winner = "opponents"
|
winner_player = alive[0]
|
||||||
winner_player = alive[0] if alive.size() == 1 else null
|
winner = "human" if winner_player.is_human else "opponents"
|
||||||
say("Du wurdest besiegt." if humans.size() == 1 else "Alle Spieler sind besiegt, die KI gewinnt.")
|
if humans.size() == 1:
|
||||||
elif alive.size() == 1:
|
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||||||
winner = "human"
|
else:
|
||||||
winner_player = alive[0]
|
say("%s gewinnt!" % winner_player.name)
|
||||||
say("Alle Gegner sind besiegt!" if humans.size() == 1 else "%s gewinnt!" % winner_player.name)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
||||||
@@ -678,28 +700,23 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
|||||||
arr[j] = tmp
|
arr[j] = tmp
|
||||||
|
|
||||||
|
|
||||||
## rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
||||||
static func new_battle(n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||||
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
|
|
||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
var life: int = int(rules.get("opponent_life", START_LIFE))
|
return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), r, false, rules)
|
||||||
var opps := []
|
|
||||||
for i in n_opp:
|
|
||||||
opps.append(DeckGen.random_opponent(i + 1, life))
|
|
||||||
return new_battle_with(opps, r, false, rules)
|
|
||||||
|
|
||||||
|
|
||||||
static func new_battle_with(opps: Array, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
var human := Player.new("Spieler", true, START_LIFE)
|
var human := Player.new("Spieler", true, START_LIFE)
|
||||||
human.perks = Array(rules.get("human_perks", []))
|
human.perks = Array(rules.get("human_perks", []))
|
||||||
human.looks = rules.get("looks", {})
|
human.looks = rules.get("looks", {})
|
||||||
var g := Game.new(human, opps, r)
|
var g := Game.new(human, opp, r)
|
||||||
for p in g.players:
|
for p in g.players:
|
||||||
if "leben_35" in p.perks and p.life == START_LIFE:
|
if "leben_35" in p.perks:
|
||||||
p.life = 35
|
p.life += 5
|
||||||
p.max_life = 35
|
p.max_life += 5
|
||||||
g.start(fixed_order)
|
g.start(fixed_order)
|
||||||
return g
|
return g
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func host(port: int = PORT) -> Error:
|
func host(port: int = PORT) -> Error:
|
||||||
peer = ENetMultiplayerPeer.new()
|
peer = ENetMultiplayerPeer.new()
|
||||||
var err := peer.create_server(port, Game.MAX_OPPONENTS)
|
var err := peer.create_server(port, 8) # ein Mitspieler, der Rest Zuschauer
|
||||||
if err != OK:
|
if err != OK:
|
||||||
status = "Kann nicht hosten (Port %d belegt?)" % port
|
status = "Kann nicht hosten (Port %d belegt?)" % port
|
||||||
return err
|
return err
|
||||||
@@ -307,10 +307,11 @@ func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
|||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
## Host: Spiel mit allen Verbundenen und n_ai KI-Gegnern starten.
|
## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI.
|
||||||
func start_battle(n_ai: int) -> void:
|
func start_battle() -> void:
|
||||||
var ids: Array = names.keys()
|
var ids: Array = names.keys()
|
||||||
ids.sort()
|
ids.sort()
|
||||||
|
ids = ids.slice(0, 2)
|
||||||
var r := Game.make_rng()
|
var r := Game.make_rng()
|
||||||
var players := []
|
var players := []
|
||||||
seats = {}
|
seats = {}
|
||||||
@@ -321,9 +322,9 @@ func start_battle(n_ai: int) -> void:
|
|||||||
if Cards.has_id(cid):
|
if Cards.has_id(cid):
|
||||||
p.looks[cid] = int(pick.looks[cid])
|
p.looks[cid] = int(pick.looks[cid])
|
||||||
players.append(p)
|
players.append(p)
|
||||||
for i in n_ai:
|
if players.size() < 2:
|
||||||
players.append(DeckGen.random_opponent(i + 1))
|
players.append(DeckGen.random_opponent())
|
||||||
game = Game.new(players[0], players.slice(1), r)
|
game = Game.new(players[0], players[1], r)
|
||||||
game.start()
|
game.start()
|
||||||
for id in ids:
|
for id in ids:
|
||||||
seats[id] = game.players.find(players[ids.find(id)])
|
seats[id] = game.players.find(players[ids.find(id)])
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
p.board.append(c)
|
p.board.append(c)
|
||||||
list.append(p)
|
list.append(p)
|
||||||
var hi := int(d.human)
|
var hi := int(d.human)
|
||||||
var g := Game.new(list[hi], list.slice(0, hi) + list.slice(hi + 1), RandomNumberGenerator.new())
|
var g := Game.new(list[hi], list[1 - hi], RandomNumberGenerator.new())
|
||||||
g.players = list
|
g.players = list
|
||||||
g.turn = int(d.turn)
|
g.turn = int(d.turn)
|
||||||
g.current_idx = int(d.current_idx)
|
g.current_idx = int(d.current_idx)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const BOOSTER_COST := 100
|
|||||||
const BOOSTER_SIZE := 5
|
const BOOSTER_SIZE := 5
|
||||||
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
||||||
const START_COINS := 150
|
const START_COINS := 150
|
||||||
const REWARD_WIN := {1: 100, 2: 150, 3: 200, 4: 250, 5: 300}
|
const REWARD_WIN := 100
|
||||||
const REWARD_LOSS := 50
|
const REWARD_LOSS := 50
|
||||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||||
const DIFFICULTIES := {
|
const DIFFICULTIES := {
|
||||||
@@ -190,10 +190,10 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
|||||||
|
|
||||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||||
func record_battle(won: bool, n_opp: int, stage: int = -1, reward_factor: float = 1.0) -> int:
|
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
|
||||||
var reward := REWARD_LOSS
|
var reward := REWARD_LOSS
|
||||||
if won:
|
if won:
|
||||||
reward = int(round(REWARD_WIN[n_opp] * reward_factor))
|
reward = int(round(REWARD_WIN * reward_factor))
|
||||||
if stage >= 0 and stage == campaign_stage:
|
if stage >= 0 and stage == campaign_stage:
|
||||||
campaign_stage += 1
|
campaign_stage += 1
|
||||||
reward = Campaign.stage(stage).reward
|
reward = Campaign.stage(stage).reward
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ extends RefCounted
|
|||||||
|
|
||||||
var screen # BattleScreen
|
var screen # BattleScreen
|
||||||
var enabled := true
|
var enabled := true
|
||||||
|
var banners: Array = [] # wartende Ansagen [Text, Farbe]
|
||||||
|
var banner_running := false
|
||||||
|
|
||||||
|
|
||||||
func _init(s) -> void:
|
func _init(s) -> void:
|
||||||
@@ -25,6 +27,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
|||||||
var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
|
var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
|
||||||
lunge(atk, center_of(ev.target, centers))
|
lunge(atk, center_of(ev.target, centers))
|
||||||
flash_target(ev.target)
|
flash_target(ev.target)
|
||||||
|
shake()
|
||||||
delay = 0.12
|
delay = 0.12
|
||||||
"damage":
|
"damage":
|
||||||
sfx("damage")
|
sfx("damage")
|
||||||
@@ -43,7 +46,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
|||||||
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
|
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
|
||||||
if where.x < 0:
|
if where.x < 0:
|
||||||
where = screen.target_center({"kind": "player", "index": ev.player})
|
where = screen.target_center({"kind": "player", "index": ev.player})
|
||||||
popup("Feldkombo %s!" % ev.name, Style.GOLD, where + Vector2(0, -30), 0.1)
|
popup("%s!" % tr(ev.name), Style.GOLD, where + Vector2(0, -30), 0.1)
|
||||||
"play":
|
"play":
|
||||||
sfx("play")
|
sfx("play")
|
||||||
"fuse":
|
"fuse":
|
||||||
@@ -51,7 +54,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
|||||||
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||||
"field":
|
"field":
|
||||||
sfx("combo")
|
sfx("combo")
|
||||||
popup("Feld: %s" % Cards.FIELD_NAMES[ev.element], Cards.ELEMENT_COLORS[ev.element], Vector2(640, 196), 0.0)
|
banner("%s: %s" % [tr(Cards.FIELD_NAMES[ev.element]), tr(Cards.FIELD_TEXT[ev.element])], Cards.ELEMENT_COLORS[ev.element])
|
||||||
"buy":
|
"buy":
|
||||||
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
||||||
"reroll":
|
"reroll":
|
||||||
@@ -165,3 +168,108 @@ func move_to(tile: CreatureView, pos: Vector2) -> void:
|
|||||||
screen.create_tween().tween_property(tile, "position", pos, 0.2)
|
screen.create_tween().tween_property(tile, "position", pos, 0.2)
|
||||||
else:
|
else:
|
||||||
tile.position = pos
|
tile.position = pos
|
||||||
|
|
||||||
|
|
||||||
|
## Ein Wesen fliegt von der Handkachel auf seinen Platz.
|
||||||
|
func fly_in(tile: CreatureView, from: Rect2) -> void:
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
tile.busy = true
|
||||||
|
tile.position = from.position
|
||||||
|
tile.scale = from.size / tile.size
|
||||||
|
var tw := tile.create_tween().set_parallel(true)
|
||||||
|
tw.tween_property(tile, "position", tile.slot, 0.22).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
tw.tween_property(tile, "scale", Vector2.ONE, 0.22).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
tw.chain().tween_callback(func():
|
||||||
|
tile.busy = false
|
||||||
|
tile.position = tile.slot)
|
||||||
|
|
||||||
|
|
||||||
|
## Eine gekaufte Karte fliegt aus dem Laden auf die Hand.
|
||||||
|
func fly_card(card: Cards.Card, from: Rect2, to: Rect2, rarity: String) -> void:
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
var v := CardView.new(card, from.size, false, rarity)
|
||||||
|
v.position = from.position
|
||||||
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
v.pivot_offset = Vector2.ZERO
|
||||||
|
screen.fx_layer.add_child(v)
|
||||||
|
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||||
|
tw.tween_property(v, "position", to.position, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||||
|
tw.tween_property(v, "scale", to.size / from.size, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||||
|
tw.tween_property(v, "modulate:a", 0.0, 0.1).set_delay(0.2)
|
||||||
|
tw.chain().tween_callback(v.queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
## Ladenkarten werden nacheinander ausgeteilt.
|
||||||
|
func deal(v: CardView, i: int) -> void:
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
v.modulate.a = 0.0
|
||||||
|
v.position.y += 24
|
||||||
|
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||||
|
tw.tween_property(v, "modulate:a", 1.0, 0.15).set_delay(i * 0.05)
|
||||||
|
tw.tween_property(v, "position:y", v.position.y - 24, 0.2).set_delay(i * 0.05).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
|
||||||
|
## Kurzes Zittern des Bretts beim Treffer.
|
||||||
|
func shake() -> void:
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
var layer: Control = screen.tile_layer
|
||||||
|
var tw: Tween = screen.create_tween()
|
||||||
|
for i in 3:
|
||||||
|
tw.tween_property(layer, "position", Vector2(3 if i % 2 == 0 else -3, 0), 0.03)
|
||||||
|
tw.tween_property(layer, "position", Vector2.ZERO, 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
## Ansage quer über die Mitte: Zugbeginn, Feldwechsel. Mehrere folgen nacheinander.
|
||||||
|
func banner(text: String, color: Color) -> void:
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
banners.append([text, color])
|
||||||
|
if not banner_running:
|
||||||
|
_next_banner()
|
||||||
|
|
||||||
|
|
||||||
|
func _next_banner() -> void:
|
||||||
|
if banners.is_empty() or not is_instance_valid(screen) or not screen.is_inside_tree():
|
||||||
|
banner_running = false
|
||||||
|
return
|
||||||
|
banner_running = true
|
||||||
|
var next: Array = banners.pop_front()
|
||||||
|
var text: String = next[0]
|
||||||
|
var color: Color = next[1]
|
||||||
|
var delay := 0.0
|
||||||
|
var band := ColorRect.new()
|
||||||
|
band.color = Color(0, 0, 0, 0.55)
|
||||||
|
band.position = Vector2(0, 300)
|
||||||
|
band.size = Vector2(1280, 90)
|
||||||
|
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
band.scale.y = 0.0
|
||||||
|
band.pivot_offset = Vector2(640, 45)
|
||||||
|
screen.fx_layer.add_child(band)
|
||||||
|
var l := Label.new()
|
||||||
|
l.text = text
|
||||||
|
l.add_theme_font_size_override("font_size", 40)
|
||||||
|
l.add_theme_font_override("font", Style.FONT_BOLD)
|
||||||
|
l.add_theme_color_override("font_color", color)
|
||||||
|
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||||
|
l.add_theme_constant_override("outline_size", 8)
|
||||||
|
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
screen.fx_layer.add_child(l)
|
||||||
|
l.reset_size()
|
||||||
|
l.position = Vector2(640, 345) - l.size / 2
|
||||||
|
l.pivot_offset = l.size / 2
|
||||||
|
l.scale = Vector2(0.6, 0.6)
|
||||||
|
l.modulate.a = 0.0
|
||||||
|
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||||
|
tw.tween_property(band, "scale:y", 1.0, 0.15).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
tw.tween_property(l, "scale", Vector2.ONE, 0.25).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
tw.tween_property(l, "modulate:a", 1.0, 0.12).set_delay(delay)
|
||||||
|
tw.tween_property(l, "modulate:a", 0.0, 0.3).set_delay(delay + 0.8)
|
||||||
|
tw.tween_property(band, "scale:y", 0.0, 0.2).set_delay(delay + 0.9)
|
||||||
|
tw.chain().tween_callback(func():
|
||||||
|
l.queue_free()
|
||||||
|
band.queue_free()
|
||||||
|
_next_banner())
|
||||||
|
|||||||
@@ -1,42 +1,71 @@
|
|||||||
class_name BattleLayout
|
class_name BattleLayout
|
||||||
## Feste Bildschirmaufteilung des Kampfes (1280x800).
|
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Laden mit Neuwürfeln
|
||||||
|
## und Zug beenden, Hand, eigenes Brett (mit eigener Leiste links), Feldband, oben der Gegner
|
||||||
|
## mit seinem Brett.
|
||||||
|
|
||||||
const SIZES := {1: [110, 110, 10], 2: [96, 110, 8], 3: [78, 100, 4], 4: [54, 72, 4], 5: [44, 62, 3]}
|
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||||
const HAND_W := 130
|
const B_LOG := Rect2(112, 10, 96, 32)
|
||||||
const HAND_H := 185
|
const OPP_BOX := Rect2(330, 10, 620, 56)
|
||||||
const HAND_GAP := 8
|
const FIELD := Rect2(240, 226, 800, 32)
|
||||||
const BAR := Rect2(335, 415, 610, 68)
|
const MSG := Rect2(240, 262, 800, 22)
|
||||||
const OWN_X := (1280 - 5 * 110 - 4 * 10) / 2
|
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
||||||
|
const STATUS := Rect2(20, 288, 210, 160)
|
||||||
|
const B_FUSE := Rect2(1050, 298, 210, 40)
|
||||||
static func compact(n_others: int) -> bool:
|
const B_UNDO := Rect2(1050, 346, 210, 40)
|
||||||
return n_others >= 4
|
const OWN_X := 345
|
||||||
|
const HAND_W := 96
|
||||||
|
const HAND_H := 62
|
||||||
static func hero_box_rect(n_others: int, i: int) -> Rect2:
|
const HAND_GAP := 4
|
||||||
var col_w := 1280 / n_others
|
const HAND_Y := 458
|
||||||
var margin := 6 if compact(n_others) else 10
|
const SHOP_Y := 532
|
||||||
return Rect2(i * col_w + margin, 10, col_w - 2 * margin, 64)
|
const SHOP_W := 150
|
||||||
|
const SHOP_H := 205
|
||||||
|
const B_REROLL := Rect2(20, 606, 200, 52)
|
||||||
|
const B_END := Rect2(1060, 600, 200, 60)
|
||||||
|
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||||
|
const PREVIEW_SIZE := Vector2(180, 252)
|
||||||
|
const CENTER_X := 240
|
||||||
|
const CENTER_W := 800
|
||||||
|
|
||||||
|
|
||||||
static func own_slot(j: int) -> Rect2:
|
static func own_slot(j: int) -> Rect2:
|
||||||
return Rect2(OWN_X + j * 120, 250, 110, 140)
|
return Rect2(OWN_X + j * 120, 298, 110, 140)
|
||||||
|
|
||||||
|
|
||||||
static func other_slot(n_others: int, i: int, j: int) -> Rect2:
|
## Brett des Gegners unter seinem Kasten.
|
||||||
var col_w := 1280 / n_others
|
static func opp_slot(j: int) -> Rect2:
|
||||||
var sz: Array = SIZES[n_others]
|
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||||
var bx: int = i * col_w + (col_w - 5 * sz[0] - 4 * sz[2]) / 2
|
|
||||||
return Rect2(bx + j * (sz[0] + sz[2]), 84, sz[0], sz[1])
|
|
||||||
|
|
||||||
|
|
||||||
## Laden: fünf Angebote über der Hand.
|
## Laden: fünf große Angebote unten, wie in TFT.
|
||||||
static func shop_rect(i: int) -> Rect2:
|
static func shop_rect(i: int) -> Rect2:
|
||||||
return Rect2(340 + i * 121, 487, 115, 112)
|
return Rect2(245 + i * (SHOP_W + 10), SHOP_Y, SHOP_W, SHOP_H)
|
||||||
|
|
||||||
|
|
||||||
|
## Hand: flache Kacheln zwischen Brett und Laden (die Bank).
|
||||||
static func hand_rect(count: int, i: int) -> Rect2:
|
static func hand_rect(count: int, i: int) -> Rect2:
|
||||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||||
var x := (1280 - total) / 2
|
var x := CENTER_X + (CENTER_W - total) / 2
|
||||||
return Rect2(x + i * (HAND_W + HAND_GAP), 800 - HAND_H - 8, HAND_W, HAND_H)
|
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||||
|
|
||||||
|
|
||||||
|
## Schwebende Vorschau neben der gezeigten Karte: über Hand- und Ladenkarten, sonst rechts
|
||||||
|
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
|
||||||
|
static func preview_rect(anchor: Rect2) -> Rect2:
|
||||||
|
if anchor.position.y >= HAND_Y - 10:
|
||||||
|
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x)
|
||||||
|
return Rect2(Vector2(x, anchor.position.y - 12 - PREVIEW_SIZE.y), PREVIEW_SIZE)
|
||||||
|
var x := anchor.end.x + 12
|
||||||
|
if x + PREVIEW_SIZE.x > 1270:
|
||||||
|
x = anchor.position.x - 12 - PREVIEW_SIZE.x
|
||||||
|
var y := clampf(anchor.get_center().y - PREVIEW_SIZE.y / 2, 10, 800 - PREVIEW_SIZE.y - 10)
|
||||||
|
return Rect2(Vector2(x, y), PREVIEW_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
## Zusatzzeile (Resonanz, Feld, Feldkombo) neben der Vorschau, rechts davon oder links.
|
||||||
|
static func info_rect(preview: Rect2) -> Rect2:
|
||||||
|
var w := 230.0
|
||||||
|
var x := preview.end.x + 8
|
||||||
|
if x + w > 1275:
|
||||||
|
x = preview.position.x - 8 - w
|
||||||
|
return Rect2(x, preview.position.y + preview.size.y / 2 - 40, w, 80)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ extends Control
|
|||||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var n_opp: int
|
|
||||||
var stage := -1 # Kampagnenstufe oder -1
|
var stage := -1 # Kampagnenstufe oder -1
|
||||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||||
@@ -24,8 +23,6 @@ var ai_copy: Game # Spielkopie, auf der die KI rechnet
|
|||||||
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
|
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
|
||||||
var reward := -1
|
var reward := -1
|
||||||
var ended := false
|
var ended := false
|
||||||
var zoom_player = null
|
|
||||||
var preview_timer := 0.0
|
|
||||||
var pad: BattleGamepad
|
var pad: BattleGamepad
|
||||||
var gp_zone: String:
|
var gp_zone: String:
|
||||||
get:
|
get:
|
||||||
@@ -36,7 +33,6 @@ var gp_index: int:
|
|||||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||||
var saved_game: Game = null # fortgesetzter Kampf (siehe resume)
|
|
||||||
var undo_log_len := 0
|
var undo_log_len := 0
|
||||||
var undo_t := 0.0
|
var undo_t := 0.0
|
||||||
|
|
||||||
@@ -54,11 +50,12 @@ var hero_layer: Control
|
|||||||
var tile_layer: Control
|
var tile_layer: Control
|
||||||
var hand_layer: Control
|
var hand_layer: Control
|
||||||
var shop_layer: Control
|
var shop_layer: Control
|
||||||
var zoom_layer: Control
|
var log_overlay: Control
|
||||||
|
var b_log: Button
|
||||||
|
var status_panel: Panel
|
||||||
var fx_layer: Control
|
var fx_layer: Control
|
||||||
var log_box: RichTextLabel
|
var log_box: RichTextLabel
|
||||||
var preview_holder: Control
|
var preview_holder: Control
|
||||||
var bar_panel: Panel
|
|
||||||
var name_label: Label
|
var name_label: Label
|
||||||
var life_label: Label
|
var life_label: Label
|
||||||
var energy_label: Label
|
var energy_label: Label
|
||||||
@@ -69,38 +66,35 @@ var b_end: Button
|
|||||||
var b_fuse: Button
|
var b_fuse: Button
|
||||||
var b_reroll: Button
|
var b_reroll: Button
|
||||||
var b_giveup: Button
|
var b_giveup: Button
|
||||||
var field_label: Label
|
var field_view: FieldView
|
||||||
|
var link_view: Links # Resonanzlinien unter den Feldreihen
|
||||||
|
var info_label: Label # Vorschau: Resonanz, Feld, Feldkombo
|
||||||
|
var board_panel: Panel # eigenes Brett, färbt sich mit dem Feld
|
||||||
|
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||||
|
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||||
|
var deal_shop := false # nächster Aufbau teilt den Laden animiert aus
|
||||||
var overlay: Control
|
var overlay: Control
|
||||||
var result_label: Label
|
var result_label: Label
|
||||||
var reward_label: Label
|
var reward_label: Label
|
||||||
var notice_label: Label
|
var notice_label: Label
|
||||||
var confirm: Control
|
var confirm: Control
|
||||||
var b_save_leave: Button
|
|
||||||
var cursor_layer: Control
|
var cursor_layer: Control
|
||||||
var wait_label: Label
|
var wait_label: Label
|
||||||
var b_skip: Button
|
var b_skip: Button
|
||||||
var b_undo: Button
|
var b_undo: Button
|
||||||
|
|
||||||
|
|
||||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
func _init(campaign_stage: int = -1) -> void:
|
||||||
n_opp = n
|
|
||||||
stage = campaign_stage
|
stage = campaign_stage
|
||||||
|
|
||||||
|
|
||||||
static func online(seat: int) -> BattleScreen:
|
static func online(seat: int) -> BattleScreen:
|
||||||
var b := BattleScreen.new(0, -1)
|
var b := BattleScreen.new(-1)
|
||||||
b.net_play = true
|
b.net_play = true
|
||||||
b.online_seat = seat
|
b.online_seat = seat
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
|
||||||
static func resume(data: Dictionary) -> BattleScreen:
|
|
||||||
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
|
|
||||||
s.saved_game = Game.from_snapshot(data.game)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
func is_online() -> bool:
|
func is_online() -> bool:
|
||||||
return net_play
|
return net_play
|
||||||
|
|
||||||
@@ -137,13 +131,11 @@ func _ready() -> void:
|
|||||||
net.snapshot_received.connect(_on_snapshot)
|
net.snapshot_received.connect(_on_snapshot)
|
||||||
wait_label.visible = true
|
wait_label.visible = true
|
||||||
return # der erste Zustand kommt vom Host
|
return # der erste Zustand kommt vom Host
|
||||||
elif saved_game != null:
|
|
||||||
game = saved_game
|
|
||||||
elif stage >= 0:
|
elif stage >= 0:
|
||||||
var r := Game.make_rng()
|
var r := Game.make_rng()
|
||||||
game = Game.new_battle_with(Campaign.opponents(stage), r, false, own_rules())
|
game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
|
||||||
else:
|
else:
|
||||||
game = Game.new_battle(n_opp, Game.make_rng(), own_rules())
|
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||||
_begin()
|
_begin()
|
||||||
|
|
||||||
@@ -160,7 +152,6 @@ func own_rules() -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func _begin() -> void:
|
func _begin() -> void:
|
||||||
n_opp = game.players.size() - 1
|
|
||||||
if is_online():
|
if is_online():
|
||||||
perspective = game.players[maxi(online_seat, 0)]
|
perspective = game.players[maxi(online_seat, 0)]
|
||||||
else:
|
else:
|
||||||
@@ -170,67 +161,94 @@ func _begin() -> void:
|
|||||||
if is_online() and net.is_host:
|
if is_online() and net.is_host:
|
||||||
net.broadcast()
|
net.broadcast()
|
||||||
game.events.clear()
|
game.events.clear()
|
||||||
b_save_leave.visible = resumable()
|
|
||||||
save_battle()
|
|
||||||
refresh()
|
refresh()
|
||||||
|
announce_turn()
|
||||||
|
|
||||||
|
|
||||||
func _build_static() -> void:
|
func _build_static() -> void:
|
||||||
Style.panel(self, Rect2(10, 205, 320, 290))
|
var L := BattleLayout
|
||||||
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20))
|
# oben links: Aufgeben und Verlauf, klein
|
||||||
log_box = RichTextLabel.new()
|
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
|
||||||
log_box.position = Vector2(18, 230)
|
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||||
log_box.size = Vector2(304, 258)
|
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||||
log_box.bbcode_enabled = true
|
b_log.add_theme_font_size_override("font_size", 13)
|
||||||
log_box.scroll_following = true
|
# Mitte: Feldband, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||||
log_box.add_theme_font_size_override("normal_font_size", 13)
|
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
field_view = FieldView.new(L.FIELD)
|
||||||
add_child(log_box)
|
add_child(field_view)
|
||||||
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
||||||
Style.panel(self, Rect2(950, 205, 320, 380))
|
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
||||||
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10)
|
var slots := Control.new()
|
||||||
bar_panel = Style.panel(self, BattleLayout.BAR)
|
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
name_label = Style.label(self, "", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
slots.draw.connect(func():
|
||||||
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true)
|
for j in Game.MAX_BOARD:
|
||||||
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true)
|
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
|
||||||
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 24), "right")
|
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||||||
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
add_child(slots)
|
||||||
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
status_panel = Style.panel(self, L.STATUS)
|
||||||
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
var sx: float = L.STATUS.position.x
|
||||||
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
|
var sy: float = L.STATUS.position.y
|
||||||
|
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true)
|
||||||
|
name_label.clip_text = true
|
||||||
|
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true)
|
||||||
|
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
|
||||||
|
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left")
|
||||||
|
status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
|
||||||
|
b_fuse = Style.button(self, "Verschmelzen", L.B_FUSE, toggle_fuse, Color("3c2858"))
|
||||||
|
b_fuse.add_theme_font_size_override("font_size", 15)
|
||||||
|
b_fuse.visible = false
|
||||||
|
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||||
|
b_undo.add_theme_font_size_override("font_size", 15)
|
||||||
|
b_undo.visible = false
|
||||||
|
# unten: Laden mit Neuwürfeln links und Zug beenden rechts
|
||||||
|
b_reroll = Style.button(self, "", L.B_REROLL, do_reroll, Color("285a5a"))
|
||||||
|
b_reroll.add_theme_font_size_override("font_size", 15)
|
||||||
|
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||||
|
b_end.add_theme_font_size_override("font_size", 20)
|
||||||
|
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
||||||
b_skip.add_theme_font_size_override("font_size", 14)
|
b_skip.add_theme_font_size_override("font_size", 14)
|
||||||
b_skip.visible = false
|
b_skip.visible = false
|
||||||
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
|
|
||||||
b_undo.visible = false
|
|
||||||
field_label = Style.label(self, "", 15, Style.MUTED, Rect2(340, 211, 600, 24), "center")
|
|
||||||
b_fuse = Style.button(self, "Verschmelzen", Rect2(10, 503, 150, 40), toggle_fuse, Color("3c2858"))
|
|
||||||
b_fuse.visible = false
|
|
||||||
b_reroll = Style.button(self, "", Rect2(170, 503, 150, 40), do_reroll, Color("285a5a"))
|
|
||||||
b_reroll.add_theme_font_size_override("font_size", 14)
|
|
||||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||||
wait_label.visible = false
|
wait_label.visible = false
|
||||||
hero_layer = _layer()
|
hero_layer = _layer()
|
||||||
|
link_view = Links.new()
|
||||||
|
add_child(link_view)
|
||||||
tile_layer = _layer()
|
tile_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
shop_layer = _layer()
|
shop_layer = _layer()
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
zoom_layer = _layer()
|
info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
|
||||||
|
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
info_label.visible = false
|
||||||
fx_layer = _layer()
|
fx_layer = _layer()
|
||||||
cursor_layer = _layer()
|
cursor_layer = _layer()
|
||||||
|
# Verlauf als Überlagerung
|
||||||
|
log_overlay = Style.overlay(self)
|
||||||
|
log_overlay.get_child(0).gui_input.connect(func(ev):
|
||||||
|
if ev is InputEventMouseButton and ev.pressed:
|
||||||
|
log_overlay.visible = false)
|
||||||
|
Style.panel(log_overlay, L.LOG_PANEL, Style.PANEL, 10, Style.MUTED, 2)
|
||||||
|
Style.label(log_overlay, "Verlauf", 20, Style.GOLD, Rect2(L.LOG_PANEL.position + Vector2(20, 12), Vector2(300, 30)), "left", true)
|
||||||
|
log_box = RichTextLabel.new()
|
||||||
|
log_box.position = L.LOG_PANEL.position + Vector2(20, 50)
|
||||||
|
log_box.size = L.LOG_PANEL.size - Vector2(40, 110)
|
||||||
|
log_box.bbcode_enabled = true
|
||||||
|
log_box.scroll_following = true
|
||||||
|
log_box.add_theme_font_size_override("normal_font_size", 14)
|
||||||
|
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
log_overlay.add_child(log_box)
|
||||||
|
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
|
||||||
# Spielende
|
# Spielende
|
||||||
overlay = _overlay()
|
overlay = _overlay()
|
||||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||||
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
||||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
||||||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||||||
# Aufgeben bestätigen
|
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||||
confirm = Style.overlay(self)
|
|
||||||
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
|
|
||||||
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
func toggle_log() -> void:
|
||||||
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
|
log_overlay.visible = not log_overlay.visible
|
||||||
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
|
|
||||||
b_save_leave.add_theme_font_size_override("font_size", 14)
|
|
||||||
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
|
||||||
|
|
||||||
|
|
||||||
func _layer() -> Control:
|
func _layer() -> Control:
|
||||||
@@ -244,54 +262,27 @@ func _overlay() -> Control:
|
|||||||
return Style.overlay(self)
|
return Style.overlay(self)
|
||||||
|
|
||||||
|
|
||||||
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
|
|
||||||
func resumable() -> bool:
|
|
||||||
return counts_for_profile()
|
|
||||||
|
|
||||||
|
|
||||||
func save_battle() -> void:
|
|
||||||
if not resumable() or game == null or game.winner != "":
|
|
||||||
return
|
|
||||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "game": game.snapshot(-1)}
|
|
||||||
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
|
||||||
if f != null:
|
|
||||||
f.store_string(JSON.stringify(d))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
func save_and_leave() -> void:
|
|
||||||
save_battle()
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|
||||||
|
|
||||||
func leave() -> void:
|
func leave() -> void:
|
||||||
if is_online():
|
if is_online():
|
||||||
net.leave()
|
net.leave()
|
||||||
if resumable() and game != null and game.winner == "":
|
|
||||||
app.clear_battle_save() # aufgegeben
|
|
||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
# --- Layout-Helfer ------------------------------------------------------------------
|
# --- Layout-Helfer ------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Der Gegner aus Sicht der eigenen Perspektive (Zuschauer: der zweite Spieler).
|
||||||
|
func opponent() -> Game.Player:
|
||||||
|
return game.enemy_of(perspective)
|
||||||
|
|
||||||
|
|
||||||
func others() -> Array:
|
func others() -> Array:
|
||||||
var out := []
|
return [opponent()]
|
||||||
var n := game.players.size()
|
|
||||||
var start := game.players.find(perspective)
|
|
||||||
for i in range(1, n):
|
|
||||||
out.append(game.players[(start + i) % n])
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
func compact() -> bool:
|
|
||||||
return BattleLayout.compact(others().size())
|
|
||||||
|
|
||||||
|
|
||||||
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
||||||
if p == perspective:
|
if p == perspective:
|
||||||
return BattleLayout.own_slot(j)
|
return BattleLayout.own_slot(j)
|
||||||
var os := others()
|
return BattleLayout.opp_slot(j)
|
||||||
return BattleLayout.other_slot(os.size(), os.find(p), j)
|
|
||||||
|
|
||||||
|
|
||||||
func ref_key(ref: Dictionary) -> String:
|
func ref_key(ref: Dictionary) -> String:
|
||||||
@@ -306,47 +297,37 @@ func target_center(ref: Dictionary) -> Vector2:
|
|||||||
return Vector2(-1, -1)
|
return Vector2(-1, -1)
|
||||||
var p: Game.Player = game.players[ref.index]
|
var p: Game.Player = game.players[ref.index]
|
||||||
if p == perspective:
|
if p == perspective:
|
||||||
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
|
return BattleLayout.STATUS.get_center()
|
||||||
if hero_panels.has(p):
|
if hero_panels.has(p):
|
||||||
var panel: Panel = hero_panels[p]
|
var panel: Panel = hero_panels[p]
|
||||||
return panel.position + panel.size / 2
|
return panel.position + panel.size / 2
|
||||||
return Vector2(-1, -1)
|
return Vector2(-1, -1)
|
||||||
|
|
||||||
|
|
||||||
|
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
|
||||||
func build_hero_boxes() -> void:
|
func build_hero_boxes() -> void:
|
||||||
Style.clear(hero_layer)
|
Style.clear(hero_layer)
|
||||||
hero_panels = {}
|
hero_panels = {}
|
||||||
hero_labels = {}
|
hero_labels = {}
|
||||||
var os := others()
|
var o := opponent()
|
||||||
var c := compact()
|
var box := BattleLayout.OPP_BOX
|
||||||
for i in os.size():
|
var panel := Style.panel(hero_layer, box)
|
||||||
var o: Game.Player = os[i]
|
panel.gui_input.connect(func(ev):
|
||||||
var box := BattleLayout.hero_box_rect(os.size(), i)
|
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||||
var panel := Style.panel(hero_layer, box)
|
on_click("hero", o))
|
||||||
panel.gui_input.connect(func(ev):
|
if not o.perks.is_empty():
|
||||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
||||||
on_click("hero", o))
|
hero_panels[o] = panel
|
||||||
if not o.perks.is_empty():
|
var labels := {}
|
||||||
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||||
hero_panels[o] = panel
|
if not o.perks.is_empty():
|
||||||
var labels := {}
|
title += " ★"
|
||||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "left", true)
|
||||||
if not o.perks.is_empty():
|
labels.name.clip_text = true
|
||||||
title += " ★"
|
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true)
|
||||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (58 if c else 20), 26), "left", true)
|
labels["stats"] = Style.label(hero_layer, "", 14, Style.MUTED, Rect2(box.position.x + 80, box.position.y + 34, box.size.x - 88, 20), "right")
|
||||||
labels.name.clip_text = true
|
hero_labels[o] = labels
|
||||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
|
||||||
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
|
||||||
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
|
||||||
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
|
||||||
Rect2(box.position.x + 90, box.position.y + 38, box.size.x - 100, 20), "right")
|
|
||||||
hero_labels[o] = labels
|
|
||||||
if c:
|
|
||||||
var b := Style.button(hero_layer, "Lupe", Rect2(box.end.x - 44, box.position.y + 5, 38, 22), func():
|
|
||||||
zoom_player = o
|
|
||||||
refresh())
|
|
||||||
b.add_theme_font_size_override("font_size", 11)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||||
@@ -358,7 +339,7 @@ func my_turn() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func any_overlay() -> bool:
|
func any_overlay() -> bool:
|
||||||
return confirm.visible or overlay.visible
|
return confirm.visible or overlay.visible or log_overlay.visible
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
@@ -376,7 +357,9 @@ func _input(event: InputEvent) -> void:
|
|||||||
KEY_ESCAPE:
|
KEY_ESCAPE:
|
||||||
if confirm.visible:
|
if confirm.visible:
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
elif selected != null or pending != null or fusing or zoom_player != null:
|
elif log_overlay.visible:
|
||||||
|
log_overlay.visible = false
|
||||||
|
elif selected != null or pending != null or fusing:
|
||||||
cancel_all()
|
cancel_all()
|
||||||
elif not any_overlay():
|
elif not any_overlay():
|
||||||
confirm.visible = true
|
confirm.visible = true
|
||||||
@@ -402,7 +385,6 @@ func cancel_all() -> void:
|
|||||||
if confirm.visible:
|
if confirm.visible:
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
cancel()
|
cancel()
|
||||||
zoom_player = null
|
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -412,10 +394,6 @@ func _process(delta: float) -> void:
|
|||||||
msg_t = maxf(0.0, msg_t - delta)
|
msg_t = maxf(0.0, msg_t - delta)
|
||||||
if msg_t <= 0.0 and msg_label.text != "":
|
if msg_t <= 0.0 and msg_label.text != "":
|
||||||
msg_label.text = ""
|
msg_label.text = ""
|
||||||
if preview_timer > 0.0:
|
|
||||||
preview_timer -= delta
|
|
||||||
if preview_timer <= 0.0:
|
|
||||||
show_preview(null, false)
|
|
||||||
if undo_t > 0.0:
|
if undo_t > 0.0:
|
||||||
undo_t -= delta
|
undo_t -= delta
|
||||||
if undo_t <= 0.0:
|
if undo_t <= 0.0:
|
||||||
@@ -477,10 +455,9 @@ func _finish() -> void:
|
|||||||
var won := game.winner == "human"
|
var won := game.winner == "human"
|
||||||
if counts_for_profile():
|
if counts_for_profile():
|
||||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||||
reward = app.profile.record_battle(won, n_opp, stage, factor)
|
reward = app.profile.record_battle(won, stage, factor)
|
||||||
app.save()
|
app.save()
|
||||||
app.sfx.play("win" if won else "lose")
|
app.sfx.play("win" if won else "lose")
|
||||||
app.clear_battle_save()
|
|
||||||
else:
|
else:
|
||||||
var wp := game.winner_player
|
var wp := game.winner_player
|
||||||
var mine: bool = wp != null and wp == perspective
|
var mine: bool = wp != null and wp == perspective
|
||||||
@@ -506,10 +483,13 @@ func play(card: Cards.Card, target) -> void:
|
|||||||
return
|
return
|
||||||
var before: Game = game.clone() if not is_online() else null
|
var before: Game = game.clone() if not is_online() else null
|
||||||
var log_len := game.log.size()
|
var log_len := game.log.size()
|
||||||
|
var from := _hand_rect_of(card)
|
||||||
var err := game.play_card(perspective, card, target)
|
var err := game.play_card(perspective, card, target)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
return
|
return
|
||||||
|
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
|
||||||
|
arrivals[perspective.board[-1].uid] = from
|
||||||
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
|
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
|
||||||
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||||
undo_state = before
|
undo_state = before
|
||||||
@@ -534,9 +514,15 @@ func do_buy(slot: int) -> void:
|
|||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "buy", "slot": slot})
|
net.send_action({"type": "buy", "slot": slot})
|
||||||
return
|
return
|
||||||
|
var from := BattleLayout.shop_rect(slot)
|
||||||
|
var offer = perspective.shop[slot] if slot < perspective.shop.size() else null
|
||||||
var err := game.buy(perspective, slot)
|
var err := game.buy(perspective, slot)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
|
elif offer != null:
|
||||||
|
fx.sfx("buy")
|
||||||
|
fx.fly_card(offer, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
|
||||||
|
Cards.RARITIES[int(perspective.looks.get(offer.id, 0))])
|
||||||
|
|
||||||
|
|
||||||
## Würfelt den Laden neu (kostet Mana).
|
## Würfelt den Laden neu (kostet Mana).
|
||||||
@@ -551,6 +537,8 @@ func do_reroll() -> void:
|
|||||||
var err := game.reroll(perspective)
|
var err := game.reroll(perspective)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
|
else:
|
||||||
|
deal_shop = true
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
@@ -596,7 +584,6 @@ func end_turn() -> void:
|
|||||||
return
|
return
|
||||||
cancel()
|
cancel()
|
||||||
clear_undo()
|
clear_undo()
|
||||||
zoom_player = null
|
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "end"})
|
net.send_action({"type": "end"})
|
||||||
return
|
return
|
||||||
@@ -629,7 +616,6 @@ func on_click(kind: String, obj) -> void:
|
|||||||
play(card, obj)
|
play(card, obj)
|
||||||
else:
|
else:
|
||||||
say(tr("Abgebrochen"))
|
say(tr("Abgebrochen"))
|
||||||
zoom_player = null
|
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
if selected != null:
|
if selected != null:
|
||||||
@@ -643,7 +629,6 @@ func on_click(kind: String, obj) -> void:
|
|||||||
var err := g.attack(cr, obj)
|
var err := g.attack(cr, obj)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
zoom_player = null
|
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
match kind:
|
match kind:
|
||||||
@@ -677,17 +662,121 @@ func on_tile_click(cr: Game.Creature) -> void:
|
|||||||
on_click("enemy", cr)
|
on_click("enemy", cr)
|
||||||
|
|
||||||
|
|
||||||
func show_preview(card, entered: bool, owner_looks: Dictionary = {}) -> void:
|
## Zugansage, wenn der eigene Zug beginnt.
|
||||||
|
func announce_turn() -> void:
|
||||||
|
if game == null or not my_turn() or banner_turn == game.turn:
|
||||||
|
return
|
||||||
|
banner_turn = game.turn
|
||||||
|
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||||||
|
fx.sfx("turn")
|
||||||
|
deal_shop = true
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _hand_rect_of(card: Cards.Card) -> Rect2:
|
||||||
|
for v in hand_views:
|
||||||
|
if v.card == card:
|
||||||
|
return Rect2(v.position, v.size)
|
||||||
|
return Rect2()
|
||||||
|
|
||||||
|
|
||||||
|
## Vorschau schwebt neben der gezeigten Karte (anchor), darunter die Zusatzzeile (info).
|
||||||
|
func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: String = "", anchor: Rect2 = Rect2()) -> void:
|
||||||
Style.clear(preview_holder)
|
Style.clear(preview_holder)
|
||||||
|
info_label.visible = entered and info != ""
|
||||||
|
info_label.text = info if entered else ""
|
||||||
if entered and card != null:
|
if entered and card != null:
|
||||||
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
var r := BattleLayout.preview_rect(anchor)
|
||||||
v.position = Vector2(1005, 215)
|
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||||
|
v.position = r.position
|
||||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
preview_holder.add_child(v)
|
preview_holder.add_child(v)
|
||||||
|
var ir := BattleLayout.info_rect(r)
|
||||||
|
info_label.position = ir.position
|
||||||
|
info_label.size = ir.size
|
||||||
|
|
||||||
|
|
||||||
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
|
## Eigene Hand- und Ladenkarte: zeigt, was das Ausspielen jetzt brächte, und markiert den Kombo-Partner.
|
||||||
show_preview(card, entered, perspective.looks)
|
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||||||
|
var f := game.forecast(perspective, card)
|
||||||
|
var lines: Array[String] = []
|
||||||
|
var parts: Array[String] = []
|
||||||
|
if f.resonance > 0:
|
||||||
|
parts.append(tr("Resonanz +%d") % f.resonance)
|
||||||
|
if f.field > 0:
|
||||||
|
parts.append(tr("Feld +%d") % f.field)
|
||||||
|
if not parts.is_empty():
|
||||||
|
lines.append(" · ".join(parts))
|
||||||
|
if f.combo != "":
|
||||||
|
lines.append(tr("Feldkombo %s mit %s: %s") % [tr(f.combo), f.partner.card.title(), tr(Cards.FIELD_COMBO_TEXT[f.combo])])
|
||||||
|
for t in tiles.values():
|
||||||
|
t.mark = ""
|
||||||
|
t.queue_redraw()
|
||||||
|
if entered and f.partner != null and tiles.has(f.partner.uid):
|
||||||
|
tiles[f.partner.uid].mark = f.combo
|
||||||
|
tiles[f.partner.uid].queue_redraw()
|
||||||
|
show_preview(card, entered, perspective.looks, "\n".join(lines), anchor)
|
||||||
|
|
||||||
|
|
||||||
|
## Hand- und Ladenkarte tragen ihren Bonus und ihre Feldkombo als Vorschau.
|
||||||
|
func apply_forecast(v: CardView) -> void:
|
||||||
|
var f := game.forecast(perspective, v.card)
|
||||||
|
v.bonus = f.resonance + f.field
|
||||||
|
v.tag = f.combo
|
||||||
|
|
||||||
|
|
||||||
|
## Was den Angriff eines Wesens gerade hebt.
|
||||||
|
func creature_info(cr: Game.Creature) -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
if cr.resonance > 0:
|
||||||
|
parts.append(tr("Resonanz +%d") % cr.resonance)
|
||||||
|
if cr.field_bonus > 0:
|
||||||
|
parts.append(tr("Feld +%d") % cr.field_bonus)
|
||||||
|
var perk := cr.power() - cr.attack - cr.resonance - cr.field_bonus
|
||||||
|
if perk > 0:
|
||||||
|
parts.append(tr("Merkmal +%d") % perk)
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
## Elementvorteil gegen ein mögliches Ziel: des gewählten Angreifers oder eines Schadenszaubers auf der Hand.
|
||||||
|
func matchup_for(cr: Game.Creature, valid: Array) -> int:
|
||||||
|
if selected != null and game.can_attack(selected, cr)[0]:
|
||||||
|
return Game.matchup_bonus(selected, cr)
|
||||||
|
if pending != null and cr in valid and pending.effects.any(func(fx): return fx.kind == "damage" and fx.target in Cards.CHOSEN_TARGETS):
|
||||||
|
return Cards.matchup(pending.elements, cr.card.elements)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
## Resonanzgruppen je Reihe: Wesen mit gleichem Element werden unter der Reihe verbunden.
|
||||||
|
func resonance_groups() -> Array:
|
||||||
|
var out := []
|
||||||
|
for p in game.players:
|
||||||
|
var k := 0
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
var xs: Array[float] = []
|
||||||
|
for j in p.board.size():
|
||||||
|
if e in p.board[j].card.elements:
|
||||||
|
xs.append(slot_rect(p, j).get_center().x)
|
||||||
|
if xs.size() < 2:
|
||||||
|
continue
|
||||||
|
out.append({"color": Style.lighter(Cards.ELEMENT_COLORS[e], 0.25), "y": slot_rect(p, 0).end.y + 4 + k * 6, "xs": xs})
|
||||||
|
k += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class Links extends Control:
|
||||||
|
var groups: Array = []
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
for gr in groups:
|
||||||
|
var y: float = gr.y
|
||||||
|
draw_line(Vector2(gr.xs[0], y), Vector2(gr.xs[-1], y), gr.color, 3.0)
|
||||||
|
for x in gr.xs:
|
||||||
|
draw_circle(Vector2(x, y), 4.0, gr.color)
|
||||||
|
|
||||||
|
|
||||||
# --- Netzwerk -------------------------------------------------------------------------------
|
# --- Netzwerk -------------------------------------------------------------------------------
|
||||||
@@ -744,7 +833,7 @@ func after_action() -> void:
|
|||||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||||
refresh()
|
refresh()
|
||||||
fx.play_events(evs, centers)
|
fx.play_events(evs, centers)
|
||||||
save_battle()
|
announce_turn()
|
||||||
|
|
||||||
|
|
||||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||||
@@ -781,8 +870,7 @@ func refresh() -> void:
|
|||||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||||
if o.alive():
|
if o.alive():
|
||||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||||
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \
|
labels.stats.text = "Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy]
|
||||||
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy])
|
|
||||||
else:
|
else:
|
||||||
labels.life.text = "besiegt"
|
labels.life.text = "besiegt"
|
||||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||||
@@ -798,10 +886,11 @@ func refresh() -> void:
|
|||||||
if tiles.has(cr.uid):
|
if tiles.has(cr.uid):
|
||||||
tile = tiles[cr.uid]
|
tile = tiles[cr.uid]
|
||||||
tile.creature = cr
|
tile.creature = cr
|
||||||
if tile.size != rect.size:
|
if tile.size != rect.size: # erst die Mindestgröße, sonst bleibt die Kachel groß
|
||||||
tile.size = rect.size
|
|
||||||
tile.custom_minimum_size = rect.size
|
tile.custom_minimum_size = rect.size
|
||||||
|
tile.size = rect.size
|
||||||
tile.pivot_offset = rect.size / 2
|
tile.pivot_offset = rect.size / 2
|
||||||
|
tile.set_process(Style.rarity_animated(tile.rarity()))
|
||||||
else:
|
else:
|
||||||
tile = CreatureView.new(cr, rect.size)
|
tile = CreatureView.new(cr, rect.size)
|
||||||
tile.position = rect.position
|
tile.position = rect.position
|
||||||
@@ -809,13 +898,17 @@ func refresh() -> void:
|
|||||||
tiles[cr.uid] = tile
|
tiles[cr.uid] = tile
|
||||||
tile_layer.add_child(tile)
|
tile_layer.add_child(tile)
|
||||||
var looks: Dictionary = p.looks
|
var looks: Dictionary = p.looks
|
||||||
tile.hover.connect(func(card, entered): show_preview(card, entered, looks))
|
tile.hover.connect(func(card, entered): show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
||||||
var uid := cr.uid
|
var uid := cr.uid
|
||||||
tile.clicked.connect(func(_v):
|
tile.clicked.connect(func(_v):
|
||||||
var c := game.find_creature(uid)
|
var c := game.find_creature(uid)
|
||||||
if c != null:
|
if c != null:
|
||||||
on_tile_click(c))
|
on_tile_click(c))
|
||||||
fx.pop_in(tile)
|
if arrivals.has(cr.uid):
|
||||||
|
fx.fly_in(tile, arrivals[cr.uid])
|
||||||
|
arrivals.erase(cr.uid)
|
||||||
|
else:
|
||||||
|
fx.pop_in(tile)
|
||||||
tile.slot = rect.position
|
tile.slot = rect.position
|
||||||
if not tile.busy and tile.position != rect.position:
|
if not tile.busy and tile.position != rect.position:
|
||||||
fx.move_to(tile, rect.position)
|
fx.move_to(tile, rect.position)
|
||||||
@@ -835,6 +928,8 @@ func refresh() -> void:
|
|||||||
w = 3
|
w = 3
|
||||||
tile.border = b
|
tile.border = b
|
||||||
tile.width = w
|
tile.width = w
|
||||||
|
tile.mark = ""
|
||||||
|
tile.matchup = matchup_for(cr, valid)
|
||||||
tile.queue_redraw()
|
tile.queue_redraw()
|
||||||
if p == h:
|
if p == h:
|
||||||
own_views.append(tile)
|
own_views.append(tile)
|
||||||
@@ -845,6 +940,8 @@ func refresh() -> void:
|
|||||||
var t: CreatureView = tiles[uid]
|
var t: CreatureView = tiles[uid]
|
||||||
tiles.erase(uid)
|
tiles.erase(uid)
|
||||||
fx.fade_out(t)
|
fx.fade_out(t)
|
||||||
|
link_view.groups = resonance_groups()
|
||||||
|
link_view.queue_redraw()
|
||||||
|
|
||||||
Style.clear(shop_layer)
|
Style.clear(shop_layer)
|
||||||
if not spectating():
|
if not spectating():
|
||||||
@@ -856,17 +953,22 @@ func refresh() -> void:
|
|||||||
continue
|
continue
|
||||||
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
||||||
sv.position = sr.position
|
sv.position = sr.position
|
||||||
|
sv.lift = mine
|
||||||
|
apply_forecast(sv)
|
||||||
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
||||||
sv.hover.connect(show_hand_preview)
|
sv.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(sv.position, sv.size)))
|
||||||
var slot := i
|
var slot := i
|
||||||
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
||||||
sv.clicked.connect(func(_view): on_click("shop", slot))
|
sv.clicked.connect(func(_view): on_click("shop", slot))
|
||||||
shop_layer.add_child(sv)
|
shop_layer.add_child(sv)
|
||||||
shop_views.append(sv)
|
shop_views.append(sv)
|
||||||
|
if deal_shop:
|
||||||
|
fx.deal(sv, i)
|
||||||
|
deal_shop = false
|
||||||
|
|
||||||
Style.clear(hand_layer)
|
Style.clear(hand_layer)
|
||||||
if spectating():
|
if spectating():
|
||||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center")
|
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
||||||
else:
|
else:
|
||||||
for i in h.hand.size():
|
for i in h.hand.size():
|
||||||
var card: Cards.Card = h.hand[i]
|
var card: Cards.Card = h.hand[i]
|
||||||
@@ -874,12 +976,14 @@ func refresh() -> void:
|
|||||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
||||||
v.position = r.position
|
v.position = r.position
|
||||||
|
v.lift = mine
|
||||||
|
apply_forecast(v)
|
||||||
if card == pending:
|
if card == pending:
|
||||||
v.border = Style.YELLOW
|
v.border = Style.YELLOW
|
||||||
elif playable:
|
elif playable:
|
||||||
v.border = Style.GREEN
|
v.border = Style.GREEN
|
||||||
v.dim = not playable
|
v.dim = not playable
|
||||||
v.hover.connect(show_hand_preview)
|
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
||||||
v.clicked.connect(func(_view): on_click("hand", card))
|
v.clicked.connect(func(_view): on_click("hand", card))
|
||||||
hand_layer.add_child(v)
|
hand_layer.add_child(v)
|
||||||
hand_views.append(v)
|
hand_views.append(v)
|
||||||
@@ -887,7 +991,11 @@ func refresh() -> void:
|
|||||||
var bar_border = Style.BLUE if mine else null
|
var bar_border = Style.BLUE if mine else null
|
||||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||||
bar_border = Style.RED
|
bar_border = Style.RED
|
||||||
bar_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
status_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
||||||
|
var board_col := Style.darker(Style.PANEL, 0.8)
|
||||||
|
if g.field_element != "":
|
||||||
|
board_col = board_col.lerp(Cards.ELEMENT_COLORS[g.field_element], 0.22) # das Feld färbt das Brett
|
||||||
|
board_panel.add_theme_stylebox_override("panel", Style.box(board_col, 10))
|
||||||
name_label.text = h.name
|
name_label.text = h.name
|
||||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||||
energy_label.text = "◆ %d" % h.energy
|
energy_label.text = "◆ %d" % h.energy
|
||||||
@@ -902,18 +1010,10 @@ func refresh() -> void:
|
|||||||
b_undo.visible = undo_state != null and mine
|
b_undo.visible = undo_state != null and mine
|
||||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||||
b_reroll.visible = mine and not spectating()
|
b_reroll.visible = mine and not spectating()
|
||||||
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST
|
b_reroll.text = tr("Neu würfeln ◆ %d") % Game.REROLL_COST
|
||||||
b_reroll.disabled = h.energy < Game.REROLL_COST
|
b_reroll.disabled = h.energy < Game.REROLL_COST
|
||||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||||
if g.field_element == "":
|
field_view.update_from(g)
|
||||||
var parts: Array[String] = []
|
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
parts.append("%s %d" % [tr(e), int(g.field_charge.get(e, 0))])
|
|
||||||
field_label.text = tr("Feld: neutral") + " · " + " ".join(parts)
|
|
||||||
field_label.add_theme_color_override("font_color", Style.MUTED)
|
|
||||||
else:
|
|
||||||
field_label.text = "%s: %s" % [tr(Cards.FIELD_NAMES[g.field_element]), tr(Cards.FIELD_TEXT[g.field_element])]
|
|
||||||
field_label.add_theme_color_override("font_color", Cards.ELEMENT_COLORS[g.field_element])
|
|
||||||
var lines: Array[String] = []
|
var lines: Array[String] = []
|
||||||
var start := maxi(0, g.log.size() - 60)
|
var start := maxi(0, g.log.size() - 60)
|
||||||
for i in range(start, g.log.size()):
|
for i in range(start, g.log.size()):
|
||||||
@@ -929,10 +1029,6 @@ func refresh() -> void:
|
|||||||
log_box.text = "\n".join(lines)
|
log_box.text = "\n".join(lines)
|
||||||
pad.draw_cursor()
|
pad.draw_cursor()
|
||||||
|
|
||||||
Style.clear(zoom_layer)
|
|
||||||
if zoom_player != null and zoom_player.alive():
|
|
||||||
build_zoom(valid)
|
|
||||||
|
|
||||||
if g.winner != "":
|
if g.winner != "":
|
||||||
var wp := g.winner_player
|
var wp := g.winner_player
|
||||||
if counts_for_profile():
|
if counts_for_profile():
|
||||||
@@ -951,30 +1047,3 @@ func refresh() -> void:
|
|||||||
overlay.visible = true
|
overlay.visible = true
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
|
|
||||||
|
|
||||||
func build_zoom(valid: Array) -> void:
|
|
||||||
var o: Game.Player = zoom_player
|
|
||||||
var g := game
|
|
||||||
var box := Rect2(190, 190, 900, 300)
|
|
||||||
var back := Style.panel(zoom_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
|
||||||
back.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
||||||
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d ◆ %d (+%d)" % [o.name, o.title, o.life, o.hand.size(), o.energy, o.max_energy],
|
|
||||||
18, Style.TEXT, Rect2(box.position.x + 20, box.position.y + 12, 800, 30), "left", true)
|
|
||||||
for j in o.board.size():
|
|
||||||
var cr: Game.Creature = o.board[j]
|
|
||||||
var b = null
|
|
||||||
if cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
|
||||||
b = Style.ORANGE
|
|
||||||
var v := CreatureView.new(cr, Vector2(110, 140), b, 3 if b != null else 2)
|
|
||||||
v.position = Vector2(box.position.x + 20 + j * 122, box.position.y + 56)
|
|
||||||
v.hover.connect(show_preview)
|
|
||||||
v.clicked.connect(func(_x): on_tile_click(cr))
|
|
||||||
zoom_layer.add_child(v)
|
|
||||||
if o.board.is_empty():
|
|
||||||
Style.label(zoom_layer, "Keine Wesen auf dem Feld", 16, Style.MUTED, Rect2(box.position.x + 20, box.position.y + 110, 600, 30))
|
|
||||||
var hero_ok: bool = (o in valid) or (selected != null and g.can_attack(selected, o)[0])
|
|
||||||
var bh := Style.button(zoom_layer, "Held als Ziel", Rect2(box.position.x + 20, box.end.y - 60, 180, 44), func(): on_click("hero", o), Color("5a3c28"))
|
|
||||||
bh.disabled = not hero_ok
|
|
||||||
Style.button(zoom_layer, "Schließen", Rect2(box.end.x - 180, box.end.y - 60, 160, 44), func():
|
|
||||||
zoom_player = null
|
|
||||||
refresh())
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ var msg_label: Label
|
|||||||
var b_buy: Button
|
var b_buy: Button
|
||||||
var b_all: Button
|
var b_all: Button
|
||||||
var slots: Control
|
var slots: Control
|
||||||
|
var fx_layer: Control
|
||||||
|
var fresh := {} # gerade aufgedeckt: wird umgedreht
|
||||||
|
|
||||||
|
|
||||||
class CardBack extends Control:
|
class CardBack extends Control:
|
||||||
@@ -47,15 +49,18 @@ func slot_rect(i: int) -> Rect2:
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Style.label(self, "Booster", 36, Style.GOLD, Rect2(0, 20, 1280, 50), "center", true)
|
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
|
||||||
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
|
||||||
|
fx_layer = Control.new()
|
||||||
|
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(fx_layer)
|
||||||
slots = Control.new()
|
slots = Control.new()
|
||||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(slots)
|
add_child(slots)
|
||||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 1280, 22), "center")
|
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center")
|
||||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(290, 720, 330, 48), buy, Color("5a4628"))
|
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628"))
|
||||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all)
|
b_buy.add_theme_font_size_override("font_size", 20)
|
||||||
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -81,16 +86,44 @@ func buy() -> void:
|
|||||||
|
|
||||||
func reveal(i: int) -> void:
|
func reveal(i: int) -> void:
|
||||||
revealed[i] = true
|
revealed[i] = true
|
||||||
app.sfx.play("click")
|
fresh = {i: 0.0}
|
||||||
|
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click")
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func reveal_all() -> void:
|
func reveal_all() -> void:
|
||||||
|
fresh = {}
|
||||||
|
var k := 0
|
||||||
for i in Profile.BOOSTER_SIZE:
|
for i in Profile.BOOSTER_SIZE:
|
||||||
|
if not revealed.has(i):
|
||||||
|
fresh[i] = k * 0.12
|
||||||
|
k += 1
|
||||||
revealed[i] = true
|
revealed[i] = true
|
||||||
|
app.sfx.play("play")
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Umdrehen: die Karte wächst aus der Mitte; seltene Stufen blitzen in ihrer Farbe auf.
|
||||||
|
func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||||
|
if not Style.animations:
|
||||||
|
return
|
||||||
|
v.scale.x = 0.0
|
||||||
|
var tw := create_tween()
|
||||||
|
tw.tween_interval(delay)
|
||||||
|
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
if rarity == "gewöhnlich":
|
||||||
|
return
|
||||||
|
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
|
||||||
|
burst.pivot_offset = burst.size / 2
|
||||||
|
burst.modulate.a = 0.0
|
||||||
|
var bt := create_tween()
|
||||||
|
bt.tween_interval(delay + 0.1)
|
||||||
|
bt.tween_property(burst, "modulate:a", 1.0, 0.05)
|
||||||
|
bt.parallel().tween_property(burst, "scale", Vector2(1.25, 1.25), 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
bt.tween_property(burst, "modulate:a", 0.0, 0.3)
|
||||||
|
bt.tween_callback(burst.queue_free)
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
coins_label.text = "Münzen: %d" % p.coins
|
coins_label.text = "Münzen: %d" % p.coins
|
||||||
@@ -107,6 +140,8 @@ func refresh() -> void:
|
|||||||
var v := CardView.new(c, r.size, false, res.rarity)
|
var v := CardView.new(c, r.size, false, res.rarity)
|
||||||
v.position = r.position
|
v.position = r.position
|
||||||
slots.add_child(v)
|
slots.add_child(v)
|
||||||
|
if fresh.has(i):
|
||||||
|
flip(v, fresh[i], res.rarity)
|
||||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||||
if res.upgraded:
|
if res.upgraded:
|
||||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||||
@@ -117,3 +152,4 @@ func refresh() -> void:
|
|||||||
back.position = r.position
|
back.position = r.position
|
||||||
back.clicked.connect(reveal.bind(i))
|
back.clicked.connect(reveal.bind(i))
|
||||||
slots.add_child(back)
|
slots.add_child(back)
|
||||||
|
fresh = {}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
class_name CampaignScreen
|
class_name CampaignScreen
|
||||||
extends Control
|
extends Control
|
||||||
|
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail.
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var chapter := 1
|
var chapter := 1
|
||||||
|
var selected := 0 # gewählte Stufe (Index über alle Kapitel)
|
||||||
var perk_box: OptionButton
|
var perk_box: OptionButton
|
||||||
|
var node_buttons: Array = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||||
|
selected = mini(p.campaign_stage, Campaign.total() - 1)
|
||||||
build()
|
build()
|
||||||
|
|
||||||
|
|
||||||
func build() -> void:
|
func build() -> void:
|
||||||
Style.clear(self)
|
Style.clear(self)
|
||||||
|
node_buttons = []
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var col := Style.column(self, 1000, 16, 5)
|
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
|
||||||
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
var n := Campaign.STAGES.size()
|
||||||
var tabs := Style.row(col, 10)
|
var x := 40
|
||||||
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
for c in Campaign.CHAPTERS.size():
|
for c in Campaign.CHAPTERS.size():
|
||||||
var b := Style.cbutton(tabs, "Kapitel %d" % (c + 1), Vector2(140, 36), set_chapter.bind(c + 1), Color("285a3c") if chapter == c + 1 else null)
|
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 150, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
|
||||||
b.disabled = p.campaign_stage < c * Campaign.STAGES.size()
|
b.disabled = p.campaign_stage < c * n
|
||||||
|
x += 160
|
||||||
if p.perk_unlocked():
|
if p.perk_unlocked():
|
||||||
var pr := Style.row(tabs, 8)
|
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right")
|
||||||
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
|
||||||
perk_box = OptionButton.new()
|
perk_box = OptionButton.new()
|
||||||
perk_box.custom_minimum_size = Vector2(300, 36)
|
perk_box.position = Vector2(900, 88)
|
||||||
|
perk_box.size = Vector2(340, 38)
|
||||||
perk_box.add_item("keins")
|
perk_box.add_item("keins")
|
||||||
var keys: Array = Game.PERKS.keys()
|
var keys: Array = Game.PERKS.keys()
|
||||||
for k in keys:
|
for k in keys:
|
||||||
@@ -35,54 +40,90 @@ func build() -> void:
|
|||||||
perk_box.item_selected.connect(func(i: int):
|
perk_box.item_selected.connect(func(i: int):
|
||||||
p.perk = "" if i == 0 else keys[i - 1]
|
p.perk = "" if i == 0 else keys[i - 1]
|
||||||
app.save())
|
app.save())
|
||||||
pr.add_child(perk_box)
|
add_child(perk_box)
|
||||||
var offset := (chapter - 1) * Campaign.STAGES.size()
|
# Pfad mit Stationen
|
||||||
for k in Campaign.STAGES.size():
|
var offset := (chapter - 1) * n
|
||||||
|
var y := 190.0
|
||||||
|
var step := (1280 - 200) / float(n - 1)
|
||||||
|
var centers: Array[Vector2] = []
|
||||||
|
for k in n:
|
||||||
|
centers.append(Vector2(100 + k * step, y))
|
||||||
|
var path := Control.new()
|
||||||
|
path.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
path.draw.connect(func():
|
||||||
|
for k in n - 1:
|
||||||
|
var done: bool = offset + k + 1 <= p.campaign_stage
|
||||||
|
path.draw_line(centers[k], centers[k + 1], Style.GREEN if done else Style.PANEL_LIGHT, 4.0))
|
||||||
|
add_child(path)
|
||||||
|
for k in n:
|
||||||
var i := offset + k
|
var i := offset + k
|
||||||
var st := Campaign.stage(i)
|
|
||||||
var done := i < p.campaign_stage
|
var done := i < p.campaign_stage
|
||||||
var next := i == p.campaign_stage
|
var next := i == p.campaign_stage
|
||||||
var panel := PanelContainer.new()
|
var b := Button.new()
|
||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
b.text = str(i + 1)
|
||||||
Style.GOLD if next else null, 2 if next else 0))
|
b.position = centers[k] - Vector2(24, 24)
|
||||||
col.add_child(panel)
|
b.size = Vector2(48, 48)
|
||||||
var pad := MarginContainer.new() # Innenabstand, damit Name und Knopf nicht am Rahmen kleben
|
var base: Color = Style.PRIMARY if done else (Color("5a4628") if next else Style.PANEL)
|
||||||
for side in ["left", "right"]:
|
var edge: Color = Style.GOLD if i == selected else (Style.GREEN if done else Style.MUTED)
|
||||||
pad.add_theme_constant_override("margin_" + side, 14)
|
b.add_theme_stylebox_override("normal", Style.box(base, 24, edge, 3 if i == selected else 2))
|
||||||
panel.add_child(pad)
|
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 24, edge, 3))
|
||||||
var r := Style.row(pad, 10)
|
b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 24, edge, 3))
|
||||||
r.custom_minimum_size = Vector2(0, 50)
|
b.add_theme_stylebox_override("disabled", Style.box(Style.darker(Style.PANEL, 0.8), 24, Style.PANEL_LIGHT, 2))
|
||||||
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
|
b.add_theme_stylebox_override("focus", Style.box(Color(0, 0, 0, 0), 24, Style.GOLD, 3))
|
||||||
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
|
b.disabled = i > p.campaign_stage
|
||||||
var dots := Control.new()
|
b.pressed.connect(func():
|
||||||
dots.custom_minimum_size = Vector2(60, 0)
|
selected = i
|
||||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
build())
|
||||||
var elems: Array = st.elements
|
Style.juice(b)
|
||||||
dots.draw.connect(func():
|
add_child(b)
|
||||||
for m in elems.size():
|
node_buttons.append(b)
|
||||||
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
if next and Style.animations:
|
||||||
r.add_child(dots)
|
b.pivot_offset = b.size / 2
|
||||||
if done:
|
var tw := b.create_tween().set_loops()
|
||||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
tw.tween_property(b, "scale", Vector2(1.1, 1.1), 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||||
elif next:
|
tw.tween_property(b, "scale", Vector2.ONE, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||||
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
# Gewählte Stufe
|
||||||
else:
|
var st := Campaign.stage(selected)
|
||||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
var done := selected < p.campaign_stage
|
||||||
b.disabled = true
|
var next := selected == p.campaign_stage
|
||||||
|
Style.panel(self, Rect2(240, 270, 800, 300))
|
||||||
|
Style.label(self, "%s · %s" % [st.name, st.title], 28, Style.TEXT, Rect2(270, 290, 740, 40), "center", true)
|
||||||
|
var dots := Control.new()
|
||||||
|
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var elems: Array = st.elements
|
||||||
|
dots.draw.connect(func():
|
||||||
|
var w := elems.size() * 26 - 6
|
||||||
|
for m in elems.size():
|
||||||
|
dots.draw_circle(Vector2(640 - w / 2.0 + 10 + m * 26, 348), 10, Cards.ELEMENT_COLORS[elems[m]]))
|
||||||
|
add_child(dots)
|
||||||
|
Style.label(self, "%s · %s" % [tr("%d Leben") % int(st.life), tr("%d Münzen") % st.reward], 16, Style.MUTED, Rect2(270, 370, 740, 24), "center")
|
||||||
|
var perks: Array = st.perks
|
||||||
|
var py := 402
|
||||||
|
for k in perks:
|
||||||
|
Style.label(self, "★ " + tr(Game.PERKS[k]), 15, Style.GOLD, Rect2(270, py, 740, 22), "center")
|
||||||
|
py += 24
|
||||||
|
if done:
|
||||||
|
Style.button(self, "Wiederholen", Rect2(540, 500, 200, 50), _start.bind(selected))
|
||||||
|
elif next:
|
||||||
|
var b := Style.button(self, "Kämpfen", Rect2(540, 500, 200, 50), _start.bind(selected), Style.PRIMARY)
|
||||||
|
b.add_theme_font_size_override("font_size", 20)
|
||||||
|
else:
|
||||||
|
var b := Style.button(self, "Gesperrt", Rect2(540, 500, 200, 50), func(): pass)
|
||||||
|
b.disabled = true
|
||||||
if p.campaign_stage >= Campaign.total():
|
if p.campaign_stage >= Campaign.total():
|
||||||
Style.clabel(col, "Alle Kapitel abgeschlossen", 16, Style.GREEN, "center", true)
|
Style.label(self, "Alle Kapitel abgeschlossen", 16, Style.GREEN, Rect2(0, 600, 1280, 24), "center", true)
|
||||||
var br := Style.row(col)
|
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
|
|
||||||
|
|
||||||
func set_chapter(c: int) -> void:
|
func set_chapter(c: int) -> void:
|
||||||
chapter = c
|
chapter = c
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var first := (c - 1) * Campaign.STAGES.size()
|
||||||
|
selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first
|
||||||
build()
|
build()
|
||||||
|
|
||||||
|
|
||||||
func _start(stage: int) -> void:
|
func _start(stage: int) -> void:
|
||||||
app.show_screen(BattleScreen.new(0, stage))
|
app.show_screen(BattleScreen.new(stage))
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
class_name CardGrid
|
class_name CardGrid
|
||||||
extends ScrollContainer
|
extends ScrollContainer
|
||||||
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte.
|
## Scrollbares Kartenraster.
|
||||||
|
|
||||||
signal card_clicked(card: Cards.Card)
|
signal card_clicked(card: Cards.Card)
|
||||||
|
|
||||||
const COLS := 7
|
const COLS := 6
|
||||||
const CW := 100
|
const CW := 112
|
||||||
const CH := 140
|
const CH := 157
|
||||||
const GAP := 12
|
const GAP := 14
|
||||||
|
|
||||||
var grid: GridContainer
|
var grid: GridContainer
|
||||||
|
|
||||||
@@ -24,19 +24,16 @@ func _init(rect: Rect2) -> void:
|
|||||||
add_child(grid)
|
add_child(grid)
|
||||||
|
|
||||||
|
|
||||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool; rarity_fn(card) -> String (Optik)
|
## cards: Karten; rarity_fn(card) -> String: die Stufe, die die Karte zeigt (Optik)
|
||||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable, rarity_fn: Callable = Callable()) -> void:
|
func fill(cards: Array, rarity_fn: Callable) -> void:
|
||||||
Style.clear(grid)
|
Style.clear(grid)
|
||||||
for c in cards:
|
for c in cards:
|
||||||
var cell := Control.new()
|
var cell := Control.new()
|
||||||
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
cell.custom_minimum_size = Vector2(CW, CH)
|
||||||
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c) if rarity_fn.is_valid() else "gewöhnlich")
|
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c))
|
||||||
v.dim = dim_fn.call(c)
|
|
||||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||||
cell.add_child(v)
|
cell.add_child(v)
|
||||||
var text: String = label_fn.call(c)
|
|
||||||
Style.label(cell, text, 12, Style.MUTED if v.dim else Style.TEXT, Rect2(0, CH + 1, CW, 16), "center")
|
|
||||||
grid.add_child(cell)
|
grid.add_child(cell)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
class_name CardView
|
class_name CardView
|
||||||
extends Control
|
extends Control
|
||||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
## Zeichnet eine Karte (Laden, Hand, Sammlung, Booster, Vorschau).
|
||||||
|
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Schlüsselwörter als
|
||||||
|
## Symbole, Effekttext, Angriff und Leben als Kreise unten. Das Element steht im Hintergrund,
|
||||||
|
## die Seltenheit in Rahmen, Glanz und Edelstein. Sehr flache Karten (Hand) zeigen nur das Nötigste.
|
||||||
|
|
||||||
signal clicked(view: CardView)
|
signal clicked(view: CardView)
|
||||||
signal hover(card: Cards.Card, entered: bool)
|
signal hover(card: Cards.Card, entered: bool)
|
||||||
|
|
||||||
var card: Cards.Card
|
var card: Cards.Card
|
||||||
var border = null # Color oder null (dann Seltenheitsfarbe)
|
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||||
var rarity := "gewöhnlich" # nur Optik: Rahmenfarbe der eigenen Stufe
|
var rarity := "gewöhnlich" # nur Optik
|
||||||
var dim := false
|
var dim := false
|
||||||
|
var bonus := 0 # Vorschau im Kampf: Angriff, den die Karte auf dem Feld dazubekäme
|
||||||
|
var tag := "" # Vorschau im Kampf: Feldkombo, die das Ausspielen auslöst
|
||||||
|
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
|
||||||
|
|
||||||
|
|
||||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
||||||
card = c
|
card = c
|
||||||
rarity = rar
|
rarity = rar
|
||||||
|
set_process(Style.rarity_animated(rarity)) # bewegter Glanz
|
||||||
custom_minimum_size = sz
|
custom_minimum_size = sz
|
||||||
size = sz
|
size = sz
|
||||||
|
pivot_offset = sz / 2
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
mouse_entered.connect(func(): hover.emit(card, true))
|
mouse_entered.connect(func():
|
||||||
mouse_exited.connect(func(): hover.emit(card, false))
|
hover.emit(card, true)
|
||||||
|
_lift(true))
|
||||||
|
mouse_exited.connect(func():
|
||||||
|
hover.emit(card, false)
|
||||||
|
_lift(false))
|
||||||
if focusable:
|
if focusable:
|
||||||
focus_mode = Control.FOCUS_ALL
|
focus_mode = Control.FOCUS_ALL
|
||||||
focus_entered.connect(func():
|
focus_entered.connect(func():
|
||||||
@@ -30,6 +42,17 @@ func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "g
|
|||||||
queue_redraw())
|
queue_redraw())
|
||||||
|
|
||||||
|
|
||||||
|
func _lift(up: bool) -> void:
|
||||||
|
if not lift or not Style.animations:
|
||||||
|
return
|
||||||
|
z_index = 1 if up else 0
|
||||||
|
create_tween().tween_property(self, "scale", Vector2(1.07, 1.07) if up else Vector2.ONE, 0.1).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
@@ -41,47 +64,135 @@ func _gui_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var rect := Rect2(Vector2.ZERO, size)
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
var big := size.x >= 180
|
var seed := hash(card.id)
|
||||||
var mid := size.x >= 115
|
var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
|
||||||
var f_name := 18 if big else (14 if mid else 12)
|
if rarity != "gewöhnlich":
|
||||||
var f_text := 14 if big else (11 if mid else 10)
|
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
|
||||||
var f_stat := 24 if big else (18 if mid else 15)
|
|
||||||
Style.fill_elements(self, rect, card.elements, 8)
|
Style.fill_elements(self, rect, card.elements, 8)
|
||||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[rarity]
|
if size.y < 90:
|
||||||
Style.border(self, rect, bcol, 3 if big else 2, 8)
|
_draw_compact(rect, frame, seed)
|
||||||
# Kosten
|
else:
|
||||||
var r := 16 if big else 12
|
_draw_full(rect, frame, seed)
|
||||||
var cc := Vector2(r + 4, r + 4)
|
|
||||||
draw_circle(cc, r, Color("14141c"))
|
|
||||||
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
|
|
||||||
# Name und Kombo
|
|
||||||
var name_left := 2 * r + 10
|
|
||||||
var nm := Style.fit(card.title(), true, f_name, size.x - name_left - 6)
|
|
||||||
Style.text(self, nm, f_name, Style.TEXT, Vector2((name_left + size.x - 6) / 2.0, 4), "midtop", true)
|
|
||||||
if mid:
|
|
||||||
var sub: String = tr(card.combo()) if card.is_combo() else ""
|
|
||||||
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
|
||||||
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
|
|
||||||
var tsize := Style.fs(f_text)
|
|
||||||
var text_w: float = size.x - 20
|
|
||||||
var needed: float = Style.FONT.get_multiline_string_size(card.text(), HORIZONTAL_ALIGNMENT_LEFT, text_w, tsize).y + 8 if card.text() != "" else size.y * 0.42
|
|
||||||
var box_bottom: float = size.y - (28 if big else 22)
|
|
||||||
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) # unter Name und Kombo
|
|
||||||
var box_top: float = maxf(maxf(size.y * 0.30, band_top), box_bottom - maxf(needed, size.y * 0.42))
|
|
||||||
var box := Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
|
||||||
# Bild
|
|
||||||
var band_bottom: float = box_top - 4
|
|
||||||
if band_bottom - band_top > 14:
|
|
||||||
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
|
||||||
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
|
|
||||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
|
||||||
var max_lines := maxi(1, int((box.size.y - 6) / Style.FONT.get_height(tsize))) # große Schrift: lieber kürzen als überlaufen
|
|
||||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
|
|
||||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
|
||||||
# Werte
|
|
||||||
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
|
|
||||||
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
|
|
||||||
if dim:
|
if dim:
|
||||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||||
if has_focus():
|
if has_focus():
|
||||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||||
|
|
||||||
|
|
||||||
|
## Volle Karte: Laden, Sammlung, Booster, Vorschau.
|
||||||
|
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||||
|
var big := size.x >= 180
|
||||||
|
var mid := size.x >= 140
|
||||||
|
var f_name := 17 if big else (14 if mid else 11)
|
||||||
|
var f_text := 13 if big else (11 if mid else 10)
|
||||||
|
var f_stat := 20 if big else (17 if mid else 14)
|
||||||
|
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
|
||||||
|
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
|
||||||
|
var icon := 11.0 if big else (9.0 if mid else 7.0)
|
||||||
|
# Bild: das Elementsymbol füllt den oberen Teil
|
||||||
|
var band_top: float = size.y * 0.36
|
||||||
|
Style.glyph(self, card, Vector2(size.x / 2, band_top * 0.56), minf(size.x * 0.62, band_top * 0.95), 0.4)
|
||||||
|
Style.rarity_sheen(self, rect, rarity, seed)
|
||||||
|
# Namensband über die volle Breite
|
||||||
|
var band_h: float = Style.fs(f_name) + 10
|
||||||
|
var band := Rect2(0, band_top, size.x, band_h)
|
||||||
|
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
|
||||||
|
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
|
||||||
|
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.TEXT, Vector2(size.x / 2, band.get_center().y), "center", true)
|
||||||
|
var y := band.end.y + 4
|
||||||
|
# Kombo eines verschmolzenen Wesens
|
||||||
|
if card.is_combo():
|
||||||
|
Style.text(self, tr(card.combo()), f_text, Color("e6e6e6"), Vector2(size.x / 2, y), "midtop")
|
||||||
|
y += Style.fs(f_text) + 4
|
||||||
|
# Schlüsselwörter als Symbole, auf großen Karten mit Wort
|
||||||
|
var kws: Array[String] = []
|
||||||
|
for k in Cards.KEYWORDS:
|
||||||
|
if k in card.keywords:
|
||||||
|
kws.append(k)
|
||||||
|
if not kws.is_empty():
|
||||||
|
var with_words := big
|
||||||
|
var widths: Array[float] = []
|
||||||
|
var total := 0.0
|
||||||
|
for k in kws:
|
||||||
|
var w: float = icon * 2 + 6 + (Style.FONT.get_string_size(tr(k), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(f_text)).x + 8 if with_words else 0)
|
||||||
|
widths.append(w)
|
||||||
|
total += w + 6
|
||||||
|
if with_words and total > size.x - 12:
|
||||||
|
with_words = false
|
||||||
|
total = kws.size() * (icon * 2 + 12)
|
||||||
|
var x := (size.x - total + 6) / 2
|
||||||
|
for i in kws.size():
|
||||||
|
var k := kws[i]
|
||||||
|
Style.keyword_icon(self, k, Vector2(x + icon, y + icon + 2), icon)
|
||||||
|
if with_words:
|
||||||
|
Style.text(self, tr(k), f_text, Style.TEXT, Vector2(x + icon * 2 + 6, y + icon + 2), "midleft")
|
||||||
|
x += widths[i] + 6
|
||||||
|
else:
|
||||||
|
x += icon * 2 + 12
|
||||||
|
y += icon * 2 + 8
|
||||||
|
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
|
||||||
|
var box := Rect2()
|
||||||
|
var fx := card.effect_text()
|
||||||
|
var box_bottom: float = size.y - r_stat * 2 - 4
|
||||||
|
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
|
||||||
|
var tsize := Style.fs(f_text)
|
||||||
|
var heading := big
|
||||||
|
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8 + (tsize + 2 if heading else 0)
|
||||||
|
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
|
||||||
|
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||||
|
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
|
||||||
|
var ty := box.position.y + 4
|
||||||
|
if heading:
|
||||||
|
Style.text(self, tr("Beim Ausspielen"), f_text - 2, Style.MUTED, Vector2(box.position.x + 4, ty), "topleft")
|
||||||
|
ty += tsize + 2
|
||||||
|
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
|
||||||
|
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, ty + Style.FONT.get_ascent(tsize)),
|
||||||
|
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||||
|
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
|
||||||
|
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||||
|
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
|
||||||
|
if bonus > 0:
|
||||||
|
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||||
|
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||||
|
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||||
|
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||||
|
if tag != "":
|
||||||
|
_draw_tag(Vector2(size.x - 6, 6), f_text - 1)
|
||||||
|
Style.rarity_sparkle(self, rect, rarity, seed, box)
|
||||||
|
|
||||||
|
|
||||||
|
## Flache Handkachel: Kosten, Name, Symbole, Werte.
|
||||||
|
func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
||||||
|
Style.rarity_sheen(self, rect, rarity, seed)
|
||||||
|
Style.border(self, rect, frame, 2, 8)
|
||||||
|
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
|
||||||
|
var name_w: float = size.x - 30 - (12 if rarity != "gewöhnlich" else 0)
|
||||||
|
var nf := Style.fit_size(card.title(), true, 12, name_w)
|
||||||
|
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
|
||||||
|
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
|
||||||
|
if tag != "":
|
||||||
|
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(9)).x + 12
|
||||||
|
_draw_tag(Vector2(size.x / 2 + w / 2, size.y - 21), 9)
|
||||||
|
else:
|
||||||
|
var kws: Array[String] = []
|
||||||
|
for k in Cards.KEYWORDS:
|
||||||
|
if k in card.keywords:
|
||||||
|
kws.append(k)
|
||||||
|
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
|
||||||
|
for k in kws:
|
||||||
|
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
|
||||||
|
x += 18
|
||||||
|
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
|
||||||
|
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
|
||||||
|
if bonus > 0:
|
||||||
|
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
|
||||||
|
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||||
|
|
||||||
|
|
||||||
|
## Goldenes Schild mit der Feldkombo, rechts oben.
|
||||||
|
func _draw_tag(top_right: Vector2, font: int) -> void:
|
||||||
|
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(font)).x + 12
|
||||||
|
var h := Style.fs(font) + 6
|
||||||
|
var r := Rect2(top_right - Vector2(w, 0), Vector2(w, h))
|
||||||
|
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), r)
|
||||||
|
Style.text(self, tr(tag), font, Style.GOLD, r.get_center(), "center", true)
|
||||||
|
|||||||
@@ -18,33 +18,32 @@ var b_upgrade: Button
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||||
for i in Cards.ELEMENTS.size():
|
for i in Cards.ELEMENTS.size():
|
||||||
var e: String = Cards.ELEMENTS[i]
|
var e: String = Cards.ELEMENTS[i]
|
||||||
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||||
fb.add_theme_font_size_override("font_size", 14)
|
fb.add_theme_font_size_override("font_size", 14)
|
||||||
search_box = LineEdit.new()
|
search_box = LineEdit.new()
|
||||||
search_box.position = Vector2(424, 55)
|
search_box.position = Vector2(444, 86)
|
||||||
search_box.size = Vector2(200, 34)
|
search_box.size = Vector2(220, 34)
|
||||||
search_box.placeholder_text = "Suche"
|
search_box.placeholder_text = "Suche"
|
||||||
search_box.text_changed.connect(func(t: String):
|
search_box.text_changed.connect(func(t: String):
|
||||||
search = t
|
search = t
|
||||||
refresh())
|
refresh())
|
||||||
add_child(search_box)
|
add_child(search_box)
|
||||||
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
|
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||||
grid.card_clicked.connect(select)
|
grid.card_clicked.connect(select)
|
||||||
add_child(grid)
|
add_child(grid)
|
||||||
Style.panel(self, Rect2(845, 50, 420, 680))
|
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||||
dust_label = Style.label(self, "", 22, Style.BLUE, Rect2(860, 58, 390, 30), "left", true)
|
dust_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||||
preview_holder = Control.new()
|
preview_holder = Control.new()
|
||||||
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(preview_holder)
|
add_child(preview_holder)
|
||||||
info_label = Style.label(self, "", 14, Style.MUTED, Rect2(860, 396, 390, 66), "center")
|
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
||||||
b_upgrade = Style.button(self, "", Rect2(860, 480, 390, 44), upgrade, Color("285a3c"))
|
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
||||||
b_dismantle = Style.button(self, "", Rect2(860, 534, 390, 44), dismantle, Color("5a3c28"))
|
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
||||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 600, 390, 48), "center")
|
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
||||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +57,7 @@ func set_filter(e: String) -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
|
||||||
func cards() -> Array:
|
func cards() -> Array:
|
||||||
var out: Array = Cards.all().duplicate()
|
var out: Array = Cards.all().duplicate()
|
||||||
if filter != "":
|
if filter != "":
|
||||||
@@ -65,6 +65,11 @@ func cards() -> Array:
|
|||||||
if search != "":
|
if search != "":
|
||||||
var q := search.to_lower()
|
var q := search.to_lower()
|
||||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var order := {}
|
||||||
|
for i in out.size():
|
||||||
|
order[out[i].id] = i
|
||||||
|
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ func dismantle() -> void:
|
|||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
dust_label.text = "Staub: %d" % p.dust
|
dust_label.text = "Staub: %d" % p.dust
|
||||||
grid.fill(cards(), func(c): return tr(p.rarity(c.id)), func(_c): return false, func(c): return p.rarity(c.id))
|
grid.fill(cards(), func(c): return p.rarity(c.id))
|
||||||
Style.clear(preview_holder)
|
Style.clear(preview_holder)
|
||||||
if selected == null:
|
if selected == null:
|
||||||
info_label.text = "Keine Karte gewählt"
|
info_label.text = "Keine Karte gewählt"
|
||||||
@@ -118,8 +123,8 @@ func refresh() -> void:
|
|||||||
b_upgrade.disabled = true
|
b_upgrade.disabled = true
|
||||||
b_dismantle.disabled = true
|
b_dismantle.disabled = true
|
||||||
return
|
return
|
||||||
var v := CardView.new(selected, Vector2(200, 280), false, p.rarity(selected.id))
|
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
|
||||||
v.position = Vector2(955, 100)
|
v.position = Vector2(920, 152)
|
||||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
preview_holder.add_child(v)
|
preview_holder.add_child(v)
|
||||||
var lvl := p.level(selected.id)
|
var lvl := p.level(selected.id)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ var width := 2
|
|||||||
var flash := false # kurz rot: wurde gerade angegriffen
|
var flash := false # kurz rot: wurde gerade angegriffen
|
||||||
var slot: Vector2 # Zielposition im Raster
|
var slot: Vector2 # Zielposition im Raster
|
||||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||||
|
var mark := "" # Feldkombo, die dieses Wesen als Partner auslösen würde
|
||||||
|
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
|
||||||
|
|
||||||
|
|
||||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||||
@@ -20,58 +22,93 @@ func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
|||||||
custom_minimum_size = sz
|
custom_minimum_size = sz
|
||||||
size = sz
|
size = sz
|
||||||
pivot_offset = sz / 2
|
pivot_offset = sz / 2
|
||||||
|
set_process(Style.rarity_animated(rarity())) # bewegter Glanz
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
mouse_entered.connect(func(): hover.emit(creature.card, true))
|
mouse_entered.connect(func(): hover.emit(creature.card, true))
|
||||||
mouse_exited.connect(func(): hover.emit(creature.card, false))
|
mouse_exited.connect(func(): hover.emit(creature.card, false))
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
clicked.emit(self)
|
clicked.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func rarity() -> String:
|
||||||
|
var owner = creature.owner()
|
||||||
|
return Cards.RARITIES[int(owner.looks.get(creature.card.id, 0)) if owner != null else 0]
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var rect := Rect2(Vector2.ZERO, size)
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
var small := size.x < 100
|
var small := size.x < 100
|
||||||
var mini := size.x < 60
|
var mini := size.x < 60
|
||||||
|
var rar := rarity()
|
||||||
|
var seed := hash(creature.card.id) + creature.uid
|
||||||
|
if rar != "gewöhnlich":
|
||||||
|
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 4 if mini else 6)
|
||||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||||
var owner = creature.owner()
|
if not mini:
|
||||||
var lvl: int = int(owner.looks.get(creature.card.id, 0)) if owner != null else 0
|
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
|
||||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[Cards.RARITIES[lvl]]
|
Style.rarity_sheen(self, rect, rar, seed)
|
||||||
|
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||||
if flash:
|
if flash:
|
||||||
bcol = Style.RED
|
bcol = Style.RED
|
||||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||||
|
if mark != "":
|
||||||
|
Style.border(self, rect.grow(3), Style.GOLD, 3, 8)
|
||||||
if mini:
|
if mini:
|
||||||
_draw_mini()
|
_draw_mini()
|
||||||
|
Style.rarity_sparkle(self, rect, rar, seed)
|
||||||
return
|
return
|
||||||
var fs := 11 if small else 13
|
var fs := 11 if small else 13
|
||||||
var nm := Style.fit(creature.card.title(), true, fs, size.x - 6)
|
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
|
||||||
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
||||||
var badges: Array[String] = []
|
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
|
||||||
if creature.taunt():
|
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||||
badges.append("Wächter")
|
# Schlüsselwörter als Symbole
|
||||||
if creature.shield:
|
var icon := 7.0 if small else 9.0
|
||||||
badges.append("Schild")
|
var kws: Array[String] = []
|
||||||
if creature.attacks_per_turn() == 2:
|
for k in Cards.KEYWORDS:
|
||||||
badges.append("x2")
|
if k in creature.card.keywords:
|
||||||
if creature.frozen:
|
kws.append(k)
|
||||||
badges.append("Eis")
|
if creature.shield and not ("Schild" in kws):
|
||||||
if creature.resonance > 0:
|
kws.append("Schild")
|
||||||
badges.append("Resonanz")
|
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
|
||||||
if creature.field_bonus > 0:
|
var y := Style.fs(fs) + 14 + icon
|
||||||
badges.append("Feld")
|
for k in kws:
|
||||||
var y := 18.0 if small else 22.0
|
Style.keyword_icon(self, k, Vector2(x + icon, y), icon)
|
||||||
for b in badges:
|
x += icon * 2 + 6
|
||||||
Style.text(self, b, 10 if small else 12, Color("ebebf5"), Vector2(size.x / 2, y), "midtop")
|
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
|
||||||
y += 12 if small else 15
|
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||||
var f := 16 if small else 20
|
if mark != "":
|
||||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
var w := Style.FONT_BOLD.get_string_size(tr(mark), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(11)).x + 14
|
||||||
Style.text(self, str(creature.power()), f, atk_col, Vector2(6, size.y - 4), "bottomleft", true)
|
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), Rect2(size.x / 2 - w / 2, -9, w, 18))
|
||||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
Style.text(self, tr(mark), 11, Style.GOLD, Vector2(size.x / 2, 0), "center", true)
|
||||||
Style.text(self, str(creature.health), f, hp_col, Vector2(size.x - 6, size.y - 4), "bottomright", true)
|
|
||||||
if creature.frozen:
|
if creature.frozen:
|
||||||
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
||||||
|
Style.text_outlined(self, "❄", 22 if small else 28, Color("dff2ff"), size / 2, "center", 3)
|
||||||
|
Style.rarity_sparkle(self, rect, rar, seed)
|
||||||
|
|
||||||
|
|
||||||
|
## Angriff (grün mit „+n“, wenn Resonanz, Feld oder Merkmale ihn heben), Leben, Elementvorteil.
|
||||||
|
func _draw_stats(f: int, f_small: int, r: float) -> void:
|
||||||
|
var extra := creature.power() - creature.attack
|
||||||
|
var atk_col := Style.GREEN if extra > 0 else Style.AMBER
|
||||||
|
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
|
||||||
|
if extra > 0:
|
||||||
|
Style.text_outlined(self, "+%d" % extra, f_small, Style.GREEN, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
|
||||||
|
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
|
||||||
|
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
|
||||||
|
if matchup != 0:
|
||||||
|
var c := Vector2(size.x - 12, 12)
|
||||||
|
draw_circle(c, 11, Color("14141c"))
|
||||||
|
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
|
||||||
|
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
|
||||||
|
|
||||||
|
|
||||||
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
|
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
|
||||||
@@ -84,15 +121,14 @@ func _draw_mini() -> void:
|
|||||||
if creature.attacks_per_turn() == 2:
|
if creature.attacks_per_turn() == 2:
|
||||||
letters.append("2")
|
letters.append("2")
|
||||||
if creature.frozen:
|
if creature.frozen:
|
||||||
letters.append("E")
|
letters.append("❄")
|
||||||
if creature.resonance > 0:
|
|
||||||
letters.append("R")
|
|
||||||
if creature.field_bonus > 0:
|
|
||||||
letters.append("F")
|
|
||||||
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
||||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
var extra := creature.power() - creature.attack
|
||||||
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true)
|
Style.text_outlined(self, str(creature.power()), 13, Style.GREEN if extra > 0 else Style.YELLOW, Vector2(9, size.y - 9), "center", 3)
|
||||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||||
Style.text(self, str(creature.health), 13, hp_col, Vector2(size.x - 4, size.y - 2), "bottomright", true)
|
Style.text_outlined(self, str(creature.health), 13, hp_col, Vector2(size.x - 9, size.y - 9), "center", 3)
|
||||||
|
if matchup != 0:
|
||||||
|
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), 10,
|
||||||
|
Style.GREEN if matchup > 0 else Style.RED, Vector2(size.x - 3, 2), "topright", true)
|
||||||
if creature.frozen:
|
if creature.frozen:
|
||||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
||||||
|
|||||||
53
scripts/ui/field_view.gd
Normal file
53
scripts/ui/field_view.gd
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class_name FieldView
|
||||||
|
extends Control
|
||||||
|
## Das gemeinsame Feld: je Element vier Ladepunkte, bei vollen Punkten kippt das Feld.
|
||||||
|
## Das aktive Feld steht farbig mit seinem Bonus da.
|
||||||
|
|
||||||
|
var element := ""
|
||||||
|
var charge := {}
|
||||||
|
|
||||||
|
|
||||||
|
func _init(rect: Rect2) -> void:
|
||||||
|
position = rect.position
|
||||||
|
size = rect.size
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
|
||||||
|
func update_from(g: Game) -> void:
|
||||||
|
element = g.field_element
|
||||||
|
charge = g.field_charge.duplicate()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
## Der Inhalt als Text (Tests, Protokoll).
|
||||||
|
func text() -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
if e == element:
|
||||||
|
parts.append("%s: +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")])
|
||||||
|
else:
|
||||||
|
parts.append("%s %d/%d" % [tr(e), int(charge.get(e, 0)), Cards.FIELD_CHARGE])
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var w := size.x / Cards.ELEMENTS.size()
|
||||||
|
for i in Cards.ELEMENTS.size():
|
||||||
|
var e: String = Cards.ELEMENTS[i]
|
||||||
|
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||||
|
var x0 := i * w
|
||||||
|
var cy := size.y / 2
|
||||||
|
if e == element:
|
||||||
|
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||||
|
Style.text(self, "%s +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")], 13, Style.lighter(col, 0.4), Vector2(x0 + w / 2, cy), "center", true)
|
||||||
|
continue
|
||||||
|
Style.text(self, tr(e), 13, Style.MUTED, Vector2(x0 + 12, cy), "midleft")
|
||||||
|
var n := int(charge.get(e, 0))
|
||||||
|
var px := x0 + 12 + Style.FONT.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 14
|
||||||
|
for k in Cards.FIELD_CHARGE:
|
||||||
|
var c := Vector2(px + k * 13, cy)
|
||||||
|
if k < n:
|
||||||
|
draw_circle(c, 4.5, col)
|
||||||
|
else:
|
||||||
|
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
||||||
|
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
||||||
1
scripts/ui/field_view.gd.uid
Normal file
1
scripts/ui/field_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://i4rdq5rmcyuk
|
||||||
@@ -11,8 +11,6 @@ var b_host: Button
|
|||||||
var b_join: Button
|
var b_join: Button
|
||||||
var b_watch: Button
|
var b_watch: Button
|
||||||
var b_start: Button
|
var b_start: Button
|
||||||
var ai_buttons: Array = []
|
|
||||||
var n_ai := 0
|
|
||||||
var addr_label: Label
|
var addr_label: Label
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +18,8 @@ func _ready() -> void:
|
|||||||
net = app.net
|
net = app.net
|
||||||
net.my_name = "Spieler"
|
net.my_name = "Spieler"
|
||||||
net.my_pick = {"looks": app.profile.looks()}
|
net.my_pick = {"looks": app.profile.looks()}
|
||||||
var col := Style.column(self, 760, 40, 10)
|
Style.header(self, "Netzwerkspiel", _back)
|
||||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
var col := Style.column(self, 760, 110, 10)
|
||||||
Style.spacer(col, 10)
|
|
||||||
var hr := Style.row(col, 12)
|
var hr := Style.row(col, 12)
|
||||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
|
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
|
||||||
@@ -38,17 +35,10 @@ func _ready() -> void:
|
|||||||
Style.spacer(col, 6)
|
Style.spacer(col, 6)
|
||||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
||||||
Style.spacer(col, 6)
|
|
||||||
Style.clabel(col, "KI-Gegner", 16, Style.MUTED)
|
|
||||||
var ar := Style.row(col, 10)
|
|
||||||
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
for n in range(0, 5):
|
|
||||||
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 40), func(): _set_ai(n)))
|
|
||||||
Style.spacer(col, 10)
|
Style.spacer(col, 10)
|
||||||
var br := Style.row(col, 20)
|
var br := Style.row(col, 20)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
|
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 48), _back)
|
|
||||||
net.lobby_changed.connect(refresh)
|
net.lobby_changed.connect(refresh)
|
||||||
net.started.connect(_started)
|
net.started.connect(_started)
|
||||||
net.ended.connect(func(_reason): refresh())
|
net.ended.connect(func(_reason): refresh())
|
||||||
@@ -79,13 +69,8 @@ func _watch() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func _set_ai(n: int) -> void:
|
|
||||||
n_ai = n
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _start() -> void:
|
func _start() -> void:
|
||||||
net.start_battle(n_ai)
|
net.start_battle()
|
||||||
|
|
||||||
|
|
||||||
func _started(seat: int) -> void:
|
func _started(seat: int) -> void:
|
||||||
@@ -112,9 +97,8 @@ func refresh() -> void:
|
|||||||
b_watch.disabled = net.connected
|
b_watch.disabled = net.connected
|
||||||
ip_box.editable = not net.connected
|
ip_box.editable = not net.connected
|
||||||
var humans := ids.size()
|
var humans := ids.size()
|
||||||
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
b_start.disabled = not net.is_host
|
||||||
for i in ai_buttons.size():
|
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
|
||||||
ai_buttons[i].disabled = not net.is_host or i == n_ai or humans + i > Game.MAX_OPPONENTS + 1
|
|
||||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ func apply_settings() -> void:
|
|||||||
sfx.set_music_volume(profile.settings.music)
|
sfx.set_music_volume(profile.settings.music)
|
||||||
set_language(profile.settings.language)
|
set_language(profile.settings.language)
|
||||||
Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0
|
Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0
|
||||||
|
Style.animations = bool(profile.settings.get("animations", true))
|
||||||
theme = Style.make_theme()
|
theme = Style.make_theme()
|
||||||
|
|
||||||
|
|
||||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
||||||
func reset_profile() -> void:
|
func reset_profile() -> void:
|
||||||
profile = Profile.starter()
|
profile = Profile.starter()
|
||||||
clear_battle_save()
|
|
||||||
save()
|
save()
|
||||||
apply_settings()
|
apply_settings()
|
||||||
|
|
||||||
@@ -54,6 +54,9 @@ func show_screen(s: Control) -> void:
|
|||||||
s.app = self
|
s.app = self
|
||||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
add_child(s)
|
add_child(s)
|
||||||
|
if Style.animations:
|
||||||
|
s.modulate.a = 0.0
|
||||||
|
create_tween().tween_property(s, "modulate:a", 1.0, 0.15)
|
||||||
if pad_mode:
|
if pad_mode:
|
||||||
focus_first_button.call_deferred()
|
focus_first_button.call_deferred()
|
||||||
|
|
||||||
@@ -107,16 +110,6 @@ func save() -> void:
|
|||||||
profile.save(save_path)
|
profile.save(save_path)
|
||||||
|
|
||||||
|
|
||||||
## Datei für einen unterbrochenen Kampf (neben dem Spielstand).
|
|
||||||
func battle_save_path() -> String:
|
|
||||||
return save_path.get_basename() + "_kampf.json"
|
|
||||||
|
|
||||||
|
|
||||||
func clear_battle_save() -> void:
|
|
||||||
if FileAccess.file_exists(battle_save_path()):
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(battle_save_path()))
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
if net != null and net.is_inside_tree():
|
if net != null and net.is_inside_tree():
|
||||||
net.leave()
|
net.leave()
|
||||||
|
|||||||
@@ -1,53 +1,55 @@
|
|||||||
class_name MenuScreen
|
class_name MenuScreen
|
||||||
extends Control
|
extends Control
|
||||||
|
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster.
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var b_resume: Button
|
|
||||||
var coins_label: Label
|
var coins_label: Label
|
||||||
var coll_label: Label
|
var coll_label: Label
|
||||||
var notice_label: Label
|
var notice_label: Label
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var col := Style.column(self, 640, 40, 5)
|
var p: Profile = app.profile
|
||||||
Style.clabel(col, "Dinger", 64, Style.GOLD, "center", true)
|
Style.label(self, "Dinger", 76, Style.GOLD, Rect2(80, 64, 600, 90), "left", true)
|
||||||
Style.clabel(col, "Ein Kartenspiel: jeder gegen jeden", 22, Style.MUTED)
|
Style.label(self, "Ein Kartenspiel: Duell um das Feld", 22, Style.MUTED, Rect2(84, 156, 600, 30), "left")
|
||||||
Style.spacer(col, 8)
|
coins_label = Style.chip(self, "", Rect2(930, 20, 310, 38))
|
||||||
coins_label = Style.clabel(col, "", 20, Style.GOLD)
|
coll_label = Style.label(self, "", 15, Style.MUTED, Rect2(700, 64, 540, 24), "right")
|
||||||
coll_label = Style.clabel(col, "", 16, Style.MUTED)
|
var y := 230
|
||||||
Style.spacer(col, 8)
|
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
||||||
var grid := GridContainer.new()
|
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
||||||
grid.columns = 2
|
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
|
||||||
grid.add_theme_constant_override("h_separation", 12)
|
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
||||||
grid.add_theme_constant_override("v_separation", 8)
|
b.add_theme_font_size_override("font_size", 24)
|
||||||
var center := CenterContainer.new()
|
y += 74
|
||||||
center.add_child(grid)
|
y += 18
|
||||||
col.add_child(center)
|
var x := 80
|
||||||
var sz := Vector2(270, 44)
|
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
|
||||||
if FileAccess.file_exists(app.battle_save_path()):
|
["Sammlung", func(): app.show_screen(CollectionScreen.new())],
|
||||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
|
||||||
Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
["Beenden", func(): get_tree().quit()]]:
|
||||||
Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||||
Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
x += 176
|
||||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
|
||||||
Style.cbutton(grid, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
|
|
||||||
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
|
||||||
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
|
||||||
Style.spacer(col, 10)
|
|
||||||
notice_label = Style.clabel(col, "", 16, Style.GREEN)
|
|
||||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
_showcase(p)
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen.
|
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
|
||||||
func _resume() -> void:
|
func _showcase(p: Profile) -> void:
|
||||||
var json := JSON.new()
|
var cards: Array = Cards.all().duplicate()
|
||||||
var text := FileAccess.get_file_as_string(app.battle_save_path())
|
cards.sort_custom(func(a, b):
|
||||||
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"):
|
if p.level(a.id) != p.level(b.id):
|
||||||
app.clear_battle_save()
|
return p.level(a.id) > p.level(b.id)
|
||||||
app.show_screen(MenuScreen.new())
|
return hash(a.id) % 97 < hash(b.id) % 97)
|
||||||
return
|
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
||||||
app.show_screen(BattleScreen.resume(json.data))
|
for i in 3:
|
||||||
|
var c: Cards.Card = cards[i]
|
||||||
|
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
|
||||||
|
v.position = spots[i][0]
|
||||||
|
v.rotation = spots[i][1]
|
||||||
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(v)
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
|
|||||||
@@ -1,79 +1,54 @@
|
|||||||
class_name OpponentSelectScreen
|
class_name OpponentSelectScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Einzelspieler: Schwierigkeit und Gegnerzahl wählen. Die Karten kommen im Kampf
|
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||||
## aus dem Laden (siehe DeckGen.shop_cards).
|
## aus dem Laden (siehe DeckGen.shop_cards).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var diff_buttons := {}
|
var diff_buttons := {}
|
||||||
var count_slider: HSlider
|
|
||||||
var count_label: Label
|
|
||||||
var info_label: Label
|
var info_label: Label
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var col := Style.column(self, 1000, 50, 12)
|
Style.header(self, "Einzelspieler", func(): app.show_screen(MenuScreen.new()))
|
||||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
var col := Style.column(self, 1000, 170, 12)
|
||||||
Style.spacer(col, 8)
|
|
||||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||||
var dr := Style.row(col, 10)
|
var dr := Style.row(col, 10)
|
||||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
for key in Profile.DIFFICULTIES:
|
for key in Profile.DIFFICULTIES:
|
||||||
var k: String = key
|
var k: String = key
|
||||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
||||||
Style.spacer(col, 20)
|
|
||||||
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
|
|
||||||
var sr := Style.row(col, 12)
|
|
||||||
sr.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
|
|
||||||
count_slider = HSlider.new()
|
|
||||||
count_slider.min_value = 1
|
|
||||||
count_slider.max_value = Game.MAX_OPPONENTS
|
|
||||||
count_slider.step = 1
|
|
||||||
count_slider.value = 1
|
|
||||||
count_slider.tick_count = Game.MAX_OPPONENTS
|
|
||||||
count_slider.ticks_on_borders = true
|
|
||||||
count_slider.custom_minimum_size = Vector2(420, 32)
|
|
||||||
count_slider.value_changed.connect(func(_v): refresh())
|
|
||||||
sr.add_child(count_slider)
|
|
||||||
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
|
|
||||||
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||||
Style.spacer(col, 24)
|
Style.spacer(col, 24)
|
||||||
var br := Style.row(col, 16)
|
var br := Style.row(col, 16)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
var b := Style.cbutton(br, "Starten", Vector2(260, 56), start, Style.PRIMARY)
|
||||||
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
|
b.add_theme_font_size_override("font_size", 22)
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func set_difficulty(key: String) -> void:
|
func set_difficulty(key: String) -> void:
|
||||||
app.profile.settings.difficulty = key
|
app.profile.settings.difficulty = key
|
||||||
app.save()
|
app.save()
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func opponents() -> int:
|
|
||||||
return int(count_slider.value)
|
|
||||||
|
|
||||||
|
|
||||||
func start() -> void:
|
func start() -> void:
|
||||||
app.show_screen(BattleScreen.new(opponents()))
|
app.show_screen(BattleScreen.new())
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
for key in diff_buttons:
|
for key in diff_buttons:
|
||||||
_mark(diff_buttons[key], key == p.settings.difficulty)
|
_mark(diff_buttons[key], key == p.settings.difficulty)
|
||||||
var n := opponents()
|
var d := p.difficulty()
|
||||||
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))]
|
||||||
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
|
||||||
|
|
||||||
|
|
||||||
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
||||||
var base: Color = col if active else Style.PANEL_LIGHT
|
var base: Color = col if active else Style.PANEL_LIGHT
|
||||||
b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2))
|
b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event.is_action_pressed("ui_cancel"):
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ var speed_label: Label
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var s: Dictionary = app.profile.settings
|
var s: Dictionary = app.profile.settings
|
||||||
var col := Style.column(self, 700, 50, 24)
|
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
|
||||||
Style.clabel(col, "Einstellungen", 36, Style.GOLD, "center", true)
|
var col := Style.column(self, 700, 110, 24)
|
||||||
Style.spacer(col, 30)
|
|
||||||
var grid := GridContainer.new()
|
var grid := GridContainer.new()
|
||||||
grid.columns = 3
|
grid.columns = 3
|
||||||
grid.add_theme_constant_override("h_separation", 24)
|
grid.add_theme_constant_override("h_separation", 24)
|
||||||
@@ -80,7 +79,6 @@ func _ready() -> void:
|
|||||||
var r := Style.row(col, 16)
|
var r := Style.row(col, 16)
|
||||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
|
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
|
||||||
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.",
|
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.",
|
||||||
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
|
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
|
||||||
_labels()
|
_labels()
|
||||||
@@ -135,6 +133,7 @@ func _on_speed(v: float) -> void:
|
|||||||
|
|
||||||
func _on_anim(on: bool) -> void:
|
func _on_anim(on: bool) -> void:
|
||||||
app.profile.settings.animations = on
|
app.profile.settings.animations = on
|
||||||
|
Style.animations = on
|
||||||
app.save()
|
app.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ func _ready() -> void:
|
|||||||
streams["damage"] = _tone(140, 0.12, "square")
|
streams["damage"] = _tone(140, 0.12, "square")
|
||||||
streams["heal"] = _tone(523, 0.22, "sine", 784)
|
streams["heal"] = _tone(523, 0.22, "sine", 784)
|
||||||
streams["combo"] = _tone(660, 0.2, "sine", 1100)
|
streams["combo"] = _tone(660, 0.2, "sine", 1100)
|
||||||
|
streams["buy"] = _tone(1200, 0.08, "sine", 1700)
|
||||||
|
streams["turn"] = _tone(520, 0.3, "sine", 1040)
|
||||||
streams["win"] = _tone(400, 0.6, "sine", 1200)
|
streams["win"] = _tone(400, 0.6, "sine", 1200)
|
||||||
streams["lose"] = _tone(320, 0.7, "square", 90)
|
streams["lose"] = _tone(320, 0.7, "square", 90)
|
||||||
for i in 4:
|
for i in 4:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class_name Style
|
|||||||
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
||||||
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
||||||
static var font_scale := 1.0 # Einstellung „Große Schrift“
|
static var font_scale := 1.0 # Einstellung „Große Schrift“
|
||||||
|
static var animations := true # Einstellung „Animationen“: der Glanz seltener Karten bewegt sich
|
||||||
|
|
||||||
const BG := Color("1a1c24")
|
const BG := Color("1a1c24")
|
||||||
const PANEL := Color("282b36")
|
const PANEL := Color("282b36")
|
||||||
@@ -17,6 +18,11 @@ const BLUE := Color("6eaaf0")
|
|||||||
const YELLOW := Color("ffe65a")
|
const YELLOW := Color("ffe65a")
|
||||||
const ORANGE := Color("ff963c")
|
const ORANGE := Color("ff963c")
|
||||||
const PURPLE := Color("8c78dc")
|
const PURPLE := Color("8c78dc")
|
||||||
|
const AMBER := Color("f0b429") # Angriff
|
||||||
|
const CRIMSON := Color("d64545") # Leben
|
||||||
|
const MANA := Color("3a78d2") # Kostenkristall
|
||||||
|
const PRIMARY := Color("285a3c") # Hauptaktion
|
||||||
|
const DANGER := Color("5a2828")
|
||||||
|
|
||||||
|
|
||||||
## Schriftgröße mit der Einstellung „Große Schrift“.
|
## Schriftgröße mit der Einstellung „Große Schrift“.
|
||||||
@@ -80,7 +86,11 @@ static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8,
|
|||||||
var p := Panel.new()
|
var p := Panel.new()
|
||||||
p.position = rect.position
|
p.position = rect.position
|
||||||
p.size = rect.size
|
p.size = rect.size
|
||||||
p.add_theme_stylebox_override("panel", box(bg, radius, border_col, border_w))
|
var sb := box(bg, radius, border_col, border_w)
|
||||||
|
sb.shadow_color = Color(0, 0, 0, 0.35)
|
||||||
|
sb.shadow_size = 6
|
||||||
|
sb.shadow_offset = Vector2(0, 3)
|
||||||
|
p.add_theme_stylebox_override("panel", sb)
|
||||||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
parent.add_child(p)
|
parent.add_child(p)
|
||||||
return p
|
return p
|
||||||
@@ -115,10 +125,35 @@ static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable
|
|||||||
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
|
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
|
||||||
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
|
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
|
||||||
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
|
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
|
||||||
|
juice(b)
|
||||||
parent.add_child(b)
|
parent.add_child(b)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
## Knöpfe federn beim Drücken kurz ein.
|
||||||
|
static func juice(b: Button) -> void:
|
||||||
|
b.button_down.connect(func():
|
||||||
|
if animations:
|
||||||
|
b.pivot_offset = b.size / 2
|
||||||
|
b.create_tween().tween_property(b, "scale", Vector2(0.96, 0.96), 0.05))
|
||||||
|
b.button_up.connect(func():
|
||||||
|
if animations:
|
||||||
|
b.pivot_offset = b.size / 2
|
||||||
|
b.create_tween().tween_property(b, "scale", Vector2.ONE, 0.12).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT))
|
||||||
|
|
||||||
|
|
||||||
|
## Kopfzeile eines Unterbildschirms: Titel links, Zurück rechts oben.
|
||||||
|
static func header(parent: Node, title: String, on_back: Callable) -> Button:
|
||||||
|
label(parent, title, 34, GOLD, Rect2(40, 22, 700, 46), "left", true)
|
||||||
|
return button(parent, "Zurück", Rect2(1120, 26, 120, 40), on_back)
|
||||||
|
|
||||||
|
|
||||||
|
## Kleine Kachel mit Text, etwa für Münzen und Staub.
|
||||||
|
static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label:
|
||||||
|
panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1)
|
||||||
|
return label(parent, text, 17, color, rect, "center", true)
|
||||||
|
|
||||||
|
|
||||||
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
|
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
|
||||||
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
|
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
|
||||||
var v := VBoxContainer.new()
|
var v := VBoxContainer.new()
|
||||||
@@ -230,6 +265,33 @@ static func text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vec
|
|||||||
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||||
|
|
||||||
|
|
||||||
|
## Text mit dunklem Rand: bleibt auf jeder Farbe lesbar (Werte, Kosten).
|
||||||
|
static func text_outlined(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "center", outline: int = 4) -> void:
|
||||||
|
var size := fs(size_in)
|
||||||
|
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||||
|
var x := pos.x
|
||||||
|
var y := pos.y
|
||||||
|
match anchor:
|
||||||
|
"center":
|
||||||
|
x -= dim.x / 2
|
||||||
|
y -= dim.y / 2
|
||||||
|
"midtop":
|
||||||
|
x -= dim.x / 2
|
||||||
|
"midleft":
|
||||||
|
y -= dim.y / 2
|
||||||
|
var at := Vector2(x, y + FONT_BOLD.get_ascent(size))
|
||||||
|
ci.draw_string_outline(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, outline, Color(0.05, 0.05, 0.08, 0.95))
|
||||||
|
ci.draw_string(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||||
|
|
||||||
|
|
||||||
|
## Schriftgröße, mit der ein Text in eine Breite passt (bis zur Mindestgröße).
|
||||||
|
static func fit_size(s: String, bold: bool, size: int, max_w: float, min_size: int = 9) -> int:
|
||||||
|
var font: Font = FONT_BOLD if bold else FONT
|
||||||
|
while size > min_size and font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(size)).x > max_w:
|
||||||
|
size -= 1
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
|
static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
|
||||||
var font: Font = FONT_BOLD if bold else FONT
|
var font: Font = FONT_BOLD if bold else FONT
|
||||||
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
|
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
|
||||||
@@ -246,6 +308,88 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
|||||||
ci.draw_style_box(sb, rect)
|
ci.draw_style_box(sb, rect)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Seltenheit als Aussehen ------------------------------------------------------
|
||||||
|
|
||||||
|
## Ab episch bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||||
|
static func rarity_animated(rar: String) -> bool:
|
||||||
|
return animations and Cards.RARITIES.find(rar) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
## Farbe einer Stufe; mythisch schillert über die Zeit durch alle Farben.
|
||||||
|
static func rarity_color(rar: String, seed: int = 0) -> Color:
|
||||||
|
if rar == "mythisch" and animations:
|
||||||
|
return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0)
|
||||||
|
return Cards.RARITY_COLORS[rar]
|
||||||
|
|
||||||
|
|
||||||
|
## Rahmenfarbe einer Stufe: gewöhnlich bleibt schlicht, alles darüber trägt seine Farbe.
|
||||||
|
static func rarity_frame(rar: String, seed: int = 0) -> Color:
|
||||||
|
return Color("3a3e4c") if rar == "gewöhnlich" else rarity_color(rar, seed)
|
||||||
|
|
||||||
|
|
||||||
|
## Lichtstreifen wie auf einer Folienkarte: selten steht er, ab episch wandert er über die Karte.
|
||||||
|
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||||
|
var lvl := Cards.RARITIES.find(rar)
|
||||||
|
if lvl < 1:
|
||||||
|
return
|
||||||
|
var phase := (seed % 100) / 100.0
|
||||||
|
if lvl >= 2 and animations:
|
||||||
|
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||||
|
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||||
|
var w := rect.size.x * 0.34
|
||||||
|
var k := rect.size.y * 0.75 # Schräge
|
||||||
|
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
|
||||||
|
for layer in [[1.0, 0.07], [0.55, 0.09], [0.22, 0.13]]:
|
||||||
|
var lw: float = w * layer[0]
|
||||||
|
var lx := x0 + (w - lw) / 2
|
||||||
|
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
|
||||||
|
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||||
|
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1]))
|
||||||
|
|
||||||
|
|
||||||
|
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||||
|
## avoid: Bereich, der frei bleibt (der Kartentext).
|
||||||
|
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
|
||||||
|
var lvl := Cards.RARITIES.find(rar)
|
||||||
|
if lvl < 3:
|
||||||
|
return
|
||||||
|
var t := Time.get_ticks_msec() / 1000.0
|
||||||
|
var n := 5 if lvl == 3 else 9
|
||||||
|
for i in n:
|
||||||
|
var h := absi(hash(Vector2i(seed, i)))
|
||||||
|
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
|
||||||
|
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
|
||||||
|
if tw < 0.15 or avoid.has_point(p):
|
||||||
|
continue
|
||||||
|
var s := 2.0 + 4.0 * tw
|
||||||
|
var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
|
||||||
|
ci.draw_line(p - Vector2(s, 0), p + Vector2(s, 0), col, 1.4)
|
||||||
|
ci.draw_line(p - Vector2(0, s), p + Vector2(0, s), col, 1.4)
|
||||||
|
ci.draw_circle(p, 1.3, col)
|
||||||
|
|
||||||
|
|
||||||
|
## Weicher Schein um eine Karte höherer Stufe; vor dem Hintergrund zeichnen.
|
||||||
|
static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spread: int = 8) -> void:
|
||||||
|
var sb := StyleBoxFlat.new()
|
||||||
|
sb.bg_color = Color(0, 0, 0, 0)
|
||||||
|
sb.set_corner_radius_all(radius)
|
||||||
|
sb.shadow_color = Color(color, 0.6)
|
||||||
|
sb.shadow_size = spread
|
||||||
|
ci.draw_style_box(sb, rect)
|
||||||
|
|
||||||
|
|
||||||
|
## Edelstein in der Farbe der Stufe (ab selten).
|
||||||
|
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
|
||||||
|
if rar == "gewöhnlich":
|
||||||
|
return
|
||||||
|
var col := rarity_color(rar, seed)
|
||||||
|
var diamond := func(h: float, k: Color) -> void:
|
||||||
|
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.72, 0), c + Vector2(0, h), c + Vector2(-h * 0.72, 0)]), k)
|
||||||
|
diamond.call(s + 2, Color("14141c"))
|
||||||
|
diamond.call(s, col)
|
||||||
|
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
|
||||||
|
|
||||||
|
|
||||||
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
|
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
|
||||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||||
if elements.size() == 1:
|
if elements.size() == 1:
|
||||||
@@ -265,12 +409,63 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
|
|||||||
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
|
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Kartenteile ------------------------------------------------------
|
||||||
|
|
||||||
|
## Kostenkristall: sechseckig, blau, Zahl in der Mitte.
|
||||||
|
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void:
|
||||||
|
var pts := PackedVector2Array()
|
||||||
|
var outer := PackedVector2Array()
|
||||||
|
for i in 6:
|
||||||
|
var a := -PI / 2 + i * PI / 3
|
||||||
|
pts.append(c + Vector2(cos(a), sin(a)) * r)
|
||||||
|
outer.append(c + Vector2(cos(a), sin(a)) * (r + 2))
|
||||||
|
ci.draw_colored_polygon(outer, Color("14141c"))
|
||||||
|
ci.draw_colored_polygon(pts, MANA)
|
||||||
|
var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)])
|
||||||
|
ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22))
|
||||||
|
text_outlined(ci, str(cost), font, Color.WHITE, c, "center", 3)
|
||||||
|
|
||||||
|
|
||||||
|
## Wertkreis (Angriff gelb, Leben rot) mit Zahl.
|
||||||
|
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int) -> void:
|
||||||
|
ci.draw_circle(c, r + 2, Color("14141c"))
|
||||||
|
ci.draw_circle(c, r, color)
|
||||||
|
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
|
||||||
|
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
|
||||||
|
|
||||||
|
|
||||||
|
## Schlüsselwort als Symbol auf dunklem Kreis: Wächter Mauer, Schild Schild, Flink Doppelpfeil,
|
||||||
|
## Doppelschlag gekreuzte Klingen.
|
||||||
|
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
|
||||||
|
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
|
||||||
|
var w := maxf(1.5, r * 0.22)
|
||||||
|
match kw:
|
||||||
|
"Wächter":
|
||||||
|
ci.draw_rect(Rect2(c + Vector2(-r * 0.75, -r * 0.15), Vector2(r * 1.5, r * 0.85)), col)
|
||||||
|
for i in 3:
|
||||||
|
ci.draw_rect(Rect2(c + Vector2(-r * 0.75 + i * r * 0.6, -r * 0.6), Vector2(r * 0.3, r * 0.5)), col)
|
||||||
|
"Schild":
|
||||||
|
var pts := PackedVector2Array([c + Vector2(-r * 0.7, -r * 0.65), c + Vector2(r * 0.7, -r * 0.65), c + Vector2(r * 0.7, r * 0.05),
|
||||||
|
c + Vector2(0, r * 0.8), c + Vector2(-r * 0.7, r * 0.05)])
|
||||||
|
ci.draw_colored_polygon(pts, col)
|
||||||
|
ci.draw_line(c + Vector2(0, -r * 0.55), c + Vector2(0, r * 0.6), Color(0.05, 0.05, 0.08, 0.6), w)
|
||||||
|
"Flink":
|
||||||
|
for i in 2:
|
||||||
|
var x := c.x - r * 0.55 + i * r * 0.55
|
||||||
|
ci.draw_polyline(PackedVector2Array([Vector2(x, c.y - r * 0.6), Vector2(x + r * 0.5, c.y), Vector2(x, c.y + r * 0.6)]), col, w)
|
||||||
|
"Doppelschlag":
|
||||||
|
ci.draw_line(c + Vector2(-r * 0.65, r * 0.65), c + Vector2(r * 0.6, -r * 0.6), col, w)
|
||||||
|
ci.draw_line(c + Vector2(r * 0.65, r * 0.65), c + Vector2(-r * 0.6, -r * 0.6), col, w)
|
||||||
|
ci.draw_line(c + Vector2(-r * 0.7, r * 0.35), c + Vector2(-r * 0.35, r * 0.7), col, w)
|
||||||
|
ci.draw_line(c + Vector2(r * 0.7, r * 0.35), c + Vector2(r * 0.35, r * 0.7), col, w)
|
||||||
|
|
||||||
|
|
||||||
# --- Kartensymbole (prozedural, je Element) --------------------------
|
# --- Kartensymbole (prozedural, je Element) --------------------------
|
||||||
|
|
||||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95) -> void:
|
||||||
var variant := absi(hash(card.id)) % 3
|
var variant := absi(hash(card.id)) % 3
|
||||||
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||||
col.a = 0.95
|
col.a = alpha
|
||||||
var elems: Array = card.elements
|
var elems: Array = card.elements
|
||||||
if elems.size() == 1:
|
if elems.size() == 1:
|
||||||
_element_glyph(ci, elems[0], center, size, variant, col)
|
_element_glyph(ci, elems[0], center, size, variant, col)
|
||||||
@@ -278,7 +473,7 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
|||||||
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
|
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
|
||||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||||
col2.a = 0.95
|
col2.a = alpha
|
||||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||||
if elems.size() >= 3:
|
if elems.size() >= 3:
|
||||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||||
@@ -321,7 +516,7 @@ static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
|
|||||||
ci.draw_colored_polygon(pts, col)
|
ci.draw_colored_polygon(pts, col)
|
||||||
var inner := PackedVector2Array([
|
var inner := PackedVector2Array([
|
||||||
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
|
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
|
||||||
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75))
|
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75 * col.a))
|
||||||
|
|
||||||
|
|
||||||
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
|
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Fensterprobe: Kosten von refresh() und längste Bildzeit während KI-Zügen (5 Gegner).
|
## Fensterprobe: Kosten von refresh() und längste Bildzeit während KI-Zügen.
|
||||||
|
|
||||||
var main: Node
|
var main: Node
|
||||||
var max_dt := 0.0
|
var max_dt := 0.0
|
||||||
@@ -14,7 +14,7 @@ func _ready() -> void:
|
|||||||
add_child(main)
|
add_child(main)
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
var scr: BattleScreen = BattleScreen.new(5)
|
var scr: BattleScreen = BattleScreen.new()
|
||||||
main.show_screen(scr)
|
main.show_screen(scr)
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
@@ -23,7 +23,7 @@ func _ready() -> void:
|
|||||||
var t0 := Time.get_ticks_usec()
|
var t0 := Time.get_ticks_usec()
|
||||||
for i in 30:
|
for i in 30:
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
print("refresh() mit 5 Gegnern und 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
print("refresh() mit 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
||||||
scr.ai_delay = 0.2
|
scr.ai_delay = 0.2
|
||||||
measuring = true
|
measuring = true
|
||||||
var start := Time.get_ticks_msec()
|
var start := Time.get_ticks_msec()
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func _ready() -> void:
|
|||||||
net.host()
|
net.host()
|
||||||
net.lobby_changed.connect(func():
|
net.lobby_changed.connect(func():
|
||||||
if net.game == null and net.names.size() >= 2:
|
if net.game == null and net.names.size() >= 2:
|
||||||
net.start_battle(0))
|
net.start_battle())
|
||||||
net.action_applied.connect(func():
|
net.action_applied.connect(func():
|
||||||
net.broadcast()
|
net.broadcast()
|
||||||
_act_host.call_deferred())
|
_act_host.call_deferred())
|
||||||
|
|||||||
@@ -15,12 +15,9 @@ func _initialize() -> void:
|
|||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 3
|
r.seed = 3
|
||||||
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
|
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
|
||||||
print("Vorschau 1..5 Gegner: %.1f ms" % ms(func():
|
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
||||||
for n in range(1, 6):
|
for n in [1]:
|
||||||
for i in n:
|
var g := Game.new_battle(r)
|
||||||
DeckGen.random_opponent(i + 1)))
|
|
||||||
for n in [1, 3, 5]:
|
|
||||||
var g := Game.new_battle(n, r)
|
|
||||||
# ein volles Brett für alle, damit die Entscheidung teuer wird
|
# ein volles Brett für alle, damit die Entscheidung teuer wird
|
||||||
var uid := 5000
|
var uid := 5000
|
||||||
for p in g.players:
|
for p in g.players:
|
||||||
@@ -41,7 +38,7 @@ func _initialize() -> void:
|
|||||||
var t_budget := ms(func(): AI.choose_action(g, h))
|
var t_budget := ms(func(): AI.choose_action(g, h))
|
||||||
var cands := AI.candidate_actions(g, h).size()
|
var cands := AI.candidate_actions(g, h).size()
|
||||||
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
|
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
|
||||||
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(5, r).clone(), 20))
|
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(r).clone(), 20))
|
||||||
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
|
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
|
||||||
print("Snapshot 6 Spieler: %.2f ms" % ms(func(): Game.new_battle(5, r).snapshot(0), 10))
|
print("Snapshot: %.2f ms" % ms(func(): Game.new_battle(r).snapshot(0), 10))
|
||||||
quit()
|
quit()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
## Balance-Simulation, die KI spielt auch den Menschen. Bei gleichen Regeln
|
## Balance-Simulation, die KI spielt auch den Menschen. Bei gleichen Regeln
|
||||||
## ist etwa 1/(Gegner+1) Siege zu erwarten:
|
## sind etwa 50 % Siege zu erwarten:
|
||||||
## godot --headless --path . -s tests/simulate.gd
|
## godot --headless --path . -s tests/simulate.gd
|
||||||
|
|
||||||
|
|
||||||
@@ -9,25 +9,37 @@ func _initialize() -> void:
|
|||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 0
|
r.seed = 0
|
||||||
var t0 := Time.get_ticks_msec()
|
var t0 := Time.get_ticks_msec()
|
||||||
print("Alle gierig (Erwartung etwa 1/(Gegner+1)):")
|
print("Beide gierig (Erwartung etwa 50 %):")
|
||||||
for n in range(1, Game.MAX_OPPONENTS + 1):
|
_run(games, r, true, true)
|
||||||
_run(n, games, r, true, true)
|
|
||||||
print("Du mit Vorausschau, Gegner gierig:")
|
print("Du mit Vorausschau, Gegner gierig:")
|
||||||
for n in [1, 2, 3]:
|
_run(games / 2, r, false, true)
|
||||||
_run(n, games / 2, r, false, true)
|
print("Beide mit Vorausschau:")
|
||||||
print("Alle mit Vorausschau:")
|
_run(games / 2, r, false, false)
|
||||||
_run(1, games / 2, r, false, false)
|
print("Kampagne (du mit Vorausschau), je Stufe:")
|
||||||
_run(2, games / 4, r, false, false)
|
for stage in [0, 4, 9, 14, 19, 24, 29]:
|
||||||
|
_campaign(stage, games / 4, r)
|
||||||
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
|
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
|
|
||||||
func _run(n_opp: int, games: int, r: RandomNumberGenerator,
|
func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> void:
|
||||||
human_greedy: bool, opp_greedy: bool) -> void:
|
var wins := 0
|
||||||
|
for i in games:
|
||||||
|
var g := Game.new_battle_with(Campaign.opponent(stage), r)
|
||||||
|
while g.winner == "":
|
||||||
|
AI.take_turn(g, g.current(), false)
|
||||||
|
assert(g.turn < 300, "Spiel endet nicht")
|
||||||
|
if g.winner == "human":
|
||||||
|
wins += 1
|
||||||
|
var st := Campaign.stage(stage)
|
||||||
|
print("Stufe %2d %-12s %2d Leben, %d Merkmale: Siege %3d%%" % [stage + 1, st.name, int(st.life), st.perks.size(), wins * 100 / games])
|
||||||
|
|
||||||
|
|
||||||
|
func _run(games: int, r: RandomNumberGenerator, human_greedy: bool, opp_greedy: bool) -> void:
|
||||||
var wins := 0
|
var wins := 0
|
||||||
var turns := 0
|
var turns := 0
|
||||||
for i in games:
|
for i in games:
|
||||||
var g := Game.new_battle(n_opp, r)
|
var g := Game.new_battle(r)
|
||||||
while g.winner == "":
|
while g.winner == "":
|
||||||
var p := g.current()
|
var p := g.current()
|
||||||
AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy)
|
AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy)
|
||||||
@@ -35,4 +47,4 @@ func _run(n_opp: int, games: int, r: RandomNumberGenerator,
|
|||||||
if g.winner == "human":
|
if g.winner == "human":
|
||||||
wins += 1
|
wins += 1
|
||||||
turns += g.turn
|
turns += g.turn
|
||||||
print("gegen %d: Siege %3d%% Ø Züge %.1f" % [n_opp, wins * 100 / games, float(turns) / games])
|
print("Siege %3d%% Ø Züge %.1f" % [wins * 100 / games, float(turns) / games])
|
||||||
|
|||||||
@@ -18,25 +18,23 @@ func C(id: String) -> Cards.Card:
|
|||||||
return Cards.by_id(id)
|
return Cards.by_id(id)
|
||||||
|
|
||||||
|
|
||||||
func setup(human_hand: Array = [], opp_hand: Array = [], n_opp: int = 1) -> Array:
|
## Duell mit vollem Mana; der Gegner hat 15 Leben. Liefert [Spiel, Spieler, Gegner].
|
||||||
|
func setup(human_hand: Array = [], opp_hand: Array = []) -> Array:
|
||||||
var human := Game.Player.new("Spieler", true, 30)
|
var human := Game.Player.new("Spieler", true, 30)
|
||||||
var opps := []
|
var o := Game.Player.new("Gegner", false, 15)
|
||||||
for i in n_opp:
|
|
||||||
opps.append(Game.Player.new("Gegner %d" % (i + 1), false, 15))
|
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 0
|
r.seed = 0
|
||||||
var g := Game.new(human, opps, r)
|
var g := Game.new(human, o, r)
|
||||||
g.turn = 1
|
g.turn = 1
|
||||||
human.max_energy = 10
|
human.max_energy = 10
|
||||||
human.energy = 10
|
human.energy = 10
|
||||||
for cid in human_hand:
|
for cid in human_hand:
|
||||||
human.hand.append(C(cid))
|
human.hand.append(C(cid))
|
||||||
for o in opps:
|
o.max_energy = 10
|
||||||
o.max_energy = 10
|
o.energy = 10
|
||||||
o.energy = 10
|
for cid in opp_hand:
|
||||||
for cid in opp_hand:
|
o.hand.append(C(cid))
|
||||||
o.hand.append(C(cid))
|
return [g, human, o]
|
||||||
return [g, human, opps]
|
|
||||||
|
|
||||||
|
|
||||||
var next_uid := 9000
|
var next_uid := 9000
|
||||||
@@ -60,7 +58,6 @@ func _initialize() -> void:
|
|||||||
test_wesen_spielen()
|
test_wesen_spielen()
|
||||||
test_flink()
|
test_flink()
|
||||||
test_waechter()
|
test_waechter()
|
||||||
test_waechter_nur_eigener_besitzer()
|
|
||||||
test_kampf_beidseitig()
|
test_kampf_beidseitig()
|
||||||
test_schild()
|
test_schild()
|
||||||
test_doppelschlag()
|
test_doppelschlag()
|
||||||
@@ -76,15 +73,13 @@ func _initialize() -> void:
|
|||||||
test_mana_effekt()
|
test_mana_effekt()
|
||||||
test_gleiche_regeln()
|
test_gleiche_regeln()
|
||||||
test_zugreihenfolge()
|
test_zugreihenfolge()
|
||||||
test_tote_gegner()
|
|
||||||
test_jeder_gegen_jeden()
|
|
||||||
test_gegnerzahl()
|
|
||||||
test_sieg_niederlage()
|
test_sieg_niederlage()
|
||||||
test_booster()
|
test_booster()
|
||||||
test_spielstand()
|
test_spielstand()
|
||||||
test_resonanz()
|
test_resonanz()
|
||||||
test_elementvorteil()
|
test_elementvorteil()
|
||||||
test_feld()
|
test_feld()
|
||||||
|
test_vorschau()
|
||||||
test_verschmelzen()
|
test_verschmelzen()
|
||||||
test_feldkombos()
|
test_feldkombos()
|
||||||
test_schild_effekt_und_sonne()
|
test_schild_effekt_und_sonne()
|
||||||
@@ -141,7 +136,7 @@ func test_flink() -> void:
|
|||||||
var s := setup(["l_falke"])
|
var s := setup(["l_falke"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
g.play_card(h, C("l_falke"))
|
g.play_card(h, C("l_falke"))
|
||||||
check(g.attack(h.board[0], o) == "", "Flink greift sofort an")
|
check(g.attack(h.board[0], o) == "", "Flink greift sofort an")
|
||||||
check(o.life == 12, "Falke 3 Schaden")
|
check(o.life == 12, "Falke 3 Schaden")
|
||||||
@@ -151,7 +146,7 @@ func test_waechter() -> void:
|
|||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_wolf")
|
var a := put(h, "f_wolf")
|
||||||
var w := put(o, "e_wall")
|
var w := put(o, "e_wall")
|
||||||
check(g.attack(a, o) != "", "Wächter blockt Angriff auf Held")
|
check(g.attack(a, o) != "", "Wächter blockt Angriff auf Held")
|
||||||
@@ -159,20 +154,11 @@ func test_waechter() -> void:
|
|||||||
check(w.health == 1 and a.health == 2, "Schaden verrechnet")
|
check(w.health == 1 and a.health == 2, "Schaden verrechnet")
|
||||||
|
|
||||||
|
|
||||||
func test_waechter_nur_eigener_besitzer() -> void:
|
|
||||||
var s := setup([], [], 2)
|
|
||||||
var g: Game = s[0]
|
|
||||||
var h: Game.Player = s[1]
|
|
||||||
var a := put(h, "f_wolf")
|
|
||||||
put(s[2][0], "e_wall")
|
|
||||||
check(g.attack(a, s[2][1]) == "" and s[2][1].life == 12, "Wächter schützt nur Besitzer")
|
|
||||||
|
|
||||||
|
|
||||||
func test_kampf_beidseitig() -> void:
|
func test_kampf_beidseitig() -> void:
|
||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_asche")
|
var a := put(h, "f_asche")
|
||||||
var d := put(o, "w_reiter")
|
var d := put(o, "w_reiter")
|
||||||
g.attack(a, d)
|
g.attack(a, d)
|
||||||
@@ -184,7 +170,7 @@ func test_schild() -> void:
|
|||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var sh := put(o, "ee_bastion")
|
var sh := put(o, "ee_bastion")
|
||||||
var a := put(h, "f_asche")
|
var a := put(h, "f_asche")
|
||||||
g.attack(a, sh)
|
g.attack(a, sh)
|
||||||
@@ -198,7 +184,7 @@ func test_doppelschlag() -> void:
|
|||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "l_schlange")
|
var a := put(h, "l_schlange")
|
||||||
g.attack(a, o)
|
g.attack(a, o)
|
||||||
g.attack(a, o)
|
g.attack(a, o)
|
||||||
@@ -210,7 +196,7 @@ func test_einfrieren() -> void:
|
|||||||
var s := setup([], ["w_frost"])
|
var s := setup([], ["w_frost"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_wolf")
|
var a := put(h, "f_wolf")
|
||||||
g.end_turn()
|
g.end_turn()
|
||||||
check(g.play_card(o, C("w_frost"), a) == "", "Frostmagier spielen")
|
check(g.play_card(o, C("w_frost"), a) == "", "Frostmagier spielen")
|
||||||
@@ -226,30 +212,29 @@ func test_effekt_ziel() -> void:
|
|||||||
var s := setup(["w_frost", "f_ritter"])
|
var s := setup(["w_frost", "f_ritter"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
check(g.play_card(h, C("w_frost")) == "" and h.board.size() == 1, "ohne Ziel kommt das Wesen trotzdem, der Effekt verpufft")
|
check(g.play_card(h, C("w_frost")) == "" and h.board.size() == 1, "ohne Ziel kommt das Wesen trotzdem, der Effekt verpufft")
|
||||||
g.play_card(h, C("f_ritter"), o)
|
g.play_card(h, C("f_ritter"), o)
|
||||||
check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden")
|
check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden")
|
||||||
|
|
||||||
|
|
||||||
func test_flaechenschaden() -> void:
|
func test_flaechenschaden() -> void:
|
||||||
var s := setup(["ff_phoenix"], [], 2)
|
var s := setup(["ff_phoenix"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o1: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var o2: Game.Player = s[2][1]
|
put(o, "f_kobold")
|
||||||
put(o1, "f_kobold")
|
put(o, "e_golem")
|
||||||
put(o2, "e_golem")
|
|
||||||
g.play_card(h, C("ff_phoenix"))
|
g.play_card(h, C("ff_phoenix"))
|
||||||
check(o1.life == 11 and o2.life == 11, "Helden 4 Schaden")
|
check(o.life == 11, "Held 4 Schaden")
|
||||||
check(o1.board.is_empty() and o2.board[0].health == 1, "Wesen getroffen")
|
check(o.board.size() == 1 and o.board[0].health == 1, "Wesen getroffen, Kobold tot")
|
||||||
|
|
||||||
|
|
||||||
func test_zurueck_auf_hand() -> void:
|
func test_zurueck_auf_hand() -> void:
|
||||||
var s := setup(["wl_nebel"])
|
var s := setup(["wl_nebel"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var d := put(o, "e_troll")
|
var d := put(o, "e_troll")
|
||||||
g.play_card(h, C("wl_nebel"), d)
|
g.play_card(h, C("wl_nebel"), d)
|
||||||
check(o.board.is_empty() and C("e_troll") in o.hand, "zurück auf Hand")
|
check(o.board.is_empty() and C("e_troll") in o.hand, "zurück auf Hand")
|
||||||
@@ -285,11 +270,10 @@ func test_limits() -> void:
|
|||||||
|
|
||||||
## Grenzfälle, die schon einmal falsch waren.
|
## Grenzfälle, die schon einmal falsch waren.
|
||||||
func test_grenzfaelle() -> void:
|
func test_grenzfaelle() -> void:
|
||||||
var s := setup(["f_wolf"], [], 2)
|
var s := setup(["f_wolf"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o1: Game.Player = s[2][0]
|
var o1: Game.Player = s[2]
|
||||||
var o2: Game.Player = s[2][1]
|
|
||||||
# Nur Karten von der eigenen Hand
|
# Nur Karten von der eigenen Hand
|
||||||
check(not g.can_play(h, C("e_troll"))[0] and g.play_card(h, C("e_troll")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
|
check(not g.can_play(h, C("e_troll"))[0] and g.play_card(h, C("e_troll")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
|
||||||
check(g.play_card(h, C("f_wolf")) == "" and h.board.size() == 1, "eigene Handkarte geht")
|
check(g.play_card(h, C("f_wolf")) == "" and h.board.size() == 1, "eigene Handkarte geht")
|
||||||
@@ -298,7 +282,7 @@ func test_grenzfaelle() -> void:
|
|||||||
o1.life = 0
|
o1.life = 0
|
||||||
g.cleanup()
|
g.cleanup()
|
||||||
check(o1.board.is_empty() and not o1.alive() and g.log.any(func(l): return l.find("verschwindet mit") >= 0), "besiegter Spieler räumt sein Feld")
|
check(o1.board.is_empty() and not o1.alive() and g.log.any(func(l): return l.find("verschwindet mit") >= 0), "besiegter Spieler räumt sein Feld")
|
||||||
check(not (o1 in g.enemies_of(h)) and o2 in g.enemies_of(h), "Besiegte sind keine Ziele mehr")
|
check(g.enemies_of(h).is_empty(), "Besiegte sind keine Ziele mehr")
|
||||||
check(dead != null, "Wesen war vorher da")
|
check(dead != null, "Wesen war vorher da")
|
||||||
# Verschmolzene Karte auf der Hand übersteht den Spielstand
|
# Verschmolzene Karte auf der Hand übersteht den Spielstand
|
||||||
var a := put(h, "f_kobold")
|
var a := put(h, "f_kobold")
|
||||||
@@ -314,10 +298,10 @@ func test_grenzfaelle() -> void:
|
|||||||
## Zweite Runde Grenzfälle: Konter ohne Angriff, Chimäre, Ereignisse mit verschmolzener Karte,
|
## Zweite Runde Grenzfälle: Konter ohne Angriff, Chimäre, Ereignisse mit verschmolzener Karte,
|
||||||
## ausgeschiedener Netzwerkspieler mitten im eigenen Zug.
|
## ausgeschiedener Netzwerkspieler mitten im eigenen Zug.
|
||||||
func test_grenzfaelle_2() -> void:
|
func test_grenzfaelle_2() -> void:
|
||||||
var s := setup([], [], 2)
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o1: Game.Player = s[2][0]
|
var o1: Game.Player = s[2]
|
||||||
# Ein Wesen ohne Angriff schlägt nicht zurück, auch nicht mit Elementvorteil
|
# Ein Wesen ohne Angriff schlägt nicht zurück, auch nicht mit Elementvorteil
|
||||||
var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser
|
var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser
|
||||||
var reiter := put(h, "w_reiter") # Wasser 3/3
|
var reiter := put(h, "w_reiter") # Wasser 3/3
|
||||||
@@ -347,10 +331,10 @@ func test_grenzfaelle_2() -> void:
|
|||||||
net.game = g
|
net.game = g
|
||||||
net.rejoin_seconds = 0.0
|
net.rejoin_seconds = 0.0
|
||||||
net.waiting = {"Spieler": {"seat": g.players.find(h), "since": 0.0}}
|
net.waiting = {"Spieler": {"seat": g.players.find(h), "since": 0.0}}
|
||||||
o1.is_human = true # noch ein Mensch am Tisch, sonst wäre die Partie schlicht verloren
|
o1.is_human = true # ein Duell zweier Menschen
|
||||||
check(g.current() == h, "Spieler ist dran")
|
check(g.current() == h, "Spieler ist dran")
|
||||||
net.tick()
|
net.tick()
|
||||||
check(not h.alive() and h.board.is_empty() and g.current() != h, "Ausgeschiedener räumt das Feld und der Zug geht weiter")
|
check(not h.alive() and h.board.is_empty() and g.winner != "" and g.winner_player == o1, "Ausgeschiedener räumt das Feld, der andere gewinnt")
|
||||||
net.free()
|
net.free()
|
||||||
|
|
||||||
|
|
||||||
@@ -421,7 +405,7 @@ func test_mana_effekt() -> void:
|
|||||||
func test_gleiche_regeln() -> void:
|
func test_gleiche_regeln() -> void:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 0
|
r.seed = 0
|
||||||
var g := Game.new_battle(3, r)
|
var g := Game.new_battle(r)
|
||||||
var first: Game.Player = g.players[0]
|
var first: Game.Player = g.players[0]
|
||||||
check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Mana, kein Bonus")
|
check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Mana, kein Bonus")
|
||||||
check(first.shop.size() == Game.SHOP_SIZE and first.hand.is_empty(), "jeder beginnt mit einem Laden und leerer Hand")
|
check(first.shop.size() == Game.SHOP_SIZE and first.hand.is_empty(), "jeder beginnt mit einem Laden und leerer Hand")
|
||||||
@@ -432,9 +416,9 @@ func test_gleiche_regeln() -> void:
|
|||||||
g.end_turn()
|
g.end_turn()
|
||||||
var second: Game.Player = g.players[1]
|
var second: Game.Player = g.players[1]
|
||||||
check(g.current() == second and second.max_energy == 1 and second.energy == 2, "Nachzieher: 1 Mana plus Ausgleich")
|
check(g.current() == second and second.max_energy == 1 and second.energy == 2, "Nachzieher: 1 Mana plus Ausgleich")
|
||||||
for i in 4:
|
for i in 2:
|
||||||
g.end_turn()
|
g.end_turn()
|
||||||
check(g.current() == second and second.max_energy == 2, "Ausgleich nur einmal")
|
check(g.current() == second and second.max_energy == 2 and second.energy == 4, "Ausgleich nur einmal: 2 übrig plus 2 Einkommen")
|
||||||
|
|
||||||
|
|
||||||
func test_zugreihenfolge() -> void:
|
func test_zugreihenfolge() -> void:
|
||||||
@@ -442,9 +426,9 @@ func test_zugreihenfolge() -> void:
|
|||||||
for seed in 12:
|
for seed in 12:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = seed
|
r.seed = seed
|
||||||
var g := Game.new_battle(2, r)
|
var g := Game.new_battle(r)
|
||||||
starters[g.players[0].name] = true
|
starters[g.players[0].name] = true
|
||||||
check(g.players.size() == 3 and g.human in g.players and g.opponents.size() == 2, "alle am Tisch")
|
check(g.players.size() == 2 and g.human in g.players and g.opponent in g.players, "beide am Tisch")
|
||||||
for i in g.players.size():
|
for i in g.players.size():
|
||||||
check(g.players[i].order == i, "Reihenfolge nummeriert")
|
check(g.players[i].order == i, "Reihenfolge nummeriert")
|
||||||
check(g.log[0].begins_with("Zugreihenfolge:"), "Reihenfolge im Verlauf")
|
check(g.log[0].begins_with("Zugreihenfolge:"), "Reihenfolge im Verlauf")
|
||||||
@@ -453,58 +437,15 @@ func test_zugreihenfolge() -> void:
|
|||||||
check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size())
|
check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size())
|
||||||
|
|
||||||
|
|
||||||
func test_tote_gegner() -> void:
|
|
||||||
var s := setup([], [], 2)
|
|
||||||
var g: Game = s[0]
|
|
||||||
s[2][0].life = 0
|
|
||||||
g.end_turn()
|
|
||||||
check(g.current() == s[2][1], "toter Gegner übersprungen")
|
|
||||||
g.end_turn()
|
|
||||||
check(g.current() == s[1] and g.turn == 2, "zurück beim Spieler, Zug 2")
|
|
||||||
|
|
||||||
|
|
||||||
func test_jeder_gegen_jeden() -> void:
|
|
||||||
var s := setup([], [], 2)
|
|
||||||
var g: Game = s[0]
|
|
||||||
var h: Game.Player = s[1]
|
|
||||||
var o1: Game.Player = s[2][0]
|
|
||||||
var o2: Game.Player = s[2][1]
|
|
||||||
check(g.enemies_of(o1) == [h, o2], "Gegner bekämpft Spieler und anderen Gegner")
|
|
||||||
check(g.enemies_of(h) == [o1, o2], "Spieler bekämpft alle Gegner")
|
|
||||||
g.end_turn()
|
|
||||||
o1.hand = []
|
|
||||||
var a := put(o1, "f_wolf")
|
|
||||||
check(g.can_attack(a, o2)[0], "Gegner darf Gegner angreifen")
|
|
||||||
check(g.attack(a, o2) == "" and o2.life == 12, "Gegner trifft Gegner")
|
|
||||||
check(AI.choose_action(g, o1).type == "buy", "KI kauft aus ihrem Laden")
|
|
||||||
o1.shop = []
|
|
||||||
check(AI.choose_action(g, o1).type == "end", "ohne Angebot und ohne Angriff endet der Zug")
|
|
||||||
o2.life = 2
|
|
||||||
var b := put(o1, "f_kobold")
|
|
||||||
check(AI.choose_action(g, o1).target == o2, "KI nutzt Lethal gegen anderen Gegner")
|
|
||||||
|
|
||||||
|
|
||||||
func test_gegnerzahl() -> void:
|
|
||||||
var r := RandomNumberGenerator.new()
|
|
||||||
r.seed = 2
|
|
||||||
for n in range(1, Game.MAX_OPPONENTS + 1):
|
|
||||||
var g := Game.new_battle(n, r)
|
|
||||||
check(g.opponents.size() == n, "%d Gegner" % n)
|
|
||||||
check(g.players.all(func(o): return o.life == Game.START_LIFE), "gleiches Leben bei %d" % n)
|
|
||||||
check(g.players[0].max_energy == 1 and g.current() == g.players[0], "Startspieler bei %d" % n)
|
|
||||||
check(g.human.shop.size() == Game.SHOP_SIZE, "Laden bei %d" % n)
|
|
||||||
check(Profile.REWARD_WIN.has(n), "Belohnung bei %d" % n)
|
|
||||||
|
|
||||||
|
|
||||||
func test_sieg_niederlage() -> void:
|
func test_sieg_niederlage() -> void:
|
||||||
var s := setup([], [], 2)
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
g.damage(s[2][0], 99)
|
g.damage(s[2], 14)
|
||||||
g.check_end()
|
g.check_end()
|
||||||
check(g.winner == "", "noch offen")
|
check(g.winner == "", "noch offen")
|
||||||
g.damage(s[2][1], 99)
|
g.damage(s[2], 1)
|
||||||
g.check_end()
|
g.check_end()
|
||||||
check(g.winner == "human", "Sieg")
|
check(g.winner == "human" and g.winner_player == s[1], "Sieg")
|
||||||
var s2 := setup()
|
var s2 := setup()
|
||||||
var g2: Game = s2[0]
|
var g2: Game = s2[0]
|
||||||
g2.damage(s2[1], 30)
|
g2.damage(s2[1], 30)
|
||||||
@@ -562,7 +503,7 @@ func test_ki_lethal() -> void:
|
|||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
o.life = 3
|
o.life = 3
|
||||||
var a := put(h, "f_asche")
|
var a := put(h, "f_asche")
|
||||||
put(o, "w_reiter")
|
put(o, "w_reiter")
|
||||||
@@ -573,23 +514,20 @@ func test_ki_lethal() -> void:
|
|||||||
func test_ki_spiele_enden() -> void:
|
func test_ki_spiele_enden() -> void:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 7
|
r.seed = 7
|
||||||
for n in [1, 3, 5]:
|
for i in 12:
|
||||||
for i in 6:
|
var g := Game.new_battle(r)
|
||||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
var guard := 0
|
||||||
Game.shuffle(pool, r)
|
while g.winner == "" and guard < 300:
|
||||||
var g := Game.new_battle(n, r)
|
AI.take_turn(g, g.current(), i % 2 == 0)
|
||||||
var guard := 0
|
guard += 1
|
||||||
while g.winner == "" and guard < 300:
|
check(g.winner != "", "Spiel endet (Partie %d)" % i)
|
||||||
AI.take_turn(g, g.current(), i % 2 == 0)
|
|
||||||
guard += 1
|
|
||||||
check(g.winner != "", "Spiel endet (%d Gegner)" % n)
|
|
||||||
|
|
||||||
|
|
||||||
func test_resonanz() -> void:
|
func test_resonanz() -> void:
|
||||||
var s := setup(["f_wolf", "f_kobold", "e_wall"])
|
var s := setup(["f_wolf", "f_kobold", "e_wall"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
g.play_card(h, C("f_wolf"))
|
g.play_card(h, C("f_wolf"))
|
||||||
check(h.board[0].power() == 3, "allein keine Resonanz")
|
check(h.board[0].power() == 3, "allein keine Resonanz")
|
||||||
g.play_card(h, C("f_kobold"))
|
g.play_card(h, C("f_kobold"))
|
||||||
@@ -621,7 +559,7 @@ func test_elementvorteil() -> void:
|
|||||||
var s := setup(["f_ritter"])
|
var s := setup(["f_ritter"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var mine := put(h, "w_reiter") # Wasser 3/3
|
var mine := put(h, "w_reiter") # Wasser 3/3
|
||||||
var his := put(o, "f_wolf") # Feuer 3/2
|
var his := put(o, "f_wolf") # Feuer 3/2
|
||||||
g.attack(mine, his)
|
g.attack(mine, his)
|
||||||
@@ -633,7 +571,7 @@ func test_elementvorteil() -> void:
|
|||||||
s = setup(["f_ritter"])
|
s = setup(["f_ritter"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
o = s[2][0]
|
o = s[2]
|
||||||
var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft
|
var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft
|
||||||
g.play_card(h, C("f_ritter"), air)
|
g.play_card(h, C("f_ritter"), air)
|
||||||
check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr")
|
check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr")
|
||||||
@@ -644,7 +582,7 @@ func test_feld() -> void:
|
|||||||
var s := setup(["f_ritter"])
|
var s := setup(["f_ritter"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
check(g.field_element == "" and int(g.field_charge["Feuer"]) == 0, "das Feld beginnt neutral")
|
check(g.field_element == "" and int(g.field_charge["Feuer"]) == 0, "das Feld beginnt neutral")
|
||||||
var wolf := put(h, "f_wolf")
|
var wolf := put(h, "f_wolf")
|
||||||
for i in Cards.FIELD_CHARGE - 1:
|
for i in Cards.FIELD_CHARGE - 1:
|
||||||
@@ -669,11 +607,34 @@ func test_feld() -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Verschmelzen: zwei eigene Wesen werden eins, Werte Durchschnitt plus 1.
|
## Verschmelzen: zwei eigene Wesen werden eins, Werte Durchschnitt plus 1.
|
||||||
|
## Vorschau fürs Ausspielen: Resonanz, Feldbonus und Feldkombo mit Partner.
|
||||||
|
func test_vorschau() -> void:
|
||||||
|
var s := setup(["f_kobold", "e_wall"])
|
||||||
|
var g: Game = s[0]
|
||||||
|
var h: Game.Player = s[1]
|
||||||
|
var f := g.forecast(h, C("f_kobold"))
|
||||||
|
check(f.resonance == 0 and f.field == 0 and f.combo == "" and f.partner == null, "leeres Feld: keine Vorschau")
|
||||||
|
var wolf := put(h, "f_wolf")
|
||||||
|
put(h, "f_asche")
|
||||||
|
var reiter := put(h, "w_reiter")
|
||||||
|
f = g.forecast(h, C("f_kobold"))
|
||||||
|
check(f.resonance == 2 and f.combo == "Dampf" and f.partner == reiter, "zwei Feuerpartner geben +2, das Wasserwesen die Kombo Dampf")
|
||||||
|
f = g.forecast(h, C("e_wall"))
|
||||||
|
check(f.resonance == 0 and f.combo == "Lava" and f.partner == wolf, "Erde: kein Partner, Kombo Lava mit dem ersten Feuerwesen")
|
||||||
|
g.field_element = "Feuer"
|
||||||
|
check(g.forecast(h, C("f_kobold")).field == 1 and g.forecast(h, C("e_wall")).field == 0, "der Feldbonus zählt nur fürs eigene Element")
|
||||||
|
h.perks.append("resonanz_doppelt")
|
||||||
|
check(g.forecast(h, C("f_kobold")).resonance == 4, "doppelte Resonanz verdoppelt die Vorschau")
|
||||||
|
var real := g.play_card(h, C("f_kobold"))
|
||||||
|
var kobold: Game.Creature = h.board[-1]
|
||||||
|
check(real == "" and kobold.power() == 2 + 4 + 1, "das Ausspielen bringt genau den vorhergesagten Angriff")
|
||||||
|
|
||||||
|
|
||||||
func test_verschmelzen() -> void:
|
func test_verschmelzen() -> void:
|
||||||
var s := setup()
|
var s := setup()
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_wolf") # Feuer 3/2, Kosten 2
|
var a := put(h, "f_wolf") # Feuer 3/2, Kosten 2
|
||||||
var b := put(h, "w_reiter") # Wasser 3/3, Kosten 2
|
var b := put(h, "w_reiter") # Wasser 3/3, Kosten 2
|
||||||
check(Cards.fusion_cost(a.card, b.card) == 2 and Cards.fusion_cost(C("f_kobold"), C("f_asche")) == 2, "Kosten: Durchschnitt, aufgerundet")
|
check(Cards.fusion_cost(a.card, b.card) == 2 and Cards.fusion_cost(C("f_kobold"), C("f_asche")) == 2, "Kosten: Durchschnitt, aufgerundet")
|
||||||
@@ -719,7 +680,7 @@ func test_feldkombos() -> void:
|
|||||||
var s := setup(["w_reiter"])
|
var s := setup(["w_reiter"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
put(h, "f_wolf")
|
put(h, "f_wolf")
|
||||||
var weak := put(o, "f_kobold")
|
var weak := put(o, "f_kobold")
|
||||||
var strong := put(o, "e_troll")
|
var strong := put(o, "e_troll")
|
||||||
@@ -730,20 +691,19 @@ func test_feldkombos() -> void:
|
|||||||
s = setup(["e_golem"])
|
s = setup(["e_golem"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
o = s[2][0]
|
o = s[2]
|
||||||
put(h, "f_wolf")
|
put(h, "f_wolf")
|
||||||
var k := put(o, "f_kobold")
|
var k := put(o, "f_kobold")
|
||||||
var big := put(o, "e_troll")
|
var big := put(o, "e_troll")
|
||||||
g.play_card(h, C("e_golem"))
|
g.play_card(h, C("e_golem"))
|
||||||
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
|
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
|
||||||
# Blitz: 2 Schaden an den schwächsten Gegner
|
# Blitz: 2 Schaden an den Gegner
|
||||||
s = setup(["l_wind"], [], 2)
|
s = setup(["l_wind"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
put(h, "f_wolf")
|
put(h, "f_wolf")
|
||||||
s[2][1].life = 5
|
|
||||||
g.play_card(h, C("l_wind"))
|
g.play_card(h, C("l_wind"))
|
||||||
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner")
|
check(s[2].life == 13, "Blitz trifft den Gegner")
|
||||||
# Schlamm heilt, Sturm gibt Mana, Sand gibt +1/+1
|
# Schlamm heilt, Sturm gibt Mana, Sand gibt +1/+1
|
||||||
s = setup(["e_wall", "l_wind", "w_reiter"])
|
s = setup(["e_wall", "l_wind", "w_reiter"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
@@ -773,7 +733,7 @@ func test_feldkombos() -> void:
|
|||||||
s = setup(["fe_golem"])
|
s = setup(["fe_golem"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
o = s[2][0]
|
o = s[2]
|
||||||
put(h, "w_reiter")
|
put(h, "w_reiter")
|
||||||
var t := put(o, "e_troll")
|
var t := put(o, "e_troll")
|
||||||
g.play_card(h, C("fe_golem"))
|
g.play_card(h, C("fe_golem"))
|
||||||
@@ -784,7 +744,7 @@ func test_schild_effekt_und_sonne() -> void:
|
|||||||
var s := setup(["el_duenen", "fff_sonne"])
|
var s := setup(["el_duenen", "fff_sonne"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_wolf")
|
var a := put(h, "f_wolf")
|
||||||
g.play_card(h, C("el_duenen"), a)
|
g.play_card(h, C("el_duenen"), a)
|
||||||
check(a.shield, "Dünenläufer gibt Schild")
|
check(a.shield, "Dünenläufer gibt Schild")
|
||||||
@@ -792,7 +752,7 @@ func test_schild_effekt_und_sonne() -> void:
|
|||||||
s = setup(["fff_sonne"])
|
s = setup(["fff_sonne"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
o = s[2][0]
|
o = s[2]
|
||||||
check(C("fff_sonne").elements == ["Feuer"] and C("fff_sonne").cost == 9, "Kern der Sonne ist ein teures Feuerwesen")
|
check(C("fff_sonne").elements == ["Feuer"] and C("fff_sonne").cost == 9, "Kern der Sonne ist ein teures Feuerwesen")
|
||||||
var troll := put(o, "e_troll")
|
var troll := put(o, "e_troll")
|
||||||
g.play_card(h, C("fff_sonne"))
|
g.play_card(h, C("fff_sonne"))
|
||||||
@@ -803,7 +763,7 @@ func test_kopie() -> void:
|
|||||||
var s := setup(["f_wolf"])
|
var s := setup(["f_wolf"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
var a := put(h, "f_asche")
|
var a := put(h, "f_asche")
|
||||||
var g2 := g.clone()
|
var g2 := g.clone()
|
||||||
var a2 := g2.find_creature(a.uid)
|
var a2 := g2.find_creature(a.uid)
|
||||||
@@ -823,7 +783,7 @@ func test_ki_besser_als_gierig() -> void:
|
|||||||
for i in games:
|
for i in games:
|
||||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
var pool: Array = Cards.ELEMENTS.duplicate()
|
||||||
Game.shuffle(pool, r)
|
Game.shuffle(pool, r)
|
||||||
var g := Game.new_battle(1, r)
|
var g := Game.new_battle(r)
|
||||||
# abwechselnd beginnt die schlaue oder die gierige KI
|
# abwechselnd beginnt die schlaue oder die gierige KI
|
||||||
var smart_is_human := i % 2 == 0
|
var smart_is_human := i % 2 == 0
|
||||||
var guard := 0
|
var guard := 0
|
||||||
@@ -855,22 +815,22 @@ func test_kampagne() -> void:
|
|||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 4
|
r.seed = 4
|
||||||
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
||||||
var opps := Campaign.opponents(4)
|
var boss := Campaign.opponent(4)
|
||||||
check(opps.size() == 3 and opps[0].name == "Vulkar" and opps[0].title == "Herr der Lava", "Stufe 5: Vulkar mit 2 Begleitern")
|
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 32, "Stufe 5: Vulkar mit 32 Leben")
|
||||||
check(opps[0].elements == ["Feuer", "Erde"] and opps[0].shop.all(func(c): return c.elements.all(func(e): return e in ["Feuer", "Erde"])), "Boss handelt nur mit seinen Elementen")
|
check(boss.elements == ["Feuer", "Erde"] and boss.shop.all(func(c): return c.elements.all(func(e): return e in ["Feuer", "Erde"])), "Boss handelt nur mit seinen Elementen")
|
||||||
var g := Game.new_battle_with(opps, r)
|
var g := Game.new_battle_with(boss, r)
|
||||||
check(g.opponents.size() == 3 and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
|
check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
|
||||||
var p := Profile.starter()
|
var p := Profile.starter()
|
||||||
var reward := p.record_battle(true, 1, 0)
|
var reward := p.record_battle(true, 0)
|
||||||
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
|
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
|
||||||
reward = p.record_battle(true, 1, 0)
|
reward = p.record_battle(true, 0)
|
||||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN[1], "Wiederholung gibt normale Belohnung")
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung")
|
||||||
reward = p.record_battle(false, 2, 1)
|
reward = p.record_battle(false, 1)
|
||||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
||||||
p.campaign_stage = 9
|
p.campaign_stage = 9
|
||||||
p.record_battle(true, 5, 9)
|
p.record_battle(true, 9)
|
||||||
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
|
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
|
||||||
var sim := Game.new_battle_with(Campaign.opponents(0), r)
|
var sim := Game.new_battle_with(Campaign.opponent(0), r)
|
||||||
var guard := 0
|
var guard := 0
|
||||||
while sim.winner == "" and guard < 300:
|
while sim.winner == "" and guard < 300:
|
||||||
AI.take_turn(sim, sim.current(), true)
|
AI.take_turn(sim, sim.current(), true)
|
||||||
@@ -916,7 +876,7 @@ func test_merkmale() -> void:
|
|||||||
var s := setup(["f_wolf", "l_falke", "e_wall"])
|
var s := setup(["f_wolf", "l_falke", "e_wall"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
h.perks = ["feuer_angriff", "flink_angriff", "waechter_leben", "schild_wesen", "alle_flink", "resonanz_doppelt"]
|
h.perks = ["feuer_angriff", "flink_angriff", "waechter_leben", "schild_wesen", "alle_flink", "resonanz_doppelt"]
|
||||||
g.play_card(h, C("f_wolf"))
|
g.play_card(h, C("f_wolf"))
|
||||||
var wolf: Game.Creature = h.board[0]
|
var wolf: Game.Creature = h.board[0]
|
||||||
@@ -932,15 +892,15 @@ func test_merkmale() -> void:
|
|||||||
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
|
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 2
|
r.seed = 2
|
||||||
var mara: Game.Player = Campaign.opponents(1)[0]
|
var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1), r).opponent
|
||||||
check(mara.life == 35 and mara.perks == ["leben_35"], "Mara hat 35 Leben")
|
check(mara.life == 35 and mara.max_life == 35 and mara.perks == ["leben_35"], "Mara hat mit ihrem Merkmal 35 Leben")
|
||||||
var g3 := Game.new_battle_with(Campaign.opponents(9), r, true)
|
var g3 := Game.new_battle_with(Campaign.opponent(9), r, true)
|
||||||
var archi: Game.Player = g3.opponents[0]
|
var archi: Game.Player = g3.opponent
|
||||||
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
|
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
|
||||||
g3.end_turn()
|
g3.end_turn()
|
||||||
check(g3.current() == archi and archi.energy == 3 + 1, "Archimagier beginnt mit 3 Mana (plus Ausgleich)")
|
check(g3.current() == archi and archi.energy == 3 + 1, "Archimagier beginnt mit 3 Mana (plus Ausgleich)")
|
||||||
var nyx: Game.Player = Campaign.opponents(5)[0]
|
var nyx: Game.Player = Campaign.opponent(5)
|
||||||
var g4 := Game.new_battle_with([nyx], r, true)
|
var g4 := Game.new_battle_with(nyx, r, true)
|
||||||
g4.end_turn()
|
g4.end_turn()
|
||||||
# Einkommen 1, dazu 1 Mana für ihr Merkmal und 1 als Ausgleich für den späteren Start
|
# Einkommen 1, dazu 1 Mana für ihr Merkmal und 1 als Ausgleich für den späteren Start
|
||||||
check(nyx.max_energy == 1 and nyx.energy == 3, "Nyx bekommt 1 Mana mehr als ihr Einkommen")
|
check(nyx.max_energy == 1 and nyx.energy == 3, "Nyx bekommt 1 Mana mehr als ihr Einkommen")
|
||||||
@@ -952,7 +912,7 @@ func test_ki_zwei_schritte() -> void:
|
|||||||
var s := setup(["el_koenig"])
|
var s := setup(["el_koenig"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
h.turns = 2
|
h.turns = 2
|
||||||
o.life = 5
|
o.life = 5
|
||||||
put(h, "f_wolf", false)
|
put(h, "f_wolf", false)
|
||||||
@@ -966,10 +926,10 @@ func test_ki_zwei_schritte() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_snapshot() -> void:
|
func test_snapshot() -> void:
|
||||||
var s := setup(["f_wolf"], ["w_frost"], 2)
|
var s := setup(["f_wolf"], ["w_frost"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2][0]
|
var o: Game.Player = s[2]
|
||||||
g.play_card(h, C("f_wolf"))
|
g.play_card(h, C("f_wolf"))
|
||||||
put(o, "e_troll")
|
put(o, "e_troll")
|
||||||
var hi := g.players.find(h)
|
var hi := g.players.find(h)
|
||||||
@@ -979,7 +939,7 @@ func test_snapshot() -> void:
|
|||||||
var snap := g.snapshot(hi)
|
var snap := g.snapshot(hi)
|
||||||
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
||||||
var g2 := Game.from_snapshot(json)
|
var g2 := Game.from_snapshot(json)
|
||||||
check(g2.players.size() == 3 and g2.turn == g.turn and g2.current_idx == g.current_idx, "Grunddaten")
|
check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx, "Grunddaten")
|
||||||
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar")
|
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar")
|
||||||
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_frost"), "fremde Hand nur als Anzahl")
|
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_frost"), "fremde Hand nur als Anzahl")
|
||||||
check(g2.players[oi].shop.is_empty() and g2.players[hi].shop.size() == Game.SHOP_SIZE, "eigener Laden sichtbar, fremder nicht")
|
check(g2.players[oi].shop.is_empty() and g2.players[hi].shop.size() == Game.SHOP_SIZE, "eigener Laden sichtbar, fremder nicht")
|
||||||
@@ -1002,17 +962,17 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
|
|||||||
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 8
|
r.seed = 8
|
||||||
var opps := Campaign.opponents(19)
|
var archi2 := Campaign.opponent(19)
|
||||||
check(opps.size() == 5 and opps[0].name == "Archimagier" and opps[0].perks.size() == 3, "letzte Stufe: Archimagier mit drei Merkmalen, voller Tisch")
|
check(archi2.name == "Archimagier" and archi2.perks.size() == 3 and archi2.life == 34, "letzte Stufe von Kapitel 2: Archimagier mit drei Merkmalen und 34 Leben")
|
||||||
var p := Profile.starter()
|
var p := Profile.starter()
|
||||||
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
|
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
|
||||||
p.campaign_stage = 10
|
p.campaign_stage = 10
|
||||||
check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet")
|
check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet")
|
||||||
p.perk = "feuer_angriff"
|
p.perk = "feuer_angriff"
|
||||||
var g := Game.new_battle(1, r, {"human_perks": [p.perk]})
|
var g := Game.new_battle(r, {"human_perks": [p.perk]})
|
||||||
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
||||||
p.campaign_stage = 19
|
p.campaign_stage = 19
|
||||||
p.record_battle(true, 5, 19)
|
p.record_battle(true, 19)
|
||||||
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
||||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
||||||
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
||||||
@@ -1021,7 +981,7 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
|
|||||||
func test_ki_zeitbudget() -> void:
|
func test_ki_zeitbudget() -> void:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 12
|
r.seed = 12
|
||||||
var g := Game.new_battle(5, r)
|
var g := Game.new_battle(r)
|
||||||
var h: Game.Player = g.players[0]
|
var h: Game.Player = g.players[0]
|
||||||
h.energy = 10
|
h.energy = 10
|
||||||
h.max_energy = 10
|
h.max_energy = 10
|
||||||
@@ -1058,7 +1018,7 @@ func test_englische_karten() -> void:
|
|||||||
func test_kampf_speichern() -> void:
|
func test_kampf_speichern() -> void:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 7
|
r.seed = 7
|
||||||
var g := Game.new_battle(2, r)
|
var g := Game.new_battle(r)
|
||||||
g.start()
|
g.start()
|
||||||
AI.take_turn(g, g.current())
|
AI.take_turn(g, g.current())
|
||||||
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
|
||||||
@@ -1075,7 +1035,7 @@ func test_kampf_speichern() -> void:
|
|||||||
while g2.winner == "" and guard < 300:
|
while g2.winner == "" and guard < 300:
|
||||||
AI.take_turn(g2, g2.current())
|
AI.take_turn(g2, g2.current())
|
||||||
guard += 1
|
guard += 1
|
||||||
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar")
|
check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar")
|
||||||
var s2: Dictionary = g.snapshot(-2)
|
var s2: Dictionary = g.snapshot(-2)
|
||||||
check(s2.players.all(func(p): return not (p.hand is Array) and p.shop.is_empty()), "Zuschauer-Sicht ohne Hände und Läden")
|
check(s2.players.all(func(p): return not (p.hand is Array) and p.shop.is_empty()), "Zuschauer-Sicht ohne Hände und Läden")
|
||||||
|
|
||||||
|
|||||||
148
tests/ui_test.gd
148
tests/ui_test.gd
@@ -26,6 +26,7 @@ func frames(n: int = 2) -> void:
|
|||||||
func shot(name: String) -> void:
|
func shot(name: String) -> void:
|
||||||
if shots == "":
|
if shots == "":
|
||||||
return
|
return
|
||||||
|
await frames(10) # Einblenden und Austeilen abwarten
|
||||||
await RenderingServer.frame_post_draw
|
await RenderingServer.frame_post_draw
|
||||||
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
|
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
|
||||||
|
|
||||||
@@ -142,18 +143,15 @@ func _ready() -> void:
|
|||||||
for suffix in ["", ".bak", ".corrupt"]:
|
for suffix in ["", ".bak", ".corrupt"]:
|
||||||
if FileAccess.file_exists(SAVE + suffix):
|
if FileAccess.file_exists(SAVE + suffix):
|
||||||
DirAccess.remove_absolute(abs + suffix)
|
DirAccess.remove_absolute(abs + suffix)
|
||||||
if FileAccess.file_exists(SAVE.get_basename() + "_kampf.json"):
|
|
||||||
DirAccess.remove_absolute(abs.get_basename() + "_kampf.json")
|
|
||||||
main = load("res://scenes/main.tscn").instantiate()
|
main = load("res://scenes/main.tscn").instantiate()
|
||||||
main.save_path = SAVE
|
main.save_path = SAVE
|
||||||
add_child(main)
|
add_child(main)
|
||||||
await frames(3)
|
await frames(3)
|
||||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||||
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
||||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen,
|
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen]
|
||||||
test_kampf_fortsetzen]
|
|
||||||
if not fast:
|
if not fast:
|
||||||
suite += [test_kampf.bind(2, 3), test_kampf_abbrechen]
|
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
||||||
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
||||||
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
||||||
if OS.get_environment("DINGER_NETZ") != "":
|
if OS.get_environment("DINGER_NETZ") != "":
|
||||||
@@ -254,7 +252,7 @@ func human_turn(scr) -> void:
|
|||||||
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected)
|
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected)
|
||||||
check(scr.selected == cr, "Wesen gewählt")
|
check(scr.selected == cr, "Wesen gewählt")
|
||||||
var targets := []
|
var targets := []
|
||||||
for o in g.opponents:
|
for o in [g.opponent]:
|
||||||
if o.alive() and g.can_attack(cr, o)[0]:
|
if o.alive() and g.can_attack(cr, o)[0]:
|
||||||
targets.append(o)
|
targets.append(o)
|
||||||
for c in o.board:
|
for c in o.board:
|
||||||
@@ -267,11 +265,9 @@ func human_turn(scr) -> void:
|
|||||||
await click(scr.b_end)
|
await click(scr.b_end)
|
||||||
|
|
||||||
|
|
||||||
func start_battle(n_opp: int) -> void:
|
func start_battle() -> void:
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||||
screen().count_slider.value = n_opp
|
|
||||||
await frames(1)
|
|
||||||
await click(find_button("Starten"))
|
await click(find_button("Starten"))
|
||||||
|
|
||||||
|
|
||||||
@@ -279,51 +275,33 @@ func test_gegnerauswahl() -> void:
|
|||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
var sel = screen()
|
var sel = screen()
|
||||||
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
||||||
check(find_button("Neu würfeln") == null and find_button("1 Gegner") == null and find_button("Starten") != null, "kein Würfeln und keine Gegnerknöpfe, nur Starten")
|
check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
|
||||||
check(find_button("Feuer") == null and find_button("Luft") == null, "keine Elementwahl mehr")
|
|
||||||
check(sel.count_slider.min_value == 1 and sel.count_slider.max_value == Game.MAX_OPPONENTS and sel.count_slider.step == 1, "Gegnerzahl als Schieberegler von 1 bis %d" % Game.MAX_OPPONENTS)
|
|
||||||
check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
|
|
||||||
var rect: Rect2 = sel.count_slider.get_global_rect()
|
|
||||||
await click_at(Vector2(rect.end.x - 4, rect.get_center().y)) # ganz rechts anklicken
|
|
||||||
check(sel.opponents() == Game.MAX_OPPONENTS and sel.count_label.text == "%d Gegner" % Game.MAX_OPPONENTS, "Mausklick am rechten Rand stellt %d Gegner ein" % Game.MAX_OPPONENTS)
|
|
||||||
sel.count_slider.grab_focus()
|
|
||||||
await key(KEY_LEFT)
|
|
||||||
check(sel.opponents() == Game.MAX_OPPONENTS - 1, "Pfeiltaste links nimmt einen Gegner weg")
|
|
||||||
sel.count_slider.value = 4
|
|
||||||
await frames(1)
|
|
||||||
check(sel.opponents() == 4 and sel.count_label.text == "4 Gegner" and "5 am Tisch" in sel.info_label.text
|
|
||||||
and str(Profile.REWARD_WIN[4]) in sel.info_label.text, "Regler auf 4: fünf am Tisch mit Belohnung")
|
|
||||||
await click(find_button("Schwer"))
|
await click(find_button("Schwer"))
|
||||||
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
|
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
|
||||||
|
check(str(Profile.DIFFICULTIES.schwer.life) in sel.info_label.text and str(int(Profile.REWARD_WIN * 1.5)) in sel.info_label.text, "Leben des Gegners und Belohnung stehen dabei")
|
||||||
await shot("07_gegnerauswahl")
|
await shot("07_gegnerauswahl")
|
||||||
sel.count_slider.value = 1
|
|
||||||
await frames(1)
|
|
||||||
await click(find_button("Starten"))
|
await click(find_button("Starten"))
|
||||||
var hard = screen()
|
var hard = screen()
|
||||||
check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet")
|
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
||||||
var opp: Game.Player = hard.game.opponents[0]
|
var opp: Game.Player = hard.game.opponent
|
||||||
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
||||||
check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
|
check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
|
||||||
"Start mit vollem Laden und leerer Hand")
|
"Start mit vollem Laden und leerer Hand")
|
||||||
|
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
|
||||||
|
await shot("11_kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
await click(find_button("Leicht"))
|
await click(find_button("Leicht"))
|
||||||
await click(find_button("Starten"))
|
await click(find_button("Starten"))
|
||||||
var easy = screen()
|
var easy = screen()
|
||||||
opp = easy.game.opponents[0]
|
opp = easy.game.opponent
|
||||||
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
|
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
|
||||||
|
check(easy.game.players[0].max_energy == 1 and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
|
||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
await click(find_button("Normal"))
|
await click(find_button("Normal"))
|
||||||
await click(find_button("Zurück"))
|
await click(find_button("Zurück"))
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
for n in [1, 5]:
|
|
||||||
await start_battle(n)
|
|
||||||
var scr = screen()
|
|
||||||
check(scr is BattleScreen and scr.game.opponents.size() == n, "Kampf gegen %d gestartet" % n)
|
|
||||||
check(scr.game.players[0].max_energy == 1 and scr.game.opponents[0].life == Game.START_LIFE, "gleiche Regeln gegen %d" % n)
|
|
||||||
await shot("11_kampf_%d" % n)
|
|
||||||
await give_up()
|
|
||||||
|
|
||||||
|
|
||||||
## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben.
|
## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben.
|
||||||
@@ -348,32 +326,6 @@ func give_up() -> void:
|
|||||||
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
|
||||||
|
|
||||||
|
|
||||||
## Kampf speichern, im Menü fortsetzen, Aufgeben löscht die Datei.
|
|
||||||
func test_kampf_fortsetzen() -> void:
|
|
||||||
await start_battle(1)
|
|
||||||
var scr = screen()
|
|
||||||
scr.ai_delay = 0.0
|
|
||||||
await wait_for_human(scr)
|
|
||||||
var kampf: String = main.battle_save_path()
|
|
||||||
check(FileAccess.file_exists(kampf), "Kampf wird laufend gespeichert")
|
|
||||||
await click(scr.b_end)
|
|
||||||
await wait_for_human(scr)
|
|
||||||
var turn: int = scr.game.turn
|
|
||||||
var life: int = scr.game.human.life
|
|
||||||
var hand: int = scr.game.human.hand.size()
|
|
||||||
await click(find_button("Aufgeben"))
|
|
||||||
await click(find_button("Speichern und zurück"))
|
|
||||||
check(screen() is MenuScreen and find_button("Kampf fortsetzen") != null, "Speichern führt ins Menü mit Fortsetzen-Knopf")
|
|
||||||
await click(find_button("Kampf fortsetzen"))
|
|
||||||
var scr2 = screen()
|
|
||||||
check(scr2 is BattleScreen and scr2.game.turn == turn and scr2.game.human.life == life and scr2.game.human.hand.size() == hand
|
|
||||||
and scr2.game.current() == scr2.game.human, "Kampf fortgesetzt mit gleichem Zug, Leben und Hand")
|
|
||||||
scr2.ai_delay = 0.0
|
|
||||||
await shot("30_kampf_fortgesetzt")
|
|
||||||
await give_up()
|
|
||||||
check(not FileAccess.file_exists(kampf) and find_button("Kampf fortsetzen") == null, "Aufgeben löscht den gespeicherten Kampf")
|
|
||||||
|
|
||||||
|
|
||||||
func test_einstellungen() -> void:
|
func test_einstellungen() -> void:
|
||||||
await click(find_button("Einstellungen"))
|
await click(find_button("Einstellungen"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
@@ -414,6 +366,14 @@ func test_sammlung() -> void:
|
|||||||
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "gewöhnlich", "Karte gewählt, noch gewöhnlich")
|
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "gewöhnlich", "Karte gewählt, noch gewöhnlich")
|
||||||
await click(scr.b_upgrade)
|
await click(scr.b_upgrade)
|
||||||
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["selten"], "auf selten aufgewertet")
|
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["selten"], "auf selten aufgewertet")
|
||||||
|
scr.search_box.text = ""
|
||||||
|
scr.search_box.text_changed.emit("")
|
||||||
|
await frames(1)
|
||||||
|
var first: CardView = scr.grid.views()[0]
|
||||||
|
check(first.card.id == "ff_phoenix" and first.rarity == "selten", "die aufgewertete Karte steht vorn in der Sammlung")
|
||||||
|
scr.search_box.text = "Phönix"
|
||||||
|
scr.search_box.text_changed.emit("Phönix")
|
||||||
|
await frames(1)
|
||||||
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
|
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
|
||||||
var dust: int = p.dust
|
var dust: int = p.dust
|
||||||
await click(scr.b_dismantle)
|
await click(scr.b_dismantle)
|
||||||
@@ -491,7 +451,7 @@ func test_sprache() -> void:
|
|||||||
|
|
||||||
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
|
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
|
||||||
func test_kampf_steuerung() -> void:
|
func test_kampf_steuerung() -> void:
|
||||||
await start_battle(1)
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BattleScreen, "Kampf gestartet")
|
check(scr is BattleScreen, "Kampf gestartet")
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
@@ -551,7 +511,7 @@ func test_kampf_steuerung() -> void:
|
|||||||
|
|
||||||
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
|
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
|
||||||
func test_kampf_grenzen() -> void:
|
func test_kampf_grenzen() -> void:
|
||||||
await start_battle(1)
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
@@ -603,26 +563,45 @@ func test_kampf_grenzen() -> void:
|
|||||||
|
|
||||||
## Feldanzeige und Verschmelzen im Kampf.
|
## Feldanzeige und Verschmelzen im Kampf.
|
||||||
func test_kampf_feld_und_verschmelzen() -> void:
|
func test_kampf_feld_und_verschmelzen() -> void:
|
||||||
await start_battle(1)
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BattleScreen, "Kampf gestartet")
|
check(scr is BattleScreen, "Kampf gestartet")
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
g.human.board.clear()
|
g.human.board.clear()
|
||||||
for id in ["f_wolf", "w_reiter"]:
|
var put := func(p: Game.Player, id: String) -> Game.Creature:
|
||||||
var cr := Game.Creature.new(Cards.by_id(id), g.human, g.next_uid)
|
var cr := Game.Creature.new(Cards.by_id(id), p, g.next_uid)
|
||||||
g.next_uid += 1
|
g.next_uid += 1
|
||||||
g.human.board.append(cr)
|
cr.ready = true
|
||||||
|
cr.attacks_left = 1
|
||||||
|
p.board.append(cr)
|
||||||
|
return cr
|
||||||
|
put.call(g.human, "f_wolf")
|
||||||
|
put.call(g.human, "w_reiter")
|
||||||
|
var falke: Game.Creature = put.call(scr.others()[0], "l_falke")
|
||||||
|
g.human.hand = [Cards.by_id("f_kobold")]
|
||||||
g.human.energy = 10
|
g.human.energy = 10
|
||||||
|
g.field_charge["Wasser"] = 2
|
||||||
|
scr.refresh()
|
||||||
|
await frames(2)
|
||||||
|
check(scr.field_view.text() == "Feuer 0/4 · Wasser 2/4 · Erde 0/4 · Luft 0/4", "neutrales Feld zeigt die Ladung: " + scr.field_view.text())
|
||||||
for i in Cards.FIELD_CHARGE:
|
for i in Cards.FIELD_CHARGE:
|
||||||
g.charge_field(Cards.by_id("f_kobold"))
|
g.charge_field(Cards.by_id("f_kobold"))
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(2)
|
await frames(2)
|
||||||
check(scr.field_label.text.begins_with("Vulkan"), "Feldanzeige nennt das Feld: " + scr.field_label.text)
|
check(scr.field_view.text().begins_with("Vulkan: +1 Angriff"), "Feldanzeige nennt das Feld: " + scr.field_view.text())
|
||||||
check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff")
|
check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff")
|
||||||
|
check(scr.hand_views[0].bonus == 2 and scr.hand_views[0].tag == "Dampf", "die Handkarte zeigt +2 (Resonanz und Feld) und die Kombo Dampf")
|
||||||
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
|
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
|
||||||
await shot("23_feld")
|
await shot("23_feld")
|
||||||
|
await hover(scr.hand_views[0])
|
||||||
|
check(scr.tiles[g.human.board[1].uid].mark == "Dampf" and "Dampf" in scr.info_label.text, "beim Zeigen auf die Handkarte ist der Kombo-Partner markiert")
|
||||||
|
await click(scr.own_views[0])
|
||||||
|
check(scr.selected == g.human.board[0] and scr.tiles[falke.uid].matchup == 1, "Angreifer gewählt: das Luftwesen zeigt +1 für Feuer")
|
||||||
|
await shot("23b_angriffsziel")
|
||||||
|
await right_click() # Auswahl wieder aufheben
|
||||||
|
check(scr.selected == null and scr.tiles[falke.uid].matchup == 0, "Auswahl aufgehoben, Vorteil verschwindet")
|
||||||
await click(scr.b_fuse)
|
await click(scr.b_fuse)
|
||||||
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
||||||
await click(scr.own_views[0])
|
await click(scr.own_views[0])
|
||||||
@@ -834,13 +813,15 @@ func test_kampagne() -> void:
|
|||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is CampaignScreen, "Kampagne geöffnet")
|
check(scr is CampaignScreen, "Kampagne geöffnet")
|
||||||
await shot("15_kampagne")
|
await shot("15_kampagne")
|
||||||
check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen")
|
check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES.size() - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen")
|
||||||
|
await click(scr.node_buttons[3])
|
||||||
|
check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen")
|
||||||
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
|
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
|
||||||
await click(find_button("Kämpfen"))
|
await click(find_button("Kämpfen"))
|
||||||
var battle = screen()
|
var battle = screen()
|
||||||
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
||||||
check(battle.game.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner")
|
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
|
||||||
check(battle.game.opponents[0].perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
||||||
await shot("16_kampagne_kampf")
|
await shot("16_kampagne_kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
|
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
|
||||||
@@ -854,7 +835,7 @@ func test_kampagne() -> void:
|
|||||||
await shot("27_kampagne_kapitel2")
|
await shot("27_kampagne_kapitel2")
|
||||||
await click(find_button("Kämpfen"))
|
await click(find_button("Kämpfen"))
|
||||||
var b2 = screen()
|
var b2 = screen()
|
||||||
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponents[0].perks.size() == 2, "Kapitel-2-Boss mit zwei Merkmalen")
|
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 30, "Kapitel-2-Boss mit zwei Merkmalen und 30 Leben")
|
||||||
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
|
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
main.profile.campaign_stage = 0
|
main.profile.campaign_stage = 0
|
||||||
@@ -862,13 +843,13 @@ func test_kampagne() -> void:
|
|||||||
main.save()
|
main.save()
|
||||||
|
|
||||||
|
|
||||||
func test_kampf(n_opp: int, shot_turn: int) -> void:
|
func test_kampf(shot_turn: int) -> void:
|
||||||
await start_battle(n_opp)
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BattleScreen, "Kampfbildschirm")
|
check(scr is BattleScreen, "Kampfbildschirm")
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
check(g.opponents.size() == n_opp and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
|
check(g.players.size() == 2 and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
check(g.current() == g.human, "Spieler ist dran")
|
check(g.current() == g.human, "Spieler ist dran")
|
||||||
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
|
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
|
||||||
@@ -898,12 +879,12 @@ func test_kampf(n_opp: int, shot_turn: int) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_kampf_abbrechen() -> void:
|
func test_kampf_abbrechen() -> void:
|
||||||
await start_battle(5)
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
check(g.opponents.size() == 5 and g.human.max_energy == 1, "5 Gegner")
|
check(g.opponent != null and g.human.max_energy == 1, "Duell läuft")
|
||||||
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
||||||
g.human.energy = 10
|
g.human.energy = 10
|
||||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
||||||
@@ -913,7 +894,7 @@ func test_kampf_abbrechen() -> void:
|
|||||||
scr.refresh()
|
scr.refresh()
|
||||||
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
||||||
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
|
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
|
||||||
await hover(scr.log_box) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
await hover(scr.field_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
||||||
await hover(reiter_view)
|
await hover(reiter_view)
|
||||||
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
|
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
|
||||||
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
|
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
|
||||||
@@ -930,11 +911,10 @@ func test_kampf_abbrechen() -> void:
|
|||||||
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
||||||
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
|
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
|
||||||
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
||||||
await click(find_button("Lupe"))
|
await click(scr.b_log)
|
||||||
check(scr.zoom_player == scr.others()[0], "Lupe öffnet die erste Spalte")
|
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
|
||||||
await shot("12_lupe")
|
await key(KEY_ESCAPE)
|
||||||
await click(find_button("Schließen"))
|
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
|
||||||
check(scr.zoom_player == null, "Lupe geschlossen")
|
|
||||||
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
||||||
for round in 4:
|
for round in 4:
|
||||||
await click(scr.b_end)
|
await click(scr.b_end)
|
||||||
@@ -946,4 +926,4 @@ func test_kampf_abbrechen() -> void:
|
|||||||
and g.log.any(func(l): return " greift " in l)
|
and g.log.any(func(l): return " greift " in l)
|
||||||
check(ai_attacked, "Gegner spielen Karten und greifen an")
|
check(ai_attacked, "Gegner spielen Karten und greifen an")
|
||||||
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
|
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
|
||||||
await shot("10_kampf_fuenf")
|
await shot("10_kampf_runden")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
keys,en
|
keys,en
|
||||||
Dinger,Dinger
|
Dinger,Dinger
|
||||||
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all
|
Ein Kartenspiel: Duell um das Feld,A card game: duel for the field
|
||||||
Einzelspieler,Single player
|
Einzelspieler,Single player
|
||||||
Kampagne,Campaign
|
Kampagne,Campaign
|
||||||
Netzwerkspiel,Network game
|
Netzwerkspiel,Network game
|
||||||
@@ -62,18 +62,14 @@ Normal,Normal
|
|||||||
Schwer,Hard
|
Schwer,Hard
|
||||||
Profil zurücksetzen,Reset profile
|
Profil zurücksetzen,Reset profile
|
||||||
Große Schrift,Large text
|
Große Schrift,Large text
|
||||||
Duell,Duel
|
|
||||||
%d am Tisch,%d at the table
|
|
||||||
%d Münzen,%d coins
|
%d Münzen,%d coins
|
||||||
Abbrechen,Cancel
|
Abbrechen,Cancel
|
||||||
"Ja, zurücksetzen","Yes, reset"
|
"Ja, zurücksetzen","Yes, reset"
|
||||||
"Speichern und zurück","Save and go back"
|
|
||||||
"Kampf fortsetzen","Resume battle"
|
|
||||||
%s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature
|
%s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature
|
||||||
%s Schaden an ein Ziel,%s damage to a target
|
%s Schaden an ein Ziel,%s damage to a target
|
||||||
%s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures
|
%s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures
|
||||||
%s Schaden an alle Gegner,%s damage to all opponents
|
%s Schaden an den Gegner,%s damage to the opponent
|
||||||
%s Schaden an alle Gegner und ihre Wesen,%s damage to all opponents and their creatures
|
%s Schaden an den Gegner und seine Wesen,%s damage to the opponent and their creatures
|
||||||
Heile dich um %s,Heal yourself for %s
|
Heile dich um %s,Heal yourself for %s
|
||||||
Erhalte 1 Mana,Gain 1 mana
|
Erhalte 1 Mana,Gain 1 mana
|
||||||
Erhalte %s Mana,Gain %s mana
|
Erhalte %s Mana,Gain %s mana
|
||||||
@@ -117,14 +113,10 @@ Zuschauen,Watch
|
|||||||
Zuschauer,Spectator
|
Zuschauer,Spectator
|
||||||
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
||||||
Starten,Start
|
Starten,Start
|
||||||
1 Gegner,1 opponent
|
|
||||||
%d Gegner,%d opponents
|
|
||||||
KI-Gegner,AI opponents
|
|
||||||
Sammlung,Collection
|
Sammlung,Collection
|
||||||
Verschmelzen,Fuse
|
Verschmelzen,Fuse
|
||||||
Zwei eigene Wesen wählen,Pick two of your creatures
|
Zwei eigene Wesen wählen,Pick two of your creatures
|
||||||
Zweites Wesen wählen,Pick the second creature
|
Zweites Wesen wählen,Pick the second creature
|
||||||
Feld: neutral,Field: neutral
|
|
||||||
Zwei verschiedene Wesen wählen,Pick two different creatures
|
Zwei verschiedene Wesen wählen,Pick two different creatures
|
||||||
Nur eigene Wesen,Only your own creatures
|
Nur eigene Wesen,Only your own creatures
|
||||||
Vulkan,Volcano
|
Vulkan,Volcano
|
||||||
@@ -150,10 +142,26 @@ Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
|||||||
Aufgewertet,Upgraded
|
Aufgewertet,Upgraded
|
||||||
+%d Staub,+%d dust
|
+%d Staub,+%d dust
|
||||||
+%d je Zug,+%d per turn
|
+%d je Zug,+%d per turn
|
||||||
Neu würfeln (%d),Reroll (%d)
|
Neu würfeln ◆ %d,Reroll ◆ %d
|
||||||
|
Dein Zug,Your turn
|
||||||
|
Beim Ausspielen,On play
|
||||||
Nicht genug Mana,Not enough mana
|
Nicht genug Mana,Not enough mana
|
||||||
Kein Angebot,No offer
|
Kein Angebot,No offer
|
||||||
Hand voll,Hand full
|
Hand voll,Hand full
|
||||||
Profil wirklich zurücksetzen?,Really reset your profile?
|
Profil wirklich zurücksetzen?,Really reset your profile?
|
||||||
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
||||||
Chimäre,Chimera
|
Chimäre,Chimera
|
||||||
|
Angriff,attack
|
||||||
|
Eis,Ice
|
||||||
|
Resonanz +%d,Resonance +%d
|
||||||
|
Feld +%d,Field +%d
|
||||||
|
Merkmal +%d,Perk +%d
|
||||||
|
Feldkombo %s mit %s: %s,Field combo %s with %s: %s
|
||||||
|
1 Schaden an das stärkste gegnerische Wesen,1 damage to the strongest enemy creature
|
||||||
|
2 Schaden an den Gegner,2 damage to the opponent
|
||||||
|
Heile dich um 3,Heal yourself for 3
|
||||||
|
Das neue Wesen erhält +1/+1,The new creature gets +1/+1
|
||||||
|
%d Leben,%d life
|
||||||
|
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win
|
||||||
|
Start gegen die KI,Start against the AI
|
||||||
|
Beginnt mit 5 Leben mehr,Starts with 5 extra life
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user