update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
team3
2026-10-08 14:28:52 +02:00
parent 5bc8569717
commit c245c7af87
21 changed files with 496 additions and 347 deletions

View File

@@ -23,15 +23,15 @@ Der Spielstand liegt unter `user://spielstand.json`
- 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen - 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana - Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen - Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel - Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar. Auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach. Das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana; eine Handkarte abwerfen bringt 1 Mana (Rechtsklick)
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst - Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementboni; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf - Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern - Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück - Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
- Elementboni: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); drei offene Wesen eines Elements auf dem Tisch geben dem Element +1/+1 (fällt der Bonus weg, sinkt nur das Höchstleben). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni - Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je 10 Mana an ausgespielten Karten eines Elements geben den eigenen Wesen dieses Elements +1 Angriff (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Zwei Leisten in der Mitte, oben die des Gegners, unten die eigene, zeigen je Element zwei Symbole, gefüllt je offenem eigenen Wesen, und das Mana der ausgespielten Karten
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“ - Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; so greift und kontert auch ein Wesen mit 0 Angriff, nur nicht ohne Vorteil; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar. Nichts schlägt durch - Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre - Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen - Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 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
@@ -39,13 +39,13 @@ Der Spielstand liegt unter `user://spielstand.json`
- 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)
- 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; Ton und Musik sind standardmäßig aus
## Tests ## Tests
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`: `tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft). Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
`DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit). Beide Suiten brechen beim ersten Fehler ab. Der Fenster-Test spielt eine kurze Partie (einfache KI, Gegner mit 1 Leben) und läuft in wenigen Sekunden.
``` ```
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren

View File

@@ -15,8 +15,8 @@
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern - Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000) - Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück - Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was man sich in diesem Zug leisten kann - Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs, beim Ziehen wählt man eine, eine neue rückt nach; das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), und Verschmelzen; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten - Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), und Verschmelzen, Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug) - Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst - Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf - Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
@@ -27,7 +27,7 @@
- Jede Karte gehört zu genau einem Element - Jede Karte gehört zu genau einem Element
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) - Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag) - Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
- Drei offene Wesen eines Elements auf dem Tisch: +1/+1 für das Element, kein Malus; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni - Je 10 Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements, jeder Spieler zählt für sich; verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht) - Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung - Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
- 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

View File

@@ -6,7 +6,7 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff - **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 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 kurz. Auf dem Brett: eigene Seite unten, Gegner oben, links Kästen und Stapel, rechts oben Knöpfe, Wesen
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt. als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des - **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand. Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
@@ -39,24 +39,27 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
Nur, was gerade zählt, ist zu sehen. Von unten nach oben: Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 2“ (Klick zieht), rechts - **Hand** als bis zu fünf Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners
„Zug beenden“. gleich groß als Rückseiten oben; der
Stapel links auf halber Höhe als drei offene Karten übereinander (Klick zieht die Karte, das erste
Ziehen im Zug frei), darüber der Gegnerkasten, darunter der eigene.
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein - **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner). das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana, - **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist. Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
- **Elementleiste** mit drei Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1. - **Elementleiste** je Spieler, die des Gegners darüber.
Daneben das Symbol des Sondereffekts, sobald zwei eigene Wesen des Elements liegen. Je Element zwei Symbole in seiner Form, gefüllt je offenem eigenen Wesen (beide voll: Sondereffekt),
daneben das Mana der ausgespielten Karten; je 10 gibt es +1 Angriff.
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick - **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
Doppelschlag kaufen; per Y zusätzlich verschmelzen. Doppelschlag kaufen; per Y zusätzlich verschmelzen.
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie - **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 Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
macht Ziele, Texte und Balance eindeutig. macht Ziele, Texte und Balance eindeutig.
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld - **Vorschau** steht fest rechts auf halber Höhe, darunter die Zusatzzeile, mit Resonanz, Feld
und Feldkombo darunter. und Feldkombo darunter.
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung. - **Verlauf**, **Aufgeben** und **Zug beenden** als Knöpfe oben rechts; der Verlauf öffnet als Überlagerung.
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen - Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht. steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.

View File

@@ -201,7 +201,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
best = firsts[i].action best = firsts[i].action
if best.type == "end": if best.type == "end":
if game.can_draw(p)[0]: if game.can_draw(p)[0]:
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen return {"type": "draw", "index": pick_index(game, p)} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
return best return best
return map_action(game, view, best) return map_action(game, view, best)
@@ -258,10 +258,24 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if t != null: if t != null:
return {"type": "attack", "creature": cr, "target": t} return {"type": "attack", "creature": cr, "target": t}
if game.can_draw(p)[0]: if game.can_draw(p)[0]:
return {"type": "draw"} return {"type": "draw", "index": pick_index(game, p)}
return {"type": "end"} return {"type": "end"}
## Welche der offenen Karten: die wertvollste, die nach dem Ziehen noch bezahlbar wäre, sonst die billigste.
static func pick_index(game: Game, p: Game.Player) -> int:
var left: int = p.energy - game.draw_cost(p)
var best := 0
var best_score := -INF
for i in p.offer.size():
var c: Cards.Card = p.offer[i]
var score := float(c.attack + c.health) if c.cost <= left else -10.0 - c.cost
if score > best_score:
best_score = score
best = i
return best
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen; ## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
## der Held nur, wenn kein Wesen mehr steht. ## der Held nur, wenn kein Wesen mehr steht.
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature): static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
@@ -288,7 +302,7 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
"play": "play":
game.play_card(p, action.card, bool(action.get("face_down", false))) game.play_card(p, action.card, bool(action.get("face_down", false)))
"draw": "draw":
game.draw(p) game.draw(p, int(action.get("index", 0)))
"reveal": "reveal":
game.reveal(p, action.creature) game.reveal(p, action.creature)
"attack": "attack":

View File

@@ -98,11 +98,11 @@ static func cost_of(attack: int, health: int) -> int:
return (2 * attack + health) / 3 return (2 * attack + health) / 3
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren, ## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter ## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
## als das stärkere Wesen, und jeder Partner ab 1 bringt mindestens +1. ## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
static func fusion_value(a: int, b: int) -> int: static func fusion_value(a: int, b: int) -> int:
return maxi(a, b) + ceili(mini(a, b) / 3.0) return maxi(a, b) + maxi(1, ceili(mini(a, b) / 3.0))
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt). ## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).

View File

@@ -10,7 +10,9 @@ const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Re
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
const START_LIFE := 30 const START_LIFE := 30
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
const ELEMENT_STACK := 3 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1 const ELEMENT_MANA := 10 # je so viel Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements
const OFFER := 3 # offene Karten auf dem Stapel, höchstens so teuer wie das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
# Merkmale der Kampagnen-Bosse (Player.perks). # Merkmale der Kampagnen-Bosse (Player.perks).
const PERKS := { const PERKS := {
@@ -105,8 +107,9 @@ class Creature extends RefCounted:
return maxi(0, attacks_per_turn() - attacks_done) return maxi(0, attacks_per_turn() - attacks_done)
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist). ## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
## Darf dieses Wesen angreifen? Ob es Schaden macht, hängt vom Ziel ab (Elementvorteil, Game.can_attack).
func can_attack() -> bool: func can_attack() -> bool:
return not hidden and attacks_left() > 0 and power() > 0 and (ready or has("Flink")) return not hidden and attacks_left() > 0 and (ready or has("Flink"))
func clone(new_owner) -> Creature: func clone(new_owner) -> Creature:
var c := Creature.new(card, new_owner, uid) var c := Creature.new(card, new_owner, uid)
@@ -134,6 +137,9 @@ class Player extends RefCounted:
var perks: Array = [] # Boss-Merkmale (siehe PERKS) var perks: Array = [] # Boss-Merkmale (siehe PERKS)
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile) var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
var turns := 0 # begonnene eigene Züge var turns := 0 # begonnene eigene Züge
var played := {} # Element -> Mana der bisher ausgespielten Karten (je ELEMENT_MANA: +1 Angriff)
var offer: Array = [] # die OFFER offenen Karten auf dem Stapel (nur der Besitzer sieht sie)
var drew := false # in diesem Zug schon gezogen? Das erste Ziehen ist frei
func _init(n: String, h: bool, l: int, t: String = "") -> void: func _init(n: String, h: bool, l: int, t: String = "") -> void:
name = n name = n
@@ -141,6 +147,8 @@ class Player extends RefCounted:
is_human = h is_human = h
life = l life = l
max_life = l max_life = l
for e in Cards.ELEMENTS:
played[e] = 0
func alive() -> bool: func alive() -> bool:
return life > 0 return life > 0
@@ -163,6 +171,9 @@ class Player extends RefCounted:
p.perks = perks.duplicate() p.perks = perks.duplicate()
p.looks = looks p.looks = looks
p.turns = turns p.turns = turns
p.played = played.duplicate()
p.offer = offer.duplicate()
p.drew = drew
for c in board: for c in board:
p.board.append(c.clone(p)) p.board.append(c.clone(p))
return p return p
@@ -276,6 +287,7 @@ func start(fixed_order: bool = false) -> void:
p.order = i p.order = i
for c in draw_cards(START_HAND, START_MANA): for c in draw_cards(START_HAND, START_MANA):
p.hand.append(c.copy()) p.hand.append(c.copy())
p.offer = draw_cards(OFFER, START_MANA)
turn = 1 turn = 1
current_idx = 0 current_idx = 0
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name))) say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
@@ -285,9 +297,9 @@ func start(fixed_order: bool = false) -> void:
begin_turn(current()) begin_turn(current())
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten ## Nimmt n Karten vom Stapel, die höchstens max_cost kosten; zu teure wandern unter den Stapel.
## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer. ## Ist der Stapel leer, kommt ein frischer.
func draw_cards(n: int, max_cost: int) -> Array: func draw_cards(n: int, max_cost: int = 999) -> Array:
var out := [] var out := []
var skipped := [] var skipped := []
var looked := 0 var looked := 0
@@ -306,7 +318,8 @@ func draw_cards(n: int, max_cost: int) -> Array:
return out return out
## Ziehen: kostet DRAW_COST, eine Karte kommt auf die Hand. ## Ziehen: eine der offenen Karten kommt auf die Hand, eine neue rückt nach. Das erste Ziehen
## im Zug ist frei, jedes weitere kostet DRAW_COST.
func can_draw(p: Player) -> Array: func can_draw(p: Player) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
@@ -318,16 +331,20 @@ func can_draw(p: Player) -> Array:
func draw_cost(p: Player) -> int: func draw_cost(p: Player) -> int:
return 0 if "ziehen_gratis" in p.perks else DRAW_COST return 0 if not p.drew or "ziehen_gratis" in p.perks else DRAW_COST
func draw(p: Player) -> String: func draw(p: Player, index: int = 0) -> String:
var ok := can_draw(p) var ok := can_draw(p)
if not ok[0]: if not ok[0]:
return ok[1] return ok[1]
if index < 0 or index >= p.offer.size():
return "Keine Karte gewählt"
p.energy -= draw_cost(p) p.energy -= draw_cost(p)
var card: Cards.Card = draw_cards(1, p.max_energy)[0] p.drew = true
var card: Cards.Card = p.offer[index]
p.hand.append(card.copy()) p.hand.append(card.copy())
p.offer[index] = draw_cards(1, maxi(p.max_energy, START_MANA))[0] # nie teurer als das Mana dieses Zugs
say("%s zieht." % p.name) say("%s zieht." % p.name)
emit({"type": "draw", "player": players.find(p)}) emit({"type": "draw", "player": players.find(p)})
return "" return ""
@@ -342,15 +359,9 @@ func begin_turn(p: Player) -> void:
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
c.attacks_done = 0 c.attacks_done = 0
p.turns += 1 p.turns += 1
p.drew = false # das erste Ziehen im Zug ist wieder frei
say("--- Zug %d: %s ---" % [turn, p.name]) say("--- Zug %d: %s ---" % [turn, p.name])
emit({"type": "turn", "player": players.find(p)}) emit({"type": "turn", "player": players.find(p)})
# Jeder Zug beginnt mit einer Karte vom Stapel, die man sich in diesem Zug leisten kann.
if p.hand.size() < MAX_HAND:
p.hand.append(draw_cards(1, p.max_energy)[0].copy())
say("%s zieht eine Karte." % p.name)
emit({"type": "draw", "player": players.find(p)})
else:
say("%s hat die Hand voll und zieht nicht." % p.name)
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran. ## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
@@ -405,6 +416,8 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
cr.health += 1 cr.health += 1
cr.max_health += 1 cr.max_health += 1
p.board.append(cr) p.board.append(cr)
for e in card.elements:
p.played[e] = int(p.played.get(e, 0)) + card.cost
if face_down: if face_down:
say("%s legt ein Wesen verdeckt." % p.name) say("%s legt ein Wesen verdeckt." % p.name)
else: else:
@@ -416,6 +429,26 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
return "" return ""
## Abwerfen: eine Handkarte loswerden und dafür DISCARD_GAIN Mana bekommen.
func can_discard(p: Player, card: Cards.Card) -> Array:
if winner != "" or p != current():
return [false, "Nicht dein Zug"]
if not (card in p.hand):
return [false, "Nicht auf der Hand"]
return [true, ""]
func discard(p: Player, card: Cards.Card) -> String:
var ok := can_discard(p, card)
if not ok[0]:
return ok[1]
p.hand.erase(card)
p.energy += DISCARD_GAIN
say("%s wirft %s ab." % [p.name, card.name])
emit({"type": "discard", "player": players.find(p)})
return ""
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug ## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
## nach dem Legen. Aufgedeckt greift es sofort an. ## nach dem Legen. Aufgedeckt greift es sofort an.
func can_reveal(p: Player, cr: Creature) -> Array: func can_reveal(p: Player, cr: Creature) -> Array:
@@ -506,11 +539,11 @@ func fuse(p: Player, a, b) -> String:
## das Ergebnis ist nur angriffsbereit, wenn beide es waren. ## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
func _fuse_board(p: Player, a: Creature, b: Creature) -> String: func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack), var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura)) Cards.fusion_value(a.max_health, b.max_health))
card.look = maxi(look_of(p, a.card), look_of(p, b.card)) card.look = maxi(look_of(p, a.card), look_of(p, b.card))
var cr := Creature.new(card, p, next_uid) var cr := Creature.new(card, p, next_uid)
next_uid += 1 next_uid += 1
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura))) cr.health = mini(card.health, Cards.fusion_value(a.health, b.health))
cr.ready = a.ready and b.ready cr.ready = a.ready and b.ready
cr.attacks_done = maxi(a.attacks_done, b.attacks_done) cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
var at := p.board.find(a) var at := p.board.find(a)
@@ -527,59 +560,38 @@ func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
# --- Elementboni --------------------------------------------------------------- # --- Elementboni ---------------------------------------------------------------
## Offene Wesen je Element auf dem ganzen Tisch. ## +1 Angriff je volle ELEMENT_MANA an ausgespielten Karten eines Elements des Spielers; mehrere Elemente zählen alle.
func element_counts() -> Dictionary: static func aura_of(elements: Array, played: Dictionary) -> int:
var counts := {}
for e in Cards.ELEMENTS:
counts[e] = 0
for p in players:
for c in p.board:
if not c.hidden:
for e in c.card.elements:
counts[e] += 1
return counts
## +1 je Element mit ELEMENT_STACK offenen Wesen auf dem Tisch; mehrere Elemente zählen alle.
static func aura_of(elements: Array, counts: Dictionary) -> int:
var a := 0 var a := 0
for e in elements: for e in elements:
if int(counts[e]) >= ELEMENT_STACK: a += int(played.get(e, 0)) / ELEMENT_MANA
a += 1
return a return a
## Bonus eines offenen Wesens; verdeckte zählen nicht und bekommen nichts. ## Bonus eines offenen Wesens aus den ausgespielten Karten seines Besitzers; verdeckte bekommen nichts.
func aura_for(c: Creature, counts: Dictionary) -> int: func aura_for(c: Creature) -> int:
if c.hidden:
return 0
var a := aura_of(c.card.elements, counts)
var o = c.owner() var o = c.owner()
if o != null and "element_doppelt" in o.perks: if c.hidden or o == null:
return 0
var a := aura_of(c.card.elements, o.played)
if "element_doppelt" in o.perks:
a *= 2 a *= 2
return a return a
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er, ## Rechnet die Elementboni neu; sie wirken nur auf den Angriff (Creature.power).
## sinkt nur das Höchstleben, das Leben wird höchstens darauf gekappt.
func apply_auras() -> void: func apply_auras() -> void:
var counts := element_counts()
for p in players: for p in players:
for c in p.board: for c in p.board:
var a := aura_for(c, counts) c.aura = aura_for(c)
if a != c.aura:
var delta: int = a - c.aura
c.max_health += delta
c.health = mini(c.health + maxi(delta, 0), c.max_health)
c.aura = a
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme. ## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
func forecast(p: Player, card: Cards.Card) -> int: func forecast(p: Player, card: Cards.Card) -> int:
var counts := element_counts() var played := p.played.duplicate()
for e in card.elements: for e in card.elements:
counts[e] += 1 played[e] = int(played.get(e, 0)) + card.cost
return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1) return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
# --- Kampf ------------------------------------------------------------------- # --- Kampf -------------------------------------------------------------------
@@ -591,13 +603,15 @@ func can_attack(cr: Creature, target) -> Array:
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if cr.hidden: if cr.hidden:
return [false, "%s liegt verdeckt" % cr.name] return [false, "%s liegt verdeckt" % cr.name]
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0 and cr.power() > 0: if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0:
return [false, "%s greift erst im nächsten Zug an" % cr.name] return [false, "%s greift erst im nächsten Zug an" % cr.name]
if not cr.can_attack(): if not cr.can_attack():
return [false, "%s kann nicht angreifen" % cr.name] return [false, "%s kann nicht angreifen" % cr.name]
var tgt_owner = target.owner() if target is Creature else target var tgt_owner = target.owner() if target is Creature else target
if not (tgt_owner in enemies_of(cr.owner())): if not (tgt_owner in enemies_of(cr.owner())):
return [false, "Kein gültiges Ziel"] return [false, "Kein gültiges Ziel"]
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff reicht nur mit Elementvorteil
return [false, "%s würde keinen Schaden machen" % cr.name]
if not (target is Creature) and not tgt_owner.board.is_empty(): if not (target is Creature) and not tgt_owner.board.is_empty():
return [false, "Erst die Wesen"] return [false, "Erst die Wesen"]
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")): if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
@@ -616,7 +630,7 @@ func attack(cr: Creature, target) -> String:
say("%s greift %s an." % [cr.name, target.name]) say("%s greift %s an." % [cr.name, target.name])
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)}) emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
damage(target, cr.power() + matchup_bonus(cr, target)) damage(target, cr.power() + matchup_bonus(cr, target))
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht if target is Creature: # Konter mit Elementvorteil; wer so nichts austeilt, kontert nicht (damage ignoriert 0)
damage(cr, target.power() + matchup_bonus(target, cr)) damage(cr, target.power() + matchup_bonus(target, cr))
cleanup() cleanup()
check_end() check_end()

View File

@@ -398,13 +398,17 @@ func _apply(id: int, action: Dictionary) -> void:
var cr := game.find_creature(int(action.get("uid", -1))) var cr := game.find_creature(int(action.get("uid", -1)))
if cr != null: if cr != null:
game.reveal(p, cr) game.reveal(p, cr)
"discard":
var hi := int(action.get("hand", -1))
if hi >= 0 and hi < p.hand.size():
game.discard(p, p.hand[hi])
"attack": "attack":
var cr := game.find_creature(int(action.get("uid", -1))) var cr := game.find_creature(int(action.get("uid", -1)))
var t = resolve_ref(action.get("target")) var t = resolve_ref(action.get("target"))
if cr != null and t != null: if cr != null and t != null:
game.attack(cr, t) game.attack(cr, t)
"draw": "draw":
game.draw(p) game.draw(p, int(action.get("index", 0)))
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand) "fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
var a = game.find_creature(int(action.get("uid", -1))) var a = game.find_creature(int(action.get("uid", -1)))
var b = game.find_creature(int(action.get("other", -1))) var b = game.find_creature(int(action.get("other", -1)))

View File

@@ -27,8 +27,9 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
"ready": c.ready, "hidden": c.hidden, "aura": c.aura}) "ready": c.ready, "hidden": c.hidden, "aura": c.aura})
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life, ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
"energy": p.energy, "max_energy": p.max_energy, "energy": p.energy, "max_energy": p.max_energy,
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "played": p.played,
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board}) "hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board,
"offer": p.offer.map(card_ref) if mine else [], "drew": p.drew})
var evs := [] var evs := []
for ev in g.events: for ev in g.events:
var e: Dictionary = ev.duplicate() var e: Dictionary = ev.duplicate()
@@ -55,12 +56,17 @@ static func from_snapshot(d: Dictionary) -> Game:
for cid in pd.get("looks", {}): for cid in pd.get("looks", {}):
p.looks[cid] = int(pd.looks[cid]) p.looks[cid] = int(pd.looks[cid])
p.turns = int(pd.turns) p.turns = int(pd.turns)
for e in pd.get("played", {}):
p.played[e] = int(pd.played[e])
if pd.hand is Array: if pd.hand is Array:
for ref in pd.hand: for ref in pd.hand:
p.hand.append(card_of(ref)) p.hand.append(card_of(ref))
else: else:
for i in int(pd.hand): for i in int(pd.hand):
p.hand.append(placeholder) p.hand.append(placeholder)
for ref in pd.get("offer", []):
p.offer.append(card_of(ref))
p.drew = bool(pd.get("drew", false))
for cd in pd.board: for cd in pd.board:
var card := card_of(cd.card) var card := card_of(cd.card)
var c := Game.Creature.new(card, p, int(cd.uid)) var c := Game.Creature.new(card, p, int(cd.uid))

View File

@@ -26,7 +26,7 @@ var coins := 0
var dust := 0 var dust := 0
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false} var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt) var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1) var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
@@ -64,7 +64,7 @@ static func from_dict(d: Dictionary) -> Profile:
if Cards.has_id(cid): if Cards.has_id(cid):
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1) p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
var se = d.get("settings", {}) var se = d.get("settings", {})
p.settings = {"volume": float(se.get("volume", 0.8)), "music": float(se.get("music", 0.5)), p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)), "ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")), "language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
"big_font": bool(se.get("big_font", false))} "big_font": bool(se.get("big_font", false))}

View File

@@ -202,7 +202,7 @@ func _next_banner() -> void:
var delay := 0.0 var delay := 0.0
var band := ColorRect.new() var band := ColorRect.new()
band.color = Color(0, 0, 0, 0.55) band.color = Color(0, 0, 0, 0.55)
band.position = Vector2(0, 300) band.position = Vector2(0, 355)
band.size = Vector2(1280, 90) band.size = Vector2(1280, 90)
band.mouse_filter = Control.MOUSE_FILTER_IGNORE band.mouse_filter = Control.MOUSE_FILTER_IGNORE
band.scale.y = 0.0 band.scale.y = 0.0
@@ -218,7 +218,7 @@ func _next_banner() -> void:
l.mouse_filter = Control.MOUSE_FILTER_IGNORE l.mouse_filter = Control.MOUSE_FILTER_IGNORE
screen.fx_layer.add_child(l) screen.fx_layer.add_child(l)
l.reset_size() l.reset_size()
l.position = Vector2(640, 345) - l.size / 2 l.position = Vector2(640, 400) - l.size / 2
l.pivot_offset = l.size / 2 l.pivot_offset = l.size / 2
l.scale = Vector2(0.6, 0.6) l.scale = Vector2(0.6, 0.6)
l.modulate.a = 0.0 l.modulate.a = 0.0

View File

@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
func items() -> Array: func items() -> Array:
match zone: match zone:
"deck": "deck":
return [screen.deck] if screen.deck.visible else [] return screen.offer_views
"hand": "hand":
return screen.hand_views return screen.hand_views
"own": "own":
@@ -105,7 +105,7 @@ func confirm() -> void:
var item = items[index] var item = items[index]
match zone: match zone:
"deck": "deck":
screen.on_click("deck", null) screen.on_click("deck", index)
"hand": "hand":
screen.on_click("hand", item.card) screen.on_click("hand", item.card)
"own": "own":

View File

@@ -1,23 +1,29 @@
class_name BattleLayout class_name BattleLayout
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem ## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links), ## (Rückseiten), sein Brett, seine Elementleiste, die eigene Elementleiste (die Naht liegt auf halber
## Elementband, oben der Gegner mit seinem Brett. ## Höhe), eigenes Brett, eigene Hand. Links: Kasten des Gegners, die drei offenen Karten des
## Stapels (mittig), eigener Kasten. Rechts oben die Knöpfe, rechts mittig die Vorschau, darunter Meldungen.
const B_GIVEUP := Rect2(10, 10, 96, 32) const B_GIVEUP := Rect2(1070, 10, 96, 32)
const B_LOG := Rect2(112, 10, 96, 32) const B_LOG := Rect2(1172, 10, 96, 32)
const OPP_BOX := Rect2(330, 10, 620, 56) const B_END := Rect2(1070, 48, 198, 32)
const FIELD := Rect2(240, 226, 800, 32) const OPP_BOX := Rect2(20, 11, 210, 96)
const MSG := Rect2(240, 262, 800, 22) const OPP_HAND_Y := 11
const OWN_PANEL := Rect2(240, 288, 800, 160) const FIELD_OPP := Rect2(240, 364, 800, 32)
const STATUS := Rect2(20, 288, 210, 160) const FIELD := Rect2(240, 404, 800, 32)
const B_UNDO := Rect2(1050, 298, 210, 40) const MSG := Rect2(1040, 630, 220, 60)
const STATUS := Rect2(20, 694, 210, 96)
const B_UNDO := Rect2(1070, 86, 198, 32)
const OWN_X := 345 const OWN_X := 345
const HAND_W := 150 const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
const HAND_H := 205 const HAND_H := 140
const HAND_GAP := 10 const HAND_GAP := 10
const HAND_Y := 532 const HAND_Y := 610
const DECK := Rect2(50, 540, 130, 182) const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
const B_END := Rect2(1060, 600, 200, 60) const OFFER_X := 70 # die drei offenen Karten des Stapels, links übereinander um die Mitte
const OFFER_Y := 180
const OFFER_GAP := 10
const DECK_LABEL := Rect2(20, 632, 210, 22)
const LOG_PANEL := Rect2(240, 60, 800, 560) const LOG_PANEL := Rect2(240, 60, 800, 560)
const PREVIEW_SIZE := Vector2(180, 252) const PREVIEW_SIZE := Vector2(180, 252)
const CENTER_X := 240 const CENTER_X := 240
@@ -25,38 +31,41 @@ const CENTER_W := 800
static func own_slot(j: int) -> Rect2: static func own_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 298, 110, 140) return Rect2(OWN_X + j * 120, 440, 110, 140)
## Brett des Gegners unter seinem Kasten. ## Brett des Gegners über seiner Elementleiste.
static func opp_slot(j: int) -> Rect2: static func opp_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 76, 110, 140) return Rect2(OWN_X + j * 120, 220, 110, 140)
## Hand: bis zu fünf große Karten unten, mittig. ## Hand: bis zu fünf Karten unten, mittig; den Bogen legt hand_arc dazu.
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 := CENTER_X + (CENTER_W - total) / 2 var x := CENTER_X + (CENTER_W - total) / 2
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, 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 Handkarten, sonst rechts ## Hand des Gegners oben, gleich groß, nur die Rückseiten.
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild. static func opp_hand_rect(count: int, i: int) -> Rect2:
static func preview_rect(anchor: Rect2) -> Rect2: var r := hand_rect(count, i)
if anchor.position.y >= HAND_Y - 10: r.position.y = OPP_HAND_Y
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x) return r
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 (Elementbonus, Eigenschaften) neben der Vorschau, rechts davon oder links. ## Leichter Bogen: Karte i von count dreht sich um ihre Mitte und sinkt zum Rand hin ab
## (mirrored: für den Gegner oben spiegelverkehrt). Liefert [Grad, y-Versatz].
static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
var t := i - (count - 1) / 2.0
var sign := -1.0 if mirrored else 1.0
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
static func preview_rect(_anchor: Rect2) -> Rect2:
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
static func info_rect(preview: Rect2) -> Rect2: static func info_rect(preview: Rect2) -> Rect2:
var w := 230.0 return Rect2(1030, preview.end.y + 8, 230, 80)
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)

View File

@@ -60,7 +60,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
var hero_layer: Control var hero_layer: Control
var tile_layer: Control var tile_layer: Control
var hand_layer: Control var hand_layer: Control
var deck: Deck var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
var offer_layer: Control
var deck_label: Label
var draw_enabled := false
var log_overlay: Control var log_overlay: Control
var b_log: Button var b_log: Button
var status_panel: Panel var status_panel: Panel
@@ -70,14 +73,12 @@ var preview_holder: Control
var name_label: Label var name_label: Label
var life_label: Label var life_label: Label
var energy_label: Label var energy_label: Label
var income_label: Label
var status_label: Label
var msg_label: Label var msg_label: Label
var b_end: Button var b_end: Button
var b_giveup: Button var b_giveup: Button
var element_view: ElementView var element_view: ElementView # eigene Elementleiste
var opp_element_view: ElementView # die des Gegners darüber
var info_label: Label # Vorschau: Elementbonus, Merkmale var info_label: Label # Vorschau: Elementbonus, Merkmale
var board_panel: Panel # eigenes Brett
var menu_layer: Control # Kartenmenü (Rechtsklick) var menu_layer: Control # Kartenmenü (Rechtsklick)
var menu_target = null # Karte oder Wesen, für das das Menü offen ist var menu_target = null # Karte oder Wesen, für das das Menü offen ist
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
@@ -181,43 +182,43 @@ func _build_static() -> void:
b_giveup.add_theme_font_size_override("font_size", 13) b_giveup.add_theme_font_size_override("font_size", 13)
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log) b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
b_log.add_theme_font_size_override("font_size", 13) b_log.add_theme_font_size_override("font_size", 13)
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
b_end.add_theme_font_size_override("font_size", 15)
b_skip = Style.button(self, "Überspringen", L.B_END, func(): skip_ai = true)
b_skip.add_theme_font_size_override("font_size", 15)
b_skip.visible = false
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben # Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
Style.panel(self, L.FIELD_OPP, Style.darker(Style.PANEL, 0.9), 8)
opp_element_view = ElementView.new(L.FIELD_OPP)
add_child(opp_element_view)
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8) Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
element_view = ElementView.new(L.FIELD) element_view = ElementView.new(L.FIELD)
add_child(element_view) add_child(element_view)
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true) msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10) msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
var slots := Control.new() var slots := Control.new()
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
slots.draw.connect(func(): slots.draw.connect(func():
for j in Game.MAX_BOARD: for j in Game.MAX_BOARD:
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8) Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.05), 2, 8)
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8)) Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
add_child(slots) add_child(slots)
status_panel = Style.panel(self, L.STATUS) status_panel = Style.panel(self, L.STATUS)
var sx: float = L.STATUS.position.x var sx: float = L.STATUS.position.x
var sy: float = L.STATUS.position.y var sy: float = L.STATUS.position.y
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true) var sw: float = L.STATUS.size.x
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 8, sy + 6, sw - 16, 26), "center", true)
name_label.clip_text = true name_label.clip_text = true
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true) life_label = Style.label(self, "", 30, Style.RED, Rect2(sx + 8, sy + 42, sw / 2 - 8, 40), "left", true)
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true) energy_label = Style.label(self, "", 30, Style.BLUE, Rect2(sx + sw / 2, sy + 42, sw / 2 - 8, 40), "right", 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_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo) b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
b_undo.add_theme_font_size_override("font_size", 15) b_undo.add_theme_font_size_override("font_size", 13)
b_undo.visible = false b_undo.visible = false
# unten: Zieh-Stapel links, Hand in der Mitte, Zug beenden rechts deck_label = Style.label(self, "", 13, Style.MUTED, L.DECK_LABEL, "center")
deck = Deck.new(L.DECK)
deck.clicked.connect(func(): on_click("deck", null))
add_child(deck)
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.visible = false
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()
offer_layer = _layer()
tile_layer = _layer() tile_layer = _layer()
hand_layer = _layer() hand_layer = _layer()
preview_holder = _layer() preview_holder = _layer()
@@ -317,7 +318,7 @@ func target_center(ref: Dictionary) -> Vector2:
return Vector2(-1, -1) return Vector2(-1, -1)
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden. ## Kasten des Gegners links oben, gebaut wie der eigene; 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 = {}
@@ -335,11 +336,11 @@ func build_hero_boxes() -> void:
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
if not o.perks.is_empty(): if not o.perks.is_empty():
title += " ★" title += " ★"
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) labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 6, box.size.x - 16, 26), "center", true)
labels.name.clip_text = true labels.name.clip_text = true
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true) labels["life"] = Style.label(hero_layer, "", 30, Style.RED, Rect2(box.position.x + 8, box.position.y + 42, box.size.x / 2 - 8, 40), "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["stats"] = Style.label(hero_layer, "", 30, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 42, box.size.x / 2 - 8, 40), "right", true)
hero_labels[o] = labels hero_labels[o] = labels
@@ -366,8 +367,10 @@ func _input(event: InputEvent) -> void:
open_menu(under.card, Rect2(under.position, under.size)) open_menu(under.card, Rect2(under.position, under.size))
elif under is CreatureView and under in own_views and my_turn(): elif under is CreatureView and under in own_views and my_turn():
open_menu(under.creature, Rect2(under.slot, under.size)) open_menu(under.creature, Rect2(under.slot, under.size))
else: elif selected != null or fuse_first != null or any_overlay() or not my_turn():
cancel_all() cancel_all()
else:
show_menu(tr("Zug %d") % game.turn, [[tr("Zug beenden"), end_turn, true]], Rect2(event.position, Vector2.ZERO))
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
return return
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE: if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
@@ -677,6 +680,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func(): entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
play(card, true) play(card, true)
after_action(), ok[0]]) after_action(), ok[0]])
entries.append([tr("Abwerfen +%d ◆") % Game.DISCARD_GAIN, func(): do_discard(card), game.can_discard(perspective, card)[0]])
elif not is_card and target.hidden: elif not is_card and target.hidden:
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]]) entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
if full: if full:
@@ -688,6 +692,12 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen
close_menu() close_menu()
return return
show_menu(card.title(), entries, anchor)
## Kleines Menü neben anchor: Überschrift und Knöpfe [Text, Callable, aktiv].
func show_menu(title: String, entries: Array, anchor: Rect2) -> void:
Style.clear(menu_layer)
var w := 230.0 var w := 230.0
var h := 18.0 + entries.size() * 44 + 34 var h := 18.0 + entries.size() * 44 + 34
var x := clampf(anchor.end.x + 8, 10, 1270 - w) var x := clampf(anchor.end.x + 8, 10, 1270 - w)
@@ -696,7 +706,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
var y := clampf(anchor.position.y, 10, 790 - h) var y := clampf(anchor.position.y, 10, 790 - h)
var box := Rect2(x, y, w, h) var box := Rect2(x, y, w, h)
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2) Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
Style.label(menu_layer, card.title(), 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true) Style.label(menu_layer, title, 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
var by := box.position.y + 36 var by := box.position.y + 36
for e in entries: for e in entries:
var act: Callable = e[1] var act: Callable = e[1]
@@ -716,13 +726,25 @@ func close_menu() -> void:
menu_target = null menu_target = null
## Ziehen: kostet Mana, eine Karte kommt auf die Hand. ## Abwerfen: eine Handkarte gegen Mana loswerden.
func do_draw() -> void: func do_discard(card: Cards.Card) -> void:
clear_undo() clear_undo()
if is_client(): if is_client():
net.send_action({"type": "draw"}) net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
return return
var err := game.draw(perspective) var err := game.discard(perspective, card)
if err != "":
say(tr(err))
after_action()
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
func do_draw(index: int = 0) -> void:
clear_undo()
if is_client():
net.send_action({"type": "draw", "index": index})
return
var err := game.draw(perspective, index)
if err != "": if err != "":
say(tr(err)) say(tr(err))
else: else:
@@ -811,7 +833,7 @@ func on_click(kind: String, obj) -> void:
return return
match kind: match kind:
"deck": "deck":
do_draw() do_draw(int(obj) if obj != null else 0)
"hand": "hand":
var ok := g.can_play(perspective, obj) var ok := g.can_play(perspective, obj)
if not ok[0]: if not ok[0]:
@@ -881,7 +903,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
var a := game.forecast(perspective, card) var a := game.forecast(perspective, card)
var info := "" var info := ""
if a > 0: if a > 0:
info = tr("Elementbonus +%d/+%d") % [a, a] info = tr("Elementbonus +%d") % a
show_preview(card, entered, perspective.looks, info, anchor) show_preview(card, entered, perspective.looks, info, anchor)
@@ -889,7 +911,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
func creature_info(cr: Game.Creature) -> String: func creature_info(cr: Game.Creature) -> String:
var parts: Array[String] = [] var parts: Array[String] = []
if cr.aura > 0: if cr.aura > 0:
parts.append(tr("Elementbonus +%d/+%d") % [cr.aura, cr.aura]) parts.append(tr("Elementbonus +%d") % cr.aura)
var perk := cr.power() - cr.attack - cr.aura var perk := cr.power() - cr.attack - cr.aura
if perk > 0: if perk > 0:
parts.append(tr("Merkmal +%d") % perk) parts.append(tr("Merkmal +%d") % perk)
@@ -922,39 +944,25 @@ func update_attack_preview() -> void:
for v in own_views: for v in own_views:
var mine: bool = v.creature == selected var mine: bool = v.creature == selected
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0 v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
var counter: int = target.power() + Game.matchup_bonus(target, selected) if target != null and target.power() > 0 else 0 var counter: int = maxi(0, target.power() + Game.matchup_bonus(target, selected)) if target != null else 0
v.doomed = mine and target != null and counter >= selected.health v.doomed = mine and target != null and counter >= selected.health
v.queue_redraw() v.queue_redraw()
class Deck extends Control: ## Eine verdeckte Handkarte des Gegners in voller Größe.
signal clicked class CardBack extends Control:
var enabled := true
var count := 0
var cost := Game.DRAW_COST
func _init(rect: Rect2) -> void: func _init(rect: Rect2) -> void:
position = rect.position position = rect.position
size = rect.size size = rect.size
mouse_filter = Control.MOUSE_FILTER_STOP mouse_filter = Control.MOUSE_FILTER_IGNORE
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event()
clicked.emit()
func _draw() -> void: func _draw() -> void:
for k in range(2, -1, -1): var r := Rect2(Vector2.ZERO, size)
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12)).grow(0) draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 2), r)
r.position.y += 6 Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled else Style.MUTED, 2), r) Style.text(self, "?", 40, Style.GOLD, size / 2, "center", true)
if k == 0:
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
if not enabled:
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
@@ -1048,7 +1056,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 = "Hand %d ◆ %d" % [o.hand.size(), o.energy] labels.stats.text = "◆ %d" % o.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)
@@ -1088,6 +1096,11 @@ func refresh() -> void:
var c := game.find_creature(uid) var c := game.find_creature(uid)
if c != null: if c != null:
_press(c, tile, pos)) _press(c, tile, pos))
tile.double_clicked.connect(func(_v):
var c := game.find_creature(uid)
if c != null and c.owner() == perspective and my_turn() and not any_overlay():
selected = c # Doppelklick: gleich auf das Leben des Gegners
on_click("hero", opponent()))
if arrivals.has(cr.uid): if arrivals.has(cr.uid):
fx.fly_in(tile, arrivals[cr.uid]) fx.fly_in(tile, arrivals[cr.uid])
arrivals.erase(cr.uid) arrivals.erase(cr.uid)
@@ -1125,13 +1138,33 @@ func refresh() -> void:
tiles.erase(uid) tiles.erase(uid)
fx.fade_out(t) fx.fade_out(t)
update_attack_preview() update_attack_preview()
deck.visible = not spectating() Style.clear(offer_layer)
deck.enabled = mine and g.can_draw(h)[0] offer_views = []
deck.count = g.deck.size() draw_enabled = mine and g.can_draw(h)[0]
deck.cost = g.draw_cost(h) if not spectating():
deck.queue_redraw() for i in h.offer.size():
var oc: Cards.Card = h.offer[i]
var orect := Rect2(BattleLayout.OFFER_X, BattleLayout.OFFER_Y + i * (BattleLayout.HAND_H + BattleLayout.OFFER_GAP), BattleLayout.HAND_W, BattleLayout.HAND_H)
var ov := CardView.new(oc, orect.size, false, oc.rarity_in(h.looks))
ov.position = orect.position
ov.dim = not draw_enabled
var idx := i
ov.clicked.connect(func(_v): on_click("deck", idx))
ov.hover.connect(func(c, e): show_preview(c, e, h.looks, "", orect))
offer_layer.add_child(ov)
offer_views.append(ov)
deck_label.text = "%s ◆ %d · %d" % [tr("Ziehen"), g.draw_cost(h), g.deck.size()]
deck_label.visible = not spectating()
Style.clear(hand_layer) Style.clear(hand_layer)
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
for i in o.hand.size():
var back := CardBack.new(BattleLayout.opp_hand_rect(o.hand.size(), i))
var arc: Array = BattleLayout.hand_arc(o.hand.size(), i, true)
back.pivot_offset = back.size / 2
back.rotation_degrees = arc[0]
back.position.y += arc[1]
hand_layer.add_child(back)
if spectating(): if spectating():
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 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:
@@ -1141,6 +1174,9 @@ 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, card.rarity_in(h.looks)) var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
v.position = r.position v.position = r.position
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
v.rotation_degrees = arc[0]
v.position.y += arc[1]
v.lift = mine v.lift = mine
v.bonus = g.forecast(h, card) v.bonus = g.forecast(h, card)
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]: if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
@@ -1163,16 +1199,13 @@ func refresh() -> void:
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
income_label.text = tr("nächster Zug ◆ %d") % (h.max_energy + 1)
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
if spectating():
status_label.text = tr("Zuschauer") + " · " + status_label.text
b_end.disabled = not mine b_end.disabled = not mine
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client() var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
b_skip.visible = ai_running b_skip.visible = ai_running
b_end.visible = not ai_running and not spectating() b_end.visible = not ai_running and not spectating()
b_undo.visible = undo_state != null and mine b_undo.visible = undo_state != null and mine
element_view.update_from(g, h) element_view.update_from(g, h)
opp_element_view.update_from(g, opponent())
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()):

View File

@@ -163,7 +163,7 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
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(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.LIFE, f_stat) Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
if bonus > 0: if bonus > 0:
Style.text_outlined(self, "+%d/+%d" % [bonus, bonus], f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3) 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.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.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) Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)

View File

@@ -3,6 +3,7 @@ extends Control
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen). ## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
signal double_clicked(view: CreatureView) # Doppelklick: eigenes Wesen greift das Leben des Gegners an
signal hover(card: Cards.Card, entered: bool) signal hover(card: Cards.Card, entered: bool)
var creature: Game.Creature var creature: Game.Creature
@@ -38,7 +39,10 @@ func _process(_delta: float) -> void:
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()
pressed.emit(self, event.global_position) if event.double_click:
double_clicked.emit(self)
else:
pressed.emit(self, event.global_position)
func rarity() -> String: func rarity() -> String:
@@ -76,7 +80,7 @@ func _draw() -> void:
var badge := "" var badge := ""
var bw := 0.0 var bw := 0.0
if creature.aura != 0: if creature.aura != 0:
badge = "+%d/+%d" % [creature.aura, creature.aura] if creature.aura > 0 else "−%d/−%d" % [-creature.aura, -creature.aura] badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8 bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
var by := Style.fs(fs) + 13 var by := Style.fs(fs) + 13
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75)) draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
@@ -127,11 +131,11 @@ func _draw_back(rect: Rect2) -> void:
## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem ## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, wer 0 Angriff hat, kontert gar nicht), ## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, im Vorteil 1 mehr, auch bei 0 Angriff),
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil. ## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
func shown_stats() -> Array: func shown_stats() -> Array:
if matchup != 0: if matchup != 0:
var atk := 0 if creature.power() <= 0 else maxi(0, creature.power() - matchup) var atk := maxi(0, creature.power() - matchup)
return [atk, creature.health] return [atk, creature.health]
if attack_shift != 0: if attack_shift != 0:
return [maxi(0, creature.power() + attack_shift), creature.health] return [maxi(0, creature.power() + attack_shift), creature.health]

View File

@@ -1,11 +1,13 @@
class_name ElementView class_name ElementView
extends Control extends Control
## Elementboni auf einen Blick: offene Wesen je Element auf dem Tisch (ab drei +1/+1) und der ## Elementzähler der eigenen Seite, mittig: je Element zweimal das Symbol seines Sondereffekts in
## Sondereffekt der eigenen Seite (ab zwei eigenen: Erde Spott, Luft Flink, Wasser Regeneration, ## der Elementfarbe, leuchtend je offenem eigenen Wesen (beide: Effekt aktiv), daneben in der
## Feuer Doppelschlag), als Symbol in der Elementfarbe. ## Elementfarbe das Mana der bisher ausgespielten Karten (je Game.ELEMENT_MANA gibt es +1 Angriff).
var counts := {} # Tisch const ENTRY_W := 180.0
var own := {} # eigene Seite
var own := {} # offene eigene Wesen je Element
var played := {} # ausgespielte Karten je Element
func _init(rect: Rect2) -> void: func _init(rect: Rect2) -> void:
@@ -13,62 +15,53 @@ func _init(rect: Rect2) -> void:
size = rect.size size = rect.size
mouse_filter = Control.MOUSE_FILTER_IGNORE mouse_filter = Control.MOUSE_FILTER_IGNORE
for e in Cards.ELEMENTS: for e in Cards.ELEMENTS:
counts[e] = 0
own[e] = 0 own[e] = 0
played[e] = 0
func update_from(g: Game, me: Game.Player) -> void: func update_from(_g: Game, me: Game.Player) -> void:
counts = g.element_counts()
for e in Cards.ELEMENTS: for e in Cards.ELEMENTS:
own[e] = me.element_count(e) if me != null else 0 own[e] = me.element_count(e) if me != null else 0
played[e] = int(me.played.get(e, 0)) if me != null else 0
queue_redraw() queue_redraw()
func strong(e: String) -> bool:
return int(counts.get(e, 0)) >= Game.ELEMENT_STACK
func paired(e: String) -> bool: func paired(e: String) -> bool:
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
## Der Inhalt als Text (Tests, Protokoll). func bonus(e: String) -> int:
return int(played.get(e, 0)) / Game.ELEMENT_MANA
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
func text() -> String: func text() -> String:
var parts: Array[String] = [] var parts: Array[String] = []
for e in Cards.ELEMENTS: for e in Cards.ELEMENTS:
var s := "%s %d" % [tr(e), int(counts.get(e, 0))] var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
if strong(e):
s += " (+1/+1)"
if paired(e): if paired(e):
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e]) s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
if bonus(e) > 0:
s += " (+%d)" % bonus(e)
parts.append(s) parts.append(s)
return " · ".join(parts) return " · ".join(parts)
func _draw() -> void: func _draw() -> void:
var w := size.x / Cards.ELEMENTS.size() var x0 := (size.x - ENTRY_W * Cards.ELEMENTS.size()) / 2
var cy := size.y / 2
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 col: Color = Cards.ELEMENT_COLORS[e] var col: Color = Cards.ELEMENT_COLORS[e]
var x0 := i * w var active := paired(e) or bonus(e) > 0
var cy := size.y / 2 var px := x0 + i * ENTRY_W + 10
var n := int(counts.get(e, 0)) Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, cy), "midleft", active)
if strong(e): px += Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 10
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y)) for k in Game.ELEMENT_PAIR:
elif paired(e): var filled := k < int(own.get(e, 0))
draw_style_box(Style.box(Color(col, 0.18), 6, Color(col, 0.6), 1), Rect2(x0 + 2, 0, w - 4, size.y)) Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, cy), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
var active := strong(e) or paired(e) px += Game.ELEMENT_PAIR * 24 + 8
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(x0 + 12, cy), "midleft", active) Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, cy), "midleft", true)
var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12 px += 20
for k in Game.ELEMENT_STACK: if bonus(e) > 0:
var c := Vector2(px + k * 13, cy) Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
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)
px += Game.ELEMENT_STACK * 13 + 2
if paired(e):
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
if strong(e):
Style.text(self, "+1/+1", 13, Style.GREEN, Vector2(x0 + w - 12, cy), "midright", true)

View File

@@ -4,9 +4,10 @@ extends Node
var streams := {} var streams := {}
var players: Array[AudioStreamPlayer] = [] var players: Array[AudioStreamPlayer] = []
var volume := 0.8 var volume := 0.0 # Ton ist standardmäßig aus
var muted := false # Tests: nie etwas abspielen, egal was die Regler sagen
var music_player: AudioStreamPlayer var music_player: AudioStreamPlayer
var music_volume := 0.5 var music_volume := 0.0
var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start) var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start)
var music_box := {} var music_box := {}
@@ -78,7 +79,7 @@ func _music() -> AudioStreamWAV:
func set_music_volume(v: float) -> void: func set_music_volume(v: float) -> void:
music_volume = clampf(v, 0.0, 1.0) music_volume = clampf(v, 0.0, 1.0)
if music_volume <= 0.0: if music_volume <= 0.0 or muted:
music_player.stop() music_player.stop()
else: else:
music_player.volume_db = linear_to_db(music_volume * 0.6) music_player.volume_db = linear_to_db(music_volume * 0.6)
@@ -91,7 +92,7 @@ func set_volume(v: float) -> void:
func play(kind: String) -> void: func play(kind: String) -> void:
if volume <= 0.0 or not streams.has(kind): if volume <= 0.0 or muted or not streams.has(kind):
return return
for p in players: for p in players:
if not p.playing: if not p.playing:

View File

@@ -12,6 +12,8 @@ func check(cond: bool, msg: String) -> void:
else: else:
failed += 1 failed += 1
printerr("FEHLER: " + msg) printerr("FEHLER: " + msg)
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
quit(1)
func C(id: String) -> Cards.Card: func C(id: String) -> Cards.Card:
@@ -55,6 +57,7 @@ func _initialize() -> void:
test_kombos() test_kombos()
test_mana_und_kosten() test_mana_und_kosten()
test_ziehen() test_ziehen()
test_abwerfen()
test_wesen_und_angriff() test_wesen_und_angriff()
test_elementvorteil() test_elementvorteil()
test_verdeckt() test_verdeckt()
@@ -129,64 +132,44 @@ func test_mana_und_kosten() -> void:
h.energy = 5 h.energy = 5
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_wolf").cost, "Ausspielen kostet die Kartenkosten") check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_wolf").cost, "Ausspielen kostet die Kartenkosten")
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy) check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 4 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren") check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 5 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren, mindestens 1")
## Start: fünf im ersten Zug bezahlbare Karten, keine Wahl. Jeder Zug beginnt mit einer Karte; ## Start: fünf im ersten Zug bezahlbare Karten, drei offene auf dem Stapel. Ziehen: eine der drei
## Ziehen: 2 Mana, eine weitere bezahlbare Karte vom Stapel. ## wählen, eine neue rückt nach; das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana.
func test_ziehen() -> void: func test_ziehen() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 1 r.seed = 1
var g := Game.new_battle(r) var g := Game.new_battle(r)
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben") check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * (Game.START_HAND + Game.OFFER), "Stapel: jede Karte dreimal, Hände und Angebote schon ausgegeben")
for p in g.players: for p in g.players:
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand") check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind") check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare") check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
check(p.offer.size() == Game.OFFER and p.offer.all(func(c): return c.cost <= Game.START_MANA), "drei offene Karten auf dem Stapel, alle bezahlbar")
var first: Game.Player = g.current() var first: Game.Player = g.current()
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit") check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
first.hand.pop_back() first.hand.pop_back()
check(g.draw(first) == "Nicht genug Mana", "im ersten Zug reicht das Mana nicht zum Ziehen") var wanted: Cards.Card = first.offer[1]
first.energy = 3
first.max_energy = 3
var mana := first.energy
var deck_before := g.deck.size() var deck_before := g.deck.size()
check(g.draw(first) == "" and first.energy == mana - Game.DRAW_COST and first.hand.size() == Game.START_HAND, "gezogen: 2 Mana, eine Karte") check(g.draw_cost(first) == 0 and g.draw(first, 1) == "" and first.energy == Game.START_MANA and first.hand.back().id == wanted.id, "erstes Ziehen frei: die gewählte Karte kommt auf die Hand")
check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel") check(first.offer.size() == Game.OFFER and first.offer[1] != wanted and first.offer[1].cost <= first.max_energy and g.deck.size() == deck_before - 1, "eine bezahlbare neue Karte rückt an die Stelle")
first.energy = 0
first.hand.pop_back() first.hand.pop_back()
check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen") check(g.draw_cost(first) == Game.DRAW_COST and g.draw(first) == "Nicht genug Mana", "zweites Ziehen kostet 2, im ersten Zug zu teuer")
first.energy = 3
check(g.draw(first, 0) == "" and first.energy == 3 - Game.DRAW_COST, "mit Mana: 2 bezahlt")
var second: Game.Player = g.enemy_of(first) var second: Game.Player = g.enemy_of(first)
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug") check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
# Leerer Stapel wird neu gemischt # Leerer Stapel wird neu gemischt
g.deck = [] g.deck = []
first.hand.pop_back()
first.energy = 3 first.energy = 3
check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt") check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
# Mit viel Mana tauchen auch teure Karten auf # Nächster Zug: nichts kommt von selbst, das erste Ziehen ist wieder frei
first.max_energy = 20 var hand_now := first.hand.size()
first.energy = 20 g.end_turn()
var seen_expensive := false g.end_turn()
for i in 12: check(g.current() == first and first.hand.size() == hand_now and g.draw_cost(first) == 0, "neuer Zug: keine Karte von selbst, das erste Ziehen wieder frei")
first.hand = []
first.energy = 20
g.draw(first)
if first.hand[0].cost > 5:
seen_expensive = true
check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
# Zugbeginn: eine Karte gratis, solange die Hand nicht voll ist
var g5 := Game.new_battle(r)
var p1: Game.Player = g5.current()
var p2: Game.Player = g5.enemy_of(p1)
g5.end_turn()
check(p2.hand.size() == Game.START_HAND, "volle Hand: zu Zugbeginn kommt nichts")
p2.hand.pop_back()
p2.hand.pop_back()
g5.end_turn()
p1.hand.pop_back()
g5.end_turn()
check(p2.hand.size() == Game.START_HAND - 1 and p2.hand.back().cost <= p2.max_energy, "Zugbeginn: eine bezahlbare Karte gezogen")
g5.end_turn()
check(p1.hand.size() == Game.START_HAND, "auch der Startspieler zieht zu Zugbeginn")
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn. ## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
@@ -243,6 +226,20 @@ func test_elementvorteil() -> void:
var t3 := put(s3[2], "w_tiefen") var t3 := put(s3[2], "w_tiefen")
s3[0].attack(w3, t3) s3[0].attack(w3, t3)
check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr") check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
# 0 Angriff: nur mit Elementvorteil geht etwas
var s4 := setup()
var funke := put(s4[1], "f_funke") # Feuer 0/2
var hauch := put(s4[2], "l_hauch") # Luft 0/2
var wall := put(s4[2], "e_wall") # Erde 0/4
check(s4[0].can_attack(funke, wall)[1] == "Glutfunke würde keinen Schaden machen", "0 Angriff ohne Vorteil: kein Angriff")
check(s4[0].attack(funke, hauch) == "" and hauch.health == 1 and funke.health == 2, "0 Angriff mit Vorteil: 1 Schaden, kein Konter")
var s5 := setup()
var hauch2 := put(s5[1], "l_hauch")
var funke2 := put(s5[2], "f_funke")
check(s5[0].attack(hauch2, funke2) == "Windhauch würde keinen Schaden machen" and funke2.health == 2, "im Nachteil nichts")
var wall2 := put(s5[2], "e_wall")
var geist5 := put(s5[1], "w_geist") # Wasser 2/2
check(s5[0].attack(geist5, wall2) == "" and wall2.health == 4 - 1 and geist5.health == 2 - 1, "Steinwall kontert mit Elementvorteil für 1")
var s2 := setup() var s2 := setup()
var g2: Game = s2[0] var g2: Game = s2[0]
var geist := put(s2[1], "w_geist") # Wasser 2/2 var geist := put(s2[1], "w_geist") # Wasser 2/2
@@ -286,46 +283,52 @@ func test_verdeckt() -> void:
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft ## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
## Flink, Wasser Regeneration, Feuer Doppelschlag); drei auf dem Tisch geben dem Element +1/+1. ## Flink, Wasser Regeneration, Feuer Doppelschlag); je 10 Mana an ausgespielten Karten eines Elements geben
## den eigenen Wesen des Elements +1 Angriff. Jeder Spieler zählt für sich.
func test_elementboni() -> void: func test_elementboni() -> void:
var s := setup() var s := setup(["f_asche", "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] var o: Game.Player = s[2]
var fire := [put(h, "f_wolf"), put(h, "f_kobold")] var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
var bird := put(o, "l_wind") # Luft 1/1 var bird := put(o, "l_wind") # Luft 1/1
g.apply_auras() g.apply_auras()
check(fire[0].aura == 0 and fire[1].aura == 0, "zwei reichen nicht für +1/+1") check(fire[0].aura == 0 and int(h.played["Feuer"]) == 0, "nur gelegt, nicht ausgespielt: kein Bonus")
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide") check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
var hidden_fire := put(o, "f_asche", true, true) var hidden_fire := put(o, "f_asche", true, true)
g.apply_auras() g.apply_auras()
check(fire[0].aura == 0 and g.element_counts()["Feuer"] == 2 and not hidden_fire.has("Doppelschlag"), "verdeckte zählen nicht und bekommen nichts") check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
var third := put(o, "f_ritter") # Feuer beim Gegner zählt für den Tisch # Ausgespielte Karten zählen; je vier gibt es +1 Angriff für die eigenen Wesen des Elements
g.apply_auras() h.played["Feuer"] = 7
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and third.aura == 1, "drei Feuer auf dem Tisch: +1/+1 für alle Feuerwesen") g.play_card(h, h.hand[0]) # Aschekrieger für 3: macht 10 Mana voll
check(bird.aura == 0 and bird.health == 1, "kein Malus für das geschlagene Element") check(int(h.played["Feuer"]) == 10 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 2 and h.board[2].aura == 1, "10 Mana Feuer ausgespielt: +1 Angriff für alle eigenen Feuerwesen, Leben bleibt")
check(not third.has("Doppelschlag"), "der Gegner hat nur ein offenes Feuer: kein Sondereffekt") check(bird.aura == 0 and bird.health == 1 and hidden_fire.aura == 0, "der Gegner zählt für sich: nichts")
g._flip(hidden_fire) g._flip(hidden_fire)
check(hidden_fire.aura == 1 and third.has("Doppelschlag") and hidden_fire.has("Doppelschlag"), "aufgedeckt zählt es sofort: Bonus und Paar") check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
fire[0].health = 1 g.play_card(h, h.hand[0], true) # Ritter verdeckt
o.board.erase(third) var secret: Game.Creature = h.board.back()
o.board.erase(hidden_fire) check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 13, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen")
g.apply_auras() g._flip(secret)
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1 and fire[1].health == 1, "Bonus geht wieder: nur das Höchstleben sinkt, Leben wird höchstens gekappt") check(secret.aura == 1 and secret.power() == 5 and secret.health == 3, "aufgedeckt kommt der Bonus")
# Vorschau # Vorschau
check(g.forecast(h, C("f_asche")) == 1 and g.forecast(h, C("l_wind")) == 0, "Vorschau: Feuer würde den Stapel voll machen") h.played["Luft"] = 9
check(g.forecast(h, C("l_wind")) == 1 and g.forecast(h, C("w_geist")) == 0, "Vorschau: der Windgeist macht 10 Mana Luft voll und bekäme +1")
# Merkmal: doppelt # Merkmal: doppelt
h.perks = ["element_doppelt"] h.perks = ["element_doppelt"]
put(h, "f_asche")
g.apply_auras() g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt") check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt")
h.perks = [] h.perks = []
h.played["Feuer"] = 20
g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 2, "20 Mana Feuer: +2 Angriff")
# Die anderen Paare # Die anderen Paare
var walls := [put(h, "e_wall"), put(h, "e_wall")] var s5 := setup()
check(walls[0].has("Spott") and not fire[0].has("Spott"), "zwei Erde: Spott nur für Erdwesen") var walls := [put(s5[1], "e_wall"), put(s5[1], "e_wall")]
var winds := [put(h, "l_wind", false), put(h, "l_wind", false)] check(walls[0].has("Spott") and not put(s5[1], "f_wolf").has("Spott"), "zwei Erde: Spott nur für Erdwesen")
var winds := [put(s5[1], "l_wind", false), put(s5[1], "l_wind", false)]
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an") check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
var geister := [put(h, "w_geist"), put(h, "w_geist")] var s6 := setup()
var geister := [put(s6[1], "w_geist"), put(s6[1], "w_geist")]
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration") check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni # Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
var s2 := setup() var s2 := setup()
@@ -338,10 +341,25 @@ func test_elementboni() -> void:
g2.apply_auras() g2.apply_auras()
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte") check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare") check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
put(s2[2], "f_asche") h2.played["Feuer"] = 10
put(s2[2], "w_reiter") h2.played["Wasser"] = 10
g2.apply_auras() g2.apply_auras()
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2") check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "10 Mana Feuer und 10 Mana Wasser: Dampf bekommt +2 Angriff")
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
func test_abwerfen() -> void:
var s := setup(["f_wolf", "f_kobold"])
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var card: Cards.Card = h.hand[0]
check(g.discard(h, card) == "" and h.hand.size() == 1 and h.energy == 10 + Game.DISCARD_GAIN and not (card in h.hand), "abgeworfen: Karte weg, 1 Mana mehr")
check(g.log.back() == "Spieler wirft Flammenwolf ab.", "Verlauf")
check(g.discard(h, card) == "Nicht auf der Hand", "nicht zweimal")
h.energy = 0
check(g.discard(h, h.hand[0]) == "" and h.energy == 1, "auch ohne Mana, bringt eins")
check(g.discard(o, C("f_wolf")) == "Nicht dein Zug", "nur im eigenen Zug")
## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs. ## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
@@ -419,17 +437,15 @@ func test_verschmelzen() -> void:
var h3: Game.Player = s3[1] var h3: Game.Player = s3[1]
var fa := put(h3, "f_wolf") var fa := put(h3, "f_wolf")
var fb := put(h3, "w_tiefen") var fb := put(h3, "w_tiefen")
for i in 2: h3.played["Feuer"] = 10
put(h3, "f_kobold")
g3.apply_auras() g3.apply_auras()
check(fa.aura == 1 and fa.max_health == 3, "drei Feuer vor dem Verschmelzen") check(fa.aura == 1 and fa.power() == 4 and fa.max_health == 2, "10 Mana Feuer ausgespielt vor dem Verschmelzen")
g3.fuse(h3, fa, fb) g3.fuse(h3, fa, fb)
var ff: Game.Creature = h3.board[0] var ff: Game.Creature = h3.board[0]
check(ff.aura == 1 and ff.max_health == 8 and ff.health == 8 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal (7 + 1)") check(ff.aura == 1 and ff.max_health == 7 and ff.health == 7 and ff.power() == 6, "nach dem Verschmelzen: 5 + 1 Angriff, Leben ohne Bonus")
for i in 2: h3.played["Wasser"] = 10
put(h3, "w_geist")
g3.apply_auras() g3.apply_auras()
check(g3.element_counts()["Wasser"] == 3 and ff.aura == 2 and ff.max_health == 9, "Feuer und Wasser voll: Dampf bekommt +2/+2") check(ff.aura == 2 and ff.power() == 7 and ff.max_health == 7, "Feuer und Wasser voll: Dampf bekommt +2 Angriff")
# Auf der Hand # Auf der Hand
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"]) var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
var g4: Game = s4[0] var g4: Game = s4[0]
@@ -653,6 +669,8 @@ func test_spielstand_migration() -> void:
check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen") check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen")
check(p.collection.is_empty() and p.perk == "", "Sammlung auf Standard, unbekanntes Merkmal verworfen") check(p.collection.is_empty() and p.perk == "", "Sammlung auf Standard, unbekanntes Merkmal verworfen")
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen") check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
var fresh := Profile.new()
check(fresh.settings.volume == 0.0 and fresh.settings.music == 0.0, "Ton und Musik sind standardmäßig aus")
p.collection["f_wolf"] = 3 p.collection["f_wolf"] = 3
var d := p.to_dict() var d := p.to_dict()
check(d.version == Profile.SAVE_VERSION, "Version gespeichert") check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
@@ -731,6 +749,8 @@ func test_merkmale() -> void:
g.play_card(h, h.hand[0]) g.play_card(h, h.hand[0])
check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1") check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1")
g.deck = DeckGen.deck(g.rng) g.deck = DeckGen.deck(g.rng)
h.offer = g.draw_cards(Game.OFFER)
h.drew = true # das erste Ziehen wäre ohnehin frei
var mana := h.energy var mana := h.energy
check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts") check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
for perk in Game.PERKS: for perk in Game.PERKS:
@@ -784,6 +804,7 @@ func test_snapshot() -> void:
h.energy = 5 h.energy = 5
o.energy = 5 o.energy = 5
g.deck = DeckGen.deck(g.rng) g.deck = DeckGen.deck(g.rng)
h.offer = g.draw_cards(Game.OFFER)
g.draw(h) g.draw(h)
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))
@@ -793,6 +814,8 @@ func test_snapshot() -> void:
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl") check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl")
var cr2 := g2.find_creature(h.board[0].uid) var cr2 := g2.find_creature(h.board[0].uid)
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and not cr2.ready, "Wesen mit Besitzer und Zustand") check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and not cr2.ready, "Wesen mit Besitzer und Zustand")
check(int(g2.players[hi].played["Feuer"]) == C("f_wolf").cost, "ausgespieltes Elementmana reist mit")
check(g2.players[hi].offer.size() == Game.OFFER and snap.players[oi].offer.is_empty(), "das eigene Angebot reist mit, das des Gegners bleibt verborgen")
var troll2 := g2.find_creature(troll.uid) var troll2 := g2.find_creature(troll.uid)
check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit") check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit")
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen") check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")

View File

@@ -16,6 +16,8 @@ func check(cond: bool, msg: String) -> void:
else: else:
failed += 1 failed += 1
printerr("FEHLER: " + msg) printerr("FEHLER: " + msg)
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
get_tree().quit(1)
func frames(n: int = 2) -> void: func frames(n: int = 2) -> void:
@@ -54,6 +56,21 @@ func right_click() -> void:
await right_click_at(Vector2(5, 5)) await right_click_at(Vector2(5, 5))
func double_click_at(pos: Vector2) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
down.double_click = true
get_viewport().push_input(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
up.double_click = false
get_viewport().push_input(up)
await frames(2)
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void: func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
var down := InputEventMouseButton.new() var down := InputEventMouseButton.new()
down.position = pos down.position = pos
@@ -150,12 +167,11 @@ func _ready() -> void:
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)
main.sfx.muted = true # Tests bleiben still
await frames(3) await frames(3)
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_elemente_und_verschmelzen, test_kampf_grenzen] test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
if not fast: suite += [test_kampf.bind(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") != "":
@@ -280,13 +296,19 @@ func human_turn(scr) -> void:
break break
var mana: int = g.human.energy var mana: int = g.human.energy
var hand: int = g.human.hand.size() var hand: int = g.human.hand.size()
await click(scr.deck) var cost: int = g.draw_cost(g.human)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand + 1, "gezogen: eine Karte") await click(scr.offer_views[0])
check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen
for i in 12: for i in 12:
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack()) var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack() and not (v.creature in tried))
if ready.is_empty() or g.winner != "": if ready.is_empty() or g.winner != "":
break break
var cr: Game.Creature = ready[0].creature var cr: Game.Creature = ready[0].creature
var any_target: bool = g.can_attack(cr, g.opponent)[0] or g.opponent.board.any(func(c): return g.can_attack(cr, c)[0])
if not any_target:
tried.append(cr)
continue
await settle(scr) await settle(scr)
await hover(ready[0]) await hover(ready[0])
var under = get_viewport().gui_get_hovered_control() var under = get_viewport().gui_get_hovered_control()
@@ -333,7 +355,7 @@ func test_gegnerauswahl() -> void:
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand") check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar") check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar")
await shot("05_starthand") await shot("05_starthand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte") check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben")
await shot("11_kampf") await shot("11_kampf")
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
@@ -479,7 +501,7 @@ func test_sprache() -> void:
await frames(2) await frames(2)
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch") check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen") check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
check(TranslationServer.translate("Zug %d · Du bist dran") == "Turn %d · Your turn" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt") check(TranslationServer.translate("Abwerfen +%d ◆") == "Discard +%d ◆" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt")
check(find_button("Back") != null, "Knöpfe übersetzt") check(find_button("Back") != null, "Knöpfe übersetzt")
await shot("26_englisch") await shot("26_englisch")
await click(find_button("Back")) await click(find_button("Back"))
@@ -506,12 +528,13 @@ func test_kampf_steuerung() -> void:
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
g.human.hand = [Cards.by_id("f_wolf").copy()] g.human.hand = [Cards.by_id("f_wolf").copy()]
scr.refresh() scr.refresh()
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit") check(scr.hand_views.size() == 1 and scr.draw_enabled and scr.offer_views.size() == Game.OFFER, "eine Handkarte, drei offene auf dem Stapel")
# Ziehen: 2 Mana weg, eine Karte landet auf der Hand # Ziehen: die gewählte offene Karte landet auf der Hand, das erste Ziehen ist frei
var mana: int = g.human.energy var mana: int = g.human.energy
await click(scr.deck) var wanted: Cards.Card = g.human.offer[1]
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == 2 and scr.hand_views.size() == 2, "gezogen: eine Karte auf der Hand") await click(scr.offer_views[1])
check(g.human.hand[1].cost <= g.human.max_energy, "nur bezahlbare Karten") check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.hand[1].id == wanted.id, "gezogen: die gewählte Karte, frei")
check(g.human.offer.size() == Game.OFFER and g.human.offer.all(func(c): return c.cost <= g.human.max_energy) and g.draw_cost(g.human) == Game.DRAW_COST, "eine bezahlbare neue liegt nach, das nächste Ziehen kostet 2")
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an # Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
var board_before: int = g.human.board.size() var board_before: int = g.human.board.size()
mana = g.human.energy mana = g.human.energy
@@ -530,7 +553,7 @@ func test_kampf_steuerung() -> void:
mana = g.human.energy mana = g.human.energy
var hand_before: int = g.human.hand.size() var hand_before: int = g.human.hand.size()
await key(KEY_Z) await key(KEY_Z)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht") check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht die erste offene Karte für 2")
await shot("22_kampf_steuerung") await shot("22_kampf_steuerung")
# Gamepad: A auf dem Stapel zieht # Gamepad: A auf dem Stapel zieht
await pad(JOY_BUTTON_DPAD_LEFT) await pad(JOY_BUTTON_DPAD_LEFT)
@@ -571,21 +594,21 @@ func test_kampf_grenzen() -> void:
var guard := 0 var guard := 0
while g.human.hand.size() < Game.MAX_HAND and guard < 30: while g.human.hand.size() < Game.MAX_HAND and guard < 30:
guard += 1 guard += 1
await click(scr.deck) await click(scr.offer_views[0])
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND) check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen") check(not scr.draw_enabled, "bei voller Hand lässt sich nichts ziehen")
await click(scr.deck) await click(scr.offer_views[0])
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text) check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
# Feld voll spielen # Feld voll spielen
guard = 0 guard = 0
while g.human.board.size() < Game.MAX_BOARD and guard < 30: while g.human.board.size() < Game.MAX_BOARD and guard < 30:
guard += 1 guard += 1
if g.human.hand.is_empty(): if g.human.hand.is_empty():
await click(scr.deck) await click(scr.offer_views[0])
await key(KEY_1) await key(KEY_1)
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD) check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
if g.human.hand.is_empty(): if g.human.hand.is_empty():
await click(scr.deck) await click(scr.offer_views[0])
await key(KEY_1) await key(KEY_1)
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text) check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
await shot("25_kampf_voll") await shot("25_kampf_voll")
@@ -596,9 +619,10 @@ func test_kampf_grenzen() -> void:
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei") check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
# Ohne Mana kein Ziehen # Ohne Mana kein Ziehen
g.human.energy = 0 g.human.energy = 0
g.human.drew = true
scr.refresh() scr.refresh()
await frames(1) await frames(1)
check(not scr.deck.enabled, "ohne Mana kein Ziehen") check(not scr.draw_enabled, "ohne Mana kein zweites Ziehen")
await give_up() await give_up()
@@ -613,6 +637,8 @@ func test_kampf_elemente_und_verschmelzen() -> void:
var g: Game = scr.game var g: Game = scr.game
g.human.board.clear() g.human.board.clear()
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
for e in Cards.ELEMENTS:
scr.opponent().played[e] = 0
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature: var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid) var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
g.next_uid += 1 g.next_uid += 1
@@ -620,7 +646,9 @@ func test_kampf_elemente_und_verschmelzen() -> void:
cr.hidden = hidden cr.hidden = hidden
p.board.append(cr) p.board.append(cr)
return cr return cr
put.call(g.human, "f_wolf") var wolf_cr: Game.Creature = put.call(g.human, "f_wolf")
wolf_cr.health = 4 # übersteht die Konter von Falke und Kobold, damit der Test ihn weiter nutzen kann
wolf_cr.max_health = 4
put.call(g.human, "f_asche") put.call(g.human, "f_asche")
put.call(g.human, "w_reiter") put.call(g.human, "w_reiter")
var falke: Game.Creature = put.call(scr.opponent(), "l_falke") var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
@@ -631,21 +659,25 @@ func test_kampf_elemente_und_verschmelzen() -> void:
g.apply_auras() g.apply_auras()
scr.refresh() scr.refresh()
await frames(2) await frames(2)
check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen, Feuerpaar: " + scr.element_view.text()) check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0 · Erde 0/0 · Luft 0/0", "Zähler: offene eigene Wesen und ausgespielte Karten, Feuerpaar: " + scr.element_view.text())
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 1, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; der Kobold würde den Stapel voll machen") check(scr.opp_element_view.text() == "Feuer 0/0 · Wasser 0/0 · Erde 0/0 · Luft 1/0", "die Leiste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text())
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; noch kein Bonus in Sicht")
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen # Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
await hover(scr.hand_views[0]) await hover(scr.hand_views[0])
await right_click_at(scr.hand_views[0].get_global_rect().get_center()) await right_click_at(scr.hand_views[0].get_global_rect().get_center())
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü") check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen")
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü") check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
await shot("23_kartenmenue") await shot("23_kartenmenue")
await right_click() await right_click()
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü") check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
await right_click_at(Vector2(1150, 300)) # leere Fläche
check(scr.menu_layer.visible and find_button("Zug beenden", scr.menu_layer) != null, "Rechtsklick ins Leere bietet Zug beenden an")
await right_click()
# Mittlere Maustaste legt die Handkarte verdeckt # Mittlere Maustaste legt die Handkarte verdeckt
var mana: int = g.human.energy var mana: int = g.human.energy
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE) await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
var mine_hidden: Game.Creature = g.human.board[3] var mine_hidden: Game.Creature = g.human.board[3]
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2"), "mittlere Maustaste legt verdeckt, zählt nicht") check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2/1"), "mittlere Maustaste legt verdeckt: zählt als ausgespielt, nicht als offen")
# Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü # Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü
await right_click_at(scr.own_views[0].get_global_rect().get_center()) await right_click_at(scr.own_views[0].get_global_rect().get_center())
check(not scr.menu_layer.visible, "kein Menü ohne Einträge") check(not scr.menu_layer.visible, "kein Menü ohne Einträge")
@@ -656,10 +688,15 @@ func test_kampf_elemente_und_verschmelzen() -> void:
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text) check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
mine_hidden.ready = true mine_hidden.ready = true
await click(scr.own_views[3]) await click(scr.own_views[3])
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (+1/+1)"), "aufgedeckt: drei Feuer, +1/+1: " + scr.element_view.text()) check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text())
check(wolf.power() == 4 and falke.power() == 3 and mine_hidden.can_attack(), "Feuerwesen +1, kein Malus für Luft, aufgedeckt greift sofort an") check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter 10 Mana kein Bonus, aufgedeckt greift sofort an")
g.human.played["Feuer"] = 10
g.apply_auras()
scr.refresh()
await frames(1)
check(scr.element_view.text().begins_with("Feuer 3/10 (Doppelschlag) (+1)") and wolf.power() == 4 and falke.power() == 3, "10 Mana Feuer: +1 Angriff für die eigenen Feuerwesen: " + scr.element_view.text())
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus") check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
await shot("23b_drei_feuer") await shot("23b_vier_feuer")
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt # Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
await click(scr.own_views[0]) await click(scr.own_views[0])
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte") check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
@@ -671,7 +708,8 @@ func test_kampf_elemente_und_verschmelzen() -> void:
await click(scr.tiles[secret.uid]) await click(scr.tiles[secret.uid])
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold") check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt") check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
await right_click() await double_click_at(scr.own_views[0].get_global_rect().get_center())
check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text)
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten # Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
mana = g.human.energy mana = g.human.energy
var before: int = g.human.board.size() var before: int = g.human.board.size()
@@ -936,6 +974,14 @@ func test_kampf(shot_turn: int) -> void:
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")
await shot("07_kampf_start") await shot("07_kampf_start")
# Kurze Partie: einfache KI, Gegner fast tot, drei bereite Drachen räumen auf
scr.smart_ai = false
g.opponent.life = 1
for i in 3:
var drache := Game.Creature.new(Cards.by_id("f_drache").copy(), g.human, 9100 + i)
drache.ready = true
g.human.board.append(drache)
scr.refresh()
var coins: int = main.profile.coins var coins: int = main.profile.coins
var guard := 0 var guard := 0
while g.winner == "" and guard < 60: while g.winner == "" and guard < 60:

View File

@@ -36,13 +36,10 @@ Gesperrt,Locked
Spiel eröffnen,Host game Spiel eröffnen,Host game
Beitreten,Join Beitreten,Join
Du bist dran,Your turn Du bist dran,Your turn
Gegnerzüge überspringen,Skip AI turns
Rückgängig,Undo Rückgängig,Undo
Kapitel 1,Chapter 1 Kapitel 1,Chapter 1
Kapitel 2,Chapter 2 Kapitel 2,Chapter 2
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d" "Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
Zug %d · Du bist dran,Turn %d · Your turn
Zug %d · %s ist dran …,Turn %d · %s is playing …
Wähle ein Angriffsziel,Choose an attack target Wähle ein Angriffsziel,Choose an attack target
Abgebrochen,Cancelled Abgebrochen,Cancelled
%s kann nicht angreifen,%s cannot attack %s kann nicht angreifen,%s cannot attack
@@ -111,9 +108,8 @@ Verdeckt legen ◆ %d,Set face-down ◆ %d
Aufdecken,Reveal Aufdecken,Reveal
verdeckt,face-down verdeckt,face-down
Aufgedeckt!,Revealed! Aufgedeckt!,Revealed!
Elementbonus +%d/+%d,Element bonus +%d/+%d Elementbonus +%d,Element bonus +%d
Merkmal +%d,Perk +%d Merkmal +%d,Perk +%d
nächster Zug ◆ %d,next turn ◆ %d
Erst die Wesen,Creatures first Erst die Wesen,Creatures first
%s liegt verdeckt,%s is face-down %s liegt verdeckt,%s is face-down
%s liegt schon offen,%s is already face-up %s liegt schon offen,%s is already face-up
@@ -154,3 +150,6 @@ greift erst im nächsten Zug an,attacks next turn
%s durch das Element,%s from its element %s durch das Element,%s from its element
Zwei verschiedene Karten wählen,Pick two different cards Zwei verschiedene Karten wählen,Pick two different cards
stirbt,dies stirbt,dies
Abwerfen +%d ◆,Discard +%d ◆
Überspringen,Skip
Zug %d,Turn %d
1 keys en
36 Spiel eröffnen Host game
37 Beitreten Join
38 Du bist dran Your turn
Gegnerzüge überspringen Skip AI turns
39 Rückgängig Undo
40 Kapitel 1 Chapter 1
41 Kapitel 2 Chapter 2
42 Münzen: %d · Staub: %d Coins: %d · Dust: %d
Zug %d · Du bist dran Turn %d · Your turn
Zug %d · %s ist dran … Turn %d · %s is playing …
43 Wähle ein Angriffsziel Choose an attack target
44 Abgebrochen Cancelled
45 %s kann nicht angreifen %s cannot attack
108 Aufdecken Reveal
109 verdeckt face-down
110 Aufgedeckt! Revealed!
111 Elementbonus +%d/+%d Elementbonus +%d Element bonus +%d/+%d Element bonus +%d
112 Merkmal +%d Perk +%d
nächster Zug ◆ %d next turn ◆ %d
113 Erst die Wesen Creatures first
114 %s liegt verdeckt %s is face-down
115 %s liegt schon offen %s is already face-up
150 %s durch das Element %s from its element
151 Zwei verschiedene Karten wählen Pick two different cards
152 stirbt dies
153 Abwerfen +%d ◆ Discard +%d ◆
154 Überspringen Skip
155 Zug %d Turn %d

Binary file not shown.