update
This commit is contained in:
16
README.md
16
README.md
@@ -23,15 +23,15 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
|
- 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
|
||||||
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana
|
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana
|
||||||
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
|
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
|
||||||
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
|
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar. Auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach. Das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana; eine Handkarte abwerfen bringt 1 Mana (Rechtsklick)
|
||||||
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
||||||
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementboni; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
||||||
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
||||||
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
|
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
|
||||||
- Elementboni: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); drei offene Wesen eines Elements auf dem Tisch geben dem Element +1/+1 (fällt der Bonus weg, sinkt nur das Höchstleben). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni
|
- Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je 10 Mana an ausgespielten Karten eines Elements geben den eigenen Wesen dieses Elements +1 Angriff (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Zwei Leisten in der Mitte, oben die des Gegners, unten die eigene, zeigen je Element zwei Symbole, gefüllt je offenem eigenen Wesen, und das Mana der ausgespielten Karten
|
||||||
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
|
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; so greift und kontert auch ein Wesen mit 0 Angriff, nur nicht ohne Vorteil; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
|
||||||
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar. Nichts schlägt durch
|
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch
|
||||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
|
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
|
||||||
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
||||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||||
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
||||||
@@ -39,13 +39,13 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
|
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
|
||||||
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
||||||
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
|
||||||
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen
|
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen; Ton und Musik sind standardmäßig aus
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
|
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
|
||||||
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
|
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
|
||||||
`DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit).
|
Beide Suiten brechen beim ersten Fehler ab. Der Fenster-Test spielt eine kurze Partie (einfache KI, Gegner mit 1 Leben) und läuft in wenigen Sekunden.
|
||||||
|
|
||||||
```
|
```
|
||||||
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren
|
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
|
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
|
||||||
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
|
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
|
||||||
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
|
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
|
||||||
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was man sich in diesem Zug leisten kann
|
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; auf dem Stapel liegen drei Karten offen, nie teurer als das Mana des Zugs, beim Ziehen wählt man eine, eine neue rückt nach; das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana
|
||||||
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), und Verschmelzen; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
|
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), und Verschmelzen, Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
|
||||||
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
|
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
|
||||||
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
||||||
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
|
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
- Jede Karte gehört zu genau einem Element
|
- Jede Karte gehört zu genau einem Element
|
||||||
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
||||||
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
|
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
|
||||||
- Drei offene Wesen eines Elements auf dem Tisch: +1/+1 für das Element, kein Malus; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
- Je 10 Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements, jeder Spieler zählt für sich; verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
||||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
||||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
||||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||||
|
|||||||
17
design.md
17
design.md
@@ -6,7 +6,7 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
|||||||
|
|
||||||
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
|
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
|
||||||
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
||||||
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, Verlauf links, Zug beenden rechts, Wesen
|
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, links Kästen und Stapel, rechts oben Knöpfe, Wesen
|
||||||
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
||||||
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
|
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
|
||||||
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
|
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
|
||||||
@@ -39,24 +39,27 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
|||||||
|
|
||||||
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||||
|
|
||||||
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 2“ (Klick zieht), rechts
|
- **Hand** als bis zu fünf Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners
|
||||||
„Zug beenden“.
|
gleich groß als Rückseiten oben; der
|
||||||
|
Stapel links auf halber Höhe als drei offene Karten übereinander (Klick zieht die Karte, das erste
|
||||||
|
Ziehen im Zug frei), darüber der Gegnerkasten, darunter der eigene.
|
||||||
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
||||||
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
||||||
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
|
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
|
||||||
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
|
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
|
||||||
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
|
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
|
||||||
- **Elementleiste** mit drei Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1.
|
- **Elementleiste** je Spieler, die des Gegners darüber.
|
||||||
Daneben das Symbol des Sondereffekts, sobald zwei eigene Wesen des Elements liegen.
|
Je Element zwei Symbole in seiner Form, gefüllt je offenem eigenen Wesen (beide voll: Sondereffekt),
|
||||||
|
daneben das Mana der ausgespielten Karten; je 10 gibt es +1 Angriff.
|
||||||
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
|
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
|
||||||
oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
|
oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
|
||||||
Doppelschlag kaufen; per Y zusätzlich verschmelzen.
|
Doppelschlag kaufen; per Y zusätzlich verschmelzen.
|
||||||
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie
|
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie
|
||||||
Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
|
Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
|
||||||
macht Ziele, Texte und Balance eindeutig.
|
macht Ziele, Texte und Balance eindeutig.
|
||||||
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
|
- **Vorschau** steht fest rechts auf halber Höhe, darunter die Zusatzzeile, mit Resonanz, Feld
|
||||||
und Feldkombo darunter.
|
und Feldkombo darunter.
|
||||||
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
|
- **Verlauf**, **Aufgeben** und **Zug beenden** als Knöpfe oben rechts; der Verlauf öffnet als Überlagerung.
|
||||||
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||||
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
best = firsts[i].action
|
best = firsts[i].action
|
||||||
if best.type == "end":
|
if best.type == "end":
|
||||||
if game.can_draw(p)[0]:
|
if game.can_draw(p)[0]:
|
||||||
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
return {"type": "draw", "index": pick_index(game, p)} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||||
return best
|
return best
|
||||||
return map_action(game, view, best)
|
return map_action(game, view, best)
|
||||||
|
|
||||||
@@ -258,10 +258,24 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
if t != null:
|
if t != null:
|
||||||
return {"type": "attack", "creature": cr, "target": t}
|
return {"type": "attack", "creature": cr, "target": t}
|
||||||
if game.can_draw(p)[0]:
|
if game.can_draw(p)[0]:
|
||||||
return {"type": "draw"}
|
return {"type": "draw", "index": pick_index(game, p)}
|
||||||
return {"type": "end"}
|
return {"type": "end"}
|
||||||
|
|
||||||
|
|
||||||
|
## Welche der offenen Karten: die wertvollste, die nach dem Ziehen noch bezahlbar wäre, sonst die billigste.
|
||||||
|
static func pick_index(game: Game, p: Game.Player) -> int:
|
||||||
|
var left: int = p.energy - game.draw_cost(p)
|
||||||
|
var best := 0
|
||||||
|
var best_score := -INF
|
||||||
|
for i in p.offer.size():
|
||||||
|
var c: Cards.Card = p.offer[i]
|
||||||
|
var score := float(c.attack + c.health) if c.cost <= left else -10.0 - c.cost
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best = i
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
||||||
## der Held nur, wenn kein Wesen mehr steht.
|
## der Held nur, wenn kein Wesen mehr steht.
|
||||||
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
||||||
@@ -288,7 +302,7 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
|
|||||||
"play":
|
"play":
|
||||||
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||||
"draw":
|
"draw":
|
||||||
game.draw(p)
|
game.draw(p, int(action.get("index", 0)))
|
||||||
"reveal":
|
"reveal":
|
||||||
game.reveal(p, action.creature)
|
game.reveal(p, action.creature)
|
||||||
"attack":
|
"attack":
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ static func cost_of(attack: int, health: int) -> int:
|
|||||||
return (2 * attack + health) / 3
|
return (2 * attack + health) / 3
|
||||||
|
|
||||||
|
|
||||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren,
|
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||||
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
||||||
## als das stärkere Wesen, und jeder Partner ab 1 bringt mindestens +1.
|
## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
|
||||||
static func fusion_value(a: int, b: int) -> int:
|
static func fusion_value(a: int, b: int) -> int:
|
||||||
return maxi(a, b) + ceili(mini(a, b) / 3.0)
|
return maxi(a, b) + maxi(1, ceili(mini(a, b) / 3.0))
|
||||||
|
|
||||||
|
|
||||||
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
||||||
|
|||||||
126
scripts/game.gd
126
scripts/game.gd
@@ -10,7 +10,9 @@ const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Re
|
|||||||
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
|
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
|
||||||
const START_LIFE := 30
|
const START_LIFE := 30
|
||||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||||
const ELEMENT_STACK := 3 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1
|
const ELEMENT_MANA := 10 # je so viel Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements
|
||||||
|
const OFFER := 3 # offene Karten auf dem Stapel, höchstens so teuer wie das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach
|
||||||
|
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||||||
|
|
||||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||||
const PERKS := {
|
const PERKS := {
|
||||||
@@ -105,8 +107,9 @@ class Creature extends RefCounted:
|
|||||||
return maxi(0, attacks_per_turn() - attacks_done)
|
return maxi(0, attacks_per_turn() - attacks_done)
|
||||||
|
|
||||||
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
|
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
|
||||||
|
## Darf dieses Wesen angreifen? Ob es Schaden macht, hängt vom Ziel ab (Elementvorteil, Game.can_attack).
|
||||||
func can_attack() -> bool:
|
func can_attack() -> bool:
|
||||||
return not hidden and attacks_left() > 0 and power() > 0 and (ready or has("Flink"))
|
return not hidden and attacks_left() > 0 and (ready or has("Flink"))
|
||||||
|
|
||||||
func clone(new_owner) -> Creature:
|
func clone(new_owner) -> Creature:
|
||||||
var c := Creature.new(card, new_owner, uid)
|
var c := Creature.new(card, new_owner, uid)
|
||||||
@@ -134,6 +137,9 @@ class Player extends RefCounted:
|
|||||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||||
var turns := 0 # begonnene eigene Züge
|
var turns := 0 # begonnene eigene Züge
|
||||||
|
var played := {} # Element -> Mana der bisher ausgespielten Karten (je ELEMENT_MANA: +1 Angriff)
|
||||||
|
var offer: Array = [] # die OFFER offenen Karten auf dem Stapel (nur der Besitzer sieht sie)
|
||||||
|
var drew := false # in diesem Zug schon gezogen? Das erste Ziehen ist frei
|
||||||
|
|
||||||
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||||||
name = n
|
name = n
|
||||||
@@ -141,6 +147,8 @@ class Player extends RefCounted:
|
|||||||
is_human = h
|
is_human = h
|
||||||
life = l
|
life = l
|
||||||
max_life = l
|
max_life = l
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
played[e] = 0
|
||||||
|
|
||||||
func alive() -> bool:
|
func alive() -> bool:
|
||||||
return life > 0
|
return life > 0
|
||||||
@@ -163,6 +171,9 @@ class Player extends RefCounted:
|
|||||||
p.perks = perks.duplicate()
|
p.perks = perks.duplicate()
|
||||||
p.looks = looks
|
p.looks = looks
|
||||||
p.turns = turns
|
p.turns = turns
|
||||||
|
p.played = played.duplicate()
|
||||||
|
p.offer = offer.duplicate()
|
||||||
|
p.drew = drew
|
||||||
for c in board:
|
for c in board:
|
||||||
p.board.append(c.clone(p))
|
p.board.append(c.clone(p))
|
||||||
return p
|
return p
|
||||||
@@ -276,6 +287,7 @@ func start(fixed_order: bool = false) -> void:
|
|||||||
p.order = i
|
p.order = i
|
||||||
for c in draw_cards(START_HAND, START_MANA):
|
for c in draw_cards(START_HAND, START_MANA):
|
||||||
p.hand.append(c.copy())
|
p.hand.append(c.copy())
|
||||||
|
p.offer = draw_cards(OFFER, START_MANA)
|
||||||
turn = 1
|
turn = 1
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||||
@@ -285,9 +297,9 @@ func start(fixed_order: bool = false) -> void:
|
|||||||
begin_turn(current())
|
begin_turn(current())
|
||||||
|
|
||||||
|
|
||||||
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten
|
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten; zu teure wandern unter den Stapel.
|
||||||
## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer.
|
## Ist der Stapel leer, kommt ein frischer.
|
||||||
func draw_cards(n: int, max_cost: int) -> Array:
|
func draw_cards(n: int, max_cost: int = 999) -> Array:
|
||||||
var out := []
|
var out := []
|
||||||
var skipped := []
|
var skipped := []
|
||||||
var looked := 0
|
var looked := 0
|
||||||
@@ -306,7 +318,8 @@ func draw_cards(n: int, max_cost: int) -> Array:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Ziehen: kostet DRAW_COST, eine Karte kommt auf die Hand.
|
## Ziehen: eine der offenen Karten kommt auf die Hand, eine neue rückt nach. Das erste Ziehen
|
||||||
|
## im Zug ist frei, jedes weitere kostet DRAW_COST.
|
||||||
func can_draw(p: Player) -> Array:
|
func can_draw(p: Player) -> Array:
|
||||||
if winner != "" or p != current():
|
if winner != "" or p != current():
|
||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
@@ -318,16 +331,20 @@ func can_draw(p: Player) -> Array:
|
|||||||
|
|
||||||
|
|
||||||
func draw_cost(p: Player) -> int:
|
func draw_cost(p: Player) -> int:
|
||||||
return 0 if "ziehen_gratis" in p.perks else DRAW_COST
|
return 0 if not p.drew or "ziehen_gratis" in p.perks else DRAW_COST
|
||||||
|
|
||||||
|
|
||||||
func draw(p: Player) -> String:
|
func draw(p: Player, index: int = 0) -> String:
|
||||||
var ok := can_draw(p)
|
var ok := can_draw(p)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
return ok[1]
|
return ok[1]
|
||||||
|
if index < 0 or index >= p.offer.size():
|
||||||
|
return "Keine Karte gewählt"
|
||||||
p.energy -= draw_cost(p)
|
p.energy -= draw_cost(p)
|
||||||
var card: Cards.Card = draw_cards(1, p.max_energy)[0]
|
p.drew = true
|
||||||
|
var card: Cards.Card = p.offer[index]
|
||||||
p.hand.append(card.copy())
|
p.hand.append(card.copy())
|
||||||
|
p.offer[index] = draw_cards(1, maxi(p.max_energy, START_MANA))[0] # nie teurer als das Mana dieses Zugs
|
||||||
say("%s zieht." % p.name)
|
say("%s zieht." % p.name)
|
||||||
emit({"type": "draw", "player": players.find(p)})
|
emit({"type": "draw", "player": players.find(p)})
|
||||||
return ""
|
return ""
|
||||||
@@ -342,15 +359,9 @@ func begin_turn(p: Player) -> void:
|
|||||||
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
||||||
c.attacks_done = 0
|
c.attacks_done = 0
|
||||||
p.turns += 1
|
p.turns += 1
|
||||||
|
p.drew = false # das erste Ziehen im Zug ist wieder frei
|
||||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||||
emit({"type": "turn", "player": players.find(p)})
|
emit({"type": "turn", "player": players.find(p)})
|
||||||
# Jeder Zug beginnt mit einer Karte vom Stapel, die man sich in diesem Zug leisten kann.
|
|
||||||
if p.hand.size() < MAX_HAND:
|
|
||||||
p.hand.append(draw_cards(1, p.max_energy)[0].copy())
|
|
||||||
say("%s zieht eine Karte." % p.name)
|
|
||||||
emit({"type": "draw", "player": players.find(p)})
|
|
||||||
else:
|
|
||||||
say("%s hat die Hand voll und zieht nicht." % p.name)
|
|
||||||
|
|
||||||
|
|
||||||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||||||
@@ -405,6 +416,8 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
|||||||
cr.health += 1
|
cr.health += 1
|
||||||
cr.max_health += 1
|
cr.max_health += 1
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
|
for e in card.elements:
|
||||||
|
p.played[e] = int(p.played.get(e, 0)) + card.cost
|
||||||
if face_down:
|
if face_down:
|
||||||
say("%s legt ein Wesen verdeckt." % p.name)
|
say("%s legt ein Wesen verdeckt." % p.name)
|
||||||
else:
|
else:
|
||||||
@@ -416,6 +429,26 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Abwerfen: eine Handkarte loswerden und dafür DISCARD_GAIN Mana bekommen.
|
||||||
|
func can_discard(p: Player, card: Cards.Card) -> Array:
|
||||||
|
if winner != "" or p != current():
|
||||||
|
return [false, "Nicht dein Zug"]
|
||||||
|
if not (card in p.hand):
|
||||||
|
return [false, "Nicht auf der Hand"]
|
||||||
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
|
func discard(p: Player, card: Cards.Card) -> String:
|
||||||
|
var ok := can_discard(p, card)
|
||||||
|
if not ok[0]:
|
||||||
|
return ok[1]
|
||||||
|
p.hand.erase(card)
|
||||||
|
p.energy += DISCARD_GAIN
|
||||||
|
say("%s wirft %s ab." % [p.name, card.name])
|
||||||
|
emit({"type": "discard", "player": players.find(p)})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
|
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
|
||||||
## nach dem Legen. Aufgedeckt greift es sofort an.
|
## nach dem Legen. Aufgedeckt greift es sofort an.
|
||||||
func can_reveal(p: Player, cr: Creature) -> Array:
|
func can_reveal(p: Player, cr: Creature) -> Array:
|
||||||
@@ -506,11 +539,11 @@ func fuse(p: Player, a, b) -> String:
|
|||||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||||
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
Cards.fusion_value(a.max_health, b.max_health))
|
||||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||||
var cr := Creature.new(card, p, next_uid)
|
var cr := Creature.new(card, p, next_uid)
|
||||||
next_uid += 1
|
next_uid += 1
|
||||||
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
cr.health = mini(card.health, Cards.fusion_value(a.health, b.health))
|
||||||
cr.ready = a.ready and b.ready
|
cr.ready = a.ready and b.ready
|
||||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||||
var at := p.board.find(a)
|
var at := p.board.find(a)
|
||||||
@@ -527,59 +560,38 @@ func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
|||||||
|
|
||||||
# --- Elementboni ---------------------------------------------------------------
|
# --- Elementboni ---------------------------------------------------------------
|
||||||
|
|
||||||
## Offene Wesen je Element auf dem ganzen Tisch.
|
## +1 Angriff je volle ELEMENT_MANA an ausgespielten Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
||||||
func element_counts() -> Dictionary:
|
static func aura_of(elements: Array, played: Dictionary) -> int:
|
||||||
var counts := {}
|
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
counts[e] = 0
|
|
||||||
for p in players:
|
|
||||||
for c in p.board:
|
|
||||||
if not c.hidden:
|
|
||||||
for e in c.card.elements:
|
|
||||||
counts[e] += 1
|
|
||||||
return counts
|
|
||||||
|
|
||||||
|
|
||||||
## +1 je Element mit ELEMENT_STACK offenen Wesen auf dem Tisch; mehrere Elemente zählen alle.
|
|
||||||
static func aura_of(elements: Array, counts: Dictionary) -> int:
|
|
||||||
var a := 0
|
var a := 0
|
||||||
for e in elements:
|
for e in elements:
|
||||||
if int(counts[e]) >= ELEMENT_STACK:
|
a += int(played.get(e, 0)) / ELEMENT_MANA
|
||||||
a += 1
|
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
## Bonus eines offenen Wesens; verdeckte zählen nicht und bekommen nichts.
|
## Bonus eines offenen Wesens aus den ausgespielten Karten seines Besitzers; verdeckte bekommen nichts.
|
||||||
func aura_for(c: Creature, counts: Dictionary) -> int:
|
func aura_for(c: Creature) -> int:
|
||||||
if c.hidden:
|
|
||||||
return 0
|
|
||||||
var a := aura_of(c.card.elements, counts)
|
|
||||||
var o = c.owner()
|
var o = c.owner()
|
||||||
if o != null and "element_doppelt" in o.perks:
|
if c.hidden or o == null:
|
||||||
|
return 0
|
||||||
|
var a := aura_of(c.card.elements, o.played)
|
||||||
|
if "element_doppelt" in o.perks:
|
||||||
a *= 2
|
a *= 2
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er,
|
## Rechnet die Elementboni neu; sie wirken nur auf den Angriff (Creature.power).
|
||||||
## sinkt nur das Höchstleben, das Leben wird höchstens darauf gekappt.
|
|
||||||
func apply_auras() -> void:
|
func apply_auras() -> void:
|
||||||
var counts := element_counts()
|
|
||||||
for p in players:
|
for p in players:
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
var a := aura_for(c, counts)
|
c.aura = aura_for(c)
|
||||||
if a != c.aura:
|
|
||||||
var delta: int = a - c.aura
|
|
||||||
c.max_health += delta
|
|
||||||
c.health = mini(c.health + maxi(delta, 0), c.max_health)
|
|
||||||
c.aura = a
|
|
||||||
|
|
||||||
|
|
||||||
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme.
|
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
|
||||||
func forecast(p: Player, card: Cards.Card) -> int:
|
func forecast(p: Player, card: Cards.Card) -> int:
|
||||||
var counts := element_counts()
|
var played := p.played.duplicate()
|
||||||
for e in card.elements:
|
for e in card.elements:
|
||||||
counts[e] += 1
|
played[e] = int(played.get(e, 0)) + card.cost
|
||||||
return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1)
|
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
||||||
|
|
||||||
|
|
||||||
# --- Kampf -------------------------------------------------------------------
|
# --- Kampf -------------------------------------------------------------------
|
||||||
@@ -591,13 +603,15 @@ func can_attack(cr: Creature, target) -> Array:
|
|||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
if cr.hidden:
|
if cr.hidden:
|
||||||
return [false, "%s liegt verdeckt" % cr.name]
|
return [false, "%s liegt verdeckt" % cr.name]
|
||||||
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0 and cr.power() > 0:
|
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0:
|
||||||
return [false, "%s greift erst im nächsten Zug an" % cr.name]
|
return [false, "%s greift erst im nächsten Zug an" % cr.name]
|
||||||
if not cr.can_attack():
|
if not cr.can_attack():
|
||||||
return [false, "%s kann nicht angreifen" % cr.name]
|
return [false, "%s kann nicht angreifen" % cr.name]
|
||||||
var tgt_owner = target.owner() if target is Creature else target
|
var tgt_owner = target.owner() if target is Creature else target
|
||||||
if not (tgt_owner in enemies_of(cr.owner())):
|
if not (tgt_owner in enemies_of(cr.owner())):
|
||||||
return [false, "Kein gültiges Ziel"]
|
return [false, "Kein gültiges Ziel"]
|
||||||
|
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff reicht nur mit Elementvorteil
|
||||||
|
return [false, "%s würde keinen Schaden machen" % cr.name]
|
||||||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||||
return [false, "Erst die Wesen"]
|
return [false, "Erst die Wesen"]
|
||||||
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
|
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
|
||||||
@@ -616,7 +630,7 @@ func attack(cr: Creature, target) -> String:
|
|||||||
say("%s greift %s an." % [cr.name, target.name])
|
say("%s greift %s an." % [cr.name, target.name])
|
||||||
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
||||||
damage(target, cr.power() + matchup_bonus(cr, target))
|
damage(target, cr.power() + matchup_bonus(cr, target))
|
||||||
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht
|
if target is Creature: # Konter mit Elementvorteil; wer so nichts austeilt, kontert nicht (damage ignoriert 0)
|
||||||
damage(cr, target.power() + matchup_bonus(target, cr))
|
damage(cr, target.power() + matchup_bonus(target, cr))
|
||||||
cleanup()
|
cleanup()
|
||||||
check_end()
|
check_end()
|
||||||
|
|||||||
@@ -398,13 +398,17 @@ func _apply(id: int, action: Dictionary) -> void:
|
|||||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||||
if cr != null:
|
if cr != null:
|
||||||
game.reveal(p, cr)
|
game.reveal(p, cr)
|
||||||
|
"discard":
|
||||||
|
var hi := int(action.get("hand", -1))
|
||||||
|
if hi >= 0 and hi < p.hand.size():
|
||||||
|
game.discard(p, p.hand[hi])
|
||||||
"attack":
|
"attack":
|
||||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||||
var t = resolve_ref(action.get("target"))
|
var t = resolve_ref(action.get("target"))
|
||||||
if cr != null and t != null:
|
if cr != null and t != null:
|
||||||
game.attack(cr, t)
|
game.attack(cr, t)
|
||||||
"draw":
|
"draw":
|
||||||
game.draw(p)
|
game.draw(p, int(action.get("index", 0)))
|
||||||
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
|
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
|
||||||
var a = game.find_creature(int(action.get("uid", -1)))
|
var a = game.find_creature(int(action.get("uid", -1)))
|
||||||
var b = game.find_creature(int(action.get("other", -1)))
|
var b = game.find_creature(int(action.get("other", -1)))
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
|||||||
"ready": c.ready, "hidden": c.hidden, "aura": c.aura})
|
"ready": c.ready, "hidden": c.hidden, "aura": c.aura})
|
||||||
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
||||||
"energy": p.energy, "max_energy": p.max_energy,
|
"energy": p.energy, "max_energy": p.max_energy,
|
||||||
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns,
|
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "played": p.played,
|
||||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board})
|
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board,
|
||||||
|
"offer": p.offer.map(card_ref) if mine else [], "drew": p.drew})
|
||||||
var evs := []
|
var evs := []
|
||||||
for ev in g.events:
|
for ev in g.events:
|
||||||
var e: Dictionary = ev.duplicate()
|
var e: Dictionary = ev.duplicate()
|
||||||
@@ -55,12 +56,17 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
for cid in pd.get("looks", {}):
|
for cid in pd.get("looks", {}):
|
||||||
p.looks[cid] = int(pd.looks[cid])
|
p.looks[cid] = int(pd.looks[cid])
|
||||||
p.turns = int(pd.turns)
|
p.turns = int(pd.turns)
|
||||||
|
for e in pd.get("played", {}):
|
||||||
|
p.played[e] = int(pd.played[e])
|
||||||
if pd.hand is Array:
|
if pd.hand is Array:
|
||||||
for ref in pd.hand:
|
for ref in pd.hand:
|
||||||
p.hand.append(card_of(ref))
|
p.hand.append(card_of(ref))
|
||||||
else:
|
else:
|
||||||
for i in int(pd.hand):
|
for i in int(pd.hand):
|
||||||
p.hand.append(placeholder)
|
p.hand.append(placeholder)
|
||||||
|
for ref in pd.get("offer", []):
|
||||||
|
p.offer.append(card_of(ref))
|
||||||
|
p.drew = bool(pd.get("drew", false))
|
||||||
for cd in pd.board:
|
for cd in pd.board:
|
||||||
var card := card_of(cd.card)
|
var card := card_of(cd.card)
|
||||||
var c := Game.Creature.new(card, p, int(cd.uid))
|
var c := Game.Creature.new(card, p, int(cd.uid))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var coins := 0
|
|||||||
var dust := 0
|
var dust := 0
|
||||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
||||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
||||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
if Cards.has_id(cid):
|
if Cards.has_id(cid):
|
||||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||||
var se = d.get("settings", {})
|
var se = d.get("settings", {})
|
||||||
p.settings = {"volume": float(se.get("volume", 0.8)), "music": float(se.get("music", 0.5)),
|
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||||
"big_font": bool(se.get("big_font", false))}
|
"big_font": bool(se.get("big_font", false))}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ func _next_banner() -> void:
|
|||||||
var delay := 0.0
|
var delay := 0.0
|
||||||
var band := ColorRect.new()
|
var band := ColorRect.new()
|
||||||
band.color = Color(0, 0, 0, 0.55)
|
band.color = Color(0, 0, 0, 0.55)
|
||||||
band.position = Vector2(0, 300)
|
band.position = Vector2(0, 355)
|
||||||
band.size = Vector2(1280, 90)
|
band.size = Vector2(1280, 90)
|
||||||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
band.scale.y = 0.0
|
band.scale.y = 0.0
|
||||||
@@ -218,7 +218,7 @@ func _next_banner() -> void:
|
|||||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
screen.fx_layer.add_child(l)
|
screen.fx_layer.add_child(l)
|
||||||
l.reset_size()
|
l.reset_size()
|
||||||
l.position = Vector2(640, 345) - l.size / 2
|
l.position = Vector2(640, 400) - l.size / 2
|
||||||
l.pivot_offset = l.size / 2
|
l.pivot_offset = l.size / 2
|
||||||
l.scale = Vector2(0.6, 0.6)
|
l.scale = Vector2(0.6, 0.6)
|
||||||
l.modulate.a = 0.0
|
l.modulate.a = 0.0
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
|
|||||||
func items() -> Array:
|
func items() -> Array:
|
||||||
match zone:
|
match zone:
|
||||||
"deck":
|
"deck":
|
||||||
return [screen.deck] if screen.deck.visible else []
|
return screen.offer_views
|
||||||
"hand":
|
"hand":
|
||||||
return screen.hand_views
|
return screen.hand_views
|
||||||
"own":
|
"own":
|
||||||
@@ -105,7 +105,7 @@ func confirm() -> void:
|
|||||||
var item = items[index]
|
var item = items[index]
|
||||||
match zone:
|
match zone:
|
||||||
"deck":
|
"deck":
|
||||||
screen.on_click("deck", null)
|
screen.on_click("deck", index)
|
||||||
"hand":
|
"hand":
|
||||||
screen.on_click("hand", item.card)
|
screen.on_click("hand", item.card)
|
||||||
"own":
|
"own":
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
class_name BattleLayout
|
class_name BattleLayout
|
||||||
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem
|
## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
|
||||||
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links),
|
## (Rückseiten), sein Brett, seine Elementleiste, die eigene Elementleiste (die Naht liegt auf halber
|
||||||
## Elementband, oben der Gegner mit seinem Brett.
|
## Höhe), eigenes Brett, eigene Hand. Links: Kasten des Gegners, die drei offenen Karten des
|
||||||
|
## Stapels (mittig), eigener Kasten. Rechts oben die Knöpfe, rechts mittig die Vorschau, darunter Meldungen.
|
||||||
|
|
||||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
const B_GIVEUP := Rect2(1070, 10, 96, 32)
|
||||||
const B_LOG := Rect2(112, 10, 96, 32)
|
const B_LOG := Rect2(1172, 10, 96, 32)
|
||||||
const OPP_BOX := Rect2(330, 10, 620, 56)
|
const B_END := Rect2(1070, 48, 198, 32)
|
||||||
const FIELD := Rect2(240, 226, 800, 32)
|
const OPP_BOX := Rect2(20, 11, 210, 96)
|
||||||
const MSG := Rect2(240, 262, 800, 22)
|
const OPP_HAND_Y := 11
|
||||||
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
const FIELD_OPP := Rect2(240, 364, 800, 32)
|
||||||
const STATUS := Rect2(20, 288, 210, 160)
|
const FIELD := Rect2(240, 404, 800, 32)
|
||||||
const B_UNDO := Rect2(1050, 298, 210, 40)
|
const MSG := Rect2(1040, 630, 220, 60)
|
||||||
|
const STATUS := Rect2(20, 694, 210, 96)
|
||||||
|
const B_UNDO := Rect2(1070, 86, 198, 32)
|
||||||
const OWN_X := 345
|
const OWN_X := 345
|
||||||
const HAND_W := 150
|
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||||
const HAND_H := 205
|
const HAND_H := 140
|
||||||
const HAND_GAP := 10
|
const HAND_GAP := 10
|
||||||
const HAND_Y := 532
|
const HAND_Y := 610
|
||||||
const DECK := Rect2(50, 540, 130, 182)
|
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
|
||||||
const B_END := Rect2(1060, 600, 200, 60)
|
const OFFER_X := 70 # die drei offenen Karten des Stapels, links übereinander um die Mitte
|
||||||
|
const OFFER_Y := 180
|
||||||
|
const OFFER_GAP := 10
|
||||||
|
const DECK_LABEL := Rect2(20, 632, 210, 22)
|
||||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||||
const PREVIEW_SIZE := Vector2(180, 252)
|
const PREVIEW_SIZE := Vector2(180, 252)
|
||||||
const CENTER_X := 240
|
const CENTER_X := 240
|
||||||
@@ -25,38 +31,41 @@ const CENTER_W := 800
|
|||||||
|
|
||||||
|
|
||||||
static func own_slot(j: int) -> Rect2:
|
static func own_slot(j: int) -> Rect2:
|
||||||
return Rect2(OWN_X + j * 120, 298, 110, 140)
|
return Rect2(OWN_X + j * 120, 440, 110, 140)
|
||||||
|
|
||||||
|
|
||||||
## Brett des Gegners unter seinem Kasten.
|
## Brett des Gegners über seiner Elementleiste.
|
||||||
static func opp_slot(j: int) -> Rect2:
|
static func opp_slot(j: int) -> Rect2:
|
||||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
return Rect2(OWN_X + j * 120, 220, 110, 140)
|
||||||
|
|
||||||
|
|
||||||
## Hand: bis zu fünf große Karten unten, mittig.
|
## Hand: bis zu fünf Karten unten, mittig; den Bogen legt hand_arc dazu.
|
||||||
static func hand_rect(count: int, i: int) -> Rect2:
|
static func hand_rect(count: int, i: int) -> Rect2:
|
||||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||||
var x := CENTER_X + (CENTER_W - total) / 2
|
var x := CENTER_X + (CENTER_W - total) / 2
|
||||||
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||||
|
|
||||||
|
|
||||||
## Schwebende Vorschau neben der gezeigten Karte: über Handkarten, sonst rechts
|
## Hand des Gegners oben, gleich groß, nur die Rückseiten.
|
||||||
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
|
static func opp_hand_rect(count: int, i: int) -> Rect2:
|
||||||
static func preview_rect(anchor: Rect2) -> Rect2:
|
var r := hand_rect(count, i)
|
||||||
if anchor.position.y >= HAND_Y - 10:
|
r.position.y = OPP_HAND_Y
|
||||||
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x)
|
return r
|
||||||
return Rect2(Vector2(x, anchor.position.y - 12 - PREVIEW_SIZE.y), PREVIEW_SIZE)
|
|
||||||
var x := anchor.end.x + 12
|
|
||||||
if x + PREVIEW_SIZE.x > 1270:
|
|
||||||
x = anchor.position.x - 12 - PREVIEW_SIZE.x
|
|
||||||
var y := clampf(anchor.get_center().y - PREVIEW_SIZE.y / 2, 10, 800 - PREVIEW_SIZE.y - 10)
|
|
||||||
return Rect2(Vector2(x, y), PREVIEW_SIZE)
|
|
||||||
|
|
||||||
|
|
||||||
## Zusatzzeile (Elementbonus, Eigenschaften) neben der Vorschau, rechts davon oder links.
|
## Leichter Bogen: Karte i von count dreht sich um ihre Mitte und sinkt zum Rand hin ab
|
||||||
|
## (mirrored: für den Gegner oben spiegelverkehrt). Liefert [Grad, y-Versatz].
|
||||||
|
static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
|
||||||
|
var t := i - (count - 1) / 2.0
|
||||||
|
var sign := -1.0 if mirrored else 1.0
|
||||||
|
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
|
||||||
|
|
||||||
|
|
||||||
|
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
|
||||||
|
static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||||
|
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
|
||||||
static func info_rect(preview: Rect2) -> Rect2:
|
static func info_rect(preview: Rect2) -> Rect2:
|
||||||
var w := 230.0
|
return Rect2(1030, preview.end.y + 8, 230, 80)
|
||||||
var x := preview.end.x + 8
|
|
||||||
if x + w > 1275:
|
|
||||||
x = preview.position.x - 8 - w
|
|
||||||
return Rect2(x, preview.position.y + preview.size.y / 2 - 40, w, 80)
|
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
|||||||
var hero_layer: Control
|
var hero_layer: Control
|
||||||
var tile_layer: Control
|
var tile_layer: Control
|
||||||
var hand_layer: Control
|
var hand_layer: Control
|
||||||
var deck: Deck
|
var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
|
||||||
|
var offer_layer: Control
|
||||||
|
var deck_label: Label
|
||||||
|
var draw_enabled := false
|
||||||
var log_overlay: Control
|
var log_overlay: Control
|
||||||
var b_log: Button
|
var b_log: Button
|
||||||
var status_panel: Panel
|
var status_panel: Panel
|
||||||
@@ -70,14 +73,12 @@ var preview_holder: Control
|
|||||||
var name_label: Label
|
var name_label: Label
|
||||||
var life_label: Label
|
var life_label: Label
|
||||||
var energy_label: Label
|
var energy_label: Label
|
||||||
var income_label: Label
|
|
||||||
var status_label: Label
|
|
||||||
var msg_label: Label
|
var msg_label: Label
|
||||||
var b_end: Button
|
var b_end: Button
|
||||||
var b_giveup: Button
|
var b_giveup: Button
|
||||||
var element_view: ElementView
|
var element_view: ElementView # eigene Elementleiste
|
||||||
|
var opp_element_view: ElementView # die des Gegners darüber
|
||||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||||
var board_panel: Panel # eigenes Brett
|
|
||||||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||||||
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
||||||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||||
@@ -181,43 +182,43 @@ func _build_static() -> void:
|
|||||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||||
b_log.add_theme_font_size_override("font_size", 13)
|
b_log.add_theme_font_size_override("font_size", 13)
|
||||||
|
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||||
|
b_end.add_theme_font_size_override("font_size", 15)
|
||||||
|
b_skip = Style.button(self, "Überspringen", L.B_END, func(): skip_ai = true)
|
||||||
|
b_skip.add_theme_font_size_override("font_size", 15)
|
||||||
|
b_skip.visible = false
|
||||||
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||||
|
Style.panel(self, L.FIELD_OPP, Style.darker(Style.PANEL, 0.9), 8)
|
||||||
|
opp_element_view = ElementView.new(L.FIELD_OPP)
|
||||||
|
add_child(opp_element_view)
|
||||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||||
element_view = ElementView.new(L.FIELD)
|
element_view = ElementView.new(L.FIELD)
|
||||||
add_child(element_view)
|
add_child(element_view)
|
||||||
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
|
||||||
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
var slots := Control.new()
|
var slots := Control.new()
|
||||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
slots.draw.connect(func():
|
slots.draw.connect(func():
|
||||||
for j in Game.MAX_BOARD:
|
for j in Game.MAX_BOARD:
|
||||||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
|
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.05), 2, 8)
|
||||||
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||||||
add_child(slots)
|
add_child(slots)
|
||||||
status_panel = Style.panel(self, L.STATUS)
|
status_panel = Style.panel(self, L.STATUS)
|
||||||
var sx: float = L.STATUS.position.x
|
var sx: float = L.STATUS.position.x
|
||||||
var sy: float = L.STATUS.position.y
|
var sy: float = L.STATUS.position.y
|
||||||
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true)
|
var sw: float = L.STATUS.size.x
|
||||||
|
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 8, sy + 6, sw - 16, 26), "center", true)
|
||||||
name_label.clip_text = true
|
name_label.clip_text = true
|
||||||
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true)
|
life_label = Style.label(self, "", 30, Style.RED, Rect2(sx + 8, sy + 42, sw / 2 - 8, 40), "left", true)
|
||||||
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
|
energy_label = Style.label(self, "", 30, Style.BLUE, Rect2(sx + sw / 2, sy + 42, sw / 2 - 8, 40), "right", true)
|
||||||
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left")
|
|
||||||
status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
|
|
||||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||||
b_undo.add_theme_font_size_override("font_size", 15)
|
b_undo.add_theme_font_size_override("font_size", 13)
|
||||||
b_undo.visible = false
|
b_undo.visible = false
|
||||||
# unten: Zieh-Stapel links, Hand in der Mitte, Zug beenden rechts
|
deck_label = Style.label(self, "", 13, Style.MUTED, L.DECK_LABEL, "center")
|
||||||
deck = Deck.new(L.DECK)
|
|
||||||
deck.clicked.connect(func(): on_click("deck", null))
|
|
||||||
add_child(deck)
|
|
||||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
|
||||||
b_end.add_theme_font_size_override("font_size", 20)
|
|
||||||
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
|
||||||
b_skip.add_theme_font_size_override("font_size", 14)
|
|
||||||
b_skip.visible = false
|
|
||||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||||
wait_label.visible = false
|
wait_label.visible = false
|
||||||
hero_layer = _layer()
|
hero_layer = _layer()
|
||||||
|
offer_layer = _layer()
|
||||||
tile_layer = _layer()
|
tile_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
@@ -317,7 +318,7 @@ func target_center(ref: Dictionary) -> Vector2:
|
|||||||
return Vector2(-1, -1)
|
return Vector2(-1, -1)
|
||||||
|
|
||||||
|
|
||||||
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
|
## Kasten des Gegners links oben, gebaut wie der eigene; ein Klick zielt auf den Helden.
|
||||||
func build_hero_boxes() -> void:
|
func build_hero_boxes() -> void:
|
||||||
Style.clear(hero_layer)
|
Style.clear(hero_layer)
|
||||||
hero_panels = {}
|
hero_panels = {}
|
||||||
@@ -335,11 +336,11 @@ func build_hero_boxes() -> void:
|
|||||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||||
if not o.perks.is_empty():
|
if not o.perks.is_empty():
|
||||||
title += " ★"
|
title += " ★"
|
||||||
labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "left", true)
|
labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 6, box.size.x - 16, 26), "center", true)
|
||||||
labels.name.clip_text = true
|
labels.name.clip_text = true
|
||||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true)
|
labels["life"] = Style.label(hero_layer, "", 30, Style.RED, Rect2(box.position.x + 8, box.position.y + 42, box.size.x / 2 - 8, 40), "left", true)
|
||||||
labels["stats"] = Style.label(hero_layer, "", 14, Style.MUTED, Rect2(box.position.x + 80, box.position.y + 34, box.size.x - 88, 20), "right")
|
labels["stats"] = Style.label(hero_layer, "", 30, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 42, box.size.x / 2 - 8, 40), "right", true)
|
||||||
hero_labels[o] = labels
|
hero_labels[o] = labels
|
||||||
|
|
||||||
|
|
||||||
@@ -366,8 +367,10 @@ func _input(event: InputEvent) -> void:
|
|||||||
open_menu(under.card, Rect2(under.position, under.size))
|
open_menu(under.card, Rect2(under.position, under.size))
|
||||||
elif under is CreatureView and under in own_views and my_turn():
|
elif under is CreatureView and under in own_views and my_turn():
|
||||||
open_menu(under.creature, Rect2(under.slot, under.size))
|
open_menu(under.creature, Rect2(under.slot, under.size))
|
||||||
else:
|
elif selected != null or fuse_first != null or any_overlay() or not my_turn():
|
||||||
cancel_all()
|
cancel_all()
|
||||||
|
else:
|
||||||
|
show_menu(tr("Zug %d") % game.turn, [[tr("Zug beenden"), end_turn, true]], Rect2(event.position, Vector2.ZERO))
|
||||||
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||||
@@ -677,6 +680,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
|||||||
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
||||||
play(card, true)
|
play(card, true)
|
||||||
after_action(), ok[0]])
|
after_action(), ok[0]])
|
||||||
|
entries.append([tr("Abwerfen +%d ◆") % Game.DISCARD_GAIN, func(): do_discard(card), game.can_discard(perspective, card)[0]])
|
||||||
elif not is_card and target.hidden:
|
elif not is_card and target.hidden:
|
||||||
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||||
if full:
|
if full:
|
||||||
@@ -688,6 +692,12 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
|||||||
if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen
|
if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen
|
||||||
close_menu()
|
close_menu()
|
||||||
return
|
return
|
||||||
|
show_menu(card.title(), entries, anchor)
|
||||||
|
|
||||||
|
|
||||||
|
## Kleines Menü neben anchor: Überschrift und Knöpfe [Text, Callable, aktiv].
|
||||||
|
func show_menu(title: String, entries: Array, anchor: Rect2) -> void:
|
||||||
|
Style.clear(menu_layer)
|
||||||
var w := 230.0
|
var w := 230.0
|
||||||
var h := 18.0 + entries.size() * 44 + 34
|
var h := 18.0 + entries.size() * 44 + 34
|
||||||
var x := clampf(anchor.end.x + 8, 10, 1270 - w)
|
var x := clampf(anchor.end.x + 8, 10, 1270 - w)
|
||||||
@@ -696,7 +706,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
|||||||
var y := clampf(anchor.position.y, 10, 790 - h)
|
var y := clampf(anchor.position.y, 10, 790 - h)
|
||||||
var box := Rect2(x, y, w, h)
|
var box := Rect2(x, y, w, h)
|
||||||
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||||||
Style.label(menu_layer, card.title(), 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
Style.label(menu_layer, title, 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
||||||
var by := box.position.y + 36
|
var by := box.position.y + 36
|
||||||
for e in entries:
|
for e in entries:
|
||||||
var act: Callable = e[1]
|
var act: Callable = e[1]
|
||||||
@@ -716,13 +726,25 @@ func close_menu() -> void:
|
|||||||
menu_target = null
|
menu_target = null
|
||||||
|
|
||||||
|
|
||||||
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||||||
func do_draw() -> void:
|
func do_discard(card: Cards.Card) -> void:
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "draw"})
|
net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
|
||||||
return
|
return
|
||||||
var err := game.draw(perspective)
|
var err := game.discard(perspective, card)
|
||||||
|
if err != "":
|
||||||
|
say(tr(err))
|
||||||
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
|
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
||||||
|
func do_draw(index: int = 0) -> void:
|
||||||
|
clear_undo()
|
||||||
|
if is_client():
|
||||||
|
net.send_action({"type": "draw", "index": index})
|
||||||
|
return
|
||||||
|
var err := game.draw(perspective, index)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
else:
|
else:
|
||||||
@@ -811,7 +833,7 @@ func on_click(kind: String, obj) -> void:
|
|||||||
return
|
return
|
||||||
match kind:
|
match kind:
|
||||||
"deck":
|
"deck":
|
||||||
do_draw()
|
do_draw(int(obj) if obj != null else 0)
|
||||||
"hand":
|
"hand":
|
||||||
var ok := g.can_play(perspective, obj)
|
var ok := g.can_play(perspective, obj)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
@@ -881,7 +903,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
|
|||||||
var a := game.forecast(perspective, card)
|
var a := game.forecast(perspective, card)
|
||||||
var info := ""
|
var info := ""
|
||||||
if a > 0:
|
if a > 0:
|
||||||
info = tr("Elementbonus +%d/+%d") % [a, a]
|
info = tr("Elementbonus +%d") % a
|
||||||
show_preview(card, entered, perspective.looks, info, anchor)
|
show_preview(card, entered, perspective.looks, info, anchor)
|
||||||
|
|
||||||
|
|
||||||
@@ -889,7 +911,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
|
|||||||
func creature_info(cr: Game.Creature) -> String:
|
func creature_info(cr: Game.Creature) -> String:
|
||||||
var parts: Array[String] = []
|
var parts: Array[String] = []
|
||||||
if cr.aura > 0:
|
if cr.aura > 0:
|
||||||
parts.append(tr("Elementbonus +%d/+%d") % [cr.aura, cr.aura])
|
parts.append(tr("Elementbonus +%d") % cr.aura)
|
||||||
var perk := cr.power() - cr.attack - cr.aura
|
var perk := cr.power() - cr.attack - cr.aura
|
||||||
if perk > 0:
|
if perk > 0:
|
||||||
parts.append(tr("Merkmal +%d") % perk)
|
parts.append(tr("Merkmal +%d") % perk)
|
||||||
@@ -922,39 +944,25 @@ func update_attack_preview() -> void:
|
|||||||
for v in own_views:
|
for v in own_views:
|
||||||
var mine: bool = v.creature == selected
|
var mine: bool = v.creature == selected
|
||||||
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
|
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
|
||||||
var counter: int = target.power() + Game.matchup_bonus(target, selected) if target != null and target.power() > 0 else 0
|
var counter: int = maxi(0, target.power() + Game.matchup_bonus(target, selected)) if target != null else 0
|
||||||
v.doomed = mine and target != null and counter >= selected.health
|
v.doomed = mine and target != null and counter >= selected.health
|
||||||
v.queue_redraw()
|
v.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
class Deck extends Control:
|
## Eine verdeckte Handkarte des Gegners in voller Größe.
|
||||||
signal clicked
|
class CardBack extends Control:
|
||||||
var enabled := true
|
|
||||||
var count := 0
|
|
||||||
var cost := Game.DRAW_COST
|
|
||||||
|
|
||||||
func _init(rect: Rect2) -> void:
|
func _init(rect: Rect2) -> void:
|
||||||
position = rect.position
|
position = rect.position
|
||||||
size = rect.size
|
size = rect.size
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
|
||||||
accept_event()
|
|
||||||
clicked.emit()
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
for k in range(2, -1, -1):
|
var r := Rect2(Vector2.ZERO, size)
|
||||||
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12)).grow(0)
|
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 2), r)
|
||||||
r.position.y += 6
|
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled else Style.MUTED, 2), r)
|
Style.text(self, "?", 40, Style.GOLD, size / 2, "center", true)
|
||||||
if k == 0:
|
|
||||||
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
|
||||||
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
|
|
||||||
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
|
|
||||||
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
|
|
||||||
if not enabled:
|
|
||||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1048,7 +1056,7 @@ func refresh() -> void:
|
|||||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||||
if o.alive():
|
if o.alive():
|
||||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||||
labels.stats.text = "Hand %d ◆ %d" % [o.hand.size(), o.energy]
|
labels.stats.text = "◆ %d" % o.energy
|
||||||
else:
|
else:
|
||||||
labels.life.text = "besiegt"
|
labels.life.text = "besiegt"
|
||||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||||
@@ -1088,6 +1096,11 @@ func refresh() -> void:
|
|||||||
var c := game.find_creature(uid)
|
var c := game.find_creature(uid)
|
||||||
if c != null:
|
if c != null:
|
||||||
_press(c, tile, pos))
|
_press(c, tile, pos))
|
||||||
|
tile.double_clicked.connect(func(_v):
|
||||||
|
var c := game.find_creature(uid)
|
||||||
|
if c != null and c.owner() == perspective and my_turn() and not any_overlay():
|
||||||
|
selected = c # Doppelklick: gleich auf das Leben des Gegners
|
||||||
|
on_click("hero", opponent()))
|
||||||
if arrivals.has(cr.uid):
|
if arrivals.has(cr.uid):
|
||||||
fx.fly_in(tile, arrivals[cr.uid])
|
fx.fly_in(tile, arrivals[cr.uid])
|
||||||
arrivals.erase(cr.uid)
|
arrivals.erase(cr.uid)
|
||||||
@@ -1125,13 +1138,33 @@ func refresh() -> void:
|
|||||||
tiles.erase(uid)
|
tiles.erase(uid)
|
||||||
fx.fade_out(t)
|
fx.fade_out(t)
|
||||||
update_attack_preview()
|
update_attack_preview()
|
||||||
deck.visible = not spectating()
|
Style.clear(offer_layer)
|
||||||
deck.enabled = mine and g.can_draw(h)[0]
|
offer_views = []
|
||||||
deck.count = g.deck.size()
|
draw_enabled = mine and g.can_draw(h)[0]
|
||||||
deck.cost = g.draw_cost(h)
|
if not spectating():
|
||||||
deck.queue_redraw()
|
for i in h.offer.size():
|
||||||
|
var oc: Cards.Card = h.offer[i]
|
||||||
|
var orect := Rect2(BattleLayout.OFFER_X, BattleLayout.OFFER_Y + i * (BattleLayout.HAND_H + BattleLayout.OFFER_GAP), BattleLayout.HAND_W, BattleLayout.HAND_H)
|
||||||
|
var ov := CardView.new(oc, orect.size, false, oc.rarity_in(h.looks))
|
||||||
|
ov.position = orect.position
|
||||||
|
ov.dim = not draw_enabled
|
||||||
|
var idx := i
|
||||||
|
ov.clicked.connect(func(_v): on_click("deck", idx))
|
||||||
|
ov.hover.connect(func(c, e): show_preview(c, e, h.looks, "", orect))
|
||||||
|
offer_layer.add_child(ov)
|
||||||
|
offer_views.append(ov)
|
||||||
|
deck_label.text = "%s ◆ %d · %d" % [tr("Ziehen"), g.draw_cost(h), g.deck.size()]
|
||||||
|
deck_label.visible = not spectating()
|
||||||
|
|
||||||
Style.clear(hand_layer)
|
Style.clear(hand_layer)
|
||||||
|
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
|
||||||
|
for i in o.hand.size():
|
||||||
|
var back := CardBack.new(BattleLayout.opp_hand_rect(o.hand.size(), i))
|
||||||
|
var arc: Array = BattleLayout.hand_arc(o.hand.size(), i, true)
|
||||||
|
back.pivot_offset = back.size / 2
|
||||||
|
back.rotation_degrees = arc[0]
|
||||||
|
back.position.y += arc[1]
|
||||||
|
hand_layer.add_child(back)
|
||||||
if spectating():
|
if spectating():
|
||||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
||||||
else:
|
else:
|
||||||
@@ -1141,6 +1174,9 @@ func refresh() -> void:
|
|||||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||||
var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
|
var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
|
||||||
v.position = r.position
|
v.position = r.position
|
||||||
|
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
|
||||||
|
v.rotation_degrees = arc[0]
|
||||||
|
v.position.y += arc[1]
|
||||||
v.lift = mine
|
v.lift = mine
|
||||||
v.bonus = g.forecast(h, card)
|
v.bonus = g.forecast(h, card)
|
||||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||||
@@ -1163,16 +1199,13 @@ func refresh() -> void:
|
|||||||
name_label.text = h.name
|
name_label.text = h.name
|
||||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||||
energy_label.text = "◆ %d" % h.energy
|
energy_label.text = "◆ %d" % h.energy
|
||||||
income_label.text = tr("nächster Zug ◆ %d") % (h.max_energy + 1)
|
|
||||||
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
|
||||||
if spectating():
|
|
||||||
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
|
||||||
b_end.disabled = not mine
|
b_end.disabled = not mine
|
||||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||||
b_skip.visible = ai_running
|
b_skip.visible = ai_running
|
||||||
b_end.visible = not ai_running and not spectating()
|
b_end.visible = not ai_running and not spectating()
|
||||||
b_undo.visible = undo_state != null and mine
|
b_undo.visible = undo_state != null and mine
|
||||||
element_view.update_from(g, h)
|
element_view.update_from(g, h)
|
||||||
|
opp_element_view.update_from(g, opponent())
|
||||||
var lines: Array[String] = []
|
var lines: Array[String] = []
|
||||||
var start := maxi(0, g.log.size() - 60)
|
var start := maxi(0, g.log.size() - 60)
|
||||||
for i in range(start, g.log.size()):
|
for i in range(start, g.log.size()):
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
|||||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
|
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
|
||||||
if bonus > 0:
|
if bonus > 0:
|
||||||
Style.text_outlined(self, "+%d/+%d" % [bonus, bonus], f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extends Control
|
|||||||
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
|
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
|
||||||
|
|
||||||
signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
|
signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
|
||||||
|
signal double_clicked(view: CreatureView) # Doppelklick: eigenes Wesen greift das Leben des Gegners an
|
||||||
signal hover(card: Cards.Card, entered: bool)
|
signal hover(card: Cards.Card, entered: bool)
|
||||||
|
|
||||||
var creature: Game.Creature
|
var creature: Game.Creature
|
||||||
@@ -38,7 +39,10 @@ func _process(_delta: float) -> void:
|
|||||||
func _gui_input(event: InputEvent) -> void:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
accept_event()
|
accept_event()
|
||||||
pressed.emit(self, event.global_position)
|
if event.double_click:
|
||||||
|
double_clicked.emit(self)
|
||||||
|
else:
|
||||||
|
pressed.emit(self, event.global_position)
|
||||||
|
|
||||||
|
|
||||||
func rarity() -> String:
|
func rarity() -> String:
|
||||||
@@ -76,7 +80,7 @@ func _draw() -> void:
|
|||||||
var badge := ""
|
var badge := ""
|
||||||
var bw := 0.0
|
var bw := 0.0
|
||||||
if creature.aura != 0:
|
if creature.aura != 0:
|
||||||
badge = "+%d/+%d" % [creature.aura, creature.aura] if creature.aura > 0 else "−%d/−%d" % [-creature.aura, -creature.aura]
|
badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||||
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||||
var by := Style.fs(fs) + 13
|
var by := Style.fs(fs) + 13
|
||||||
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
||||||
@@ -127,11 +131,11 @@ func _draw_back(rect: Rect2) -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem
|
## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem
|
||||||
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, wer 0 Angriff hat, kontert gar nicht),
|
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, im Vorteil 1 mehr, auch bei 0 Angriff),
|
||||||
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
|
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
|
||||||
func shown_stats() -> Array:
|
func shown_stats() -> Array:
|
||||||
if matchup != 0:
|
if matchup != 0:
|
||||||
var atk := 0 if creature.power() <= 0 else maxi(0, creature.power() - matchup)
|
var atk := maxi(0, creature.power() - matchup)
|
||||||
return [atk, creature.health]
|
return [atk, creature.health]
|
||||||
if attack_shift != 0:
|
if attack_shift != 0:
|
||||||
return [maxi(0, creature.power() + attack_shift), creature.health]
|
return [maxi(0, creature.power() + attack_shift), creature.health]
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
class_name ElementView
|
class_name ElementView
|
||||||
extends Control
|
extends Control
|
||||||
## Elementboni auf einen Blick: offene Wesen je Element auf dem Tisch (ab drei +1/+1) und der
|
## Elementzähler der eigenen Seite, mittig: je Element zweimal das Symbol seines Sondereffekts in
|
||||||
## Sondereffekt der eigenen Seite (ab zwei eigenen: Erde Spott, Luft Flink, Wasser Regeneration,
|
## der Elementfarbe, leuchtend je offenem eigenen Wesen (beide: Effekt aktiv), daneben in der
|
||||||
## Feuer Doppelschlag), als Symbol in der Elementfarbe.
|
## Elementfarbe das Mana der bisher ausgespielten Karten (je Game.ELEMENT_MANA gibt es +1 Angriff).
|
||||||
|
|
||||||
var counts := {} # Tisch
|
const ENTRY_W := 180.0
|
||||||
var own := {} # eigene Seite
|
|
||||||
|
var own := {} # offene eigene Wesen je Element
|
||||||
|
var played := {} # ausgespielte Karten je Element
|
||||||
|
|
||||||
|
|
||||||
func _init(rect: Rect2) -> void:
|
func _init(rect: Rect2) -> void:
|
||||||
@@ -13,62 +15,53 @@ func _init(rect: Rect2) -> void:
|
|||||||
size = rect.size
|
size = rect.size
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
for e in Cards.ELEMENTS:
|
for e in Cards.ELEMENTS:
|
||||||
counts[e] = 0
|
|
||||||
own[e] = 0
|
own[e] = 0
|
||||||
|
played[e] = 0
|
||||||
|
|
||||||
|
|
||||||
func update_from(g: Game, me: Game.Player) -> void:
|
func update_from(_g: Game, me: Game.Player) -> void:
|
||||||
counts = g.element_counts()
|
|
||||||
for e in Cards.ELEMENTS:
|
for e in Cards.ELEMENTS:
|
||||||
own[e] = me.element_count(e) if me != null else 0
|
own[e] = me.element_count(e) if me != null else 0
|
||||||
|
played[e] = int(me.played.get(e, 0)) if me != null else 0
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func strong(e: String) -> bool:
|
|
||||||
return int(counts.get(e, 0)) >= Game.ELEMENT_STACK
|
|
||||||
|
|
||||||
|
|
||||||
func paired(e: String) -> bool:
|
func paired(e: String) -> bool:
|
||||||
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||||
|
|
||||||
|
|
||||||
## Der Inhalt als Text (Tests, Protokoll).
|
func bonus(e: String) -> int:
|
||||||
|
return int(played.get(e, 0)) / Game.ELEMENT_MANA
|
||||||
|
|
||||||
|
|
||||||
|
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
||||||
func text() -> String:
|
func text() -> String:
|
||||||
var parts: Array[String] = []
|
var parts: Array[String] = []
|
||||||
for e in Cards.ELEMENTS:
|
for e in Cards.ELEMENTS:
|
||||||
var s := "%s %d" % [tr(e), int(counts.get(e, 0))]
|
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
|
||||||
if strong(e):
|
|
||||||
s += " (+1/+1)"
|
|
||||||
if paired(e):
|
if paired(e):
|
||||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||||
|
if bonus(e) > 0:
|
||||||
|
s += " (+%d)" % bonus(e)
|
||||||
parts.append(s)
|
parts.append(s)
|
||||||
return " · ".join(parts)
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var w := size.x / Cards.ELEMENTS.size()
|
var x0 := (size.x - ENTRY_W * Cards.ELEMENTS.size()) / 2
|
||||||
|
var cy := size.y / 2
|
||||||
for i in Cards.ELEMENTS.size():
|
for i in Cards.ELEMENTS.size():
|
||||||
var e: String = Cards.ELEMENTS[i]
|
var e: String = Cards.ELEMENTS[i]
|
||||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||||
var x0 := i * w
|
var active := paired(e) or bonus(e) > 0
|
||||||
var cy := size.y / 2
|
var px := x0 + i * ENTRY_W + 10
|
||||||
var n := int(counts.get(e, 0))
|
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, cy), "midleft", active)
|
||||||
if strong(e):
|
px += Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 10
|
||||||
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
for k in Game.ELEMENT_PAIR:
|
||||||
elif paired(e):
|
var filled := k < int(own.get(e, 0))
|
||||||
draw_style_box(Style.box(Color(col, 0.18), 6, Color(col, 0.6), 1), Rect2(x0 + 2, 0, w - 4, size.y))
|
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, cy), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||||
var active := strong(e) or paired(e)
|
px += Game.ELEMENT_PAIR * 24 + 8
|
||||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(x0 + 12, cy), "midleft", active)
|
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, cy), "midleft", true)
|
||||||
var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12
|
px += 20
|
||||||
for k in Game.ELEMENT_STACK:
|
if bonus(e) > 0:
|
||||||
var c := Vector2(px + k * 13, cy)
|
Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
|
||||||
if k < n:
|
|
||||||
draw_circle(c, 4.5, col)
|
|
||||||
else:
|
|
||||||
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
|
||||||
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
|
||||||
px += Game.ELEMENT_STACK * 13 + 2
|
|
||||||
if paired(e):
|
|
||||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
|
|
||||||
if strong(e):
|
|
||||||
Style.text(self, "+1/+1", 13, Style.GREEN, Vector2(x0 + w - 12, cy), "midright", true)
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ extends Node
|
|||||||
|
|
||||||
var streams := {}
|
var streams := {}
|
||||||
var players: Array[AudioStreamPlayer] = []
|
var players: Array[AudioStreamPlayer] = []
|
||||||
var volume := 0.8
|
var volume := 0.0 # Ton ist standardmäßig aus
|
||||||
|
var muted := false # Tests: nie etwas abspielen, egal was die Regler sagen
|
||||||
var music_player: AudioStreamPlayer
|
var music_player: AudioStreamPlayer
|
||||||
var music_volume := 0.5
|
var music_volume := 0.0
|
||||||
var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start)
|
var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start)
|
||||||
var music_box := {}
|
var music_box := {}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ func _music() -> AudioStreamWAV:
|
|||||||
|
|
||||||
func set_music_volume(v: float) -> void:
|
func set_music_volume(v: float) -> void:
|
||||||
music_volume = clampf(v, 0.0, 1.0)
|
music_volume = clampf(v, 0.0, 1.0)
|
||||||
if music_volume <= 0.0:
|
if music_volume <= 0.0 or muted:
|
||||||
music_player.stop()
|
music_player.stop()
|
||||||
else:
|
else:
|
||||||
music_player.volume_db = linear_to_db(music_volume * 0.6)
|
music_player.volume_db = linear_to_db(music_volume * 0.6)
|
||||||
@@ -91,7 +92,7 @@ func set_volume(v: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func play(kind: String) -> void:
|
func play(kind: String) -> void:
|
||||||
if volume <= 0.0 or not streams.has(kind):
|
if volume <= 0.0 or muted or not streams.has(kind):
|
||||||
return
|
return
|
||||||
for p in players:
|
for p in players:
|
||||||
if not p.playing:
|
if not p.playing:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ func check(cond: bool, msg: String) -> void:
|
|||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
printerr("FEHLER: " + msg)
|
printerr("FEHLER: " + msg)
|
||||||
|
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
func C(id: String) -> Cards.Card:
|
func C(id: String) -> Cards.Card:
|
||||||
@@ -55,6 +57,7 @@ func _initialize() -> void:
|
|||||||
test_kombos()
|
test_kombos()
|
||||||
test_mana_und_kosten()
|
test_mana_und_kosten()
|
||||||
test_ziehen()
|
test_ziehen()
|
||||||
|
test_abwerfen()
|
||||||
test_wesen_und_angriff()
|
test_wesen_und_angriff()
|
||||||
test_elementvorteil()
|
test_elementvorteil()
|
||||||
test_verdeckt()
|
test_verdeckt()
|
||||||
@@ -129,64 +132,44 @@ func test_mana_und_kosten() -> void:
|
|||||||
h.energy = 5
|
h.energy = 5
|
||||||
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_wolf").cost, "Ausspielen kostet die Kartenkosten")
|
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_wolf").cost, "Ausspielen kostet die Kartenkosten")
|
||||||
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
|
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
|
||||||
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 4 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren")
|
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 5 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren, mindestens 1")
|
||||||
|
|
||||||
|
|
||||||
## Start: fünf im ersten Zug bezahlbare Karten, keine Wahl. Jeder Zug beginnt mit einer Karte;
|
## Start: fünf im ersten Zug bezahlbare Karten, drei offene auf dem Stapel. Ziehen: eine der drei
|
||||||
## Ziehen: 2 Mana, eine weitere bezahlbare Karte vom Stapel.
|
## wählen, eine neue rückt nach; das erste Ziehen im Zug ist frei, jedes weitere kostet 2 Mana.
|
||||||
func test_ziehen() -> void:
|
func test_ziehen() -> void:
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 1
|
r.seed = 1
|
||||||
var g := Game.new_battle(r)
|
var g := Game.new_battle(r)
|
||||||
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
|
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * (Game.START_HAND + Game.OFFER), "Stapel: jede Karte dreimal, Hände und Angebote schon ausgegeben")
|
||||||
for p in g.players:
|
for p in g.players:
|
||||||
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
||||||
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
|
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
|
||||||
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
|
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
|
||||||
|
check(p.offer.size() == Game.OFFER and p.offer.all(func(c): return c.cost <= Game.START_MANA), "drei offene Karten auf dem Stapel, alle bezahlbar")
|
||||||
var first: Game.Player = g.current()
|
var first: Game.Player = g.current()
|
||||||
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
||||||
first.hand.pop_back()
|
first.hand.pop_back()
|
||||||
check(g.draw(first) == "Nicht genug Mana", "im ersten Zug reicht das Mana nicht zum Ziehen")
|
var wanted: Cards.Card = first.offer[1]
|
||||||
first.energy = 3
|
|
||||||
first.max_energy = 3
|
|
||||||
var mana := first.energy
|
|
||||||
var deck_before := g.deck.size()
|
var deck_before := g.deck.size()
|
||||||
check(g.draw(first) == "" and first.energy == mana - Game.DRAW_COST and first.hand.size() == Game.START_HAND, "gezogen: 2 Mana, eine Karte")
|
check(g.draw_cost(first) == 0 and g.draw(first, 1) == "" and first.energy == Game.START_MANA and first.hand.back().id == wanted.id, "erstes Ziehen frei: die gewählte Karte kommt auf die Hand")
|
||||||
check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel")
|
check(first.offer.size() == Game.OFFER and first.offer[1] != wanted and first.offer[1].cost <= first.max_energy and g.deck.size() == deck_before - 1, "eine bezahlbare neue Karte rückt an die Stelle")
|
||||||
first.energy = 0
|
|
||||||
first.hand.pop_back()
|
first.hand.pop_back()
|
||||||
check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen")
|
check(g.draw_cost(first) == Game.DRAW_COST and g.draw(first) == "Nicht genug Mana", "zweites Ziehen kostet 2, im ersten Zug zu teuer")
|
||||||
|
first.energy = 3
|
||||||
|
check(g.draw(first, 0) == "" and first.energy == 3 - Game.DRAW_COST, "mit Mana: 2 bezahlt")
|
||||||
var second: Game.Player = g.enemy_of(first)
|
var second: Game.Player = g.enemy_of(first)
|
||||||
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
|
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
|
||||||
# Leerer Stapel wird neu gemischt
|
# Leerer Stapel wird neu gemischt
|
||||||
g.deck = []
|
g.deck = []
|
||||||
|
first.hand.pop_back()
|
||||||
first.energy = 3
|
first.energy = 3
|
||||||
check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
|
check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
|
||||||
# Mit viel Mana tauchen auch teure Karten auf
|
# Nächster Zug: nichts kommt von selbst, das erste Ziehen ist wieder frei
|
||||||
first.max_energy = 20
|
var hand_now := first.hand.size()
|
||||||
first.energy = 20
|
g.end_turn()
|
||||||
var seen_expensive := false
|
g.end_turn()
|
||||||
for i in 12:
|
check(g.current() == first and first.hand.size() == hand_now and g.draw_cost(first) == 0, "neuer Zug: keine Karte von selbst, das erste Ziehen wieder frei")
|
||||||
first.hand = []
|
|
||||||
first.energy = 20
|
|
||||||
g.draw(first)
|
|
||||||
if first.hand[0].cost > 5:
|
|
||||||
seen_expensive = true
|
|
||||||
check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
|
|
||||||
# Zugbeginn: eine Karte gratis, solange die Hand nicht voll ist
|
|
||||||
var g5 := Game.new_battle(r)
|
|
||||||
var p1: Game.Player = g5.current()
|
|
||||||
var p2: Game.Player = g5.enemy_of(p1)
|
|
||||||
g5.end_turn()
|
|
||||||
check(p2.hand.size() == Game.START_HAND, "volle Hand: zu Zugbeginn kommt nichts")
|
|
||||||
p2.hand.pop_back()
|
|
||||||
p2.hand.pop_back()
|
|
||||||
g5.end_turn()
|
|
||||||
p1.hand.pop_back()
|
|
||||||
g5.end_turn()
|
|
||||||
check(p2.hand.size() == Game.START_HAND - 1 and p2.hand.back().cost <= p2.max_energy, "Zugbeginn: eine bezahlbare Karte gezogen")
|
|
||||||
g5.end_turn()
|
|
||||||
check(p1.hand.size() == Game.START_HAND, "auch der Startspieler zieht zu Zugbeginn")
|
|
||||||
|
|
||||||
|
|
||||||
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
|
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
|
||||||
@@ -243,6 +226,20 @@ func test_elementvorteil() -> void:
|
|||||||
var t3 := put(s3[2], "w_tiefen")
|
var t3 := put(s3[2], "w_tiefen")
|
||||||
s3[0].attack(w3, t3)
|
s3[0].attack(w3, t3)
|
||||||
check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
|
check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
|
||||||
|
# 0 Angriff: nur mit Elementvorteil geht etwas
|
||||||
|
var s4 := setup()
|
||||||
|
var funke := put(s4[1], "f_funke") # Feuer 0/2
|
||||||
|
var hauch := put(s4[2], "l_hauch") # Luft 0/2
|
||||||
|
var wall := put(s4[2], "e_wall") # Erde 0/4
|
||||||
|
check(s4[0].can_attack(funke, wall)[1] == "Glutfunke würde keinen Schaden machen", "0 Angriff ohne Vorteil: kein Angriff")
|
||||||
|
check(s4[0].attack(funke, hauch) == "" and hauch.health == 1 and funke.health == 2, "0 Angriff mit Vorteil: 1 Schaden, kein Konter")
|
||||||
|
var s5 := setup()
|
||||||
|
var hauch2 := put(s5[1], "l_hauch")
|
||||||
|
var funke2 := put(s5[2], "f_funke")
|
||||||
|
check(s5[0].attack(hauch2, funke2) == "Windhauch würde keinen Schaden machen" and funke2.health == 2, "im Nachteil nichts")
|
||||||
|
var wall2 := put(s5[2], "e_wall")
|
||||||
|
var geist5 := put(s5[1], "w_geist") # Wasser 2/2
|
||||||
|
check(s5[0].attack(geist5, wall2) == "" and wall2.health == 4 - 1 and geist5.health == 2 - 1, "Steinwall kontert mit Elementvorteil für 1")
|
||||||
var s2 := setup()
|
var s2 := setup()
|
||||||
var g2: Game = s2[0]
|
var g2: Game = s2[0]
|
||||||
var geist := put(s2[1], "w_geist") # Wasser 2/2
|
var geist := put(s2[1], "w_geist") # Wasser 2/2
|
||||||
@@ -286,46 +283,52 @@ func test_verdeckt() -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
|
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
|
||||||
## Flink, Wasser Regeneration, Feuer Doppelschlag); drei auf dem Tisch geben dem Element +1/+1.
|
## Flink, Wasser Regeneration, Feuer Doppelschlag); je 10 Mana an ausgespielten Karten eines Elements geben
|
||||||
|
## den eigenen Wesen des Elements +1 Angriff. Jeder Spieler zählt für sich.
|
||||||
func test_elementboni() -> void:
|
func test_elementboni() -> void:
|
||||||
var s := setup()
|
var s := setup(["f_asche", "f_ritter"])
|
||||||
var g: Game = s[0]
|
var g: Game = s[0]
|
||||||
var h: Game.Player = s[1]
|
var h: Game.Player = s[1]
|
||||||
var o: Game.Player = s[2]
|
var o: Game.Player = s[2]
|
||||||
var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
|
var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
|
||||||
var bird := put(o, "l_wind") # Luft 1/1
|
var bird := put(o, "l_wind") # Luft 1/1
|
||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
check(fire[0].aura == 0 and fire[1].aura == 0, "zwei reichen nicht für +1/+1")
|
check(fire[0].aura == 0 and int(h.played["Feuer"]) == 0, "nur gelegt, nicht ausgespielt: kein Bonus")
|
||||||
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
|
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
|
||||||
var hidden_fire := put(o, "f_asche", true, true)
|
var hidden_fire := put(o, "f_asche", true, true)
|
||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
check(fire[0].aura == 0 and g.element_counts()["Feuer"] == 2 and not hidden_fire.has("Doppelschlag"), "verdeckte zählen nicht und bekommen nichts")
|
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
|
||||||
var third := put(o, "f_ritter") # Feuer beim Gegner zählt für den Tisch
|
# Ausgespielte Karten zählen; je vier gibt es +1 Angriff für die eigenen Wesen des Elements
|
||||||
g.apply_auras()
|
h.played["Feuer"] = 7
|
||||||
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and third.aura == 1, "drei Feuer auf dem Tisch: +1/+1 für alle Feuerwesen")
|
g.play_card(h, h.hand[0]) # Aschekrieger für 3: macht 10 Mana voll
|
||||||
check(bird.aura == 0 and bird.health == 1, "kein Malus für das geschlagene Element")
|
check(int(h.played["Feuer"]) == 10 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 2 and h.board[2].aura == 1, "10 Mana Feuer ausgespielt: +1 Angriff für alle eigenen Feuerwesen, Leben bleibt")
|
||||||
check(not third.has("Doppelschlag"), "der Gegner hat nur ein offenes Feuer: kein Sondereffekt")
|
check(bird.aura == 0 and bird.health == 1 and hidden_fire.aura == 0, "der Gegner zählt für sich: nichts")
|
||||||
g._flip(hidden_fire)
|
g._flip(hidden_fire)
|
||||||
check(hidden_fire.aura == 1 and third.has("Doppelschlag") and hidden_fire.has("Doppelschlag"), "aufgedeckt zählt es sofort: Bonus und Paar")
|
check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
|
||||||
fire[0].health = 1
|
g.play_card(h, h.hand[0], true) # Ritter verdeckt
|
||||||
o.board.erase(third)
|
var secret: Game.Creature = h.board.back()
|
||||||
o.board.erase(hidden_fire)
|
check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 13, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen")
|
||||||
g.apply_auras()
|
g._flip(secret)
|
||||||
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1 and fire[1].health == 1, "Bonus geht wieder: nur das Höchstleben sinkt, Leben wird höchstens gekappt")
|
check(secret.aura == 1 and secret.power() == 5 and secret.health == 3, "aufgedeckt kommt der Bonus")
|
||||||
# Vorschau
|
# Vorschau
|
||||||
check(g.forecast(h, C("f_asche")) == 1 and g.forecast(h, C("l_wind")) == 0, "Vorschau: Feuer würde den Stapel voll machen")
|
h.played["Luft"] = 9
|
||||||
|
check(g.forecast(h, C("l_wind")) == 1 and g.forecast(h, C("w_geist")) == 0, "Vorschau: der Windgeist macht 10 Mana Luft voll und bekäme +1")
|
||||||
# Merkmal: doppelt
|
# Merkmal: doppelt
|
||||||
h.perks = ["element_doppelt"]
|
h.perks = ["element_doppelt"]
|
||||||
put(h, "f_asche")
|
|
||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt")
|
check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt")
|
||||||
h.perks = []
|
h.perks = []
|
||||||
|
h.played["Feuer"] = 20
|
||||||
|
g.apply_auras()
|
||||||
|
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 2, "20 Mana Feuer: +2 Angriff")
|
||||||
# Die anderen Paare
|
# Die anderen Paare
|
||||||
var walls := [put(h, "e_wall"), put(h, "e_wall")]
|
var s5 := setup()
|
||||||
check(walls[0].has("Spott") and not fire[0].has("Spott"), "zwei Erde: Spott nur für Erdwesen")
|
var walls := [put(s5[1], "e_wall"), put(s5[1], "e_wall")]
|
||||||
var winds := [put(h, "l_wind", false), put(h, "l_wind", false)]
|
check(walls[0].has("Spott") and not put(s5[1], "f_wolf").has("Spott"), "zwei Erde: Spott nur für Erdwesen")
|
||||||
|
var winds := [put(s5[1], "l_wind", false), put(s5[1], "l_wind", false)]
|
||||||
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
|
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
|
||||||
var geister := [put(h, "w_geist"), put(h, "w_geist")]
|
var s6 := setup()
|
||||||
|
var geister := [put(s6[1], "w_geist"), put(s6[1], "w_geist")]
|
||||||
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
|
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
|
||||||
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
|
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
|
||||||
var s2 := setup()
|
var s2 := setup()
|
||||||
@@ -338,10 +341,25 @@ func test_elementboni() -> void:
|
|||||||
g2.apply_auras()
|
g2.apply_auras()
|
||||||
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
|
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
|
||||||
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
|
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
|
||||||
put(s2[2], "f_asche")
|
h2.played["Feuer"] = 10
|
||||||
put(s2[2], "w_reiter")
|
h2.played["Wasser"] = 10
|
||||||
g2.apply_auras()
|
g2.apply_auras()
|
||||||
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
|
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "10 Mana Feuer und 10 Mana Wasser: Dampf bekommt +2 Angriff")
|
||||||
|
|
||||||
|
|
||||||
|
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
|
||||||
|
func test_abwerfen() -> void:
|
||||||
|
var s := setup(["f_wolf", "f_kobold"])
|
||||||
|
var g: Game = s[0]
|
||||||
|
var h: Game.Player = s[1]
|
||||||
|
var o: Game.Player = s[2]
|
||||||
|
var card: Cards.Card = h.hand[0]
|
||||||
|
check(g.discard(h, card) == "" and h.hand.size() == 1 and h.energy == 10 + Game.DISCARD_GAIN and not (card in h.hand), "abgeworfen: Karte weg, 1 Mana mehr")
|
||||||
|
check(g.log.back() == "Spieler wirft Flammenwolf ab.", "Verlauf")
|
||||||
|
check(g.discard(h, card) == "Nicht auf der Hand", "nicht zweimal")
|
||||||
|
h.energy = 0
|
||||||
|
check(g.discard(h, h.hand[0]) == "" and h.energy == 1, "auch ohne Mana, bringt eins")
|
||||||
|
check(g.discard(o, C("f_wolf")) == "Nicht dein Zug", "nur im eigenen Zug")
|
||||||
|
|
||||||
|
|
||||||
## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
|
## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
|
||||||
@@ -419,17 +437,15 @@ func test_verschmelzen() -> void:
|
|||||||
var h3: Game.Player = s3[1]
|
var h3: Game.Player = s3[1]
|
||||||
var fa := put(h3, "f_wolf")
|
var fa := put(h3, "f_wolf")
|
||||||
var fb := put(h3, "w_tiefen")
|
var fb := put(h3, "w_tiefen")
|
||||||
for i in 2:
|
h3.played["Feuer"] = 10
|
||||||
put(h3, "f_kobold")
|
|
||||||
g3.apply_auras()
|
g3.apply_auras()
|
||||||
check(fa.aura == 1 and fa.max_health == 3, "drei Feuer vor dem Verschmelzen")
|
check(fa.aura == 1 and fa.power() == 4 and fa.max_health == 2, "10 Mana Feuer ausgespielt vor dem Verschmelzen")
|
||||||
g3.fuse(h3, fa, fb)
|
g3.fuse(h3, fa, fb)
|
||||||
var ff: Game.Creature = h3.board[0]
|
var ff: Game.Creature = h3.board[0]
|
||||||
check(ff.aura == 1 and ff.max_health == 8 and ff.health == 8 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal (7 + 1)")
|
check(ff.aura == 1 and ff.max_health == 7 and ff.health == 7 and ff.power() == 6, "nach dem Verschmelzen: 5 + 1 Angriff, Leben ohne Bonus")
|
||||||
for i in 2:
|
h3.played["Wasser"] = 10
|
||||||
put(h3, "w_geist")
|
|
||||||
g3.apply_auras()
|
g3.apply_auras()
|
||||||
check(g3.element_counts()["Wasser"] == 3 and ff.aura == 2 and ff.max_health == 9, "Feuer und Wasser voll: Dampf bekommt +2/+2")
|
check(ff.aura == 2 and ff.power() == 7 and ff.max_health == 7, "Feuer und Wasser voll: Dampf bekommt +2 Angriff")
|
||||||
# Auf der Hand
|
# Auf der Hand
|
||||||
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
|
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
|
||||||
var g4: Game = s4[0]
|
var g4: Game = s4[0]
|
||||||
@@ -653,6 +669,8 @@ func test_spielstand_migration() -> void:
|
|||||||
check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen")
|
check(p.coins == 300 and p.dust == 70 and p.campaign_stage == 2, "alter Stand gelesen")
|
||||||
check(p.collection.is_empty() and p.perk == "", "Sammlung auf Standard, unbekanntes Merkmal verworfen")
|
check(p.collection.is_empty() and p.perk == "", "Sammlung auf Standard, unbekanntes Merkmal verworfen")
|
||||||
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
|
check(is_equal_approx(p.settings.volume, 0.3) and p.settings.animations, "Einstellungen übernommen")
|
||||||
|
var fresh := Profile.new()
|
||||||
|
check(fresh.settings.volume == 0.0 and fresh.settings.music == 0.0, "Ton und Musik sind standardmäßig aus")
|
||||||
p.collection["f_wolf"] = 3
|
p.collection["f_wolf"] = 3
|
||||||
var d := p.to_dict()
|
var d := p.to_dict()
|
||||||
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
||||||
@@ -731,6 +749,8 @@ func test_merkmale() -> void:
|
|||||||
g.play_card(h, h.hand[0])
|
g.play_card(h, h.hand[0])
|
||||||
check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1")
|
check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1")
|
||||||
g.deck = DeckGen.deck(g.rng)
|
g.deck = DeckGen.deck(g.rng)
|
||||||
|
h.offer = g.draw_cards(Game.OFFER)
|
||||||
|
h.drew = true # das erste Ziehen wäre ohnehin frei
|
||||||
var mana := h.energy
|
var mana := h.energy
|
||||||
check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
|
check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
|
||||||
for perk in Game.PERKS:
|
for perk in Game.PERKS:
|
||||||
@@ -784,6 +804,7 @@ func test_snapshot() -> void:
|
|||||||
h.energy = 5
|
h.energy = 5
|
||||||
o.energy = 5
|
o.energy = 5
|
||||||
g.deck = DeckGen.deck(g.rng)
|
g.deck = DeckGen.deck(g.rng)
|
||||||
|
h.offer = g.draw_cards(Game.OFFER)
|
||||||
g.draw(h)
|
g.draw(h)
|
||||||
var snap := g.snapshot(hi)
|
var snap := g.snapshot(hi)
|
||||||
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
||||||
@@ -793,6 +814,8 @@ func test_snapshot() -> void:
|
|||||||
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl")
|
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl")
|
||||||
var cr2 := g2.find_creature(h.board[0].uid)
|
var cr2 := g2.find_creature(h.board[0].uid)
|
||||||
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and not cr2.ready, "Wesen mit Besitzer und Zustand")
|
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and not cr2.ready, "Wesen mit Besitzer und Zustand")
|
||||||
|
check(int(g2.players[hi].played["Feuer"]) == C("f_wolf").cost, "ausgespieltes Elementmana reist mit")
|
||||||
|
check(g2.players[hi].offer.size() == Game.OFFER and snap.players[oi].offer.is_empty(), "das eigene Angebot reist mit, das des Gegners bleibt verborgen")
|
||||||
var troll2 := g2.find_creature(troll.uid)
|
var troll2 := g2.find_creature(troll.uid)
|
||||||
check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit")
|
check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit")
|
||||||
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")
|
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")
|
||||||
|
|||||||
104
tests/ui_test.gd
104
tests/ui_test.gd
@@ -16,6 +16,8 @@ func check(cond: bool, msg: String) -> void:
|
|||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
printerr("FEHLER: " + msg)
|
printerr("FEHLER: " + msg)
|
||||||
|
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
|
||||||
|
get_tree().quit(1)
|
||||||
|
|
||||||
|
|
||||||
func frames(n: int = 2) -> void:
|
func frames(n: int = 2) -> void:
|
||||||
@@ -54,6 +56,21 @@ func right_click() -> void:
|
|||||||
await right_click_at(Vector2(5, 5))
|
await right_click_at(Vector2(5, 5))
|
||||||
|
|
||||||
|
|
||||||
|
func double_click_at(pos: Vector2) -> void:
|
||||||
|
var down := InputEventMouseButton.new()
|
||||||
|
down.position = pos
|
||||||
|
down.global_position = pos
|
||||||
|
down.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
down.pressed = true
|
||||||
|
down.double_click = true
|
||||||
|
get_viewport().push_input(down)
|
||||||
|
var up: InputEventMouseButton = down.duplicate()
|
||||||
|
up.pressed = false
|
||||||
|
up.double_click = false
|
||||||
|
get_viewport().push_input(up)
|
||||||
|
await frames(2)
|
||||||
|
|
||||||
|
|
||||||
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
|
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
|
||||||
var down := InputEventMouseButton.new()
|
var down := InputEventMouseButton.new()
|
||||||
down.position = pos
|
down.position = pos
|
||||||
@@ -150,12 +167,11 @@ func _ready() -> void:
|
|||||||
main = load("res://scenes/main.tscn").instantiate()
|
main = load("res://scenes/main.tscn").instantiate()
|
||||||
main.save_path = SAVE
|
main.save_path = SAVE
|
||||||
add_child(main)
|
add_child(main)
|
||||||
|
main.sfx.muted = true # Tests bleiben still
|
||||||
await frames(3)
|
await frames(3)
|
||||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
|
||||||
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
||||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
|
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
|
||||||
if not fast:
|
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
||||||
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
|
||||||
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
||||||
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
||||||
if OS.get_environment("DINGER_NETZ") != "":
|
if OS.get_environment("DINGER_NETZ") != "":
|
||||||
@@ -280,13 +296,19 @@ func human_turn(scr) -> void:
|
|||||||
break
|
break
|
||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
var hand: int = g.human.hand.size()
|
var hand: int = g.human.hand.size()
|
||||||
await click(scr.deck)
|
var cost: int = g.draw_cost(g.human)
|
||||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
|
await click(scr.offer_views[0])
|
||||||
|
check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
|
||||||
|
var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen
|
||||||
for i in 12:
|
for i in 12:
|
||||||
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
|
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack() and not (v.creature in tried))
|
||||||
if ready.is_empty() or g.winner != "":
|
if ready.is_empty() or g.winner != "":
|
||||||
break
|
break
|
||||||
var cr: Game.Creature = ready[0].creature
|
var cr: Game.Creature = ready[0].creature
|
||||||
|
var any_target: bool = g.can_attack(cr, g.opponent)[0] or g.opponent.board.any(func(c): return g.can_attack(cr, c)[0])
|
||||||
|
if not any_target:
|
||||||
|
tried.append(cr)
|
||||||
|
continue
|
||||||
await settle(scr)
|
await settle(scr)
|
||||||
await hover(ready[0])
|
await hover(ready[0])
|
||||||
var under = get_viewport().gui_get_hovered_control()
|
var under = get_viewport().gui_get_hovered_control()
|
||||||
@@ -333,7 +355,7 @@ func test_gegnerauswahl() -> void:
|
|||||||
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
||||||
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar")
|
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar")
|
||||||
await shot("05_starthand")
|
await shot("05_starthand")
|
||||||
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
|
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben")
|
||||||
await shot("11_kampf")
|
await shot("11_kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
@@ -479,7 +501,7 @@ func test_sprache() -> void:
|
|||||||
await frames(2)
|
await frames(2)
|
||||||
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
|
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
|
||||||
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
|
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
|
||||||
check(TranslationServer.translate("Zug %d · Du bist dran") == "Turn %d · Your turn" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt")
|
check(TranslationServer.translate("Abwerfen +%d ◆") == "Discard +%d ◆" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt")
|
||||||
check(find_button("Back") != null, "Knöpfe übersetzt")
|
check(find_button("Back") != null, "Knöpfe übersetzt")
|
||||||
await shot("26_englisch")
|
await shot("26_englisch")
|
||||||
await click(find_button("Back"))
|
await click(find_button("Back"))
|
||||||
@@ -506,12 +528,13 @@ func test_kampf_steuerung() -> void:
|
|||||||
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
|
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
|
||||||
g.human.hand = [Cards.by_id("f_wolf").copy()]
|
g.human.hand = [Cards.by_id("f_wolf").copy()]
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
|
check(scr.hand_views.size() == 1 and scr.draw_enabled and scr.offer_views.size() == Game.OFFER, "eine Handkarte, drei offene auf dem Stapel")
|
||||||
# Ziehen: 2 Mana weg, eine Karte landet auf der Hand
|
# Ziehen: die gewählte offene Karte landet auf der Hand, das erste Ziehen ist frei
|
||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
await click(scr.deck)
|
var wanted: Cards.Card = g.human.offer[1]
|
||||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == 2 and scr.hand_views.size() == 2, "gezogen: eine Karte auf der Hand")
|
await click(scr.offer_views[1])
|
||||||
check(g.human.hand[1].cost <= g.human.max_energy, "nur bezahlbare Karten")
|
check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.hand[1].id == wanted.id, "gezogen: die gewählte Karte, frei")
|
||||||
|
check(g.human.offer.size() == Game.OFFER and g.human.offer.all(func(c): return c.cost <= g.human.max_energy) and g.draw_cost(g.human) == Game.DRAW_COST, "eine bezahlbare neue liegt nach, das nächste Ziehen kostet 2")
|
||||||
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
|
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
|
||||||
var board_before: int = g.human.board.size()
|
var board_before: int = g.human.board.size()
|
||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
@@ -530,7 +553,7 @@ func test_kampf_steuerung() -> void:
|
|||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
var hand_before: int = g.human.hand.size()
|
var hand_before: int = g.human.hand.size()
|
||||||
await key(KEY_Z)
|
await key(KEY_Z)
|
||||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht")
|
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht die erste offene Karte für 2")
|
||||||
await shot("22_kampf_steuerung")
|
await shot("22_kampf_steuerung")
|
||||||
# Gamepad: A auf dem Stapel zieht
|
# Gamepad: A auf dem Stapel zieht
|
||||||
await pad(JOY_BUTTON_DPAD_LEFT)
|
await pad(JOY_BUTTON_DPAD_LEFT)
|
||||||
@@ -571,21 +594,21 @@ func test_kampf_grenzen() -> void:
|
|||||||
var guard := 0
|
var guard := 0
|
||||||
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
await click(scr.deck)
|
await click(scr.offer_views[0])
|
||||||
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
||||||
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
|
check(not scr.draw_enabled, "bei voller Hand lässt sich nichts ziehen")
|
||||||
await click(scr.deck)
|
await click(scr.offer_views[0])
|
||||||
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
||||||
# Feld voll spielen
|
# Feld voll spielen
|
||||||
guard = 0
|
guard = 0
|
||||||
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
if g.human.hand.is_empty():
|
if g.human.hand.is_empty():
|
||||||
await click(scr.deck)
|
await click(scr.offer_views[0])
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
||||||
if g.human.hand.is_empty():
|
if g.human.hand.is_empty():
|
||||||
await click(scr.deck)
|
await click(scr.offer_views[0])
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
||||||
await shot("25_kampf_voll")
|
await shot("25_kampf_voll")
|
||||||
@@ -596,9 +619,10 @@ func test_kampf_grenzen() -> void:
|
|||||||
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
||||||
# Ohne Mana kein Ziehen
|
# Ohne Mana kein Ziehen
|
||||||
g.human.energy = 0
|
g.human.energy = 0
|
||||||
|
g.human.drew = true
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
check(not scr.deck.enabled, "ohne Mana kein Ziehen")
|
check(not scr.draw_enabled, "ohne Mana kein zweites Ziehen")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
@@ -613,6 +637,8 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
g.human.board.clear()
|
g.human.board.clear()
|
||||||
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
|
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
scr.opponent().played[e] = 0
|
||||||
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
|
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
|
||||||
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
|
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
|
||||||
g.next_uid += 1
|
g.next_uid += 1
|
||||||
@@ -620,7 +646,9 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
cr.hidden = hidden
|
cr.hidden = hidden
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
return cr
|
return cr
|
||||||
put.call(g.human, "f_wolf")
|
var wolf_cr: Game.Creature = put.call(g.human, "f_wolf")
|
||||||
|
wolf_cr.health = 4 # übersteht die Konter von Falke und Kobold, damit der Test ihn weiter nutzen kann
|
||||||
|
wolf_cr.max_health = 4
|
||||||
put.call(g.human, "f_asche")
|
put.call(g.human, "f_asche")
|
||||||
put.call(g.human, "w_reiter")
|
put.call(g.human, "w_reiter")
|
||||||
var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
|
var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
|
||||||
@@ -631,21 +659,25 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
g.apply_auras()
|
g.apply_auras()
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(2)
|
await frames(2)
|
||||||
check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen, Feuerpaar: " + scr.element_view.text())
|
check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0 · Erde 0/0 · Luft 0/0", "Zähler: offene eigene Wesen und ausgespielte Karten, Feuerpaar: " + scr.element_view.text())
|
||||||
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 1, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; der Kobold würde den Stapel voll machen")
|
check(scr.opp_element_view.text() == "Feuer 0/0 · Wasser 0/0 · Erde 0/0 · Luft 1/0", "die Leiste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text())
|
||||||
|
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; noch kein Bonus in Sicht")
|
||||||
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
|
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
|
||||||
await hover(scr.hand_views[0])
|
await hover(scr.hand_views[0])
|
||||||
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
|
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
|
||||||
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
|
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen")
|
||||||
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
|
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
|
||||||
await shot("23_kartenmenue")
|
await shot("23_kartenmenue")
|
||||||
await right_click()
|
await right_click()
|
||||||
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
|
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
|
||||||
|
await right_click_at(Vector2(1150, 300)) # leere Fläche
|
||||||
|
check(scr.menu_layer.visible and find_button("Zug beenden", scr.menu_layer) != null, "Rechtsklick ins Leere bietet Zug beenden an")
|
||||||
|
await right_click()
|
||||||
# Mittlere Maustaste legt die Handkarte verdeckt
|
# Mittlere Maustaste legt die Handkarte verdeckt
|
||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
|
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
|
||||||
var mine_hidden: Game.Creature = g.human.board[3]
|
var mine_hidden: Game.Creature = g.human.board[3]
|
||||||
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2"), "mittlere Maustaste legt verdeckt, zählt nicht")
|
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2/1"), "mittlere Maustaste legt verdeckt: zählt als ausgespielt, nicht als offen")
|
||||||
# Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü
|
# Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü
|
||||||
await right_click_at(scr.own_views[0].get_global_rect().get_center())
|
await right_click_at(scr.own_views[0].get_global_rect().get_center())
|
||||||
check(not scr.menu_layer.visible, "kein Menü ohne Einträge")
|
check(not scr.menu_layer.visible, "kein Menü ohne Einträge")
|
||||||
@@ -656,10 +688,15 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
|
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
|
||||||
mine_hidden.ready = true
|
mine_hidden.ready = true
|
||||||
await click(scr.own_views[3])
|
await click(scr.own_views[3])
|
||||||
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (+1/+1)"), "aufgedeckt: drei Feuer, +1/+1: " + scr.element_view.text())
|
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text())
|
||||||
check(wolf.power() == 4 and falke.power() == 3 and mine_hidden.can_attack(), "Feuerwesen +1, kein Malus für Luft, aufgedeckt greift sofort an")
|
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter 10 Mana kein Bonus, aufgedeckt greift sofort an")
|
||||||
|
g.human.played["Feuer"] = 10
|
||||||
|
g.apply_auras()
|
||||||
|
scr.refresh()
|
||||||
|
await frames(1)
|
||||||
|
check(scr.element_view.text().begins_with("Feuer 3/10 (Doppelschlag) (+1)") and wolf.power() == 4 and falke.power() == 3, "10 Mana Feuer: +1 Angriff für die eigenen Feuerwesen: " + scr.element_view.text())
|
||||||
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
|
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
|
||||||
await shot("23b_drei_feuer")
|
await shot("23b_vier_feuer")
|
||||||
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
||||||
await click(scr.own_views[0])
|
await click(scr.own_views[0])
|
||||||
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
|
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
|
||||||
@@ -671,7 +708,8 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
|||||||
await click(scr.tiles[secret.uid])
|
await click(scr.tiles[secret.uid])
|
||||||
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
|
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
|
||||||
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
|
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
|
||||||
await right_click()
|
await double_click_at(scr.own_views[0].get_global_rect().get_center())
|
||||||
|
check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text)
|
||||||
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
|
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
|
||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
var before: int = g.human.board.size()
|
var before: int = g.human.board.size()
|
||||||
@@ -936,6 +974,14 @@ func test_kampf(shot_turn: int) -> void:
|
|||||||
check(g.current() == g.human, "Spieler ist dran")
|
check(g.current() == g.human, "Spieler ist dran")
|
||||||
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
|
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
|
||||||
await shot("07_kampf_start")
|
await shot("07_kampf_start")
|
||||||
|
# Kurze Partie: einfache KI, Gegner fast tot, drei bereite Drachen räumen auf
|
||||||
|
scr.smart_ai = false
|
||||||
|
g.opponent.life = 1
|
||||||
|
for i in 3:
|
||||||
|
var drache := Game.Creature.new(Cards.by_id("f_drache").copy(), g.human, 9100 + i)
|
||||||
|
drache.ready = true
|
||||||
|
g.human.board.append(drache)
|
||||||
|
scr.refresh()
|
||||||
var coins: int = main.profile.coins
|
var coins: int = main.profile.coins
|
||||||
var guard := 0
|
var guard := 0
|
||||||
while g.winner == "" and guard < 60:
|
while g.winner == "" and guard < 60:
|
||||||
|
|||||||
@@ -36,13 +36,10 @@ Gesperrt,Locked
|
|||||||
Spiel eröffnen,Host game
|
Spiel eröffnen,Host game
|
||||||
Beitreten,Join
|
Beitreten,Join
|
||||||
Du bist dran,Your turn
|
Du bist dran,Your turn
|
||||||
Gegnerzüge überspringen,Skip AI turns
|
|
||||||
Rückgängig,Undo
|
Rückgängig,Undo
|
||||||
Kapitel 1,Chapter 1
|
Kapitel 1,Chapter 1
|
||||||
Kapitel 2,Chapter 2
|
Kapitel 2,Chapter 2
|
||||||
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
|
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
|
||||||
Zug %d · Du bist dran,Turn %d · Your turn
|
|
||||||
Zug %d · %s ist dran …,Turn %d · %s is playing …
|
|
||||||
Wähle ein Angriffsziel,Choose an attack target
|
Wähle ein Angriffsziel,Choose an attack target
|
||||||
Abgebrochen,Cancelled
|
Abgebrochen,Cancelled
|
||||||
%s kann nicht angreifen,%s cannot attack
|
%s kann nicht angreifen,%s cannot attack
|
||||||
@@ -111,9 +108,8 @@ Verdeckt legen ◆ %d,Set face-down ◆ %d
|
|||||||
Aufdecken,Reveal
|
Aufdecken,Reveal
|
||||||
verdeckt,face-down
|
verdeckt,face-down
|
||||||
Aufgedeckt!,Revealed!
|
Aufgedeckt!,Revealed!
|
||||||
Elementbonus +%d/+%d,Element bonus +%d/+%d
|
Elementbonus +%d,Element bonus +%d
|
||||||
Merkmal +%d,Perk +%d
|
Merkmal +%d,Perk +%d
|
||||||
nächster Zug ◆ %d,next turn ◆ %d
|
|
||||||
Erst die Wesen,Creatures first
|
Erst die Wesen,Creatures first
|
||||||
%s liegt verdeckt,%s is face-down
|
%s liegt verdeckt,%s is face-down
|
||||||
%s liegt schon offen,%s is already face-up
|
%s liegt schon offen,%s is already face-up
|
||||||
@@ -154,3 +150,6 @@ greift erst im nächsten Zug an,attacks next turn
|
|||||||
%s durch das Element,%s from its element
|
%s durch das Element,%s from its element
|
||||||
Zwei verschiedene Karten wählen,Pick two different cards
|
Zwei verschiedene Karten wählen,Pick two different cards
|
||||||
stirbt,dies
|
stirbt,dies
|
||||||
|
Abwerfen +%d ◆,Discard +%d ◆
|
||||||
|
Überspringen,Skip
|
||||||
|
Zug %d,Turn %d
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user