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-03 01:17:27 +02:00
parent 24a03ed1ee
commit 5f9442497a
27 changed files with 1040 additions and 871 deletions

View File

@@ -21,17 +21,17 @@ Der Spielstand liegt unter `user://spielstand.json`
## Spielsysteme ## Spielsysteme
- 77 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen - 77 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 Angriff plus Leben, halbiert und aufgerundet - Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1
- Mana: 5 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen - Mana: 5 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf sehen, drei behalten; Ziehen kostet 1 Mana und zeigt drei, eine bleibt; angeboten wird 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, Ziehen kostet 1 Mana und bringt eine; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
- Verbessern: Schild (3) und Doppelschlag (4) lassen sich für Mana auf jede Hand- oder Brettkarte kaufen (Rechtsklick oder Y). Frisch gelegte Wesen greifen sofort an, zu Zugbeginn heilen die eigenen Wesen ganz - Eigenschaften: Spott (2, muss zuerst angegriffen werden), Flink (2, greift sofort an), Regeneration (3, heilt am Zugende ganz) und Doppelschlag (4) lassen sich für Mana auf jede Hand- oder Brettkarte kaufen (Rechtsklick oder Y). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
- Verdeckt legen: gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementregel; aufdecken ist kostenlos, ein Angriff deckt es auf - 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
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Holo-Karten: selten farbiger Rahmen, Schein und Edelstein, episch ein wandernder Lichtstreifen, legendär Funkeln, mythisch schillernde Farben; auch auf dem Feld für alle sichtbar, Werte und Text bleiben gleich - 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 gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück - Booster (100 Münzen): fünf 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
- Vier offene Wesen eines Elements auf dem Tisch: dieses Element bekommt +1/+1, das Element, das es schlägt, −1/−1 (Wasser > Feuer > Luft > Erde > Wasser) - 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
- Elementvorteil beim Kampf: der Angreifer macht 1 Schaden mehr im Vorteil und 1 weniger im Nachteil, der Konter bleibt gleich, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht, auch bei der Vier-Elemente-Regel zählt sein bester Wert - Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): der Angreifer macht 1 Schaden mehr im Vorteil und 1 weniger im Nachteil, der Konter bleibt gleich, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht
- Angriffe treffen erst Wesen; das Leben ist nur ohne Wesen angreifbar. Schaden über das Leben eines Wesens hinaus schlägt auf den Besitzer durch - Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar. Nichts schlägt durch
- Verschmelzen: zwei eigene Wesen werden eins mit dem besseren Angriff und Leben plus 1, Elementen 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), 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, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen - Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 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
@@ -70,7 +70,7 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
## Aufbau ## Aufbau
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json` - `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementregel, Kampf, Verbessern, Verschmelzen - `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementboni, Kampf, Verbessern, Verschmelzen
- `scripts/deckgen.gd` – gemeinsamer Stapel und Gegner - `scripts/deckgen.gd` – gemeinsamer Stapel und Gegner
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk - `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI) - `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)

View File

@@ -12,20 +12,22 @@
## Sammeln ## Sammeln
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat - Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik wie bei Holo-Karten: je höher die Stufe, desto mehr Glanz (Rahmen, Schein, Lichtstreifen, Funkeln, Schillern), auch im Netzwerkspiel sichtbar - 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: höhere Stufen werten Karten auf, der Rest wird zu Staub - 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, eine Stufe abgeben bringt Staub 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 sehen, drei behalten; Ziehen kostet 1 Mana und zeigt drei, eine bleibt; angeboten wird nur, was man sich in diesem Zug leisten kann - Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten; Ziehen kostet 1 Mana und bringt eine Karte; es kommt nur, was man sich in diesem Zug leisten kann
- Mana für Ziehen, Legen (Kosten = Angriff + Leben, halbiert), Verbessern und Verschmelzen; 5 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, mindestens 1), Verbessern und Verschmelzen; 5 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
- Karten haben nur Angriff und Leben; Eigenschaften (Schild, Doppelschlag) kauft man per Rechtsklick für Mana dazu; gelegte Wesen greifen sofort an, eigene Wesen heilen zu Zugbeginn - Karten haben nur Angriff und Leben; Eigenschaften kauft man per Rechtsklick für Mana dazu, auf Hand und Brett: Spott 2 (muss zuerst angegriffen werden), Flink 2 (greift sofort an), Regeneration 3 (heilt sich am Zugende ganz), Doppelschlag 4
- Wesen lassen sich verdeckt legen; aufdecken ist frei, ein Angriff deckt auf - Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
- Leben erst ohne Wesen angreifbar; Überschuss schlägt durch - 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
- Leben erst ohne Wesen angreifbar; Wesen mit Spott zuerst; nichts schlägt durch
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen - Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
## Elemente ## Elemente
- 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)
- Vier offene Wesen eines Elements: +1/+1 für das Element, −1/−1 für das, das es schlägt; die Karte zeigt vorher, was sie bekäme - Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (der Angreifer macht 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, der Konter bleibt gleich; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht, und nimmt auch bei der Vier-Elemente-Regel seinen besten Wert) - 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
- Zwei eigene Wesen lassen sich für Mana verschmelzen: das neue erbt Elemente und Eigenschaften beider, seine Werte sind jeweils der bessere Wert plus 1 - Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (der Angreifer macht 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, der Konter bleibt gleich; 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
- 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

@@ -8,10 +8,10 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
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, Verlauf links, Zug beenden rechts, 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.
- **Hearthstones Entdecken** – drei Karten sehen, eine nehmen: Zufall mit Entscheidung. Jede - **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
Wahl zeigt an, was zum eigenen Brett passt (Elementbonus als „+1/+1“ oder „−1/−1“). Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
- **Yu-Gi-Oh** – verdeckt gelegte Monster, Angriffe erst auf Monster, Kampfschaden schlägt - **Yu-Gi-Oh** – verdeckt gelegte Monster, Angriffe erst auf Monster, Fusion durch Übereinander-
durch, Fusion. legen (hier: ziehen und halten).
- **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole - **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole
statt Sätze. statt Sätze.
- **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen, - **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen,
@@ -22,12 +22,16 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
## Karte ## Karte
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter - Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
gekaufte Eigenschaften als Symbole (Schild, gekreuzte Klingen = Doppelschlag), unten Angriff (gelb) und Leben (rot) als Kreise, dazwischen der Edelstein der Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration, gekreuzte Klingen = Doppelschlag; gekaufte weiß, vom Element in dessen Farbe), unten Angriff (gelb) und Leben (rot) als Kreise, dazwischen der Edelstein der
Stufe. Karten haben keinen Text: nur Element, Angriff, Leben. Stufe. Karten haben keinen Text: nur Element, Angriff, Leben.
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol. - Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol.
- Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau. - Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau.
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen; - Seltenheit nach Yu-Gi-Oh: Common matt; Rare silberner Name und Rahmen, stehender Glanzstreifen,
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben. Edelstein; Super Rare Holo-Bild (wandernder Regenbogenschimmer über dem Symbol); Ultra Rare
goldener Name und Rahmen plus Holo-Bild; Ultimate Rare Relief am Symbol, Holo auf Bild und
Rahmen, Funkeln, schillernde Farben. Fusionen tragen die höhere Stufe ihrer Eltern.
- Mehrere Elemente: der Hintergrund trägt das erste Element, ein rundes Medaillon hinter dem
Symbol zeigt alle Elemente als Tortenstücke.
- Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit - Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit
dem Vermerk „verdeckt“. dem Vermerk „verdeckt“.
@@ -37,33 +41,34 @@ 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 „◆ 1“ (Klick zieht), rechts - **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 1“ (Klick zieht), rechts
„Zug beenden“. „Zug beenden“.
- **Wahl** als Überlagerung: zu Beginn „Wähle 3 von 5“, nach dem Ziehen „Wähle 1 von 3“; solange - **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
sie offen ist, geht nichts anderes. goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
- **Eigenes Brett** mit fünf sichtbaren Plätzen, färbt sich mit dem aktiven Feld; links daneben das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
die eigene Leiste (Leben, Mana, Einkommen, Zug), rechts „Verschmelzen“ und „Rückgängig“, nur - **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
wenn sie gerade möglich sind. Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
- **Elementzähler** mit vier Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1, - **Elementleiste** mit drei Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1.
das geschlagene Element bekommt −1/−1. Daneben das Symbol des Sondereffekts, sobald zwei eigene Wesen des Elements liegen.
- **Kartenmenü** per Rechtsklick oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, - **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
Schild und Doppelschlag kaufen. oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
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** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
und Feldkombo darunter. und Feldkombo darunter.
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung. - **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
- Hand- und Wahlkarten 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.
## Bewegung und Ton ## Bewegung und Ton
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Ton. - Zugbeginn: Band „Dein Zug“ quer über die Mitte, Ton.
- Ziehen: die Wahl wird ausgeteilt; die genommene Karte fliegt auf die Hand.
- Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein. - Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein.
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl. - Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
- Aufdecken: „Aufgedeckt!“ am Wesen; Verbessern: die Eigenschaft steigt grün auf. - Aufdecken: „Aufgedeckt!“ am Wesen; Verbessern: die Eigenschaft steigt grün auf; Regeneration am
Zugende ebenso.
- Karten heben sich beim Zeigen an, Knöpfe federn beim Drücken, Bildschirme blenden ein. - Karten heben sich beim Zeigen an, Knöpfe federn beim Drücken, Bildschirme blenden ein.
- Booster: Karten drehen sich auf, seltene Stufen blitzen in ihrer Farbe auf. - Booster: Karten drehen sich auf, Stufen ab Rare blitzen in ihrer Farbe auf.
- Alles hängt an der Einstellung „Animationen“. - Alles hängt an der Einstellung „Animationen“.
## Menüs ## Menüs

View File

@@ -10,10 +10,10 @@ class_name AI
static func creature_value(c: Game.Creature) -> float: static func creature_value(c: Game.Creature) -> float:
var v := float(c.power() + c.health) var v := float(c.power() + c.health)
if c.shield:
v += 1.0
if c.attacks_per_turn() == 2: if c.attacks_per_turn() == 2:
v += c.power() * 0.5 v += c.power() * 0.5
if c.has("Regeneration"):
v += 1.0
return v return v
@@ -61,26 +61,25 @@ static func evaluate(g: Game, p: Game.Player) -> float:
else: else:
score -= threat * 0.3 score -= threat * 0.3
else: else:
score -= threat * 0.12 # Durchschlag: auch hinter Wesen tut Angriffskraft weh score -= threat * 0.12 # auch hinter Wesen tut gegnerische Angriffskraft weh
return score return score
# --- Vorausschau --------------------------------------------------------------- # --- Vorausschau ---------------------------------------------------------------
const MAX_TARGETS := 4 # je Angreifer nur die aussichtsreichsten Ziele prüfen const MAX_TARGETS := 4 # je Angreifer nur die aussichtsreichsten Ziele prüfen
static var allow_hidden := true # zum Messen: verdeckt legen erwägen
static var allow_upgrade := true # zum Messen: Verbessern erwägen static var allow_upgrade := true # zum Messen: Verbessern erwägen
const DEEPEN := 8 # so viele beste Erstschritte bekommen Folgeschritte const DEEPEN := 8 # so viele beste Erstschritte bekommen Folgeschritte
const FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat const FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest; Durchschlag zählt mit. ## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest.
static func _attack_rank(cr: Game.Creature, t) -> float: static func _attack_rank(cr: Game.Creature, t) -> float:
if t is Game.Player: if t is Game.Player:
return 50.0 - t.life return 50.0 - t.life
if _kills(cr, t): if _kills(cr, t):
return (100.0 if not _kills(t, cr) else 60.0) + _value(t) + _overflow(cr, t) return (100.0 if not _kills(t, cr) else 60.0) + _value(t)
return -float(t.health) return -float(t.health)
@@ -98,22 +97,20 @@ static func _top(list: Array, rank: Callable, n: int) -> Array:
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array: static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
var out := [] var out := []
for card in p.hand: for card in p.hand:
if not game.can_play(p, card)[0]: if game.can_play(p, card)[0]:
continue out.append({"type": "play", "card": card, "face_down": false})
out.append({"type": "play", "card": card, "face_down": false}) var has_target := not game.enemies_of(p).is_empty()
if allow_hidden and game.forecast(p, card) < 0:
out.append({"type": "play", "card": card, "face_down": true})
for cr in p.board: for cr in p.board:
if cr.hidden and game.can_reveal(p, cr)[0]: if cr.hidden:
out.append({"type": "reveal", "creature": cr}) if game.can_reveal(p, cr)[0]:
elif not cr.hidden and allow_upgrade: out.append({"type": "reveal", "creature": cr})
elif allow_upgrade and has_target:
# Nur Verbesserungen, die sich noch in diesem Zug auszahlen können: Doppelschlag für # Nur Verbesserungen, die sich noch in diesem Zug auszahlen können: Doppelschlag für
# ein Wesen, das gleich angreift; Schild nur, wenn kein Wesen mehr spielbar ist. # ein Wesen, das gleich angreift; Flink für eines, das sonst erst nächsten Zug dürfte.
var has_target := not game.enemies_of(p).is_empty() if cr.can_attack() and game.can_upgrade(p, cr, "Doppelschlag")[0]:
if cr.attacks_left > 0 and has_target and game.can_upgrade(p, cr, "Doppelschlag")[0]:
out.append({"type": "upgrade", "creature": cr, "keyword": "Doppelschlag"}) out.append({"type": "upgrade", "creature": cr, "keyword": "Doppelschlag"})
if not p.hand.any(func(c): return game.can_play(p, c)[0]) and game.can_upgrade(p, cr, "Schild")[0]: if not cr.ready and not cr.has("Flink") and cr.power() > 0 and game.can_upgrade(p, cr, "Flink")[0]:
out.append({"type": "upgrade", "creature": cr, "keyword": "Schild"}) out.append({"type": "upgrade", "creature": cr, "keyword": "Flink"})
var pairs := [] var pairs := []
for i in p.board.size(): for i in p.board.size():
for j in range(i + 1, p.board.size()): for j in range(i + 1, p.board.size()):
@@ -178,8 +175,6 @@ static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary
static func choose_action(game: Game, p: Game.Player) -> Dictionary: static func choose_action(game: Game, p: Game.Player) -> Dictionary:
if game.winner != "" or game.enemies_of(p).is_empty(): if game.winner != "" or game.enemies_of(p).is_empty():
return {"type": "end"} return {"type": "end"}
if not p.offer.is_empty():
return {"type": "pick", "index": pick_index(game, p)}
var view := game.masked_clone(p) var view := game.masked_clone(p)
var me: Game.Player = view.players[game.players.find(p)] var me: Game.Player = view.players[game.players.find(p)]
var t0 := Time.get_ticks_msec() var t0 := Time.get_ticks_msec()
@@ -228,18 +223,9 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
static func _kills(attacker, defender) -> bool: static func _kills(attacker, defender) -> bool:
if defender is Game.Player: if defender is Game.Player:
return attacker.power() >= defender.life return attacker.power() >= defender.life
if defender.shield:
return false
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health
## Schaden, der beim Angriff auf das Wesen auf dessen Besitzer durchschlägt.
static func _overflow(attacker, defender) -> int:
if defender is Game.Player or defender.shield:
return 0
return maxi(0, attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) - defender.health)
static func _value(c) -> int: static func _value(c) -> int:
if c is Game.Player: if c is Game.Player:
return 1000 return 1000
@@ -257,40 +243,11 @@ static func _best(list: Array, key: Callable):
return best return best
## Wert einer angebotenen Karte für p: Werte je Mana, jetzt spielbar zählt extra, der
## Elementbonus und ein Vorteil gegen das gegnerische Brett auch; viel zu teuer zählt ab.
static func offer_score(game: Game, p: Game.Player, c: Cards.Card) -> float:
var score := float(c.attack + c.health) / maxf(1.0, c.cost) * 2.0 + c.attack + c.health
if c.cost <= p.energy:
score += 3.0
if c.cost > p.max_energy + 2:
score -= 2.0 * (c.cost - p.max_energy - 2)
score += 2.0 * game.forecast(p, c)
for e in game.enemies_of(p):
for o in e.board:
if not o.hidden:
score += 1.0 * Cards.matchup(c.elements, o.card.elements)
return score
## Welche der angebotenen Karten p nimmt.
static func pick_index(game: Game, p: Game.Player) -> int:
var best := 0
var best_score := -INF
for i in p.offer.size():
var s := offer_score(game, p, p.offer[i])
if s > best_score:
best_score = s
best = i
return best
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary: static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if game.winner != "" or game.enemies_of(p).is_empty(): if game.winner != "" or game.enemies_of(p).is_empty():
return {"type": "end"} return {"type": "end"}
if not p.offer.is_empty(): # Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
return {"type": "pick", "index": pick_index(game, p)} # verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen.
# Gierig: erst das teuerste Spielbare legen, dann aufdecken und angreifen, dann ziehen.
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0]) var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
if not playable.is_empty(): if not playable.is_empty():
Game.shuffle(playable, game.rng) Game.shuffle(playable, game.rng)
@@ -299,6 +256,11 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if c.cost > card.cost: if c.cost > card.cost:
card = c card = c
return {"type": "play", "card": card, "face_down": false} return {"type": "play", "card": card, "face_down": false}
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy - 1):
var weakest: Array = p.board.duplicate()
weakest.sort_custom(func(a, b): return _value(a) < _value(b))
if game.can_fuse(p, weakest[0], weakest[1])[0]:
return {"type": "fuse", "creature": weakest[0], "other": weakest[1]}
for cr in p.board: for cr in p.board:
if cr.hidden and game.can_reveal(p, cr)[0]: if cr.hidden and game.can_reveal(p, cr)[0]:
return {"type": "reveal", "creature": cr} return {"type": "reveal", "creature": cr}
@@ -307,6 +269,10 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
var t = pick_attack_target(game, p, cr) var t = pick_attack_target(game, p, cr)
if t != null: if t != null:
return {"type": "attack", "creature": cr, "target": t} return {"type": "attack", "creature": cr, "target": t}
if not game.enemies_of(p).is_empty():
var strongest = _best(p.board.filter(func(c): return not c.hidden and game.can_upgrade(p, c, "Doppelschlag")[0]), func(c): return c.power())
if strongest != null:
return {"type": "upgrade", "creature": strongest, "keyword": "Doppelschlag"}
if game.can_draw(p)[0]: if game.can_draw(p)[0]:
return {"type": "draw"} return {"type": "draw"}
return {"type": "end"} return {"type": "end"}
@@ -337,8 +303,6 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
match action.type: match action.type:
"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)))
"pick":
game.pick(p, int(action.index))
"draw": "draw":
game.draw(p) game.draw(p)
"reveal": "reveal":

View File

@@ -10,7 +10,7 @@ const STAGES := [
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"}, {"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"},
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"}, {"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"},
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"}, {"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"},
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "schild_wesen"}, {"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "alle_flink"},
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "verbessern_billig"}, {"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "verbessern_billig"},
{"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"}, {"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"},
] ]
@@ -20,12 +20,12 @@ const STAGES := [
const STAGES2 := [ const STAGES2 := [
{"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]}, {"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
{"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]}, {"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]},
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "schild_wesen"]}, {"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "alle_flink"]},
{"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "verbessern_billig"]}, {"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "verbessern_billig"]},
{"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]}, {"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
{"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]}, {"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]},
{"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]}, {"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]},
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["schild_wesen", "leben_35"]}, {"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["alle_flink", "leben_35"]},
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["verbessern_billig", "start_energie"]}, {"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["verbessern_billig", "start_energie"]},
{"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]}, {"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]},
] ]
@@ -34,13 +34,13 @@ const STAGES2 := [
## Kapitel 3: drei Merkmale und volle Tische. ## Kapitel 3: drei Merkmale und volle Tische.
const STAGES3 := [ const STAGES3 := [
{"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]}, {"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]}, {"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "alle_flink"]},
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "schild_wesen", "leben_35"]}, {"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "alle_flink", "leben_35"]},
{"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "verbessern_billig", "extra_mana"]}, {"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "verbessern_billig", "extra_mana"]},
{"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]}, {"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]},
{"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]}, {"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]},
{"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]}, {"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]},
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["schild_wesen", "leben_35", "element_doppelt"]}, {"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["alle_flink", "leben_35", "element_doppelt"]},
{"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["verbessern_billig", "start_energie", "wesen_leben"]}, {"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["verbessern_billig", "start_energie", "wesen_leben"]},
{"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]}, {"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]},
] ]

View File

@@ -24,18 +24,27 @@ const COMBOS := {
# Elementkreis: Wasser löscht Feuer, Feuer verbrennt Luft, Luft trägt Erde ab, Erde saugt Wasser auf. # Elementkreis: Wasser löscht Feuer, Feuer verbrennt Luft, Luft trägt Erde ab, Erde saugt Wasser auf.
const BEATS := {"Wasser": "Feuer", "Feuer": "Luft", "Luft": "Erde", "Erde": "Wasser"} const BEATS := {"Wasser": "Feuer", "Feuer": "Luft", "Luft": "Erde", "Erde": "Wasser"}
# Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich. # Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich.
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"] # Stufen wie bei Yu-Gi-Oh: Common grau, Rare silber, Super Rare blau, Ultra Rare gold,
# Ultimate Rare schillert durch alle Farben.
const RARITIES: Array[String] = ["Common", "Rare", "Super Rare", "Ultra Rare", "Ultimate Rare"]
const RARITY_COLORS := { const RARITY_COLORS := {
"gewöhnlich": Color("aaaaaa"), "Common": Color("aaaaaa"),
"selten": Color("4682e6"), "Rare": Color("c8d0dc"),
"episch": Color("aa46dc"), "Super Rare": Color("4682e6"),
"legendär": Color("f0aa28"), "Ultra Rare": Color("f0aa28"),
"mythisch": Color("ff5a8c"), "Ultimate Rare": Color("ff5a8c"),
} }
# Schlüsselwörter haben Karten nicht von Haus aus; sie werden für Mana dazugekauft (Game.upgrade). # Eigenschaften haben Karten nicht von Haus aus; sie werden für Mana dazugekauft (Game.upgrade)
const KEYWORDS: Array[String] = ["Schild", "Doppelschlag"] # oder kommen vom Elementpaar der eigenen Seite (ELEMENT_KEYWORD, siehe Game.Creature.has).
const KEYWORD_COST := {"Schild": 3, "Doppelschlag": 4} const KEYWORDS: Array[String] = ["Spott", "Flink", "Regeneration", "Doppelschlag"]
const KEYWORD_TEXT := {"Schild": "Blockt den ersten Schaden", "Doppelschlag": "Greift zweimal je Zug"} const KEYWORD_COST := {"Spott": 2, "Flink": 2, "Regeneration": 3, "Doppelschlag": 4}
const KEYWORD_TEXT := {
"Spott": "Muss zuerst angegriffen werden",
"Flink": "Greift sofort an",
"Regeneration": "Heilt sich am Zugende ganz",
"Doppelschlag": "Greift zweimal je Zug",
}
const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regeneration", "Feuer": "Doppelschlag"}
const DATA_PATH := "res://data/karten.json" const DATA_PATH := "res://data/karten.json"
@@ -85,17 +94,16 @@ static func best_element(own: Array, other: Array) -> String:
return best return best
## Welches Element ein Element schlägt (und von welchem es geschlagen wird). ## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1.
static func beaten_by(element: String) -> String:
for e in BEATS:
if BEATS[e] == element:
return e
return ""
## Kosten einer Karte: Angriff plus Leben, halbiert und aufgerundet.
static func cost_of(attack: int, health: int) -> int: static func cost_of(attack: int, health: int) -> int:
return ceili((attack + health) / 2.0) return maxi(1, (2 * attack + health) / 3)
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren,
## 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.
static func fusion_value(a: int, b: int) -> int:
return maxi(a, b) + 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).
@@ -103,9 +111,13 @@ static func fusion_name(elements: Array) -> String:
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]]) return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
## Karte des verschmolzenen Wesens: Elemente und Schlüsselwörter beider Karten, die Werte ## Karte des verschmolzenen Wesens: Elemente und Eigenschaften beider Karten, Werte nach
## bestimmt Game.fuse, eine Verschmelzung mehr als der erfahrenere Elternteil. ## fusion_value (oder die übergebenen), eine Verschmelzung mehr als der erfahrenere Elternteil.
static func fused(a: Card, b: Card, attack: int, health: int) -> Card: static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
if attack < 0:
attack = fusion_value(a.attack, b.attack)
if health < 0:
health = fusion_value(a.health, b.health)
var els := [] var els := []
for e in a.elements + b.elements: for e in a.elements + b.elements:
if not (e in els): if not (e in els):
@@ -122,6 +134,7 @@ static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
if k in a.keywords or k in b.keywords: if k in a.keywords or k in b.keywords:
c.keywords.append(k) c.keywords.append(k)
c.fusions = maxi(a.fusions, b.fusions) + 1 c.fusions = maxi(a.fusions, b.fusions) + 1
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt; Game.fuse rechnet die Sammlung ein
return c return c
@@ -145,9 +158,10 @@ class Card extends RefCounted:
var elements: Array var elements: Array
var attack: int = 0 var attack: int = 0
var health: int = 0 var health: int = 0
var cost: int = 0 # abgeleitet: (Angriff + Leben) / 2, aufgerundet var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, mindestens 1
var keywords: Array = [] # dazugekauft, je Exemplar var keywords: Array = [] # dazugekauft, je Exemplar
var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …) var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …)
var look := -1 # Seltenheitsstufe verschmolzener Karten (die höhere der Eltern); -1: aus der Sammlung des Besitzers
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health} var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
@@ -155,8 +169,14 @@ class Card extends RefCounted:
d["keywords"] = keywords d["keywords"] = keywords
if fusions > 0: if fusions > 0:
d["fusions"] = fusions d["fusions"] = fusions
if look >= 0:
d["look"] = look
return d return d
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
func rarity_in(looks: Dictionary) -> String:
return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))]
static func from_dict(d: Dictionary) -> Card: static func from_dict(d: Dictionary) -> Card:
var c := Card.new() var c := Card.new()
c.id = d.id c.id = d.id
@@ -168,6 +188,7 @@ class Card extends RefCounted:
c.cost = Cards.cost_of(c.attack, c.health) c.cost = Cards.cost_of(c.attack, c.health)
c.keywords = Array(d.get("keywords", [])) c.keywords = Array(d.get("keywords", []))
c.fusions = int(d.get("fusions", 0)) c.fusions = int(d.get("fusions", 0))
c.look = int(d.get("look", -1))
return c return c
## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool. ## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool.
@@ -190,7 +211,7 @@ class Card extends RefCounted:
func fusion_cost() -> int: func fusion_cost() -> int:
return 1 << fusions return 1 << fusions
## Kartentext: die gekauften Schlüsselwörter. ## Kartentext: die gekauften Eigenschaften.
func text() -> String: func text() -> String:
if keywords.is_empty(): if keywords.is_empty():
return "" return ""

View File

@@ -1,17 +1,16 @@
class_name Game class_name Game
extends RefCounted extends RefCounted
## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen, ## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen,
## Vier-Elemente-Regel, Verschmelzen und Verbessern. ## Elementboni, Verschmelzen und Verbessern.
const MAX_BOARD := 5 const MAX_BOARD := 5
const MAX_HAND := 5 const MAX_HAND := 5
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
const DRAW_COST := 1 # Ziehen: drei Karten sehen, eine behalten const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const OFFER_SIZE := 3 const DRAW_COST := 1 # Ziehen: eine bezahlbare Karte vom Stapel
const START_OFFER := 5 # zu Beginn fünf sehen, drei behalten
const START_PICKS := 3
const START_LIFE := 30 const START_LIFE := 30
const ELEMENT_STACK := 4 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1, das geschlagene Element -1/-1 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
# Merkmale der Kampagnen-Bosse (Player.perks). # Merkmale der Kampagnen-Bosse (Player.perks).
const PERKS := { const PERKS := {
@@ -21,7 +20,7 @@ const PERKS := {
"wesen_leben": "Wesen kommen mit +1 Leben", "wesen_leben": "Wesen kommen mit +1 Leben",
"extra_mana": "Bekommt jeden Zug 1 Mana mehr", "extra_mana": "Bekommt jeden Zug 1 Mana mehr",
"leben_35": "Beginnt mit 5 Leben mehr", "leben_35": "Beginnt mit 5 Leben mehr",
"schild_wesen": "Wesen kommen mit Schild", "alle_flink": "Wesen greifen sofort an",
"verbessern_billig": "Eigenschaften kosten 1 weniger", "verbessern_billig": "Eigenschaften kosten 1 weniger",
"start_energie": "Hat im ersten Zug 2 Mana mehr", "start_energie": "Hat im ersten Zug 2 Mana mehr",
"element_doppelt": "Elementbonus zählt doppelt", "element_doppelt": "Elementbonus zählt doppelt",
@@ -49,10 +48,10 @@ class Creature extends RefCounted:
var attack: int var attack: int
var health: int var health: int
var max_health: int var max_health: int
var shield: bool var attacks_done := 0 # Angriffe in diesem Zug
var attacks_left: int var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementregel var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementboni
var aura := 0 # Vier-Elemente-Regel: +1 oder -1 auf den Angriff (Leben wird direkt angepasst) var aura := 0 # Elementbonus (+1 je Element mit vollem Stapel); das Leben ist schon angepasst
func _init(c: Cards.Card, o, id: int) -> void: func _init(c: Cards.Card, o, id: int) -> void:
card = c card = c
@@ -62,12 +61,35 @@ class Creature extends RefCounted:
attack = c.attack attack = c.attack
health = c.health health = c.health
max_health = c.health max_health = c.health
shield = "Schild" in c.keywords
attacks_left = attacks_per_turn() # frisch gelegte Wesen greifen sofort an
func owner(): func owner():
return owner_ref.get_ref() return owner_ref.get_ref()
## Eigenschaft: gekauft, als Sondereffekt des Elementpaars der eigenen Seite oder als
## Boss-Merkmal. Verdeckte Wesen haben keine.
func has(kw: String) -> bool:
if hidden:
return false
if kw in card.keywords:
return true
var o = owner()
if o == null:
return false
if kw == "Flink" and "alle_flink" in o.perks:
return true
for e in card.elements:
if Cards.ELEMENT_KEYWORD.get(e, "") == kw and o.element_count(e) >= Game.ELEMENT_PAIR:
return true
return false
## Alle wirksamen Eigenschaften in fester Reihenfolge.
func keywords() -> Array[String]:
var out: Array[String] = []
for k in Cards.KEYWORDS:
if has(k):
out.append(k)
return out
## Angriffswert inklusive Elementbonus und Boss-Merkmalen. ## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
func power() -> int: func power() -> int:
var v := attack + aura var v := attack + aura
@@ -78,18 +100,22 @@ class Creature extends RefCounted:
return v return v
func attacks_per_turn() -> int: func attacks_per_turn() -> int:
return 2 if "Doppelschlag" in card.keywords else 1 return 2 if has("Doppelschlag") else 1
func attacks_left() -> int:
return maxi(0, attacks_per_turn() - attacks_done)
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
func can_attack() -> bool: func can_attack() -> bool:
return attacks_left > 0 and not hidden and power() > 0 return not hidden and attacks_left() > 0 and power() > 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)
c.attack = attack c.attack = attack
c.health = health c.health = health
c.max_health = max_health c.max_health = max_health
c.shield = shield c.attacks_done = attacks_done
c.attacks_left = attacks_left c.ready = ready
c.hidden = hidden c.hidden = hidden
c.aura = aura c.aura = aura
return c return c
@@ -101,8 +127,6 @@ class Player extends RefCounted:
var is_human: bool var is_human: bool
var life: int var life: int
var max_life: int var max_life: int
var offer: Array = [] # offene Wahl: angebotene Karten (Start: 5, Ziehen: 3)
var picks_left := 0 # wie viele davon noch zu nehmen sind
var hand: Array = [] # eigene Exemplare (Cards.Card.copy) var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
var board: Array = [] var board: Array = []
var energy := 0 # Mana in diesem Zug; der Rest verfällt var energy := 0 # Mana in diesem Zug; der Rest verfällt
@@ -123,11 +147,17 @@ class Player extends RefCounted:
func alive() -> bool: func alive() -> bool:
return life > 0 return life > 0
## Offene eigene Wesen mit diesem Element.
func element_count(e: String) -> int:
var n := 0
for c in board:
if not c.hidden and e in c.card.elements:
n += 1
return n
func clone() -> Player: func clone() -> Player:
var p := Player.new(name, is_human, life, title) var p := Player.new(name, is_human, life, title)
p.max_life = max_life p.max_life = max_life
p.offer = offer.duplicate()
p.picks_left = picks_left
p.hand = hand.duplicate() p.hand = hand.duplicate()
p.energy = energy p.energy = energy
p.max_energy = max_energy p.max_energy = max_energy
@@ -235,7 +265,6 @@ func masked_clone(viewer: Player) -> Game:
c.attack = c.card.attack c.attack = c.card.attack
c.health = c.card.health c.health = c.card.health
c.max_health = c.card.health c.max_health = c.card.health
c.shield = false
return g return g
@@ -250,8 +279,8 @@ func start(fixed_order: bool = false) -> void:
p.order = i p.order = i
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra. # Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
p.start_bonus = 0 if i == 0 else 1 p.start_bonus = 0 if i == 0 else 1
p.offer = draw_cards(START_OFFER, START_MANA + p.start_bonus) for c in draw_cards(START_HAND, START_MANA + p.start_bonus):
p.picks_left = START_PICKS p.hand.append(c.copy())
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)))
@@ -282,17 +311,10 @@ func draw_cards(n: int, max_cost: int) -> Array:
return out return out
## Solange eine Wahl offen ist, geht nichts anderes. ## Ziehen: kostet DRAW_COST, eine Karte kommt auf die Hand.
func blocked(p: Player) -> String:
return "Erst wählen" if not p.offer.is_empty() else ""
## Ziehen: kostet DRAW_COST, zeigt OFFER_SIZE Karten, eine davon kommt auf die Hand (siehe pick).
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"]
if blocked(p) != "":
return [false, blocked(p)]
if p.hand.size() >= MAX_HAND: if p.hand.size() >= MAX_HAND:
return [false, "Deine Hand ist voll"] return [false, "Deine Hand ist voll"]
if p.energy < draw_cost(p): if p.energy < draw_cost(p):
@@ -315,36 +337,13 @@ func draw(p: Player) -> String:
if not ok[0]: if not ok[0]:
return ok[1] return ok[1]
p.energy -= draw_cost(p) p.energy -= draw_cost(p)
p.offer = draw_cards(OFFER_SIZE, p.max_energy) var card: Cards.Card = draw_cards(1, p.max_energy)[0]
p.picks_left = 1 p.hand.append(card.copy())
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 ""
## Nimmt eine der angebotenen Karten auf die Hand; jederzeit erlaubt, auch außerhalb des Zugs.
## Was liegen bleibt, wandert unter den Stapel.
func pick(p: Player, index: int) -> String:
if winner != "":
return "Vorbei"
if p.offer.is_empty():
return "Keine Wahl offen"
if index < 0 or index >= p.offer.size():
return "Keine solche Karte"
var card: Cards.Card = p.offer[index]
p.hand.append(card.copy())
p.offer.remove_at(index)
p.picks_left -= 1
if p.picks_left <= 0 or p.hand.size() >= MAX_HAND:
for rest in p.offer:
deck.insert(0, rest)
p.offer = []
p.picks_left = 0
say("%s nimmt eine Karte." % p.name)
emit({"type": "pick", "player": players.find(p)})
return ""
func begin_turn(p: Player) -> void: func begin_turn(p: Player) -> void:
p.max_energy = START_MANA + p.turns p.max_energy = START_MANA + p.turns
p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen
@@ -355,18 +354,22 @@ func begin_turn(p: Player) -> void:
say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus]) say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus])
p.start_bonus = 0 p.start_bonus = 0
for c in p.board: for c in p.board:
c.health = c.max_health # Wesen erholen sich zu Zugbeginn ganz c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
c.attacks_left = c.attacks_per_turn() c.attacks_done = 0
p.turns += 1 p.turns += 1
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)})
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
func end_turn() -> void: func end_turn() -> void:
if winner != "": if winner != "":
return return
while not current().offer.is_empty(): # offene Wahl: die erste Karte, damit nichts verfällt for c in current().board:
pick(current(), 0) if c.has("Regeneration") and c.health < c.max_health:
c.health = c.max_health
say("%s regeneriert sich." % c.name)
emit({"type": "heal", "uid": c.uid})
var n := players.size() var n := players.size()
for i in range(1, n + 1): for i in range(1, n + 1):
var idx := (current_idx + i) % n var idx := (current_idx + i) % n
@@ -384,8 +387,6 @@ func end_turn() -> void:
func can_play(p: Player, card: Cards.Card) -> Array: func can_play(p: Player, card: Cards.Card) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if not (card in p.hand): if not (card in p.hand):
return [false, "Nicht auf der Hand"] return [false, "Nicht auf der Hand"]
if p.board.size() >= MAX_BOARD: if p.board.size() >= MAX_BOARD:
@@ -395,7 +396,8 @@ func can_play(p: Player, card: Cards.Card) -> Array:
return [true, ""] return [true, ""]
## Spielt eine Karte, offen oder verdeckt. Gibt "" zurück oder eine Fehlermeldung. ## Spielt eine Karte, offen oder verdeckt. Das Wesen greift erst im nächsten Zug an (außer flink).
## Gibt "" zurück oder eine Fehlermeldung.
func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String: func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
var ok := can_play(p, card) var ok := can_play(p, card)
if not ok[0]: if not ok[0]:
@@ -410,8 +412,6 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
if "wesen_leben" in p.perks: if "wesen_leben" in p.perks:
cr.health += 1 cr.health += 1
cr.max_health += 1 cr.max_health += 1
if "schild_wesen" in p.perks:
cr.shield = true
p.board.append(cr) p.board.append(cr)
if face_down: if face_down:
say("%s legt ein Wesen verdeckt." % p.name) say("%s legt ein Wesen verdeckt." % p.name)
@@ -424,16 +424,17 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
return "" return ""
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug. ## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
## nach dem Legen. Aufgedeckt greift es sofort an.
func can_reveal(p: Player, cr: Creature) -> Array: func can_reveal(p: Player, cr: Creature) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if cr == null or not (cr in p.board): if cr == null or not (cr in p.board):
return [false, "Nur eigene Wesen"] return [false, "Nur eigene Wesen"]
if not cr.hidden: if not cr.hidden:
return [false, "%s liegt schon offen" % cr.name] return [false, "%s liegt schon offen" % cr.name]
if not cr.ready:
return [false, "%s kann erst im nächsten Zug aufgedeckt werden" % cr.name]
return [true, ""] return [true, ""]
@@ -447,7 +448,7 @@ func reveal(p: Player, cr: Creature) -> String:
return "" return ""
## Deckt ein Wesen auf; die Elementregel gilt sofort. ## Deckt ein Wesen auf; die Elementboni gelten sofort.
func _flip(cr: Creature) -> void: func _flip(cr: Creature) -> void:
cr.hidden = false cr.hidden = false
say("%s wird aufgedeckt." % cr.name) say("%s wird aufgedeckt." % cr.name)
@@ -455,14 +456,12 @@ func _flip(cr: Creature) -> void:
apply_auras() apply_auras()
# --- Verbessern: Schlüsselwörter für Mana ----------------------------------- # --- Verbessern: Eigenschaften für Mana -------------------------------------
## target: eigenes Wesen auf dem Brett oder eigene Handkarte. ## target: eigenes Wesen auf dem Brett oder eigene Handkarte.
func can_upgrade(p: Player, target, keyword: String) -> Array: func can_upgrade(p: Player, target, keyword: String) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if not Cards.KEYWORD_COST.has(keyword): if not Cards.KEYWORD_COST.has(keyword):
return [false, "Unbekannte Eigenschaft"] return [false, "Unbekannte Eigenschaft"]
var card: Cards.Card = target.card if target is Creature else target var card: Cards.Card = target.card if target is Creature else target
@@ -489,12 +488,6 @@ func upgrade(p: Player, target, keyword: String) -> String:
target.card = card target.card = card
else: else:
p.hand[p.hand.find(target)] = card p.hand[p.hand.find(target)] = card
if target is Creature:
match keyword:
"Schild":
target.shield = true
"Doppelschlag":
target.attacks_left = mini(target.attacks_per_turn(), target.attacks_left + 1)
say("%s bekommt %s." % [card.name, keyword]) say("%s bekommt %s." % [card.name, keyword])
emit({"type": "upgrade", "player": players.find(p), "keyword": keyword, "uid": target.uid if target is Creature else -1}) emit({"type": "upgrade", "player": players.find(p), "keyword": keyword, "uid": target.uid if target is Creature else -1})
return "" return ""
@@ -502,40 +495,69 @@ func upgrade(p: Player, target, keyword: String) -> String:
# --- Verschmelzen ------------------------------------------------------------ # --- Verschmelzen ------------------------------------------------------------
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einem der beiden steckt. static func card_of(x) -> Cards.Card:
static func fusion_cost(a: Creature, b: Creature) -> int: return x.card if x is Creature else x
return 1 << maxi(a.card.fusions, b.card.fusions)
func can_fuse(p: Player, a: Creature, b: Creature) -> Array: ## Seltenheitsstufe einer Karte von p: bei verschmolzenen die geerbte, sonst die der Sammlung.
static func look_of(p: Player, c: Cards.Card) -> int:
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einer der beiden steckt.
static func fusion_cost(a, b) -> int:
return 1 << maxi(card_of(a).fusions, card_of(b).fusions)
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
func can_fuse(p: Player, a, b) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if a == null or b == null or a == b: if a == null or b == null or a == b:
return [false, "Zwei verschiedene Wesen wählen"] return [false, "Zwei verschiedene Karten wählen"]
if not (a in p.board) or not (b in p.board): if a is Creature and b is Creature:
return [false, "Nur eigene Wesen"] if not (a in p.board) or not (b in p.board):
return [false, "Nur eigene Wesen"]
elif a is Creature or b is Creature:
return [false, "Nur Hand mit Hand oder Brett mit Brett"]
elif not (a in p.hand) or not (b in p.hand):
return [false, "Nicht auf der Hand"]
if p.energy < fusion_cost(a, b): if p.energy < fusion_cost(a, b):
return [false, "Nicht genug Mana"] return [false, "Nicht genug Mana"]
return [true, ""] return [true, ""]
## Verschmilzt zwei eigene Wesen zu einem offenen Wesen: Elemente und Schlüsselwörter beider, ## Verschmilzt zwei Handkarten zu einer oder zwei eigene Wesen zu einem offenen: Elemente und
## jeweils der bessere Wert (Angriff, Leben, Lebenshöchstwert) plus 1. Der Elementbonus der ## Eigenschaften beider, Werte nach Cards.fusion_value, Kosten folgen den Werten.
## Eltern wird herausgerechnet, das neue Wesen bekommt seinen eigenen. func fuse(p: Player, a, b) -> String:
func fuse(p: Player, a: Creature, b: Creature) -> String:
var ok := can_fuse(p, a, b) var ok := can_fuse(p, a, b)
if not ok[0]: if not ok[0]:
return ok[1] return ok[1]
p.energy -= fusion_cost(a, b) p.energy -= fusion_cost(a, b)
var card := Cards.fused(a.card, b.card, maxi(a.attack, b.attack) + 1, if a is Creature:
maxi(a.max_health - a.aura, b.max_health - b.aura) + 1) return _fuse_board(p, a, b)
var card := Cards.fused(a, b)
card.look = maxi(look_of(p, a), look_of(p, b))
var at := p.hand.find(a)
p.hand.erase(a)
p.hand.erase(b)
p.hand.insert(mini(at, p.hand.size()), card)
say("%s und %s verschmelzen auf der Hand zu %s (%d/%d)." % [a.name, b.name, card.name, card.attack, card.health])
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": []})
return ""
## Auf dem Brett zählen die Werte der Wesen (ohne Elementbonus, den bekommt das neue selbst);
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
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),
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
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 = maxi(a.health - a.aura, b.health - b.aura) + 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.shield = cr.shield or a.shield or b.shield cr.ready = a.ready and b.ready
cr.attacks_left = mini(a.attacks_left, b.attacks_left) cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
var at := p.board.find(a) var at := p.board.find(a)
p.board.erase(a) p.board.erase(a)
p.board.erase(b) p.board.erase(b)
@@ -548,7 +570,7 @@ func fuse(p: Player, a: Creature, b: Creature) -> String:
return "" return ""
# --- Vier-Elemente-Regel ----------------------------------------------------- # --- Elementboni ---------------------------------------------------------------
## Offene Wesen je Element auf dem ganzen Tisch. ## Offene Wesen je Element auf dem ganzen Tisch.
func element_counts() -> Dictionary: func element_counts() -> Dictionary:
@@ -563,24 +585,16 @@ func element_counts() -> Dictionary:
return counts return counts
## Bonus aus der Elementregel für diese Elemente: +1, wenn ELEMENT_STACK offene Wesen des ## +1 je Element mit ELEMENT_STACK offenen Wesen auf dem Tisch; mehrere Elemente zählen alle.
## Elements stehen, -1, wenn das Element, das es schlägt, den Stapel voll hat.
## Mehrere Elemente nehmen den besten Wert.
static func aura_of(elements: Array, counts: Dictionary) -> int: static func aura_of(elements: Array, counts: Dictionary) -> int:
var best := 0 var a := 0
for i in elements.size(): for e in elements:
var e: String = elements[i]
var a := 0
if int(counts[e]) >= ELEMENT_STACK: if int(counts[e]) >= ELEMENT_STACK:
a += 1 a += 1
var predator := Cards.beaten_by(e) return a
if predator != "" and int(counts[predator]) >= ELEMENT_STACK:
a -= 1
best = a if i == 0 else maxi(best, a)
return best
## Bonus eines offenen Wesens aus der Elementregel; verdeckte zählen nicht. ## Bonus eines offenen Wesens; verdeckte zählen nicht und bekommen nichts.
func aura_for(c: Creature, counts: Dictionary) -> int: func aura_for(c: Creature, counts: Dictionary) -> int:
if c.hidden: if c.hidden:
return 0 return 0
@@ -591,7 +605,8 @@ func aura_for(c: Creature, counts: Dictionary) -> int:
return a return a
## Rechnet die Elementregel neu; Leben folgt dem Bonus direkt (+1/+1 bzw. -1/-1). ## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er,
## sinkt nur das Höchstleben, das Leben wird höchstens darauf gekappt.
func apply_auras() -> void: func apply_auras() -> void:
var counts := element_counts() var counts := element_counts()
for p in players: for p in players:
@@ -599,8 +614,8 @@ func apply_auras() -> void:
var a := aura_for(c, counts) var a := aura_for(c, counts)
if a != c.aura: if a != c.aura:
var delta: int = a - c.aura var delta: int = a - c.aura
c.health += delta
c.max_health += delta c.max_health += delta
c.health = mini(c.health + maxi(delta, 0), c.max_health)
c.aura = a c.aura = a
@@ -614,14 +629,15 @@ func forecast(p: Player, card: Cards.Card) -> int:
# --- Kampf ------------------------------------------------------------------- # --- Kampf -------------------------------------------------------------------
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat. ## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott
## müssen vor den anderen angegriffen werden.
func can_attack(cr: Creature, target) -> Array: func can_attack(cr: Creature, target) -> Array:
if winner != "" or cr.owner() != current(): if winner != "" or cr.owner() != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if blocked(cr.owner()) != "":
return [false, blocked(cr.owner())]
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:
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
@@ -629,40 +645,24 @@ func can_attack(cr: Creature, target) -> Array:
return [false, "Kein gültiges Ziel"] return [false, "Kein gültiges Ziel"]
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")):
return [false, "Erst die Wesen mit Spott"]
return [true, ""] return [true, ""]
## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Schaden über das Leben eines Wesens ## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Gibt "" zurück oder eine Fehlermeldung.
## hinaus schlägt auf seinen Besitzer durch (sonst wäre eine Mauer unverwundbar).
## Gibt "" zurück oder eine Fehlermeldung.
func attack(cr: Creature, target) -> String: func attack(cr: Creature, target) -> String:
var ok := can_attack(cr, target) var ok := can_attack(cr, target)
if not ok[0]: if not ok[0]:
return ok[1] return ok[1]
cr.attacks_left -= 1 cr.attacks_done += 1
if target is Creature and target.hidden: if target is Creature and target.hidden:
_flip(target) _flip(target)
if target.health <= 0: # der Elementbonus hat es schon erledigt
cleanup()
check_end()
return ""
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)})
var dealt := cr.power() + matchup_bonus(cr, target) damage(target, cr.power() + matchup_bonus(cr, target))
if target is Creature: if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil
var had_shield: bool = target.shield damage(cr, target.power())
var hp_before: int = target.health
damage(target, dealt)
if not had_shield and dealt > hp_before:
var o = target.owner()
var excess := dealt - hp_before
o.life -= excess
say("%d Schaden schlagen durch auf %s." % [excess, o.name])
emit({"type": "damage", "target": ref_of(o), "amount": excess})
if target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil
damage(cr, target.power())
else:
damage(target, dealt)
cleanup() cleanup()
check_end() check_end()
return "" return ""
@@ -680,11 +680,6 @@ func damage(target, amount: int) -> void:
if amount <= 0: if amount <= 0:
return return
if target is Creature: if target is Creature:
if target.shield:
target.shield = false
say("Schild von %s blockt den Schaden." % target.name)
emit({"type": "blocked", "target": ref_of(target)})
return
target.health -= amount target.health -= amount
else: else:
target.life -= amount target.life -= amount
@@ -692,7 +687,7 @@ func damage(target, amount: int) -> void:
emit({"type": "damage", "target": ref_of(target), "amount": amount}) emit({"type": "damage", "target": ref_of(target), "amount": amount})
## Räumt Tote ab; danach gilt die Elementregel neu, was weitere Tote nach sich ziehen kann. ## Räumt Tote ab; danach gelten die Elementboni neu, was weitere Tote nach sich ziehen kann.
func cleanup() -> void: func cleanup() -> void:
for round in 8: for round in 8:
var died := false var died := false

View File

@@ -386,10 +386,6 @@ func _apply(id: int, action: Dictionary) -> void:
var p: Game.Player = game.players[seats[id]] var p: Game.Player = game.players[seats[id]]
if game.winner != "": if game.winner != "":
return return
if str(action.get("type", "")) == "pick": # wählen geht jederzeit, auch außerhalb des Zugs
game.pick(p, int(action.get("index", -1)))
action_applied.emit()
return
if game.current() != p: if game.current() != p:
return return
match str(action.get("type", "")): match str(action.get("type", "")):
@@ -415,9 +411,14 @@ func _apply(id: int, action: Dictionary) -> void:
game.attack(cr, t) game.attack(cr, t)
"draw": "draw":
game.draw(p) game.draw(p)
"fuse": "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)))
var i := int(action.get("hand", -1))
var j := int(action.get("other_hand", -1))
if a == null and i >= 0 and j >= 0 and i < p.hand.size() and j < p.hand.size():
a = p.hand[i]
b = p.hand[j]
if a != null and b != null: if a != null and b != null:
game.fuse(p, a, b) game.fuse(p, a, b)
"end": "end":

View File

@@ -11,7 +11,7 @@ static func card_of(ref) -> Cards.Card:
return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref).copy() return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref).copy()
## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde Wahl gar nicht, fremde ## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde
## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer. ## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer.
static func snapshot(g: Game, viewer: int) -> Dictionary: static func snapshot(g: Game, viewer: int) -> Dictionary:
var ps := [] var ps := []
@@ -23,14 +23,12 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
var masked: bool = c.hidden and not mine var masked: bool = c.hidden and not mine
board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card), board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card),
"attack": 0 if masked else c.attack, "health": 0 if masked else c.health, "attack": 0 if masked else c.attack, "health": 0 if masked else c.health,
"max_health": 0 if masked else c.max_health, "shield": c.shield and not masked, "max_health": 0 if masked else c.max_health, "attacks_done": c.attacks_done,
"attacks_left": c.attacks_left, "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, "start_bonus": p.start_bonus, "energy": p.energy, "max_energy": p.max_energy, "start_bonus": p.start_bonus,
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns,
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board})
"offer": p.offer.map(func(c): return c.id) if mine else [], "picks_left": p.picks_left,
"board": board})
var evs := [] var evs := []
for ev in g.events: for ev in g.events:
var e: Dictionary = ev.duplicate() var e: Dictionary = ev.duplicate()
@@ -64,17 +62,14 @@ static func from_snapshot(d: Dictionary) -> Game:
else: else:
for i in int(pd.hand): for i in int(pd.hand):
p.hand.append(placeholder) p.hand.append(placeholder)
for cid in pd.get("offer", []):
p.offer.append(Cards.by_id(cid))
p.picks_left = int(pd.get("picks_left", 0))
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))
c.attack = int(cd.attack) c.attack = int(cd.attack)
c.health = int(cd.health) c.health = int(cd.health)
c.max_health = int(cd.max_health) c.max_health = int(cd.max_health)
c.shield = bool(cd.shield) c.attacks_done = int(cd.get("attacks_done", 0))
c.attacks_left = int(cd.attacks_left) c.ready = bool(cd.get("ready", false))
c.hidden = bool(cd.get("hidden", false)) c.hidden = bool(cd.get("hidden", false))
c.aura = int(cd.get("aura", 0)) c.aura = int(cd.get("aura", 0))
p.board.append(c) p.board.append(c)

View File

@@ -8,7 +8,7 @@ const SAVE_VERSION := 6
const SAVE_PATH := "user://spielstand.json" const SAVE_PATH := "user://spielstand.json"
const BOOSTER_COST := 100 const BOOSTER_COST := 100
const BOOSTER_SIZE := 5 const BOOSTER_SIZE := 5
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5} const RARITY_WEIGHTS := {"Common": 50.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 8.0, "Ultimate Rare": 2.0}
const START_COINS := 150 const START_COINS := 150
const REWARD_WIN := 100 const REWARD_WIN := 100
const REWARD_LOSS := 50 const REWARD_LOSS := 50
@@ -19,12 +19,12 @@ const DIFFICULTIES := {
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true}, "schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
} }
# Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub. # Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub.
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600} const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
const DUST_UPGRADE := {"selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200} const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
var coins := 0 var coins := 0
var dust := 0 var dust := 0
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = gewöhnlich 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.8, "music": 0.5, "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)
@@ -116,7 +116,7 @@ func looks() -> Dictionary:
return out return out
## Anzahl Karten über der gewöhnlichen Stufe. ## Anzahl Karten über Common.
func upgraded_count() -> int: func upgraded_count() -> int:
var n := 0 var n := 0
for cid in collection: for cid in collection:
@@ -142,7 +142,7 @@ func upgrade(cid: String) -> String:
func dismantle(cid: String) -> String: func dismantle(cid: String) -> String:
var lvl := level(cid) var lvl := level(cid)
if lvl == 0: if lvl == 0:
return "Gewöhnliche Karten lassen sich nicht zerlegen" return "Common-Karten lassen sich nicht zerlegen"
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]] dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
collection[cid] = lvl - 1 collection[cid] = lvl - 1
return "" return ""
@@ -158,8 +158,8 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
var out := [] var out := []
for i in BOOSTER_SIZE: for i in BOOSTER_SIZE:
var rar: String = rarities[r.rand_weighted(weights)] var rar: String = rarities[r.rand_weighted(weights)]
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "gewöhnlich"): if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
rar = "selten" # mindestens eine seltene Karte pro Booster rar = "Rare" # mindestens eine Rare pro Booster
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar}) out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
return out return out

View File

@@ -32,8 +32,9 @@ func play_events(evs: Array, centers: Dictionary) -> void:
"damage": "damage":
sfx("damage") sfx("damage")
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay) popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
"blocked": "heal":
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay) sfx("heal")
popup(tr("Regeneration"), Style.GREEN, screen.target_center({"kind": "creature", "uid": ev.uid}), 0.0)
"play": "play":
sfx("click" if ev.get("hidden", false) else "play") sfx("click" if ev.get("hidden", false) else "play")
"reveal": "reveal":
@@ -174,33 +175,6 @@ func fly_in(tile: CreatureView, from: Rect2) -> void:
tile.position = tile.slot) tile.position = tile.slot)
## Eine gekaufte Karte fliegt aus dem Laden auf die Hand.
func fly_card(card: Cards.Card, from: Rect2, to: Rect2, rarity: String) -> void:
if not enabled:
return
var v := CardView.new(card, from.size, false, rarity)
v.position = from.position
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
v.pivot_offset = Vector2.ZERO
screen.fx_layer.add_child(v)
var tw: Tween = screen.create_tween().set_parallel(true)
tw.tween_property(v, "position", to.position, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
tw.tween_property(v, "scale", to.size / from.size, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
tw.tween_property(v, "modulate:a", 0.0, 0.1).set_delay(0.2)
tw.chain().tween_callback(v.queue_free)
## Ladenkarten werden nacheinander ausgeteilt.
func deal(v: CardView, i: int) -> void:
if not enabled:
return
v.modulate.a = 0.0
v.position.y += 24
var tw: Tween = screen.create_tween().set_parallel(true)
tw.tween_property(v, "modulate:a", 1.0, 0.15).set_delay(i * 0.05)
tw.tween_property(v, "position:y", v.position.y - 24, 0.2).set_delay(i * 0.05).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
## Kurzes Zittern des Bretts beim Treffer. ## Kurzes Zittern des Bretts beim Treffer.
func shake() -> void: func shake() -> void:
if not enabled: if not enabled:

View File

@@ -1,8 +1,9 @@
class_name BattleGamepad class_name BattleGamepad
extends RefCounted extends RefCounted
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Stapel, Hand, eigene Wesen ## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Stapel, Hand, eigene Wesen
## und Ziele (bei offener Wahl über deren Karten), A bestätigt, B bricht ab, Start beendet den ## und Ziele, A bestätigt, B bricht ab, Start beendet den Zug, Y öffnet das Kartenmenü (im
## Zug, Y öffnet das Kartenmenü (im Gegnerzug: überspringen), X macht rückgängig. ## Gegnerzug: überspringen), X macht rückgängig. Verschmelzen: im Menü „Verschmelzen mit …“,
## dann A auf dem Partner.
var screen # BattleScreen var screen # BattleScreen
var active := false var active := false
@@ -19,12 +20,6 @@ func button(button: int) -> void:
active = true active = true
if screen.game == null: if screen.game == null:
return return
if not screen.pick_views.is_empty() and zone != "pick":
zone = "pick"
index = 0
elif screen.pick_views.is_empty() and zone == "pick":
zone = "hand"
index = 0
match button: match button:
JOY_BUTTON_START: JOY_BUTTON_START:
if screen.my_turn() and not screen.any_overlay(): if screen.my_turn() and not screen.any_overlay():
@@ -44,9 +39,9 @@ func button(button: int) -> void:
if not items.is_empty() and zone in ["hand", "own"]: if not items.is_empty() and zone in ["hand", "own"]:
var item = items[clampi(index, 0, items.size() - 1)] var item = items[clampi(index, 0, items.size() - 1)]
if zone == "hand": if zone == "hand":
screen.open_menu(item.card, Rect2(item.position, item.size)) screen.open_menu(item.card, Rect2(item.position, item.size), true)
else: else:
screen.open_menu(item.creature, Rect2(item.slot, item.size)) screen.open_menu(item.creature, Rect2(item.slot, item.size), true)
JOY_BUTTON_X: JOY_BUTTON_X:
if screen.b_undo.visible: if screen.b_undo.visible:
screen.undo() screen.undo()
@@ -82,14 +77,10 @@ func items() -> Array:
match zone: match zone:
"deck": "deck":
return [screen.deck] if screen.deck.visible else [] return [screen.deck] if screen.deck.visible else []
"pick":
return screen.pick_views
"hand": "hand":
return screen.hand_views return screen.hand_views
"own": "own":
if screen.fusing: # zum Verschmelzen zählt jedes eigene Wesen, nicht nur angriffsbereite return screen.own_views
return screen.own_views
return screen.own_views.filter(func(v): return v.creature.can_attack())
"target": "target":
return targets return targets
return [] return []
@@ -115,9 +106,6 @@ func confirm() -> void:
match zone: match zone:
"deck": "deck":
screen.on_click("deck", null) screen.on_click("deck", null)
"pick":
screen.do_pick(index)
index = 0
"hand": "hand":
screen.on_click("hand", item.card) screen.on_click("hand", item.card)
"own": "own":

View File

@@ -1,7 +1,7 @@
class_name BattleLayout class_name BattleLayout
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem ## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links), ## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links),
## Feldband, oben der Gegner mit seinem Brett. Eine offene Wahl liegt als Überlagerung darüber. ## Elementband, oben der Gegner mit seinem Brett.
const B_GIVEUP := Rect2(10, 10, 96, 32) const B_GIVEUP := Rect2(10, 10, 96, 32)
const B_LOG := Rect2(112, 10, 96, 32) const B_LOG := Rect2(112, 10, 96, 32)
@@ -10,8 +10,7 @@ const FIELD := Rect2(240, 226, 800, 32)
const MSG := Rect2(240, 262, 800, 22) const MSG := Rect2(240, 262, 800, 22)
const OWN_PANEL := Rect2(240, 288, 800, 160) const OWN_PANEL := Rect2(240, 288, 800, 160)
const STATUS := Rect2(20, 288, 210, 160) const STATUS := Rect2(20, 288, 210, 160)
const B_FUSE := Rect2(1050, 298, 210, 40) const B_UNDO := Rect2(1050, 298, 210, 40)
const B_UNDO := Rect2(1050, 346, 210, 40)
const OWN_X := 345 const OWN_X := 345
const HAND_W := 150 const HAND_W := 150
const HAND_H := 205 const HAND_H := 205
@@ -19,8 +18,6 @@ const HAND_GAP := 10
const HAND_Y := 532 const HAND_Y := 532
const DECK := Rect2(50, 540, 130, 182) const DECK := Rect2(50, 540, 130, 182)
const B_END := Rect2(1060, 600, 200, 60) const B_END := Rect2(1060, 600, 200, 60)
const PICK_PANEL := Rect2(120, 150, 1040, 440)
const PICK_CARD := Vector2(180, 252)
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
@@ -36,14 +33,6 @@ static func opp_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 76, 110, 140) return Rect2(OWN_X + j * 120, 76, 110, 140)
## Angebotene Karten einer Wahl, mittig im Wahlfenster.
static func pick_rect(count: int, i: int) -> Rect2:
var gap := 20
var total := count * PICK_CARD.x + maxi(0, count - 1) * gap
var x := PICK_PANEL.position.x + (PICK_PANEL.size.x - total) / 2
return Rect2(Vector2(x + i * (PICK_CARD.x + gap), PICK_PANEL.position.y + 96), PICK_CARD)
## Hand: bis zu fünf große Karten unten, mittig. ## Hand: bis zu fünf große Karten unten, mittig.
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
@@ -51,7 +40,7 @@ static func hand_rect(count: int, i: int) -> Rect2:
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 Hand- und Ladenkarten, sonst rechts ## Schwebende Vorschau neben der gezeigten Karte: über Handkarten, sonst rechts
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild. ## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
static func preview_rect(anchor: Rect2) -> Rect2: static func preview_rect(anchor: Rect2) -> Rect2:
if anchor.position.y >= HAND_Y - 10: if anchor.position.y >= HAND_Y - 10:
@@ -64,7 +53,7 @@ static func preview_rect(anchor: Rect2) -> Rect2:
return Rect2(Vector2(x, y), PREVIEW_SIZE) return Rect2(Vector2(x, y), PREVIEW_SIZE)
## Zusatzzeile (Resonanz, Feld, Feldkombo) neben der Vorschau, rechts davon oder links. ## Zusatzzeile (Elementbonus, Eigenschaften) neben der Vorschau, rechts davon oder links.
static func info_rect(preview: Rect2) -> Rect2: static func info_rect(preview: Rect2) -> Rect2:
var w := 230.0 var w := 230.0
var x := preview.end.x + 8 var x := preview.end.x + 8

View File

@@ -1,8 +1,10 @@
class_name BattleScreen class_name BattleScreen
extends Control extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx. ## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Rechtsklick (oder Y) auf eine Handkarte oder ein eigenes Wesen öffnet das Kartenmenü: ## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
## verdeckt legen, aufdecken, Eigenschaften kaufen. ## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer). ## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
var app var app
@@ -14,8 +16,16 @@ var game: Game
var perspective: Game.Player # der Mensch, dessen Hand unten liegt var perspective: Game.Player # der Mensch, dessen Hand unten liegt
var fx: BattleFx var fx: BattleFx
var selected = null # Game.Creature var selected = null # Game.Creature
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung var hold_time := 1.0 # so lange still über dem Ziel halten, dann verschmelzen die beiden
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
var press_view: Control = null
var press_pos := Vector2.ZERO
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
var drag_target_view: Control = null
var hold_t := 0.0
var hold_pos := Vector2.ZERO
var msg_t := 0.0 var msg_t := 0.0
var ai_delay := 0.75 var ai_delay := 0.75
var ai_timer := 0.0 var ai_timer := 0.0
@@ -40,7 +50,6 @@ var undo_t := 0.0
# Ansichten (auch für Tests) # Ansichten (auch für Tests)
var tiles := {} var tiles := {}
var hand_views: Array = [] var hand_views: Array = []
var pick_views: Array = [] # Karten der offenen Wahl
var own_views: Array = [] var own_views: Array = []
var enemy_views: Array = [] var enemy_views: Array = []
var hero_panels := {} # Player -> Panel var hero_panels := {} # Player -> Panel
@@ -50,9 +59,6 @@ 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 pick_layer: Control # Überlagerung der offenen Wahl
var pick_box: Control # ihr Inhalt (wird je Wahl neu aufgebaut)
var pick_key := "" # welche Wahl gerade aufgebaut ist
var deck: Deck var deck: Deck
var log_overlay: Control var log_overlay: Control
var b_log: Button var b_log: Button
@@ -67,7 +73,6 @@ var income_label: Label
var status_label: Label var status_label: Label
var msg_label: Label var msg_label: Label
var b_end: Button var b_end: Button
var b_fuse: Button
var b_giveup: Button var b_giveup: Button
var element_view: ElementView var element_view: ElementView
var info_label: Label # Vorschau: Elementbonus, Merkmale var info_label: Label # Vorschau: Elementbonus, Merkmale
@@ -197,9 +202,6 @@ func _build_static() -> void:
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true) energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left") 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") status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
b_fuse = Style.button(self, "Verschmelzen", L.B_FUSE, toggle_fuse, Color("3c2858"))
b_fuse.add_theme_font_size_override("font_size", 15)
b_fuse.visible = false
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo) b_undo = 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", 15)
b_undo.visible = false b_undo.visible = false
@@ -222,11 +224,6 @@ func _build_static() -> void:
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_label.visible = false info_label.visible = false
fx_layer = _layer() fx_layer = _layer()
# offene Wahl: drei sehen, eine nehmen (zu Beginn fünf sehen, drei nehmen)
pick_layer = Style.overlay(self)
pick_box = Control.new()
pick_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
pick_layer.add_child(pick_box)
# Kartenmenü: durchsichtiger Fänger, der das Menü beim Klick daneben schließt # Kartenmenü: durchsichtiger Fänger, der das Menü beim Klick daneben schließt
menu_layer = Control.new() menu_layer = Control.new()
menu_layer.set_anchors_preset(Control.PRESET_FULL_RECT) menu_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
@@ -372,6 +369,20 @@ func _input(event: InputEvent) -> void:
cancel_all() cancel_all()
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:
var under := get_viewport().gui_get_hovered_control()
if under is CardView and under in hand_views and my_turn() and not any_overlay():
cancel()
play(under.card, true)
after_action()
get_viewport().set_input_as_handled()
return
if event is InputEventMouseMotion:
_drag_motion(event.position)
return
if event is InputEventMouseButton and not event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_release()
return
if event is InputEventJoypadButton and event.pressed: if event is InputEventJoypadButton and event.pressed:
pad.button(event.button_index) pad.button(event.button_index)
return return
@@ -385,7 +396,7 @@ func _input(event: InputEvent) -> void:
close_menu() close_menu()
elif log_overlay.visible: elif log_overlay.visible:
log_overlay.visible = false log_overlay.visible = false
elif selected != null or fusing: elif selected != null or fuse_first != null:
cancel_all() cancel_all()
elif not any_overlay(): elif not any_overlay():
confirm.visible = true confirm.visible = true
@@ -401,11 +412,9 @@ func _input(event: InputEvent) -> void:
if overlay.visible: if overlay.visible:
leave() leave()
_: _:
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and not spectating() and not any_overlay(): if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
var idx: int = event.keycode - KEY_1 var idx: int = event.keycode - KEY_1
if not perspective.offer.is_empty(): if idx < perspective.hand.size():
do_pick(idx)
elif my_turn() and idx < perspective.hand.size():
on_click("hand", perspective.hand[idx]) on_click("hand", perspective.hand[idx])
elif event.keycode == KEY_Z and my_turn() and not any_overlay(): elif event.keycode == KEY_Z and my_turn() and not any_overlay():
on_click("deck", null) on_click("deck", null)
@@ -429,6 +438,15 @@ func _process(delta: float) -> void:
undo_t -= delta undo_t -= delta
if undo_t <= 0.0: if undo_t <= 0.0:
clear_undo() clear_undo()
if drag_ghost != null and drag_target != null:
_set_hold(hold_t + delta)
if hold_t >= hold_time:
var a = press_target
var b = drag_target
press_target = null
_end_drag()
do_fuse(a, b)
after_action()
if is_online() and net.is_host: if is_online() and net.is_host:
net.tick() net.tick()
if game.winner != "": if game.winner != "":
@@ -503,8 +521,109 @@ func say(m: String) -> void:
func cancel() -> void: func cancel() -> void:
selected = null selected = null
fusing = false
fuse_first = null fuse_first = null
press_target = null
_end_drag()
# --- Ziehen mit der Maus: Klick beim Loslassen, Verschmelzen nach einer Sekunde über dem Ziel ---
func _press(obj, view: Control, pos: Vector2) -> void:
_end_drag()
press_target = obj
press_view = view
press_pos = pos
func _drag_motion(pos: Vector2) -> void:
if press_target == null:
return
if drag_ghost == null:
if pos.distance_to(press_pos) < 8.0 or not my_turn() or any_overlay():
return
_start_drag(pos)
drag_ghost.position = pos - drag_ghost.size / 2
var t = _fusion_target_at(pos)
if t != drag_target or pos.distance_to(hold_pos) > 6.0:
_set_hold(0.0)
drag_target = t
drag_target_view = _view_of(t)
hold_pos = pos
## Loslassen ohne Ziehen ist der Klick.
func _release() -> void:
var obj = press_target
var dragged := drag_ghost != null
press_target = null
_end_drag()
if obj == null:
return
if dragged:
refresh()
elif obj is Cards.Card:
on_click("hand", obj)
else:
on_tile_click(obj)
func _start_drag(pos: Vector2) -> void:
show_preview(null, false)
if press_target is Cards.Card:
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H) * 0.8
drag_ghost = CardView.new(press_target, size, false, press_target.rarity_in(perspective.looks))
else:
drag_ghost = CreatureView.new(press_target, BattleLayout.own_slot(0).size)
drag_ghost.mouse_filter = Control.MOUSE_FILTER_IGNORE
drag_ghost.modulate.a = 0.85
drag_ghost.position = pos - drag_ghost.size / 2
fx_layer.add_child(drag_ghost)
if press_view != null:
press_view.modulate.a = 0.4
func _end_drag() -> void:
if drag_ghost != null:
drag_ghost.queue_free()
drag_ghost = null
if is_instance_valid(press_view):
press_view.modulate.a = 1.0
_set_hold(0.0)
drag_target = null
drag_target_view = null
func _set_hold(v: float) -> void:
hold_t = v
if is_instance_valid(drag_target_view):
drag_target_view.hold = clampf(v / hold_time, 0.0, 1.0) if v > 0.0 else 0.0
drag_target_view.queue_redraw()
## Karte oder Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
func _fusion_target_at(pos: Vector2):
for v in (hand_views if press_target is Cards.Card else own_views):
var obj = v.card if v is CardView else v.creature
var r: Rect2 = Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect() # Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten
if obj != press_target and r.has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
return obj
return null
func _view_of(obj) -> Control:
if obj is Cards.Card:
for v in hand_views:
if v.card == obj:
return v
elif obj is Game.Creature and tiles.has(obj.uid):
return tiles[obj.uid]
return null
## Mögliche Partner für „Verschmelzen mit …“: andere Handkarten bzw. andere eigene Wesen.
func fusion_partners(target) -> Array:
var pool: Array = perspective.hand if target is Cards.Card else perspective.board
return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0])
func play(card: Cards.Card, face_down: bool = false) -> void: func play(card: Cards.Card, face_down: bool = false) -> void:
@@ -556,10 +675,12 @@ func do_upgrade(target, keyword: String) -> void:
after_action() after_action()
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. ## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
func open_menu(target, anchor: Rect2) -> void: ## (Gamepad, das nicht ziehen kann).
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
if spectating() or not my_turn(): if spectating() or not my_turn():
return return
cancel()
Style.clear(menu_layer) Style.clear(menu_layer)
menu_target = target menu_target = target
var entries := [] # [Text, Callable, aktiv] var entries := [] # [Text, Callable, aktiv]
@@ -570,8 +691,14 @@ func open_menu(target, anchor: Rect2) -> 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]])
elif 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:
var partners := fusion_partners(target)
entries.append([tr("Verschmelzen mit … ◆ %d") % card.fusion_cost(), func():
fuse_first = target
say(tr("Ziel wählen"))
refresh(), not partners.is_empty()])
for kw in Cards.KEYWORDS: for kw in Cards.KEYWORDS:
var k: String = kw var k: String = kw
var ok := game.can_upgrade(perspective, target, k) var ok := game.can_upgrade(perspective, target, k)
@@ -607,16 +734,7 @@ func close_menu() -> void:
menu_target = null menu_target = null
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse). ## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
func toggle_fuse() -> void:
var on := not fusing
cancel()
fusing = on
say(tr("Zwei eigene Wesen wählen") if on else tr("Abgebrochen"))
after_action()
## Ziehen: kostet Mana, öffnet die Wahl aus drei Karten.
func do_draw() -> void: func do_draw() -> void:
clear_undo() clear_undo()
if is_client(): if is_client():
@@ -629,31 +747,15 @@ func do_draw() -> void:
fx.sfx("buy") fx.sfx("buy")
## Nimmt eine Karte der offenen Wahl auf die Hand (jederzeit, auch außerhalb des Zugs). ## Verschmilzt zwei Handkarten oder zwei eigene Wesen (siehe Game.fuse).
func do_pick(index: int) -> void:
if spectating() or index < 0 or index >= pick_views.size():
return
clear_undo()
var from := Rect2(pick_views[index].position, pick_views[index].size)
var card: Cards.Card = pick_views[index].card
if is_client():
net.send_action({"type": "pick", "index": index})
return
var err := game.pick(perspective, index)
if err != "":
say(tr(err))
return
fx.sfx("play")
fx.fly_card(card, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
Cards.RARITIES[int(perspective.looks.get(card.id, 0))])
after_action()
func do_fuse(a, b) -> void: func do_fuse(a, b) -> void:
cancel() cancel()
clear_undo() clear_undo()
if is_client(): if is_client():
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid}) if a is Cards.Card:
net.send_action({"type": "fuse", "hand": perspective.hand.find(a), "other_hand": perspective.hand.find(b)})
else:
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
return return
var err := game.fuse(perspective, a, b) var err := game.fuse(perspective, a, b)
if err != "": if err != "":
@@ -703,17 +805,13 @@ func on_click(kind: String, obj) -> void:
var g := game var g := game
if not my_turn() or any_overlay(): if not my_turn() or any_overlay():
return return
if fusing: if fuse_first != null:
if kind != "own": var first = fuse_first
cancel() fuse_first = null
say(tr("Abgebrochen")) if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]:
elif fuse_first == null: do_fuse(first, obj)
fuse_first = obj
say(tr("Zweites Wesen wählen"))
elif obj == fuse_first:
fuse_first = null
else: else:
do_fuse(fuse_first, obj) say(tr("Abgebrochen"))
after_action() after_action()
return return
if selected != null: if selected != null:
@@ -745,6 +843,8 @@ func on_click(kind: String, obj) -> void:
if obj.can_attack(): if obj.can_attack():
selected = obj selected = obj
say(tr("Wähle ein Angriffsziel")) say(tr("Wähle ein Angriffsziel"))
elif not obj.ready:
say(tr("%s greift erst im nächsten Zug an") % obj.card.title())
else: else:
say(tr("%s kann nicht angreifen") % obj.card.title()) say(tr("%s kann nicht angreifen") % obj.card.title())
after_action() after_action()
@@ -783,7 +883,7 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
info_label.text = info if entered else "" info_label.text = info if entered else ""
if entered and card != null: if entered and card != null:
var r := BattleLayout.preview_rect(anchor) var r := BattleLayout.preview_rect(anchor)
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(owner_looks.get(card.id, 0))]) var v := CardView.new(card, r.size, false, card.rarity_in(owner_looks))
v.position = r.position v.position = r.position
v.mouse_filter = Control.MOUSE_FILTER_IGNORE v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v) preview_holder.add_child(v)
@@ -792,69 +892,32 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
info_label.size = ir.size info_label.size = ir.size
## Eigene Hand- und Wahlkarte: zeigt den Elementbonus, den sie offen gelegt bekäme. ## Eigene Handkarte: zeigt den Elementbonus, den sie offen gelegt bekäme.
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void: func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
if drag_ghost != null:
return
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/+%d") % [a, a]
elif a < 0:
info = tr("Elementbonus %d/%d") % [a, a]
show_preview(card, entered, perspective.looks, info, anchor) show_preview(card, entered, perspective.looks, info, anchor)
## Offene Wahl als Überlagerung: nur neu aufbauen, wenn sich das Angebot geändert hat. ## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
func build_pick() -> void:
var h := perspective
var key := ""
if not spectating() and not h.offer.is_empty():
key = "%d:%s" % [h.picks_left, ",".join(h.offer.map(func(c): return c.id))]
if key == pick_key:
return
pick_key = key
Style.clear(pick_box)
pick_views = []
if key == "":
pick_layer.visible = false
return
var box := BattleLayout.PICK_PANEL
Style.panel(pick_box, box, Style.PANEL, 12, Style.GOLD, 2)
Style.label(pick_box, tr("Wähle %d von %d") % [h.picks_left, h.offer.size()], 26, Style.GOLD,
Rect2(box.position.x, box.position.y + 24, box.size.x, 40), "center", true)
for i in h.offer.size():
var card: Cards.Card = h.offer[i]
var r := BattleLayout.pick_rect(h.offer.size(), i)
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
v.position = r.position
v.lift = true
apply_forecast(v)
var idx := i
v.clicked.connect(func(_view): do_pick(idx))
pick_box.add_child(v)
pick_views.append(v)
pick_layer.visible = true
if Style.animations:
for i in pick_views.size():
fx.deal(pick_views[i], i)
## Hand- und Wahlkarten tragen den Elementbonus als Vorschau.
func apply_forecast(v: CardView) -> void:
v.bonus = game.forecast(perspective, v.card)
## Was den Angriff eines Wesens gerade hebt oder senkt.
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/+%d") % [cr.aura, cr.aura])
elif cr.aura < 0:
parts.append(tr("Elementbonus %d/%d") % [cr.aura, 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)
for k in cr.keywords():
if not (k in cr.card.keywords):
parts.append(tr("%s durch das Element") % tr(k))
if cr.hidden: if cr.hidden:
parts.append(tr("verdeckt")) parts.append(tr("verdeckt"))
elif not cr.ready and not cr.has("Flink") and cr.owner() == perspective:
parts.append(tr("greift erst im nächsten Zug an"))
return " · ".join(parts) return " · ".join(parts)
@@ -1020,10 +1083,10 @@ func refresh() -> void:
else: else:
show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size))) show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
var uid := cr.uid var uid := cr.uid
tile.clicked.connect(func(_v): tile.pressed.connect(func(_v, pos):
var c := game.find_creature(uid) var c := game.find_creature(uid)
if c != null: if c != null:
on_tile_click(c)) _press(c, tile, pos))
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)
@@ -1037,7 +1100,7 @@ func refresh() -> void:
if cr == selected or cr == fuse_first: if cr == selected or cr == fuse_first:
b = Style.YELLOW b = Style.YELLOW
w = 4 w = 4
elif fusing and p == h and mine: elif fuse_first != null and p == h and mine and g.can_fuse(h, fuse_first, cr)[0]:
b = Style.PURPLE b = Style.PURPLE
w = 3 w = 3
elif selected != null and g.can_attack(selected, cr)[0]: elif selected != null and g.can_attack(selected, cr)[0]:
@@ -1060,7 +1123,6 @@ func refresh() -> void:
var t: CreatureView = tiles[uid] var t: CreatureView = tiles[uid]
tiles.erase(uid) tiles.erase(uid)
fx.fade_out(t) fx.fade_out(t)
build_pick()
deck.visible = not spectating() deck.visible = not spectating()
deck.enabled = mine and g.can_draw(h)[0] deck.enabled = mine and g.can_draw(h)[0]
deck.count = g.deck.size() deck.count = g.deck.size()
@@ -1075,15 +1137,20 @@ func refresh() -> void:
var card: Cards.Card = h.hand[i] var card: Cards.Card = h.hand[i]
var playable: bool = mine and g.can_play(h, card)[0] var playable: bool = mine and g.can_play(h, card)[0]
var r := BattleLayout.hand_rect(h.hand.size(), i) var r := BattleLayout.hand_rect(h.hand.size(), i)
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))]) var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
v.position = r.position v.position = r.position
v.lift = mine v.lift = mine
apply_forecast(v) v.bonus = g.forecast(h, card)
if playable: if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
v.border = Style.PURPLE
elif card == fuse_first:
v.border = Style.YELLOW
elif playable:
v.border = Style.GREEN v.border = Style.GREEN
v.dim = not playable v.dim = not playable and card != fuse_first
v.drag_enabled = true
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size))) v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
v.clicked.connect(func(_view): on_click("hand", card)) v.pressed.connect(func(_view, pos): _press(card, v, pos))
hand_layer.add_child(v) hand_layer.add_child(v)
hand_views.append(v) hand_views.append(v)
@@ -1103,18 +1170,16 @@ func refresh() -> void:
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
b_fuse.visible = mine and not spectating() and h.board.size() >= 2 element_view.update_from(g, h)
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
element_view.update_from(g)
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()):
var line := g.log[i] var line := g.log[i]
if line.begins_with("---"): if line.begins_with("---"):
lines.append("[color=#6eaaf0]%s[/color]" % line) lines.append("[color=#6eaaf0]%s[/color]" % line)
elif " verschmelzen zu " in line or " wird aufgedeckt." in line: elif " verschmelzen " in line or " wird aufgedeckt." in line:
lines.append("[color=#8c78dc]%s[/color]" % line) lines.append("[color=#8c78dc]%s[/color]" % line)
elif " bekommt " in line: elif " bekommt " in line or " regeneriert sich." in line:
lines.append("[color=#5ad282]%s[/color]" % line) lines.append("[color=#5ad282]%s[/color]" % line)
else: else:
lines.append(line) lines.append(line)

View File

@@ -87,7 +87,7 @@ func buy() -> void:
func reveal(i: int) -> void: func reveal(i: int) -> void:
revealed[i] = true revealed[i] = true
fresh = {i: 0.0} fresh = {i: 0.0}
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click") app.sfx.play("combo" if cards[i].rarity != "Common" else "click")
refresh() refresh()
@@ -111,7 +111,7 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
var tw := create_tween() var tw := create_tween()
tw.tween_interval(delay) tw.tween_interval(delay)
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
if rarity == "gewöhnlich": if rarity == "Common":
return return
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12) var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
burst.pivot_offset = burst.size / 2 burst.pivot_offset = burst.size / 2

View File

@@ -1,23 +1,27 @@
class_name CardView class_name CardView
extends Control extends Control
## Zeichnet eine Karte (Laden, Hand, Sammlung, Booster, Vorschau). ## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Schlüsselwörter als ## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Eigenschaften als
## Symbole, Effekttext, Angriff und Leben als Kreise unten. Das Element steht im Hintergrund, ## Symbole, Angriff und Leben als Kreise unten. Das erste Element färbt den Hintergrund, weitere
## die Seltenheit in Rahmen, Glanz und Edelstein. Sehr flache Karten (Hand) zeigen nur das Nötigste. ## Elemente zeigt ein Medaillon als Tortenstücke hinter dem Bild; die Seltenheit steckt in Name,
## Rahmen, Glanz und Edelstein (siehe Style). Sehr flache Karten zeigen nur das Nötigste.
signal clicked(view: CardView) signal clicked(view: CardView)
signal pressed(view: CardView, pos: Vector2) # nur mit drag_enabled: der Kampf macht daraus Klick oder Ziehen
signal hover(card: Cards.Card, entered: bool) signal hover(card: Cards.Card, entered: bool)
var card: Cards.Card var card: Cards.Card
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe) var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
var rarity := "gewöhnlich" # nur Optik var rarity := "Common" # nur Optik
var dim := false var dim := false
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme (+1 oder -1) var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme
var lift := false # hebt sich beim Zeigen an (Hand und Laden) var lift := false # hebt sich beim Zeigen an (Hand)
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick. ## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void: func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "Common") -> void:
card = c card = c
rarity = rar rarity = rar
set_process(Style.rarity_animated(rarity)) # bewegter Glanz set_process(Style.rarity_animated(rarity)) # bewegter Glanz
@@ -55,7 +59,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()
clicked.emit(self) if drag_enabled:
pressed.emit(self, event.global_position)
else:
clicked.emit(self)
elif has_focus() and event.is_action_pressed("ui_accept"): elif has_focus() and event.is_action_pressed("ui_accept"):
accept_event() accept_event()
clicked.emit(self) clicked.emit(self)
@@ -65,7 +72,7 @@ func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
var seed := hash(card.id) var seed := hash(card.id)
var frame: Color = border if border != null else Style.rarity_frame(rarity, seed) var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
if rarity != "gewöhnlich": if rarity != "Common":
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6) Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
Style.fill_elements(self, rect, card.elements, 8) Style.fill_elements(self, rect, card.elements, 8)
if size.y < 90: if size.y < 90:
@@ -76,9 +83,14 @@ func _draw() -> void:
draw_rect(rect, Color(0, 0, 0, 0.47)) draw_rect(rect, Color(0, 0, 0, 0.47))
if has_focus(): if has_focus():
Style.border(self, rect, Style.GOLD, 4, 8) Style.border(self, rect, Style.GOLD, 4, 8)
if hold > 0.0:
var c := size / 2
var r := minf(size.x, size.y) * 0.3
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
## Volle Karte: Laden, Sammlung, Booster, Vorschau. ## Volle Karte: Sammlung, Booster, Vorschau.
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void: func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
var big := size.x >= 180 var big := size.x >= 180
var mid := size.x >= 140 var mid := size.x >= 140
@@ -90,14 +102,18 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
var icon := 11.0 if big else (9.0 if mid else 7.0) var icon := 11.0 if big else (9.0 if mid else 7.0)
# Bild: das Elementsymbol füllt den oberen Teil # Bild: das Elementsymbol füllt den oberen Teil
var band_top: float = size.y * 0.36 var band_top: float = size.y * 0.36
Style.glyph(self, card, Vector2(size.x / 2, band_top * 0.56), minf(size.x * 0.62, band_top * 0.95), 0.4) var gc := Vector2(size.x / 2, band_top * 0.56)
Style.rarity_sheen(self, rect, rarity, seed) var gs := minf(size.x * 0.62, band_top * 0.95)
if card.is_combo():
Style.element_medallion(self, gc, gs * 0.6, card.elements)
Style.glyph(self, card, gc, gs, 0.4, Style.rarity_relief(rarity))
Style.rarity_sheen(self, rect, rarity, seed, Rect2(0, 0, size.x, band_top))
# Namensband über die volle Breite # Namensband über die volle Breite
var band_h: float = Style.fs(f_name) + 10 var band_h: float = Style.fs(f_name) + 10
var band := Rect2(0, band_top, size.x, band_h) var band := Rect2(0, band_top, size.x, band_h)
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82)) draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12) var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.TEXT, Vector2(size.x / 2, band.get_center().y), "center", true) Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.rarity_name_color(rarity, seed), Vector2(size.x / 2, band.get_center().y), "center", true)
var y := band.end.y + 4 var y := band.end.y + 4
# Kombo eines verschmolzenen Wesens # Kombo eines verschmolzenen Wesens
if card.is_combo(): if card.is_combo():
@@ -146,9 +162,8 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
# Werte springen aus den unteren Ecken, dazwischen der Edelstein # Werte springen aus den unteren Ecken, dazwischen der Edelstein
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat) Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat) Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
if bonus != 0: if bonus > 0:
Style.text_outlined(self, ("+%d/+%d" % [bonus, bonus]) if bonus > 0 else ("%d/%d" % [bonus, bonus]), f_text, 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.GREEN if bonus > 0 else Style.RED, 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)
@@ -160,7 +175,7 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
Style.rarity_sheen(self, rect, rarity, seed) Style.rarity_sheen(self, rect, rarity, seed)
Style.border(self, rect, frame, 2, 8) Style.border(self, rect, frame, 2, 8)
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13) Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
var name_w: float = size.x - 30 - (12 if rarity != "gewöhnlich" else 0) var name_w: float = size.x - 30 - (12 if rarity != "Common" else 0)
var nf := Style.fit_size(card.title(), true, 12, name_w) var nf := Style.fit_size(card.title(), true, 12, name_w)
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true) Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed) Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
@@ -174,6 +189,6 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
x += 18 x += 18
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12) Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12) Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
if bonus != 0: if bonus > 0:
Style.text_outlined(self, ("+%d" % bonus) if bonus > 0 else str(bonus), 10, Style.GREEN if bonus > 0 else Style.RED, Vector2(4, size.y - 29), "midleft", 3) Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
Style.rarity_sparkle(self, rect, rarity, seed) Style.rarity_sparkle(self, rect, rarity, seed)

View File

@@ -2,7 +2,7 @@ class_name CreatureView
extends Control 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 clicked(view: CreatureView) signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
signal hover(card: Cards.Card, entered: bool) signal hover(card: Cards.Card, entered: bool)
var creature: Game.Creature var creature: Game.Creature
@@ -13,6 +13,7 @@ var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren) var busy := false # wird gerade animiert (kein Neupositionieren)
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1) var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void: func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
@@ -35,27 +36,31 @@ 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()
clicked.emit(self) pressed.emit(self, event.global_position)
func rarity() -> String: func rarity() -> String:
var owner = creature.owner() var owner = creature.owner()
return Cards.RARITIES[int(owner.looks.get(creature.card.id, 0)) if owner != null else 0] return creature.card.rarity_in(owner.looks if owner != null else {})
func _draw() -> void: func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
if secret: if secret:
_draw_back(rect) _draw_back(rect)
_draw_hold()
return return
var small := size.x < 100 var small := size.x < 100
var rar := rarity() var rar := rarity()
var seed := hash(creature.card.id) + creature.uid var seed := hash(creature.card.id) + creature.uid
if rar != "gewöhnlich": if rar != "Common":
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6) Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6)
Style.fill_elements(self, rect, creature.card.elements, 6) Style.fill_elements(self, rect, creature.card.elements, 6)
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3) var gc := Vector2(size.x / 2, size.y * 0.5)
Style.rarity_sheen(self, rect, rar, seed) if creature.card.is_combo():
Style.element_medallion(self, gc, size.x * 0.42, creature.card.elements)
Style.glyph(self, creature.card, gc, size.x * 0.75, 0.3, Style.rarity_relief(rar))
Style.rarity_sheen(self, rect, rar, seed, rect)
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed) var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
if flash: if flash:
bcol = Style.RED bcol = Style.RED
@@ -64,19 +69,19 @@ func _draw() -> void:
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
var nf := Style.fit_size(creature.card.title(), true, fs, name_w) var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7)) draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true) Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.rarity_name_color(rar, seed), Vector2(3 + name_w / 2, 6), "midtop", true)
# Schlüsselwörter als Symbole (Schild nur, solange er steht) # Eigenschaften als Symbole: gekaufte weiß, die vom Elementpaar in der Elementfarbe
var icon := 7.0 if small else 9.0 var icon := 7.0 if small else 9.0
var kws: Array[String] = [] var kws := creature.keywords()
for k in Cards.KEYWORDS:
if k in creature.card.keywords and (k != "Schild" or creature.shield):
kws.append(k)
if creature.shield and not ("Schild" in kws):
kws.append("Schild")
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0 var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
var y := Style.fs(fs) + 14 + icon var y := Style.fs(fs) + 14 + icon
for k in kws: for k in kws:
Style.keyword_icon(self, k, Vector2(x + icon, y), icon) var col := Color("f2f2f6")
if not (k in creature.card.keywords):
for e in creature.card.elements:
if Cards.ELEMENT_KEYWORD.get(e, "") == k:
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col)
x += icon * 2 + 6 x += icon * 2 + 6
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0) _draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed) Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
@@ -84,6 +89,17 @@ func _draw() -> void:
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55)) draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3) Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
Style.rarity_sparkle(self, rect, rar, seed) Style.rarity_sparkle(self, rect, rar, seed)
_draw_hold()
## Ring, der sich füllt, solange eine Karte zum Verschmelzen darüber gehalten wird.
func _draw_hold() -> void:
if hold <= 0.0:
return
var c := size / 2
var r := minf(size.x, size.y) * 0.3
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
## Verdecktes fremdes Wesen: ein Kartenrücken. ## Verdecktes fremdes Wesen: ein Kartenrücken.
@@ -98,7 +114,7 @@ func _draw_back(rect: Rect2) -> void:
pass # unbekanntes Element: kein Vorteil anzeigen pass # unbekanntes Element: kein Vorteil anzeigen
## Angriff (grün oder rot, wenn die Elementregel ihn hebt oder senkt), Leben, Elementvorteil. ## Angriff (grün, wenn der Elementbonus ihn hebt), Leben, Elementvorteil des Angreifers.
func _draw_stats(f: int, f_small: int, r: float) -> void: func _draw_stats(f: int, f_small: int, r: float) -> void:
var extra := creature.power() - creature.attack var extra := creature.power() - creature.attack
var atk_col := Style.GREEN if extra > 0 else (Style.RED if extra < 0 else Style.AMBER) var atk_col := Style.GREEN if extra > 0 else (Style.RED if extra < 0 else Style.AMBER)

View File

@@ -1,9 +1,11 @@
class_name ElementView class_name ElementView
extends Control extends Control
## Die Vier-Elemente-Regel auf einen Blick: offene Wesen je Element auf dem Tisch. Ab vier ## Elementboni auf einen Blick: offene Wesen je Element auf dem Tisch (ab drei +1/+1) und der
## bekommt das Element +1/+1 und das Element, das es schlägt, -1/-1. ## Sondereffekt der eigenen Seite (ab zwei eigenen: Erde Spott, Luft Flink, Wasser Regeneration,
## Feuer Doppelschlag), als Symbol in der Elementfarbe.
var counts := {} var counts := {} # Tisch
var own := {} # eigene Seite
func _init(rect: Rect2) -> void: func _init(rect: Rect2) -> void:
@@ -12,10 +14,13 @@ func _init(rect: Rect2) -> void:
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 counts[e] = 0
own[e] = 0
func update_from(g: Game) -> void: func update_from(g: Game, me: Game.Player) -> void:
counts = g.element_counts() counts = g.element_counts()
for e in Cards.ELEMENTS:
own[e] = me.element_count(e) if me != null else 0
queue_redraw() queue_redraw()
@@ -23,9 +28,8 @@ func strong(e: String) -> bool:
return int(counts.get(e, 0)) >= Game.ELEMENT_STACK return int(counts.get(e, 0)) >= Game.ELEMENT_STACK
func weak(e: String) -> bool: func paired(e: String) -> bool:
var predator := Cards.beaten_by(e) return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
return predator != "" and strong(predator)
## Der Inhalt als Text (Tests, Protokoll). ## Der Inhalt als Text (Tests, Protokoll).
@@ -35,8 +39,8 @@ func text() -> String:
var s := "%s %d" % [tr(e), int(counts.get(e, 0))] var s := "%s %d" % [tr(e), int(counts.get(e, 0))]
if strong(e): if strong(e):
s += " (+1/+1)" s += " (+1/+1)"
if weak(e): if paired(e):
s += " (-1/-1)" s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
parts.append(s) parts.append(s)
return " · ".join(parts) return " · ".join(parts)
@@ -51,9 +55,10 @@ func _draw() -> void:
var n := int(counts.get(e, 0)) var n := int(counts.get(e, 0))
if strong(e): if strong(e):
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y)) draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
elif weak(e): elif paired(e):
draw_style_box(Style.box(Color(Style.RED, 0.12), 6, Style.RED, 1), Rect2(x0 + 2, 0, w - 4, size.y)) draw_style_box(Style.box(Color(col, 0.18), 6, Color(col, 0.6), 1), Rect2(x0 + 2, 0, w - 4, size.y))
Style.text(self, tr(e), 13, Style.TEXT if strong(e) else Style.MUTED, Vector2(x0 + 12, cy), "midleft", strong(e)) var active := strong(e) or paired(e)
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(x0 + 12, cy), "midleft", active)
var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12 var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12
for k in Game.ELEMENT_STACK: for k in Game.ELEMENT_STACK:
var c := Vector2(px + k * 13, cy) var c := Vector2(px + k * 13, cy)
@@ -62,12 +67,11 @@ func _draw() -> void:
else: else:
draw_circle(c, 4.5, Color(0, 0, 0, 0.35)) 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) draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
px += Game.ELEMENT_STACK * 13 + 2
if n > Game.ELEMENT_STACK: if n > Game.ELEMENT_STACK:
Style.text(self, "+%d" % (n - Game.ELEMENT_STACK), 11, Style.MUTED, Vector2(px + Game.ELEMENT_STACK * 13 + 2, cy), "midleft") Style.text(self, "+%d" % (n - Game.ELEMENT_STACK), 11, Style.MUTED, Vector2(px, cy), "midleft")
var tag := "" px += 18
if paired(e):
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
if strong(e): if strong(e):
tag = "+1/+1" Style.text(self, "+1/+1", 13, Style.GREEN, Vector2(x0 + w - 12, cy), "midright", true)
elif weak(e):
tag = "-1/-1"
if tag != "":
Style.text(self, tag, 13, Style.GREEN if strong(e) else Style.RED, Vector2(x0 + w - 12, cy), "midright", true)

View File

@@ -1,7 +1,7 @@
class_name OpponentSelectScreen class_name OpponentSelectScreen
extends Control extends Control
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf ## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
## per Ziehen (siehe DeckGen.offer_cards). ## vom gemeinsamen Stapel (siehe Game.start).
var app var app
var diff_buttons := {} var diff_buttons := {}

View File

@@ -309,43 +309,79 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
# --- Seltenheit als Aussehen ------------------------------------------------------ # --- Seltenheit als Aussehen ------------------------------------------------------
# Common matt; Rare silberner Name und Rahmen mit stehendem Glanzstreifen; Super Rare Holo-Bild
# (wandernder Regenbogenschimmer); Ultra Rare goldener Name und Rahmen plus Holo-Bild;
# Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln, schillernde Farben.
## Ab episch bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu. ## Ab Super Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
static func rarity_animated(rar: String) -> bool: static func rarity_animated(rar: String) -> bool:
return animations and Cards.RARITIES.find(rar) >= 2 return animations and Cards.RARITIES.find(rar) >= 2
## Farbe einer Stufe; mythisch schillert über die Zeit durch alle Farben. ## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben.
static func rarity_color(rar: String, seed: int = 0) -> Color: static func rarity_color(rar: String, seed: int = 0) -> Color:
if rar == "mythisch" and animations: if rar == "Ultimate Rare" and animations:
return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0) return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0)
return Cards.RARITY_COLORS[rar] return Cards.RARITY_COLORS[rar]
## Rahmenfarbe einer Stufe: gewöhnlich bleibt schlicht, alles darüber trägt seine Farbe. ## Rahmenfarbe einer Stufe: Common bleibt schlicht, alles darüber trägt seine Farbe.
static func rarity_frame(rar: String, seed: int = 0) -> Color: static func rarity_frame(rar: String, seed: int = 0) -> Color:
return Color("3a3e4c") if rar == "gewöhnlich" else rarity_color(rar, seed) return Color("3a3e4c") if rar == "Common" else rarity_color(rar, seed)
## Lichtstreifen wie auf einer Folienkarte: selten steht er, ab episch wandert er über die Karte. ## Farbe des Kartennamens: Rare silbern, Ultra Rare golden, Ultimate Rare schillernd, sonst weiß.
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void: static func rarity_name_color(rar: String, seed: int = 0) -> Color:
match rar:
"Rare", "Ultra Rare", "Ultimate Rare":
return lighter(rarity_color(rar, seed), 0.25)
return TEXT
## Ultimate Rare zeigt das Bild als Relief (Licht- und Schattenkante).
static func rarity_relief(rar: String) -> bool:
return Cards.RARITIES.find(rar) >= 4
## Glanz wie auf einer Folienkarte. Rare: ein stehender weißer Streifen über die Karte.
## Ab Super Rare: Holo, ein wandernder Regenbogenschimmer über das Bild (picture); Ultimate Rare
## zusätzlich schwach über die ganze Karte.
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int, picture: Rect2 = Rect2()) -> void:
var lvl := Cards.RARITIES.find(rar) var lvl := Cards.RARITIES.find(rar)
if lvl < 1: if lvl < 1:
return return
if picture.size.x <= 0:
picture = rect
if lvl == 1:
_sheen_bands(ci, rect, (seed % 100) / 100.0, [[1.0, Color(1, 1, 1, 0.07)], [0.55, Color(1, 1, 1, 0.09)], [0.22, Color(1, 1, 1, 0.13)]])
return
var phase := (seed % 100) / 100.0 var phase := (seed % 100) / 100.0
if lvl >= 2 and animations: if animations:
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0) phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
var holo := []
for i in 4:
holo.append([1.0 - i * 0.22, Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)])
_sheen_bands(ci, picture, phase, holo)
if lvl >= 4:
var soft := []
for i in 3:
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
_sheen_bands(ci, rect, fmod(phase + 0.5, 1.0), soft)
## Schräge Streifen über rect, auf rect beschnitten. layers: [Breitenanteil, Farbe].
static func _sheen_bands(ci: CanvasItem, rect: Rect2, phase: float, layers: Array) -> void:
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)]) var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
var w := rect.size.x * 0.34 var w := rect.size.x * 0.34
var k := rect.size.y * 0.75 # Schräge var k := rect.size.y * 0.75 # Schräge
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
for layer in [[1.0, 0.07], [0.55, 0.09], [0.22, 0.13]]: for layer in layers:
var lw: float = w * layer[0] var lw: float = w * layer[0]
var lx := x0 + (w - lw) / 2 var lx := x0 + (w - lw) / 2
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)]) var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
for poly in Geometry2D.intersect_polygons(band, frame): for poly in Geometry2D.intersect_polygons(band, frame):
if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1])) ci.draw_colored_polygon(poly, layer[1])
static func _area(poly: PackedVector2Array) -> float: static func _area(poly: PackedVector2Array) -> float:
@@ -357,15 +393,13 @@ static func _area(poly: PackedVector2Array) -> float:
return absf(a) / 2.0 return absf(a) / 2.0
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen. ## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
## avoid: Bereich, der frei bleibt (der Kartentext). ## avoid: Bereich, der frei bleibt (der Kartentext).
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void: static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
var lvl := Cards.RARITIES.find(rar) if Cards.RARITIES.find(rar) < 4:
if lvl < 3:
return return
var t := Time.get_ticks_msec() / 1000.0 var t := Time.get_ticks_msec() / 1000.0
var n := 5 if lvl == 3 else 9 for i in 9:
for i in n:
var h := absi(hash(Vector2i(seed, i))) var h := absi(hash(Vector2i(seed, i)))
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16)) var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0 var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
@@ -388,9 +422,9 @@ static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spr
ci.draw_style_box(sb, rect) ci.draw_style_box(sb, rect)
## Edelstein in der Farbe der Stufe (ab selten). ## Edelstein in der Farbe der Stufe (ab Rare).
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void: static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
if rar == "gewöhnlich": if rar == "Common":
return return
var col := rarity_color(rar, seed) var col := rarity_color(rar, seed)
var diamond := func(h: float, k: Color) -> void: var diamond := func(h: float, k: Color) -> void:
@@ -400,23 +434,28 @@ static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed:
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55)) ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen. ## Hintergrund in der Farbe des ersten Elements; weitere Elemente zeigt element_medallion.
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void: static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
if elements.size() == 1: ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
return
## Medaillon eines verschmolzenen Wesens: ein Kreis hinter dem Bild, als Tortenstücke in den
## Farben seiner Elemente, mit dunklem Rand.
static func element_medallion(ci: CanvasItem, c: Vector2, r: float, elements: Array) -> void:
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75))
var n := elements.size() var n := elements.size()
var w := rect.size.x / n
for i in n: for i in n:
var sb := StyleBoxFlat.new() var from := -PI / 2 + TAU * i / n
sb.bg_color = Cards.ELEMENT_COLORS[elements[i]] var to := -PI / 2 + TAU * (i + 1) / n
if i == 0: var pts := PackedVector2Array([c])
sb.corner_radius_top_left = radius var steps := 24
sb.corner_radius_bottom_left = radius for k in steps + 1:
if i == n - 1: var ang := lerpf(from, to, float(k) / steps)
sb.corner_radius_top_right = radius pts.append(c + Vector2(cos(ang), sin(ang)) * r)
sb.corner_radius_bottom_right = radius ci.draw_colored_polygon(pts, Cards.ELEMENT_COLORS[elements[i]])
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y))) for i in n:
var ang := -PI / 2 + TAU * i / n
ci.draw_line(c, c + Vector2(cos(ang), sin(ang)) * r, Color(0.05, 0.05, 0.08, 0.75), 2.0)
# --- Kartenteile ------------------------------------------------------ # --- Kartenteile ------------------------------------------------------
@@ -444,20 +483,18 @@ static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, col
text_outlined(ci, value, font, Color.WHITE, c, "center", 3) text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
## Schlüsselwort als Symbol auf dunklem Kreis: Schild Schild, Doppelschlag gekreuzte Klingen. ## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil,
## Regeneration Kreuz, Doppelschlag gekreuzte Klingen.
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void: static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7)) ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
var w := maxf(1.5, r * 0.22) var w := maxf(1.5, r * 0.22)
match kw: match kw:
"Wächter": "Spott":
ci.draw_rect(Rect2(c + Vector2(-r * 0.75, -r * 0.15), Vector2(r * 1.5, r * 0.85)), col) ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.2), col, w * 1.6)
for i in 3: ci.draw_circle(c + Vector2(0, r * 0.6), w * 0.95, col)
ci.draw_rect(Rect2(c + Vector2(-r * 0.75 + i * r * 0.6, -r * 0.6), Vector2(r * 0.3, r * 0.5)), col) "Regeneration":
"Schild": ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.7), col, w * 1.5)
var pts := PackedVector2Array([c + Vector2(-r * 0.7, -r * 0.65), c + Vector2(r * 0.7, -r * 0.65), c + Vector2(r * 0.7, r * 0.05), ci.draw_line(c + Vector2(-r * 0.7, 0), c + Vector2(r * 0.7, 0), col, w * 1.5)
c + Vector2(0, r * 0.8), c + Vector2(-r * 0.7, r * 0.05)])
ci.draw_colored_polygon(pts, col)
ci.draw_line(c + Vector2(0, -r * 0.55), c + Vector2(0, r * 0.6), Color(0.05, 0.05, 0.08, 0.6), w)
"Flink": "Flink":
for i in 2: for i in 2:
var x := c.x - r * 0.55 + i * r * 0.55 var x := c.x - r * 0.55 + i * r * 0.55
@@ -471,10 +508,24 @@ static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col:
# --- Kartensymbole (prozedural, je Element) -------------------------- # --- Kartensymbole (prozedural, je Element) --------------------------
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95) -> void: ## relief: Schatten- und Lichtkante unter dem Symbol (Ultimate Rare).
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95, relief: bool = false) -> void:
if relief:
ci.draw_set_transform(Vector2(2, 2))
_glyph_plain(ci, card, center, size, Color(0, 0, 0, alpha * 0.6))
ci.draw_set_transform(Vector2(-1, -1))
_glyph_plain(ci, card, center, size, Color(1, 1, 1, alpha * 0.5))
ci.draw_set_transform(Vector2.ZERO)
_glyph_plain(ci, card, center, size, Color(0, 0, 0, 0), alpha)
## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben.
static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void:
var variant := absi(hash(card.id)) % 3 var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38) var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
col.a = alpha col.a = alpha
if tint.a > 0:
col = tint
var elems: Array = card.elements var elems: Array = card.elements
if elems.size() == 1: if elems.size() == 1:
_element_glyph(ci, elems[0], center, size, variant, col) _element_glyph(ci, elems[0], center, size, variant, col)
@@ -483,6 +534,8 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: flo
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col) _element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38) var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
col2.a = alpha col2.a = alpha
if tint.a > 0:
col2 = tint
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2) _element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
if elems.size() >= 3: if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2) ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)

View File

@@ -19,15 +19,14 @@ func _ready() -> void:
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
var h: Game.Player = scr.game.human var h: Game.Player = scr.game.human
for i in 3:
scr.game.pick(h, 0)
h.energy = 10 h.energy = 10
scr.game.draw(h) h.hand.pop_back()
scr.game.pick(h, 0) if scr.game.current() == h:
scr.game.draw(h)
var t0 := Time.get_ticks_usec() var t0 := Time.get_ticks_usec()
for i in 30: for i in 30:
scr.refresh() scr.refresh()
print("refresh() mit 4 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30)) print("refresh() mit 5 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
scr.ai_delay = 0.2 scr.ai_delay = 0.2
measuring = true measuring = true
var start := Time.get_ticks_msec() var start := Time.get_ticks_msec()

View File

@@ -73,16 +73,13 @@ func _act_host() -> void:
_decide(game.players[seat]) _decide(game.players[seat])
## Einfache Züge mit kurzer Bedenkzeit: erst eine offene Wahl treffen, dann das erste ## Einfache Züge mit kurzer Bedenkzeit: das erste spielbare Wesen legen, dann angreifen,
## spielbare Wesen ohne Ziel legen, dann angreifen, dann ziehen, sonst Zug beenden. ## dann ziehen, sonst Zug beenden.
func _decide(me: Game.Player) -> void: func _decide(me: Game.Player) -> void:
var g := game var g := game
await get_tree().create_timer(0.25).timeout await get_tree().create_timer(0.25).timeout
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)): if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
return # inzwischen ein neuer Zustand return # inzwischen ein neuer Zustand
if not me.offer.is_empty():
net.send_action({"type": "pick", "index": 0})
return
for i in me.hand.size(): for i in me.hand.size():
var card: Cards.Card = me.hand[i] var card: Cards.Card = me.hand[i]
if game.can_play(me, card)[0]: if game.can_play(me, card)[0]:

View File

@@ -25,7 +25,6 @@ func _initialize() -> void:
var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid) var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid)
uid += 1 uid += 1
c.ready = true c.ready = true
c.attacks_left = 1
p.board.append(c) p.board.append(c)
var h: Game.Player = g.players[0] var h: Game.Player = g.players[0]
h.energy = 10 h.energy = 10

View File

@@ -43,7 +43,7 @@ var next_uid := 9000
func put(p: Game.Player, cid: String, ready: bool = true, hidden: bool = false) -> Game.Creature: func put(p: Game.Player, cid: String, ready: bool = true, hidden: bool = false) -> Game.Creature:
next_uid += 1 next_uid += 1
var cr := Game.Creature.new(C(cid), p, next_uid) var cr := Game.Creature.new(C(cid), p, next_uid)
cr.attacks_left = cr.attacks_per_turn() if ready else 0 cr.ready = ready
cr.hidden = hidden cr.hidden = hidden
p.board.append(cr) p.board.append(cr)
return cr return cr
@@ -58,7 +58,8 @@ func _initialize() -> void:
test_wesen_und_angriff() test_wesen_und_angriff()
test_elementvorteil() test_elementvorteil()
test_verdeckt() test_verdeckt()
test_vier_elemente() test_elementboni()
test_spott_und_regeneration()
test_verbessern() test_verbessern()
test_verschmelzen() test_verschmelzen()
test_limits() test_limits()
@@ -94,12 +95,12 @@ func test_kartenpool() -> void:
for e in Cards.ELEMENTS: for e in Cards.ELEMENTS:
check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e) check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e)
for c in Cards.all(): for c in Cards.all():
check(c.health > 0 and c.cost == ceili((c.attack + c.health) / 2.0), "Kosten = (Angriff + Leben) / 2: " + c.id) check(c.health > 0 and c.cost == maxi(1, floori((2 * c.attack + c.health) / 3.0)), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.keywords.is_empty() and c.text() == "", "Karten kommen ohne Eigenschaften: " + c.id) check(c.keywords.is_empty() and c.text() == "", "Karten kommen ohne Eigenschaften: " + c.id)
check(C("f_kobold").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 2, "Kostenbeispiele") check(C("f_kobold").cost == 1 and C("f_wolf").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 1 and C("f_drache").cost == 5, "Kostenbeispiele")
var a := C("f_wolf") var a := C("f_wolf")
var b := C("f_wolf") var b := C("f_wolf")
a.keywords.append("Schild") a.keywords.append("Spott")
check(b.keywords.is_empty() and Cards.by_id("f_wolf").keywords.is_empty(), "Exemplare teilen sich keine Eigenschaften") check(b.keywords.is_empty() and Cards.by_id("f_wolf").keywords.is_empty(), "Exemplare teilen sich keine Eigenschaften")
@@ -108,7 +109,6 @@ func test_kombos() -> void:
check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz") check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz")
check(not C("ff_phoenix").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen") check(not C("ff_phoenix").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen")
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre") check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
check(Cards.beaten_by("Feuer") == "Wasser" and Cards.beaten_by("Erde") == "Luft", "wer wen schlägt")
## Mana: 5 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten. ## Mana: 5 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
@@ -129,101 +129,78 @@ func test_mana_und_kosten() -> void:
check(h.energy == Game.START_MANA + 21, "keine Obergrenze") check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
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 (6) 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")
## Ziehen: 1 Mana, drei vom gemeinsamen Stapel, eine behalten; der Rest unter den Stapel. ## Start: fünf bezahlbare Karten, keine Wahl. Ziehen: 1 Mana, eine bezahlbare Karte vom Stapel.
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_OFFER, "Stapel: jede Karte dreimal, zehn schon ausgegeben") check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
for p in g.players: for p in g.players:
check(p.offer.size() == Game.START_OFFER and p.picks_left == Game.START_PICKS and p.hand.is_empty(), "Start: fünf zur Wahl, drei zu nehmen") check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(p.offer.all(func(c): return c.cost <= Game.START_MANA + p.start_bonus), "nur Karten, die im ersten Zug spielbar sind") check(p.hand.all(func(c): return c.cost <= Game.START_MANA + p.start_bonus), "nur Karten, die im ersten Zug spielbar sind")
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
var first: Game.Player = g.current() var first: Game.Player = g.current()
var deck_before := g.deck.size() check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
var wanted: Cards.Card = first.offer[4] first.hand.pop_back()
check(g.pick(first, 4) == "" and first.hand.size() == 1 and first.hand[0].id == wanted.id and first.hand[0] != wanted, "erste Startkarte genommen, als eigenes Exemplar")
check(first.offer.size() == 4 and first.picks_left == 2, "vier bleiben, zwei noch zu nehmen")
check(g.draw(first) == "Erst wählen" and g.can_play(first, first.hand[0])[1] == "Erst wählen", "bei offener Wahl geht nichts anderes")
g.pick(first, 0)
g.pick(first, 0)
check(first.hand.size() == 3 and first.offer.is_empty() and g.deck.size() == deck_before + 2, "drei behalten, die anderen zwei liegen unter dem Stapel")
var mana := first.energy var mana := first.energy
check(g.draw(first) == "" and first.energy == mana - 1 and first.offer.size() == 3 and first.picks_left == 1, "gezogen: 1 Mana, drei zur Wahl") var deck_before := g.deck.size()
check(first.offer.all(func(c): return c.cost <= first.max_energy), "nur Karten bis zum Mana dieses Zugs") check(g.draw(first) == "" and first.energy == mana - 1 and first.hand.size() == Game.START_HAND, "gezogen: 1 Mana, eine Karte")
check(first.offer[0] != first.offer[1] and first.offer[1] != first.offer[2], "drei verschiedene Karten") check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel")
check(g.pick(first, 1) == "" and first.hand.size() == 4 and first.offer.is_empty(), "eine genommen, Wahl zu")
check(g.pick(first, 0) == "Keine Wahl offen", "ohne Wahl nichts zu nehmen")
first.energy = 0 first.energy = 0
first.hand.pop_back()
check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen") check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen")
first.energy = 5
g.draw(first)
g.pick(first, 0)
check(first.hand.size() == Game.MAX_HAND and g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
var second: Game.Player = g.enemy_of(first) var second: Game.Player = g.enemy_of(first)
check(g.pick(second, 0) == "" and second.hand.size() == 1, "wählen geht auch außerhalb des Zugs") check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
g.end_turn() # jetzt ist der Zweite dran, seine Wahl ist noch offen
g.end_turn() # sein Zugende nimmt den Rest von vorn
check(second.hand.size() == 3 and second.offer.is_empty(), "Zugende nimmt offene Karten von vorn")
# Leerer Stapel wird neu gemischt # Leerer Stapel wird neu gemischt
g.deck = [] g.deck = []
var cur: Game.Player = g.current() first.energy = 3
cur.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")
cur.hand.pop_back() # Platz auf der Hand # Mit viel Mana tauchen auch teure Karten auf
check(g.draw(cur) == "" and cur.offer.size() == 3 and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt") first.max_energy = 20
# Mit viel Mana tauchen auch teure Karten auf; zu teure wandern unter den Stapel first.energy = 20
g.pick(cur, 0) var seen_expensive := false
cur.max_energy = 20 for i in 12:
cur.energy = 20 first.hand = []
var expensive_before: int = g.deck.filter(func(c): return c.cost > 8).size() g.draw(first)
g.draw(cur) if first.hand[0].cost > 5:
g.pick(cur, 0) seen_expensive = true
var bottom: Array = g.deck.slice(0, 5) check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
check(expensive_before > 0 and (g.deck.size() + 1 + cur.offer.size()) > 0 and bottom.size() == 5, "Stapel bleibt vollständig")
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
func test_wesen_und_angriff() -> void: func test_wesen_und_angriff() -> void:
var s := setup(["f_wolf"]) var s := setup(["f_wolf"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2] var o: Game.Player = s[2]
check(g.play_card(h, h.hand[0]) == "" and h.board.size() == 1 and h.board[0].can_attack(), "gespielt und sofort angriffsbereit") check(g.play_card(h, h.hand[0]) == "" and h.board.size() == 1 and not h.board[0].can_attack(), "gespielt, greift noch nicht an")
var wolf: Game.Creature = h.board[0] var wolf: Game.Creature = h.board[0]
check(g.can_attack(wolf, o)[1] == "Flammenwolf greift erst im nächsten Zug an", "Meldung: erst nächsten Zug")
g.begin_turn(h)
check(wolf.ready and wolf.can_attack(), "nach einer Runde angriffsbereit")
var e := put(o, "e_wall") # 0/4 var e := put(o, "e_wall") # 0/4
check(g.can_attack(wolf, o)[1] == "Erst die Wesen", "Leben erst ohne Wesen angreifbar") check(g.can_attack(wolf, o)[1] == "Erst die Wesen", "Leben erst ohne Wesen angreifbar")
check(g.attack(wolf, e) == "" and e.health == 4 - 3 and wolf.health == 2, "Angriff: Schaden, kein Konter von 0 Angriff") check(g.attack(wolf, e) == "" and e.health == 4 - 3 and wolf.health == 2, "Angriff: Schaden, kein Konter von 0 Angriff")
check(g.can_attack(wolf, e)[1].ends_with("kann nicht angreifen"), "nur ein Angriff je Zug") check(g.can_attack(wolf, e)[1].ends_with("kann nicht angreifen"), "nur ein Angriff je Zug")
g.begin_turn(o) g.begin_turn(o)
check(e.health == 4, "zu Zugbeginn erholt sich das Wesen seines Besitzers ganz") check(e.health == 1, "keine Heilung zu Zugbeginn")
g.begin_turn(h) g.begin_turn(h)
var life := o.life
g.attack(wolf, e) g.attack(wolf, e)
check(e.health == 1 and o.board.size() == 1, "Angriff: wieder nur 3 Schaden") check(o.board.is_empty() and g.log.any(func(l): return l == "Steinwall stirbt.") and o.life == life, "Wesen stirbt, nichts schlägt durch")
e.health = 1
g.begin_turn(h) g.begin_turn(h)
g.attack(wolf, e) check(g.attack(wolf, o) == "" and o.life == life - 3, "ohne Wesen trifft es den Helden")
check(o.board.is_empty() and g.log.any(func(l): return l == "Steinwall stirbt."), "Wesen stirbt, Überschuss schlägt durch")
g.begin_turn(h)
var life0 := o.life
check(g.attack(wolf, o) == "" and o.life == life0 - 3, "ohne Wesen trifft es den Helden")
# Gegenschlag # Gegenschlag
var troll := put(o, "e_troll") # 3/6 var troll := put(o, "e_troll") # 3/6
g.begin_turn(h) g.begin_turn(h)
g.attack(wolf, troll) g.attack(wolf, troll)
check(troll.health == 3 and wolf.health <= 0 and not (wolf in h.board), "Konter tötet den Wolf") check(troll.health == 3 and wolf.health <= 0 and not (wolf in h.board), "Konter tötet den Wolf")
# Durchschlag: was über das Leben des Wesens hinausgeht, trifft den Besitzer g.begin_turn(o)
var drache := put(h, "f_drache") # 6/5 check(troll.health == 3, "der Troll bleibt verletzt")
var kobold := put(o, "f_kobold") # 2/1
var life := o.life
g.attack(drache, kobold)
check(not (kobold in o.board) and o.life == life - 5 and g.log.any(func(l): return l == "5 Schaden schlagen durch auf Gegner."), "Durchschlag: 6 gegen 1 Leben, 5 gehen durch")
var sch := put(o, "f_kobold")
sch.shield = true
life = o.life
g.begin_turn(h)
g.attack(drache, sch)
check(sch.health == 1 and o.life == life, "Schild blockt alles, nichts schlägt durch")
func test_elementvorteil() -> void: func test_elementvorteil() -> void:
@@ -242,7 +219,7 @@ func test_elementvorteil() -> void:
var wolf := put(o, "f_wolf") # Feuer 3/2 var wolf := put(o, "f_wolf") # Feuer 3/2
var life: int = o.life var life: int = o.life
g.attack(tiefen, wolf) g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life - 3 and tiefen.health == 6 - 3, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter bleibt gleich") check(wolf.health <= 0 and o.life == life and tiefen.health == 6 - 3, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter bleibt gleich, nichts schlägt durch")
var s3 := setup() var s3 := setup()
var w3 := put(s3[1], "f_wolf") var w3 := put(s3[1], "f_wolf")
var t3 := put(s3[2], "w_tiefen") var t3 := put(s3[2], "w_tiefen")
@@ -255,7 +232,8 @@ func test_elementvorteil() -> void:
check(s2[2].life == 13, "gegen Helden gibt es keinen Vorteil") check(s2[2].life == 13, "gegen Helden gibt es keinen Vorteil")
## Verdeckt: greift nicht an, zählt nicht für die Elementregel, wird beim Angriff aufgedeckt. ## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt
## werden, greift dann sofort an; ein Angriff deckt es auf.
func test_verdeckt() -> void: func test_verdeckt() -> void:
var s := setup(["f_wolf", "f_kobold"]) var s := setup(["f_wolf", "f_kobold"])
var g: Game = s[0] var g: Game = s[0]
@@ -265,12 +243,13 @@ func test_verdeckt() -> void:
check(g.play_card(h, h.hand[0], true) == "" and h.board[0].hidden and h.energy == mana - C("f_wolf").cost, "verdeckt gelegt, gleich teuer") check(g.play_card(h, h.hand[0], true) == "" and h.board[0].hidden and h.energy == mana - C("f_wolf").cost, "verdeckt gelegt, gleich teuer")
var wolf: Game.Creature = h.board[0] var wolf: Game.Creature = h.board[0]
check(g.log.back() == "Spieler legt ein Wesen verdeckt." and g.events.back().hidden, "Verlauf verrät die Karte nicht") check(g.log.back() == "Spieler legt ein Wesen verdeckt." and g.events.back().hidden, "Verlauf verrät die Karte nicht")
check(g.reveal(h, wolf) == "Flammenwolf kann erst im nächsten Zug aufgedeckt werden" and wolf.hidden, "im Legezug nicht aufdecken")
g.begin_turn(h) g.begin_turn(h)
check(not wolf.can_attack() and g.can_attack(wolf, o)[1] == "Flammenwolf liegt verdeckt", "verdeckt greift nicht an") check(not wolf.can_attack() and g.can_attack(wolf, o)[1] == "Flammenwolf liegt verdeckt", "verdeckt greift nicht an")
check(g.reveal(h, wolf) == "" and not wolf.hidden and wolf.can_attack(), "aufgedeckt: kann sofort angreifen") check(g.reveal(h, wolf) == "" and not wolf.hidden and wolf.can_attack(), "aufgedeckt: kann sofort angreifen")
check(g.reveal(h, wolf) != "", "offen bleibt offen") check(g.reveal(h, wolf) != "", "offen bleibt offen")
# Der Gegner deckt beim Angriff auf # Der Gegner deckt beim Angriff auf
var kobold := put(o, "f_kobold", true, true) var kobold := put(o, "f_kobold", false, true)
o.hand = [] o.hand = []
check(g.attack(wolf, kobold) == "" and not kobold.hidden and kobold.health <= 0, "Angriff deckt auf und tötet") check(g.attack(wolf, kobold) == "" and not kobold.hidden and kobold.health <= 0, "Angriff deckt auf und tötet")
check(g.log.any(func(l): return l == "Feuerkobold wird aufgedeckt."), "Aufdecken steht im Verlauf") check(g.log.any(func(l): return l == "Feuerkobold wird aufgedeckt."), "Aufdecken steht im Verlauf")
@@ -288,45 +267,104 @@ func test_verdeckt() -> void:
check(own.card == "f_asche", "der Besitzer sieht seine verdeckte Karte") check(own.card == "f_asche", "der Besitzer sieht seine verdeckte Karte")
## Vier offene Wesen eines Elements: +1/+1 für das Element, -1/-1 für das, das es schlägt. ## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
func test_vier_elemente() -> void: ## Flink, Wasser Regeneration, Feuer Doppelschlag); drei auf dem Tisch geben dem Element +1/+1.
func test_elementboni() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2] var o: Game.Player = s[2]
var fire := [] var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
for i in 3:
fire.append(put(h, "f_wolf"))
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 bird.aura == 0, "drei reichen nicht") check(fire[0].aura == 0 and fire[1].aura == 0, "zwei reichen nicht für +1/+1")
var hidden_fire := put(o, "f_kobold", true, true) 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)
g.apply_auras() g.apply_auras()
check(fire[0].aura == 0, "verdeckte zählen nicht") 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(g.element_counts()["Feuer"] == 3, "Zähler ohne Verdeckte") var third := put(o, "f_ritter") # Feuer beim Gegner zählt für den Tisch
var fourth := put(o, "f_asche") # Feuer beim Gegner zählt auch
g.apply_auras() g.apply_auras()
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and fourth.aura == 1, "vier Feuer: +1/+1 für alle Feuerwesen") 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")
check(bird.aura == -1 and bird.power() == 0 and bird.health == 0, "Luft wird geschlagen: -1/-1") check(bird.aura == 0 and bird.health == 1, "kein Malus für das geschlagene Element")
g.cleanup() check(not third.has("Doppelschlag"), "der Gegner hat nur ein offenes Feuer: kein Sondereffekt")
check(not (bird in o.board), "das 1/1 ist daran gestorben") g._flip(hidden_fire)
g.reveal(o, hidden_fire) if g.current() == o else 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 == 1, "aufgedeckt zählt es und bekommt den Bonus sofort") fire[0].health = 1
o.board.erase(fourth) o.board.erase(third)
o.board.erase(hidden_fire) o.board.erase(hidden_fire)
g.apply_auras() g.apply_auras()
check(fire[0].aura == 0 and fire[0].health == 2, "Bonus geht wieder, Leben folgt") 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")
# Vorschau # Vorschau
check(g.forecast(h, C("f_kobold")) == 1 and g.forecast(h, C("l_wind")) == 0 and g.forecast(h, C("w_geist")) == 0, "Vorschau: Feuer würde den Stapel voll machen") check(g.forecast(h, C("f_asche")) == 1 and g.forecast(h, C("l_wind")) == 0, "Vorschau: Feuer würde den Stapel voll machen")
# Merkmal: doppelt # Merkmal: doppelt
h.perks = ["element_doppelt"] h.perks = ["element_doppelt"]
put(h, "f_kobold") 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")
check(g.forecast(h, C("l_wind")) == -2, "Vorschau: Luft bekäme jetzt -2/-2") h.perks = []
# Die anderen Paare
var walls := [put(h, "e_wall"), put(h, "e_wall")]
check(walls[0].has("Spott") and not fire[0].has("Spott"), "zwei Erde: Spott nur für Erdwesen")
var winds := [put(h, "l_wind", false), put(h, "l_wind", false)]
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")]
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
var s2 := setup()
var g2: Game = s2[0]
var h2: Game.Player = s2[1]
var dampf := put(h2, "f_wolf")
dampf.card = Cards.fused(C("f_wolf"), C("w_geist"))
var fw := put(h2, "f_kobold")
var ww := put(h2, "w_geist")
g2.apply_auras()
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")
put(s2[2], "f_asche")
put(s2[2], "w_reiter")
g2.apply_auras()
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
## Verbessern: Eigenschaften für Mana, je Exemplar. ## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
func test_spott_und_regeneration() -> void:
var s := setup()
var g: Game = s[0]
var h: Game.Player = s[1]
var o: Game.Player = s[2]
var wolf := put(h, "f_wolf")
var wall := put(o, "e_wall")
var kobold := put(o, "f_kobold")
wall.card.keywords.append("Spott")
check(g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott" and g.can_attack(wolf, wall)[0], "Spott muss zuerst angegriffen werden")
var secret := put(o, "e_troll", true, true)
secret.card.keywords.append("Spott")
check(not secret.has("Spott") and g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott", "verdeckter Spott zählt nicht")
o.board.erase(secret)
g.attack(wolf, wall)
wall.health = 0
g.cleanup()
g.begin_turn(h)
check(g.can_attack(wolf, kobold)[0], "ohne Spötter ist alles angreifbar")
# Regeneration
var s2 := setup()
var g2: Game = s2[0]
var h2: Game.Player = s2[1]
var reg := put(h2, "w_tiefen")
reg.card.keywords.append("Regeneration")
var plain := put(h2, "e_troll")
var enemy := put(s2[2], "w_reiter")
enemy.card.keywords.append("Regeneration")
reg.health = 1
plain.health = 1
enemy.health = 1
g2.end_turn()
check(reg.health == reg.max_health and g2.log.any(func(l): return l == "Tiefenwächter regeneriert sich."), "Regeneration heilt am Zugende ganz")
check(plain.health == 1 and enemy.health == 1, "ohne Regeneration und beim Gegner heilt nichts")
check(g2.events.any(func(e): return e.type == "heal" and e.uid == reg.uid), "Ereignis fürs Heilen")
## Verbessern: Eigenschaften für Mana, je Exemplar, auf Hand und Brett.
func test_verbessern() -> void: func test_verbessern() -> void:
var s := setup(["f_wolf"]) var s := setup(["f_wolf"])
var g: Game = s[0] var g: Game = s[0]
@@ -334,24 +372,26 @@ func test_verbessern() -> void:
var o: Game.Player = s[2] var o: Game.Player = s[2]
h.energy = 20 h.energy = 20
var card: Cards.Card = h.hand[0] var card: Cards.Card = h.hand[0]
check(g.upgrade(h, card, "Schild") == "" and h.energy == 20 - 3 and "Schild" in h.hand[0].keywords, "Schild für die Handkarte gekauft") check(g.upgrade(h, card, "Spott") == "" and h.energy == 20 - 2 and "Spott" in h.hand[0].keywords, "Spott für die Handkarte gekauft")
check(h.hand[0] != card and Cards.by_id("f_wolf").keywords.is_empty(), "eigenes Exemplar, der Pool bleibt sauber") check(h.hand[0] != card and Cards.by_id("f_wolf").keywords.is_empty(), "eigenes Exemplar, der Pool bleibt sauber")
check(g.upgrade(h, h.hand[0], "Schild") != "", "nicht doppelt") check(g.upgrade(h, h.hand[0], "Spott") != "", "nicht doppelt")
g.play_card(h, h.hand[0]) g.play_card(h, h.hand[0])
var wolf: Game.Creature = h.board[0] var wolf: Game.Creature = h.board[0]
var after_play := h.energy var after_play := h.energy
check(wolf.shield and wolf.can_attack(), "mit Schild gelegt, greift sofort an") check(wolf.has("Spott") and not wolf.can_attack(), "mit Spott gelegt, greift noch nicht an")
check(g.upgrade(h, wolf, "Doppelschlag") == "" and wolf.attacks_per_turn() == 2 and wolf.attacks_left == 2 and h.energy == after_play - 4, "Doppelschlag: zwei Angriffe") check(g.upgrade(h, wolf, "Flink") == "" and wolf.can_attack() and h.energy == after_play - 2, "Flink: greift sofort an")
check(wolf.card.text() == "Schild, Doppelschlag.", "Kartentext zeigt die Eigenschaften") check(g.upgrade(h, wolf, "Doppelschlag") == "" and wolf.attacks_per_turn() == 2 and wolf.attacks_left() == 2 and h.energy == after_play - 6, "Doppelschlag: zwei Angriffe")
check(g.upgrade(h, wolf, "Doppelschlag") != "" and g.upgrade(h, put(o, "e_wall"), "Schild") != "" and g.upgrade(h, wolf, "Flink") != "", "nicht doppelt, nicht fremd, nicht unbekannt") check(g.upgrade(h, wolf, "Regeneration") == "" and h.energy == after_play - 9, "Regeneration kostet 3")
h.energy = 2 check(wolf.card.text() == "Spott, Flink, Regeneration, Doppelschlag.", "Kartentext zeigt die Eigenschaften")
check(g.upgrade(h, wolf, "Doppelschlag") != "" and g.upgrade(h, put(o, "e_wall"), "Spott") != "" and g.upgrade(h, wolf, "Schild") != "", "nicht doppelt, nicht fremd, nicht unbekannt")
h.energy = 1
var k2 := put(h, "f_kobold") var k2 := put(h, "f_kobold")
check(g.upgrade(h, k2, "Schild") == "Nicht genug Mana", "zu teuer") check(g.upgrade(h, k2, "Spott") == "Nicht genug Mana", "zu teuer")
h.perks = ["verbessern_billig"] h.perks = ["verbessern_billig"]
check(g.upgrade(h, k2, "Schild") == "" and h.energy == 0, "Merkmal: Eigenschaften kosten 1 weniger") check(g.upgrade(h, k2, "Spott") == "" and h.energy == 0, "Merkmal: Eigenschaften kosten 1 weniger")
## Verschmelzen: der bessere Wert von beidem plus 1, Kosten 1, 2, 4, 8 … ## Verschmelzen: besserer Wert plus ein Drittel des schwächeren, Eigenschaften beider, Kosten 1, 2, 4, 8 …; auch auf der Hand.
func test_verschmelzen() -> void: func test_verschmelzen() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
@@ -359,46 +399,60 @@ func test_verschmelzen() -> void:
var a := put(h, "f_wolf") # 3/2 var a := put(h, "f_wolf") # 3/2
var b := put(h, "w_tiefen") # 4/6 var b := put(h, "w_tiefen") # 4/6
b.health = 4 b.health = 4
a.card.keywords.append("Schild") a.card.keywords.append("Spott")
check(Game.fusion_cost(a, b) == 1, "erste Verschmelzung kostet 1") check(Game.fusion_cost(a, b) == 1, "erste Verschmelzung kostet 1")
check(g.fuse(h, a, b) == "" and h.board.size() == 1 and h.energy == 9, "verschmolzen") check(g.fuse(h, a, b) == "" and h.board.size() == 1 and h.energy == 9, "verschmolzen")
var f: Game.Creature = h.board[0] var f: Game.Creature = h.board[0]
check(f.name == "Dampf" and f.attack == 5 and f.max_health == 7 and f.health == 5, "Dampf mit dem besseren Angriff und Leben plus 1") check(f.name == "Dampf" and f.attack == 5 and f.max_health == 7 and f.health == 5, "Dampf: 4+1 Angriff, 6+1 Leben, aktuell 4+1")
check("Schild" in f.card.keywords and f.card.fusions == 1 and f.card.cost == Cards.cost_of(5, 7), "Eigenschaften geerbt, einmal verschmolzen") check("Spott" in f.card.keywords and f.card.fusions == 1 and f.card.cost == 5, "Eigenschaften geerbt, einmal verschmolzen, Kosten 5")
check(f.card.elements == ["Feuer", "Wasser"], "beide Elemente") check(f.card.elements == ["Feuer", "Wasser"] and f.ready, "beide Elemente, beide waren bereit")
var c := put(h, "e_wall") var c := put(h, "e_wall", false)
check(Game.fusion_cost(f, c) == 2, "zweite Verschmelzung kostet 2") check(Game.fusion_cost(f, c) == 2, "zweite Verschmelzung kostet 2")
g.fuse(h, f, c) g.fuse(h, f, c)
var f2: Game.Creature = h.board[0] var f2: Game.Creature = h.board[0]
check(f2.name == "Chimäre" and f2.card.fusions == 2 and h.energy == 7, "Chimäre aus drei Elementen") check(f2.name == "Chimäre" and f2.card.fusions == 2 and h.energy == 7 and not f2.ready, "Chimäre aus drei Elementen, nicht bereit, weil der Wall es nicht war")
var d := put(h, "l_wind") var d := put(h, "l_wind")
check(Game.fusion_cost(f2, d) == 4, "dritte kostet 4") check(Game.fusion_cost(f2, d) == 4, "dritte kostet 4")
h.energy = 3 h.energy = 3
check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer") check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer")
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Wesen wählen", "nicht mit sich selbst") check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
# Verdeckte verschmelzen offen # Verdeckte verschmelzen offen
var s2 := setup() var s2 := setup()
var x := put(s2[1], "f_wolf", true, true) var x := put(s2[1], "f_wolf", true, true)
var y := put(s2[1], "f_kobold", true, true) var y := put(s2[1], "f_kobold", true, true)
s2[0].fuse(s2[1], x, y) s2[0].fuse(s2[1], x, y)
check(not s2[1].board[0].hidden, "das Ergebnis liegt offen") check(not s2[1].board[0].hidden and s2[1].board[0].name == "Inferno", "das Ergebnis liegt offen")
# Elementbonus der Eltern zählt nicht doppelt; Dampf nimmt den besseren Elementwert # Elementbonus der Eltern zählt nicht doppelt; Dampf bekommt die Boni beider Elemente
var s3 := setup() var s3 := setup()
var g3: Game = s3[0] var g3: Game = s3[0]
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 3: for i in 2:
put(h3, "f_kobold") put(h3, "f_kobold")
g3.apply_auras() g3.apply_auras()
check(fa.aura == 1 and fa.max_health == 3, "vier Feuer vor dem Verschmelzen") check(fa.aura == 1 and fa.max_health == 3, "drei Feuer 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") 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)")
for i in 3: for i in 2:
put(h3, "w_geist") put(h3, "w_geist")
g3.apply_auras() g3.apply_auras()
check(g3.element_counts()["Wasser"] == 4 and ff.aura == 1, "Wasser +1 und Feuer -1: Dampf nimmt den besseren Wert") check(g3.element_counts()["Wasser"] == 3 and ff.aura == 2 and ff.max_health == 9, "Feuer und Wasser voll: Dampf bekommt +2/+2")
# Auf der Hand
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
var g4: Game = s4[0]
var h4: Game.Player = s4[1]
var hand_a: Cards.Card = h4.hand[0]
var hand_b: Cards.Card = h4.hand[1]
check(g4.can_fuse(h4, hand_a, put(h4, "e_wall"))[1] == "Nur Hand mit Hand oder Brett mit Brett", "Hand und Brett mischen sich nicht")
check(g4.fuse(h4, hand_a, hand_b) == "" and h4.hand.size() == 2 and h4.energy == 9, "zwei Handkarten verschmolzen, 1 Mana")
var hc: Cards.Card = h4.hand[0]
check(hc.name == "Dampf" and hc.attack == 5 and hc.health == 7 and hc.cost == 5 and hc.fusions == 1, "Dampf auf der Hand mit 5/7 und Kosten 5")
var lava := Cards.fused(C("f_asche"), C("f_kobold"), 4, 8)
check(Cards.fused(lava, C("f_kobold")).attack == 5 and Cards.fused(lava, C("f_kobold")).health == 9, "ein Kobold macht Lava 4/8 zu 5/9, nie schlechter")
check(g4.play_card(h4, hc) == "" and h4.energy == 4 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
check(g4.log.any(func(l): return l == "Flammenwolf und Tiefenwächter verschmelzen auf der Hand zu Dampf (5/7)."), "Verlauf")
func test_limits() -> void: func test_limits() -> void:
@@ -422,7 +476,7 @@ func test_gleiche_regeln() -> void:
var g := Game.new_battle(r) var g := Game.new_battle(r)
var first: Game.Player = g.players[0] var first: Game.Player = g.players[0]
check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 5 Mana, kein Bonus") check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 5 Mana, kein Bonus")
check(first.offer.size() == Game.START_OFFER and first.hand.is_empty(), "jeder beginnt mit einer Wahl aus fünf und leerer Hand") check(g.players.all(func(p): return p.hand.size() == Game.START_HAND), "jeder beginnt mit fünf Karten")
for p in g.players: for p in g.players:
check(p.life == Game.START_LIFE, "gleiches Leben") check(p.life == Game.START_LIFE, "gleiches Leben")
if p != first: if p != first:
@@ -474,7 +528,7 @@ func test_booster() -> void:
for i in 50: for i in 50:
var rolled := Profile.roll_booster(r) var rolled := Profile.roll_booster(r)
check(rolled.size() == 5, "5 Karten") check(rolled.size() == 5, "5 Karten")
check(rolled.any(func(x): return x.rarity != "gewöhnlich"), "mindestens eine seltene") check(rolled.any(func(x): return x.rarity != "Common"), "mindestens eine Rare")
var p := Profile.new() var p := Profile.new()
p.coins = Profile.BOOSTER_COST p.coins = Profile.BOOSTER_COST
var got := p.open_booster(r) var got := p.open_booster(r)
@@ -490,7 +544,23 @@ func test_booster() -> void:
p.collection["f_wolf"] = Cards.RARITIES.size() - 1 p.collection["f_wolf"] = Cards.RARITIES.size() - 1
check(p.upgrade("f_wolf") != "", "über die höchste Stufe hinaus geht nicht") check(p.upgrade("f_wolf") != "", "über die höchste Stufe hinaus geht nicht")
p.collection.erase("f_wolf") p.collection.erase("f_wolf")
check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "gewöhnlich", "gewöhnliche Karten gehören allen") check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "Common", "Common-Karten gehören allen")
check(Profile.RARITY_WEIGHTS.values().reduce(func(a, b): return a + b, 0.0) == 100.0, "Chancen ergeben 100 %")
check(Profile.DUST_UPGRADE["Rare"] == 100 and Profile.DUST_UPGRADE["Ultimate Rare"] == 1000 and Profile.DUST_DISMANTLE["Common"] == 50 and Profile.DUST_DISMANTLE["Ultimate Rare"] == 1000, "Staubpreise")
# Verschmolzene Karten tragen die höhere Stufe der Eltern
var s := setup(["f_wolf", "w_tiefen"])
var g: Game = s[0]
var h: Game.Player = s[1]
h.looks = {"w_tiefen": 3}
check(h.hand[0].rarity_in(h.looks) == "Common" and h.hand[1].rarity_in(h.looks) == "Ultra Rare", "Stufe aus der Sammlung")
g.fuse(h, h.hand[0], h.hand[1])
check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "Fusion auf der Hand: die höhere Stufe gewinnt")
var kobold := put(h, "f_kobold")
g.play_card(h, h.hand[0])
var fused: Game.Creature = h.board[1]
g.fuse(h, fused, kobold)
check(h.board[0].card.look == 3 and h.board[0].card.rarity_in(h.looks) == "Ultra Rare", "auch auf dem Brett und über zwei Fusionen")
check(Cards.Card.from_dict(h.board[0].card.to_dict()).look == 3, "die Stufe reist mit der Karte")
func test_spielstand() -> void: func test_spielstand() -> void:
@@ -500,7 +570,7 @@ func test_spielstand() -> void:
if FileAccess.file_exists(path + suffix): if FileAccess.file_exists(path + suffix):
DirAccess.remove_absolute(abs + suffix) DirAccess.remove_absolute(abs + suffix)
var p := Profile.load_or_new(path) var p := Profile.load_or_new(path)
check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten gewöhnlich") check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten Common")
check(FileAccess.file_exists(path), "Datei angelegt") check(FileAccess.file_exists(path), "Datei angelegt")
p.coins = 999 p.coins = 999
p.collection["f_wolf"] = 2 p.collection["f_wolf"] = 2
@@ -525,8 +595,8 @@ func test_kopie() -> void:
check(g2.players[1].life == 11 and o.life == 15, "Kopie verändert das Original nicht") check(g2.players[1].life == 11 and o.life == 15, "Kopie verändert das Original nicht")
g2.play_card(g2.players[0], g2.players[0].hand[0]) g2.play_card(g2.players[0], g2.players[0].hand[0])
check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig") check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig")
g2.upgrade(g2.players[0], a2, "Schild") g2.upgrade(g2.players[0], a2, "Spott")
check(not a.shield and a.card.keywords.is_empty(), "Verbessern in der Kopie lässt das Original unberührt") check(not a.has("Spott") and a.card.keywords.is_empty() and a2.has("Spott"), "Verbessern in der Kopie lässt das Original unberührt")
check(g2.log.is_empty() and g2.events.is_empty(), "Kopie schreibt keinen Verlauf") check(g2.log.is_empty() and g2.events.is_empty(), "Kopie schreibt keinen Verlauf")
@@ -667,16 +737,15 @@ func test_merkmale() -> void:
var s := setup(["f_wolf", "l_falke"]) var s := setup(["f_wolf", "l_falke"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
h.perks = ["feuer_angriff", "wesen_angriff", "wesen_leben", "schild_wesen", "ziehen_gratis"] h.perks = ["feuer_angriff", "wesen_angriff", "wesen_leben", "alle_flink", "ziehen_gratis"]
g.play_card(h, h.hand[0]) g.play_card(h, h.hand[0])
var wolf: Game.Creature = h.board[0] var wolf: Game.Creature = h.board[0]
check(wolf.power() == 3 + 1 + 1 and wolf.health == 3 and wolf.shield and wolf.can_attack(), "Feuer +1, Wesen +1/+1, Schild") check(wolf.power() == 3 + 1 + 1 and wolf.health == 3 and wolf.has("Flink") and wolf.can_attack(), "Feuer +1, Wesen +1/+1, flink")
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)
var mana := h.energy var mana := h.energy
check(g.draw(h) == "" and h.energy == mana, "Merkmal: Ziehen kostet nichts") check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
g.pick(h, 0)
for perk in Game.PERKS: for perk in Game.PERKS:
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk) check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
@@ -695,17 +764,18 @@ func test_merkmale() -> void:
func test_ki_zwei_schritte() -> void: func test_ki_zwei_schritte() -> void:
# Gegner hat 5 Leben und kein Wesen; der Wüstenkönig (5 Angriff) liegt auf der Hand: # Gegner hat 5 Leben und kein Wesen; der Wüstenkönig (5 Angriff) liegt frisch auf dem Brett:
# erst ausspielen, dann mit ihm töten. # erst Flink kaufen, dann mit ihm töten.
var s := setup(["el_koenig"]) var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2] var o: Game.Player = s[2]
h.turns = 2 h.turns = 2
o.life = 5 o.life = 5
var king := put(h, "el_koenig", false)
put(h, "f_wolf", false) put(h, "f_wolf", false)
var act := AI.choose_action(g, h) var act := AI.choose_action(g, h)
check(act.type == "play" and act.card == h.hand[0] and not act.face_down, "KI spielt zuerst den König, offen") check(act.type == "upgrade" and act.creature == king and act.keyword == "Flink", "KI kauft zuerst Flink für den König")
AI.apply_action(g, h, act) AI.apply_action(g, h, act)
var act2 := AI.choose_action(g, h) var act2 := AI.choose_action(g, h)
check(act2.type == "attack" and act2.target == o, "dann tödlicher Angriff") check(act2.type == "attack" and act2.target == o, "dann tödlicher Angriff")
@@ -718,26 +788,26 @@ func test_snapshot() -> void:
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]
g.upgrade(h, h.hand[0], "Schild") g.upgrade(h, h.hand[0], "Spott")
g.play_card(h, h.hand[0]) g.play_card(h, h.hand[0])
put(o, "e_troll") var troll := put(o, "e_troll")
troll.attacks_done = 1
var hi := g.players.find(h) var hi := g.players.find(h)
var oi := g.players.find(o) var oi := g.players.find(o)
h.energy = 5 h.energy = 5
o.energy = 5 o.energy = 5
g.deck = DeckGen.deck(g.rng) g.deck = DeckGen.deck(g.rng)
g.draw(h) g.draw(h)
o.offer = g.draw_cards(3, 5)
o.picks_left = 1
var snap := g.snapshot(hi) var snap := g.snapshot(hi)
var json: Dictionary = JSON.parse_string(JSON.stringify(snap)) var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
var g2 := Game.from_snapshot(json) var g2 := Game.from_snapshot(json)
check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx and g2.deck.size() == g.deck.size(), "Grunddaten und Stapelgröße") check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx and g2.deck.size() == g.deck.size(), "Grunddaten und Stapelgröße")
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar") check(g2.players[hi].hand.size() == 1 and g2.players[hi].hand[0].id == h.hand[0].id, "eigene Hand sichtbar")
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")
check(g2.players[oi].offer.is_empty() and g2.players[hi].offer.size() == Game.OFFER_SIZE and g2.players[hi].picks_left == 1, "eigene Wahl sichtbar, fremde nicht")
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 cr2.shield and "Schild" in cr2.card.keywords, "Wesen mit Besitzer, Zustand und gekaufter Eigenschaft") check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and cr2.has("Spott") and not cr2.ready, "Wesen mit Besitzer, Zustand und gekaufter Eigenschaft")
var troll2 := g2.find_creature(troll.uid)
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")
check(g2.draw(g2.players[oi]) == "Nicht dein Zug", "Kopie kennt die Regeln") check(g2.draw(g2.players[oi]) == "Nicht dein Zug", "Kopie kennt die Regeln")
var snap_o := g.snapshot(oi) var snap_o := g.snapshot(oi)
@@ -773,9 +843,6 @@ func test_ki_zeitbudget() -> void:
r.seed = 12 r.seed = 12
var g := Game.new_battle(r) var g := Game.new_battle(r)
var h: Game.Player = g.players[0] var h: Game.Player = g.players[0]
for p in g.players:
p.offer = []
p.picks_left = 0
h.energy = 30 h.energy = 30
for i in Game.MAX_HAND: for i in Game.MAX_HAND:
h.hand.append(C("f_ritter")) h.hand.append(C("f_ritter"))
@@ -796,11 +863,11 @@ func test_ki_zeitbudget() -> void:
func test_englische_karten() -> void: func test_englische_karten() -> void:
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen") check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
var ritter := C("f_ritter") var ritter := C("f_ritter")
ritter.keywords = ["Schild", "Doppelschlag"] ritter.keywords = ["Spott", "Doppelschlag"]
TranslationServer.set_locale("en") TranslationServer.set_locale("en")
check(ritter.title() == "Blazing Knight" and ritter.text() == "Shield, Double Strike.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()]) check(ritter.title() == "Blazing Knight" and ritter.text() == "Taunt, Double Strike.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()])
TranslationServer.set_locale("de") TranslationServer.set_locale("de")
check(ritter.title() == "Lodernder Ritter" and ritter.text() == "Schild, Doppelschlag.", "zurück auf Deutsch") check(ritter.title() == "Lodernder Ritter" and ritter.text() == "Spott, Doppelschlag.", "zurück auf Deutsch")
## Voller Spielstand (viewer -1) stellt Hände und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes. ## Voller Spielstand (viewer -1) stellt Hände und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
@@ -826,7 +893,7 @@ func test_kampf_speichern() -> void:
guard += 1 guard += 1
check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar") check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar")
var s2: Dictionary = g.snapshot(-2) var s2: Dictionary = g.snapshot(-2)
check(s2.players.all(func(p): return not (p.hand is Array) and p.offer.is_empty()), "Zuschauer-Sicht ohne Hände und Wahlen") check(s2.players.all(func(p): return not (p.hand is Array)), "Zuschauer-Sicht ohne Hände")
## Jeder tr("...")-Text in der Oberfläche hat einen englischen Eintrag. ## Jeder tr("...")-Text in der Oberfläche hat einen englischen Eintrag.
@@ -843,7 +910,7 @@ func test_uebersetzung_vollstaendig() -> void:
key = line.substr(0, line.find(",")) key = line.substr(0, line.find(","))
keys[key] = true keys[key] = true
var missing := [] var missing := []
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + Cards.KEYWORD_TEXT.values() + Game.PERKS.values(): for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + Cards.KEYWORD_TEXT.values() + Game.PERKS.values() + Cards.ELEMENT_KEYWORD.values():
if not keys.has(k): if not keys.has(k):
missing.append(k) missing.append(k)
var d := DirAccess.open("res://scripts/ui") var d := DirAccess.open("res://scripts/ui")

View File

@@ -54,11 +54,11 @@ func right_click() -> void:
await right_click_at(Vector2(5, 5)) await right_click_at(Vector2(5, 5))
func right_click_at(pos: Vector2) -> 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
down.global_position = pos down.global_position = pos
down.button_index = MOUSE_BUTTON_RIGHT down.button_index = button
down.pressed = true down.pressed = true
get_viewport().push_input(down) get_viewport().push_input(down)
var up: InputEventMouseButton = down.duplicate() var up: InputEventMouseButton = down.duplicate()
@@ -219,20 +219,42 @@ func target_view(scr, valid: Array) -> Control:
return null return null
## Nimmt alle offenen Wahlen des Spielers (immer die erste Karte), auch über das Netz. ## Zieht eine Karte oder Kachel auf eine andere und hält sie dort, bis die beiden verschmelzen.
func pick_all(scr) -> void: func drag_fuse(scr, from: Control, to: Control) -> void:
var guard := 0 await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
while scr.game != null and not scr.perspective.offer.is_empty() and guard < 60: await settle(scr)
guard += 1 var a := from.get_global_rect().get_center()
if scr.pick_views.is_empty(): var b := to.get_global_rect().get_center()
await frames(1) var down := InputEventMouseButton.new()
continue down.position = a
var n: int = scr.perspective.offer.size() down.global_position = a
await click(scr.pick_views[0]) down.button_index = MOUSE_BUTTON_LEFT
var w := 0 down.pressed = true
while scr.game != null and scr.perspective.offer.size() == n and w < 120: get_viewport().push_input(down)
await frames(1) await frames(1)
w += 1 for i in range(1, 6):
var m := InputEventMouseMotion.new()
m.position = a.lerp(b, i / 5.0)
m.global_position = m.position
m.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(m)
await frames(1)
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel")
var waited := 0
while scr.drag_ghost != null and waited < 240:
var still := InputEventMouseMotion.new() # die Maus bleibt auf dem Ziel, auch wenn die echte zuckt
still.position = b
still.global_position = b
still.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(still)
await frames(1)
waited += 1
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
up.position = b
up.global_position = b
get_viewport().push_input(up)
await frames(2)
## Legt, was bezahlbar ist. ## Legt, was bezahlbar ist.
@@ -257,10 +279,9 @@ func human_turn(scr) -> void:
if g.winner != "" or not g.can_draw(g.human)[0]: if g.winner != "" or not g.can_draw(g.human)[0]:
break break
var mana: int = g.human.energy var mana: int = g.human.energy
var hand: int = g.human.hand.size()
await click(scr.deck) await click(scr.deck)
check(g.human.energy == mana - Game.DRAW_COST and not scr.pick_views.is_empty(), "gezogen: drei zur Wahl") check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
await pick_all(scr)
check(scr.pick_views.is_empty(), "gewählt")
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())
if ready.is_empty() or g.winner != "": if ready.is_empty() or g.winner != "":
@@ -284,7 +305,7 @@ func human_turn(scr) -> void:
targets.append(c) targets.append(c)
await settle(scr) await settle(scr)
await click(target_view(scr, targets)) await click(target_view(scr, targets))
check(scr.selected == null and cr.attacks_left < cr.attacks_per_turn(), "Angriff ausgeführt") check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
if g.winner == "": if g.winner == "":
await click(scr.b_end) await click(scr.b_end)
@@ -293,7 +314,6 @@ func start_battle() -> void:
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
check(screen() is OpponentSelectScreen, "Gegnerauswahl") check(screen() is OpponentSelectScreen, "Gegnerauswahl")
await click(find_button("Starten")) await click(find_button("Starten"))
await pick_all(screen())
func test_gegnerauswahl() -> void: func test_gegnerauswahl() -> void:
@@ -310,12 +330,9 @@ func test_gegnerauswahl() -> void:
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet") check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
var opp: Game.Player = hard.game.opponent var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life) check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
check(hard.game.human.offer.size() == Game.START_OFFER and hard.game.human.hand.is_empty() and hard.pick_layer.visible, check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
"Start: fünf Karten zur Wahl, Hand leer") check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA + 1), "alle fünf sind im ersten Zug spielbar")
check(hard.game.human.offer.all(func(c): return c.cost <= Game.START_MANA + 1), "alle fünf sind im ersten Zug spielbar") await shot("05_starthand")
await shot("05_startwahl")
await pick_all(hard)
check(hard.game.human.hand.size() == Game.START_PICKS and not hard.pick_layer.visible, "drei behalten, Wahl geschlossen")
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 oben in der Mitte")
await shot("11_kampf") await shot("11_kampf")
await give_up() await give_up()
@@ -323,7 +340,6 @@ func test_gegnerauswahl() -> void:
await click(find_button("Leicht")) await click(find_button("Leicht"))
await click(find_button("Starten")) await click(find_button("Starten"))
var easy = screen() var easy = screen()
await pick_all(easy)
opp = easy.game.opponent opp = easy.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life) check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide") check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
@@ -393,21 +409,21 @@ func test_sammlung() -> void:
await frames(1) await frames(1)
var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0] var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0]
await click(phoenix) await click(phoenix)
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "gewöhnlich", "Karte gewählt, noch gewöhnlich") check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "Common", "Karte gewählt, noch Common")
await click(scr.b_upgrade) await click(scr.b_upgrade)
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["selten"], "auf selten aufgewertet") check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
scr.search_box.text = "" scr.search_box.text = ""
scr.search_box.text_changed.emit("") scr.search_box.text_changed.emit("")
await frames(1) await frames(1)
var first: CardView = scr.grid.views()[0] var front: Array = scr.grid.views().slice(0, p.upgraded_count())
check(first.card.id == "ff_phoenix" and first.rarity == "selten", "die aufgewertete Karte steht vorn in der Sammlung") check(front.all(func(v): return v.rarity != "Common") and front.any(func(v): return v.card.id == "ff_phoenix" and v.rarity == "Rare"), "aufgewertete Karten stehen vorn in der Sammlung, der Phönix als Rare dabei")
scr.search_box.text = "Phönix" scr.search_box.text = "Phönix"
scr.search_box.text_changed.emit("Phönix") scr.search_box.text_changed.emit("Phönix")
await frames(1) await frames(1)
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert") check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
var dust: int = p.dust var dust: int = p.dust
await click(scr.b_dismantle) await click(scr.b_dismantle)
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["selten"], "wieder zerlegt") check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
await shot("20_sammlung") await shot("20_sammlung")
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
@@ -487,22 +503,21 @@ func test_kampf_steuerung() -> void:
scr.ai_delay = 0.0 scr.ai_delay = 0.0
await wait_for_human(scr) await wait_for_human(scr)
var g: Game = scr.game var g: Game = scr.game
g.human.energy = 20 # die Wahl 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")] g.human.hand = [Cards.by_id("f_wolf").copy()]
scr.refresh() scr.refresh()
check(scr.hand_views.size() == 1 and scr.deck.enabled and scr.pick_views.is_empty(), "eine Handkarte, Stapel bereit, keine Wahl offen") check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
# Ziehen: 1 Mana weg, drei zur Wahl, eine landet auf der Hand # Ziehen: 1 Mana weg, eine Karte landet auf der Hand
var mana: int = g.human.energy var mana: int = g.human.energy
await click(scr.deck) await click(scr.deck)
check(g.human.energy == mana - Game.DRAW_COST and scr.pick_views.size() == Game.OFFER_SIZE and scr.pick_layer.visible, "gezogen: drei Karten zur Wahl") 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")
var wanted: Cards.Card = scr.pick_views[1].card check(g.human.hand[1].cost <= g.human.max_energy, "nur bezahlbare Karten")
await click(scr.pick_views[1]) # Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
check(g.human.hand.size() == 2 and g.human.hand[1].id == wanted.id and not scr.pick_layer.visible, "gewählt, liegt auf der Hand")
# Ausspielen kostet die Kartenkosten
var board_before: int = g.human.board.size() var board_before: int = g.human.board.size()
mana = g.human.energy mana = g.human.energy
await key(KEY_1) await key(KEY_1)
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_wolf").cost, "Wesen per Taste gespielt, Kosten bezahlt") check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_wolf").cost, "Wesen per Taste gespielt, Kosten bezahlt")
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
# Rückgängig: Wesen zurück auf die Hand, Mana zurück # Rückgängig: Wesen zurück auf die Hand, Mana zurück
check(scr.b_undo.visible, "Rückgängig angeboten") check(scr.b_undo.visible, "Rückgängig angeboten")
await click(scr.b_undo) await click(scr.b_undo)
@@ -513,21 +528,16 @@ func test_kampf_steuerung() -> void:
check(g.human.board.size() == board_before + 1, "erneut gespielt") check(g.human.board.size() == board_before + 1, "erneut gespielt")
# Ziehen per Taste # Ziehen per Taste
mana = g.human.energy mana = g.human.energy
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 scr.pick_layer.visible, "Z zieht") check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht")
await key(KEY_1)
check(not scr.pick_layer.visible, "1 wählt die erste Karte")
await shot("22_kampf_steuerung") await shot("22_kampf_steuerung")
# Gamepad: A auf dem Stapel zieht, Steuerkreuz wählt die zweite Karte, A nimmt sie # Gamepad: A auf dem Stapel zieht
await pad(JOY_BUTTON_DPAD_LEFT) await pad(JOY_BUTTON_DPAD_LEFT)
check(scr.gp_zone == "deck" and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf dem Stapel") check(scr.gp_zone == "deck" and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf dem Stapel")
var hand_before: int = g.human.hand.size() hand_before = g.human.hand.size()
await pad(JOY_BUTTON_A) await pad(JOY_BUTTON_A)
check(scr.pick_layer.visible, "Gamepad A zieht") check(g.human.hand.size() == hand_before + 1, "Gamepad A zieht")
await pad(JOY_BUTTON_DPAD_RIGHT)
check(scr.gp_zone == "pick" and scr.gp_index == 1, "Zeiger auf der zweiten Karte der Wahl")
await pad(JOY_BUTTON_A)
check(g.human.hand.size() == hand_before + 1 and not scr.pick_layer.visible, "Gamepad A nimmt die Karte")
scr.ai_delay = 0.75 scr.ai_delay = 0.75
await key(KEY_SPACE) await key(KEY_SPACE)
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet") check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
@@ -562,7 +572,6 @@ func test_kampf_grenzen() -> void:
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.deck)
await pick_all(scr)
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.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
await click(scr.deck) await click(scr.deck)
@@ -573,20 +582,17 @@ func test_kampf_grenzen() -> void:
guard += 1 guard += 1
if g.human.hand.is_empty(): if g.human.hand.is_empty():
await click(scr.deck) await click(scr.deck)
await pick_all(scr)
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.deck)
await pick_all(scr)
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")
# Verschmelzen macht wieder Platz # Verschmelzen per Ziehen macht wieder Platz
var before: int = g.human.board.size() var before: int = g.human.board.size()
await click(scr.b_fuse) scr.hold_time = 0.3
await click(scr.own_views[0]) await drag_fuse(scr, scr.own_views[0], scr.own_views[1])
await click(scr.own_views[1])
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
@@ -596,12 +602,13 @@ func test_kampf_grenzen() -> void:
await give_up() await give_up()
## Elementregel, verdeckt legen, Kartenmenü und Verschmelzen im Kampf. ## Elementboni, verdeckt legen, Kartenmenü und Verschmelzen (per Ziehen und per Menü) im Kampf.
func test_kampf_elemente_und_verschmelzen() -> void: func test_kampf_elemente_und_verschmelzen() -> void:
await start_battle() await start_battle()
var scr = screen() var scr = screen()
check(scr is BattleScreen, "Kampf gestartet") check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0 scr.ai_delay = 0.0
scr.hold_time = 0.3
await wait_for_human(scr) await wait_for_human(scr)
var g: Game = scr.game var g: Game = scr.game
g.human.board.clear() g.human.board.clear()
@@ -609,7 +616,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
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
cr.attacks_left = 1 cr.ready = true
cr.hidden = hidden cr.hidden = hidden
p.board.append(cr) p.board.append(cr)
return cr return cr
@@ -620,56 +627,70 @@ func test_kampf_elemente_und_verschmelzen() -> void:
var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true) var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true)
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()] g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
g.human.energy = 20 g.human.energy = 20
g.human.looks = {"w_reiter": 3}
g.apply_auras() g.apply_auras()
scr.refresh() scr.refresh()
await frames(2) await frames(2)
check(scr.element_view.text() == "Feuer 2 · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen: " + scr.element_view.text()) 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.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "fremdes verdecktes Wesen zeigt nur den Rücken, noch kein Bonus in Sicht") 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")
# verdeckt legen über das Kartenmenü (Rechtsklick auf die Handkarte) # Rechtsklick auf die Handkarte: Menü mit Verdeckt legen und Eigenschaften, ohne Verschmelzen
var menu_text := "Verdeckt legen ◆ %d" % Cards.by_id("f_kobold").cost
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(menu_text, scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit „Verdeckt legen“") check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ 1", scr.menu_layer) != null and find_button("Spott ◆ 2", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
await shot("23_kartenmenue") await shot("23_kartenmenue")
await click(find_button(menu_text, scr.menu_layer)) await right_click()
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
# Mittlere Maustaste legt die Handkarte verdeckt
var mana: int = g.human.energy
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 not scr.menu_layer.visible and scr.element_view.text().begins_with("Feuer 2"), "verdeckt gelegt, zählt nicht") 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")
# aufdecken per Klick auf das eigene verdeckte Wesen (erst, wenn es aus der Hand angekommen ist) # Rechtsklick auf ein eigenes Wesen: Eigenschaften kaufen
await right_click_at(scr.own_views[0].get_global_rect().get_center())
check(scr.menu_layer.visible and find_button("Doppelschlag ◆ 4", scr.menu_layer) != null and find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "Wesen-Menü mit Eigenschaften, ohne Verschmelzen")
var wolf: Game.Creature = g.human.board[0]
await click(find_button("Spott ◆ %d" % Cards.KEYWORD_COST["Spott"], scr.menu_layer))
check(wolf.has("Spott") and "Spott" in wolf.card.keywords and g.human.energy == mana - 1 - Cards.KEYWORD_COST["Spott"], "Spott gekauft")
while scr.own_views[3].busy: while scr.own_views[3].busy:
await frames(1) await frames(1)
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"), "aufgedeckt, zählt jetzt") 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)
put.call(g.human, "f_kobold") mine_hidden.ready = true
g.apply_auras() await click(scr.own_views[3])
scr.refresh() 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())
await frames(2) 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(scr.element_view.text().begins_with("Feuer 4 (+1/+1)") and "Luft 1 (-1/-1)" in scr.element_view.text(), "vier Feuer: Bonus und Malus angezeigt: " + scr.element_view.text()) check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
check(g.human.board[0].power() == 4 and falke.power() == 2, "Feuerwesen +1, Luftwesen -1") await shot("23b_drei_feuer")
check(scr.hand_views[0].bonus == -1, "die Luftkarte auf der Hand zeigt -1")
await shot("23b_vier_feuer")
# Verbessern über das Menü
var wolf: Game.Creature = g.human.board[0]
scr.open_menu(wolf, Rect2(scr.own_views[0].slot, scr.own_views[0].size))
var mana: int = g.human.energy
await click(find_button("Schild ◆ %d" % Cards.KEYWORD_COST["Schild"], scr.menu_layer))
check(wolf.shield and "Schild" in wolf.card.keywords and g.human.energy == mana - Cards.KEYWORD_COST["Schild"], "Schild gekauft")
# 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")
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen") check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
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")
await right_click() await right_click()
# Verschmelzen # Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
await click(scr.b_fuse)
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
await click(scr.own_views[1])
check(scr.fuse_first != null, "erstes Wesen gewählt")
await shot("24_verschmelzen")
mana = g.human.energy mana = g.human.energy
await click(scr.own_views[2]) var before: int = g.human.board.size()
check(g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt") await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
check(not scr.fusing, "Modus beendet") check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (Wellenreiter war Ultra Rare)")
await shot("24_verschmelzen")
# Verschmelzen auf der Hand per Ziehen
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
scr.refresh()
await frames(1)
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Kobolde auf der Hand zu Inferno 3/2")
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
check(find_button("Verdeckt legen ◆ 2", scr.menu_layer) != null, "Gamepad-Menü: Verdeckt legen")
await click(find_button("Verschmelzen mit … ◆ 2", scr.menu_layer))
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
await click(scr.hand_views[1])
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
await give_up() await give_up()
@@ -701,7 +722,6 @@ func test_netzwerk() -> void:
var b = screen() var b = screen()
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host") check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
b.ai_delay = 0.0 b.ai_delay = 0.0
await pick_all(b)
var rounds := 0 var rounds := 0
while b.game != null and b.game.winner == "" and rounds < 2: while b.game != null and b.game.winner == "" and rounds < 2:
if b.game.current() == b.perspective: if b.game.current() == b.perspective:
@@ -807,7 +827,6 @@ func test_host_wechsel() -> void:
await frames(1) await frames(1)
w += 1 w += 1
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand") check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
await pick_all(b)
main.net.rejoin_seconds = 3.0 main.net.rejoin_seconds = 3.0
_bot_quit(true) _bot_quit(true)
w = 0 w = 0
@@ -881,7 +900,6 @@ func test_kampagne() -> void:
await click(find_button("Kämpfen")) await click(find_button("Kämpfen"))
var battle = screen() var battle = screen()
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet") check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
await pick_all(battle)
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner") check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv") check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
await shot("16_kampagne_kampf") await shot("16_kampagne_kampf")
@@ -897,7 +915,6 @@ func test_kampagne() -> void:
await shot("27_kampagne_kapitel2") await shot("27_kampagne_kapitel2")
await click(find_button("Kämpfen")) await click(find_button("Kämpfen"))
var b2 = screen() var b2 = screen()
await pick_all(b2)
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 22, "Kapitel-2-Boss mit zwei Merkmalen und 22 Leben") check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 22, "Kapitel-2-Boss mit zwei Merkmalen und 22 Leben")
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf") check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
await give_up() await give_up()
@@ -951,16 +968,17 @@ func test_kampf_abbrechen() -> void:
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit # Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
g.human.energy = 10 g.human.energy = 10
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000) var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
wolf.ready = true
g.human.board.append(wolf) g.human.board.append(wolf)
g.human.hand.append(Cards.by_id("w_reiter")) g.human.hand.append(Cards.by_id("w_reiter"))
g.human.looks["w_reiter"] = Cards.RARITIES.find("episch") g.human.looks["w_reiter"] = Cards.RARITIES.find("Super Rare")
scr.refresh() scr.refresh()
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0] var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe") check(reiter_view.rarity == "Super Rare", "Handkarte trägt die eigene Stufe")
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
await hover(reiter_view) await hover(reiter_view)
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView) var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls") check(previews.size() == 1 and previews[0].rarity == "Super Rare", "Vorschau ebenfalls")
await shot("21_kampf_vorschau") await shot("21_kampf_vorschau")
if not scr.own_views.is_empty(): if not scr.own_views.is_empty():
await click(scr.own_views[0]) await click(scr.own_views[0])

View File

@@ -65,13 +65,12 @@ Große Schrift,Large text
Abbrechen,Cancel Abbrechen,Cancel
"Ja, zurücksetzen","Yes, reset" "Ja, zurücksetzen","Yes, reset"
Flink,Swift Flink,Swift
Schild,Shield
Doppelschlag,Double Strike Doppelschlag,Double Strike
gewöhnlich,common Common,Common
selten,rare Rare,Rare
episch,epic Super Rare,Super Rare
legendär,legendary Ultra Rare,Ultra Rare
mythisch,mythic Ultimate Rare,Ultimate Rare
Feuer,Fire Feuer,Fire
Wasser,Water Wasser,Water
Erde,Earth Erde,Earth
@@ -84,9 +83,6 @@ Zuschauer,Spectator
Starten,Start Starten,Start
Sammlung,Collection Sammlung,Collection
Verschmelzen,Fuse Verschmelzen,Fuse
Zwei eigene Wesen wählen,Pick two of your creatures
Zweites Wesen wählen,Pick the second creature
Zwei verschiedene Wesen wählen,Pick two different creatures
Nur eigene Wesen,Only your own creatures Nur eigene Wesen,Only your own creatures
Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d
%s ist jetzt %s,%s is now %s %s ist jetzt %s,%s is now %s
@@ -99,7 +95,7 @@ Keine Karte gewählt,No card selected
Zerlegen (+%d Staub),Dismantle (+%d dust) Zerlegen (+%d Staub),Dismantle (+%d dust)
Nicht genug Staub (%d nötig),Not enough dust (%d needed) Nicht genug Staub (%d nötig),Not enough dust (%d needed)
Schon auf der höchsten Stufe,Already at the top tier Schon auf der höchsten Stufe,Already at the top tier
Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled Common-Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
Aufgewertet,Upgraded Aufgewertet,Upgraded
+%d Staub,+%d dust +%d Staub,+%d dust
Dein Zug,Your turn Dein Zug,Your turn
@@ -109,19 +105,13 @@ Profil wirklich zurücksetzen?,Really reset your profile?
%d Leben,%d life %d Leben,%d life
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win
Start gegen die KI,Start against the AI Start gegen die KI,Start against the AI
Wähle %d von %d,Pick %d of %d
Ziehen,Draw Ziehen,Draw
Erst wählen,Pick first
Deine Hand ist voll,Your hand is full Deine Hand ist voll,Your hand is full
Keine Wahl offen,Nothing to pick
Keine solche Karte,No such card
Vorbei,Over
Verdeckt legen ◆ %d,Set face-down ◆ %d 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/+%d,Element bonus +%d/+%d
Elementbonus %d/%d,Element bonus %d/%d
Merkmal +%d,Perk +%d Merkmal +%d,Perk +%d
nächster Zug ◆ %d,next turn ◆ %d nächster Zug ◆ %d,next turn ◆ %d
Erst die Wesen,Creatures first Erst die Wesen,Creatures first
@@ -133,7 +123,6 @@ Wesen kommen mit +1 Angriff,Creatures enter with +1 attack
Hat im ersten Zug 2 Mana mehr,Has 2 extra mana on the first turn Hat im ersten Zug 2 Mana mehr,Has 2 extra mana on the first turn
Elementbonus zählt doppelt,Element bonus counts twice Elementbonus zählt doppelt,Element bonus counts twice
Beginnt mit 5 Leben mehr,Starts with 5 extra life Beginnt mit 5 Leben mehr,Starts with 5 extra life
Blockt den ersten Schaden,Blocks the first damage
Greift zweimal je Zug,Attacks twice per turn Greift zweimal je Zug,Attacks twice per turn
Verdeckt,Face-down Verdeckt,Face-down
Inferno,Inferno Inferno,Inferno
@@ -149,6 +138,19 @@ Sand,Sand
Feuerwesen haben +1 Angriff,Fire creatures have +1 attack Feuerwesen haben +1 Angriff,Fire creatures have +1 attack
Wesen kommen mit +1 Leben,Creatures enter with +1 health Wesen kommen mit +1 Leben,Creatures enter with +1 health
Bekommt jeden Zug 1 Mana mehr,Gets 1 extra mana each turn Bekommt jeden Zug 1 Mana mehr,Gets 1 extra mana each turn
Wesen kommen mit Schild,Creatures enter with a shield
Ziehen kostet kein Mana,Drawing costs no mana Ziehen kostet kein Mana,Drawing costs no mana
Eigenschaften kosten 1 weniger,Traits cost 1 less Eigenschaften kosten 1 weniger,Traits cost 1 less
Spott,Taunt
Regeneration,Regeneration
Muss zuerst angegriffen werden,Must be attacked first
Greift sofort an,Attacks right away
Heilt sich am Zugende ganz,Heals fully at the end of the turn
Wesen greifen sofort an,Creatures attack right away
Verschmelzen mit … ◆ %d,Fuse with … ◆ %d
Ziel wählen,Pick the partner
Erst die Wesen mit Spott,Taunting creatures first
%s kann erst im nächsten Zug aufgedeckt werden,%s can be revealed next turn
%s greift erst im nächsten Zug an,%s attacks next turn
greift erst im nächsten Zug an,attacks next turn
%s durch das Element,%s from its element
Zwei verschiedene Karten wählen,Pick two different cards
1 keys en
65 Abbrechen Cancel
66 Ja, zurücksetzen Yes, reset
67 Flink Swift
Schild Shield
68 Doppelschlag Double Strike
69 gewöhnlich Common common Common
70 selten Rare rare Rare
71 episch Super Rare epic Super Rare
72 legendär Ultra Rare legendary Ultra Rare
73 mythisch Ultimate Rare mythic Ultimate Rare
74 Feuer Fire
75 Wasser Water
76 Erde Earth
83 Starten Start
84 Sammlung Collection
85 Verschmelzen Fuse
Zwei eigene Wesen wählen Pick two of your creatures
Zweites Wesen wählen Pick the second creature
Zwei verschiedene Wesen wählen Pick two different creatures
86 Nur eigene Wesen Only your own creatures
87 Aufgewertet: %d von %d Karten · Kampagne: %d/%d Upgraded: %d of %d cards · Campaign: %d/%d
88 %s ist jetzt %s %s is now %s
95 Zerlegen (+%d Staub) Dismantle (+%d dust)
96 Nicht genug Staub (%d nötig) Not enough dust (%d needed)
97 Schon auf der höchsten Stufe Already at the top tier
98 Gewöhnliche Karten lassen sich nicht zerlegen Common-Karten lassen sich nicht zerlegen Common cards cannot be dismantled
99 Aufgewertet Upgraded
100 +%d Staub +%d dust
101 Dein Zug Your turn
105 %d Leben %d life
106 Gegner mit %d Leben · %d Münzen bei Sieg Opponent with %d life · %d coins for a win
107 Start gegen die KI Start against the AI
Wähle %d von %d Pick %d of %d
108 Ziehen Draw
Erst wählen Pick first
109 Deine Hand ist voll Your hand is full
Keine Wahl offen Nothing to pick
Keine solche Karte No such card
Vorbei Over
110 Verdeckt legen ◆ %d Set face-down ◆ %d
111 Aufdecken Reveal
112 verdeckt face-down
113 Aufgedeckt! Revealed!
114 Elementbonus +%d/+%d Element bonus +%d/+%d
Elementbonus %d/%d Element bonus %d/%d
115 Merkmal +%d Perk +%d
116 nächster Zug ◆ %d next turn ◆ %d
117 Erst die Wesen Creatures first
123 Hat im ersten Zug 2 Mana mehr Has 2 extra mana on the first turn
124 Elementbonus zählt doppelt Element bonus counts twice
125 Beginnt mit 5 Leben mehr Starts with 5 extra life
Blockt den ersten Schaden Blocks the first damage
126 Greift zweimal je Zug Attacks twice per turn
127 Verdeckt Face-down
128 Inferno Inferno
138 Feuerwesen haben +1 Angriff Fire creatures have +1 attack
139 Wesen kommen mit +1 Leben Creatures enter with +1 health
140 Bekommt jeden Zug 1 Mana mehr Gets 1 extra mana each turn
Wesen kommen mit Schild Creatures enter with a shield
141 Ziehen kostet kein Mana Drawing costs no mana
142 Eigenschaften kosten 1 weniger Traits cost 1 less
143 Spott Taunt
144 Regeneration Regeneration
145 Muss zuerst angegriffen werden Must be attacked first
146 Greift sofort an Attacks right away
147 Heilt sich am Zugende ganz Heals fully at the end of the turn
148 Wesen greifen sofort an Creatures attack right away
149 Verschmelzen mit … ◆ %d Fuse with … ◆ %d
150 Ziel wählen Pick the partner
151 Erst die Wesen mit Spott Taunting creatures first
152 %s kann erst im nächsten Zug aufgedeckt werden %s can be revealed next turn
153 %s greift erst im nächsten Zug an %s attacks next turn
154 greift erst im nächsten Zug an attacks next turn
155 %s durch das Element %s from its element
156 Zwei verschiedene Karten wählen Pick two different cards

Binary file not shown.