update
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped

This commit is contained in:
team3 committed 2026-10-14 17:35:09 +02:00
1 parent f2c609a08b
commit 1d563b9eec
34 files changed
+5486 -643

No files matched your search

+14 -11
View File
@@ -20,22 +20,25 @@ Der Spielstand liegt unter `user://spielstand.json`
## Spielsysteme ## Spielsysteme
- 150 Karten in `data/karten.json`: je Element und neutral alle Werte von 0/1 bis 5/5, die ID verrät sie (`f_3_2` = Feuer 3/2, `n_…` neutral), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen. Neutrale Karten sind nie im Vorteil oder Nachteil, bilden kein Paar, haben aber ihren eigenen Zähler (+1/+1 je drei); verschmolzen mit einem Element (Feuerritter …) bleibt dessen Vorteil, die Schwäche fällt weg - 550 Karten in `data/karten.json`: je Element und neutral alle Werte von 0/1 bis 10/10, die ID verrät sie (`f_3_2` = Feuer 3/2, `n_…` neutral), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen. Neutrale Karten sind nie im Vorteil oder Nachteil, bilden kein Paar, haben aber ihren eigenen Zähler (+1/+1 je drei); verschmolzen mit einem Element (Feuerritter …) bleibt dessen Vorteil, die Schwäche fällt weg
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts) - Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts)
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen und Legen - Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen und Legen
- Jeder spielt mit seinem eigenen Stapel aus 40 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, „Auffüllen“ nimmt die wertvollsten des gewählten Filters, bei gleichem Wert die teuersten; „Stärke“ ist der mittlere Kartenwert: ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100, also 100 für Grundkarten bis 500 für Ultimate Rare). Zu Beginn die obersten fünf Karten des gemischten Stapels. Ziehen bringt eine Karte, die erste im Zug ist frei, dann 1, 2, 3 … Mana; ist der Stapel leer, kostet jeder Zug 1, 2, 3 … Leben (Erschöpfung); eine Handkarte abwerfen bringt 1 Mana (Rechtsklick) - Jeder spielt mit seinem eigenen Stapel aus 30 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, mit Element- und Manafilter, Manakurve und Elementanteilen; „Auffüllen“ nimmt die wertvollsten des gewählten Filters, bei gleichem Wert die teuersten; „Stärke“ ist der mittlere Kartenwert: ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100, also 100 für Grundkarten bis 500 für Ultimate Rare). Zu Beginn die obersten fünf Karten des gemischten Stapels. Ziehen bringt eine Karte, die erste im Zug ist frei, dann 1, 2, 3 … Mana; ist der Stapel leer, kostet jeder Zug 1, 2, 3 … Leben (Erschöpfung); 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, 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 - Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
- Beim ersten Start wählt man eines von vier Startdecks (40 Karten: 25 eines Elements bis Angriff 4, je fünf billige der anderen), der Rest kommt aus Boostern oder der Werkstatt. Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare) und ist je Stufe +1/+1 stärker, bei gleichen Kosten; die Stufe zeigt sich auch in der Optik (Glanz, Holo, Relief, Funkeln). Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern - Man startet mit allen 110 neutralen Karten in der Sammlung, der Stapel sind 30 davon nach Manakurve (2 zu 0 Mana, 5 zu 1, 6 zu 2, 6 zu 3, 5 zu 4, 3 zu 5, 2 zu 6, 1 zu 7); die Elemente schaltet die Kampagne frei: mit jedem offenen Kapitel gibt es den Booster seines Elements, die Werkstatt bringt den Rest; Sammlung, Deckfilter und Booster zeigen nur Elemente offener Kapitel. Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare) und ist je Stufe +1/+1 stärker, bei gleichen Kosten; die Stufe zeigt sich auch in der Optik (Glanz, Holo, Relief, Funkeln). Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (45/25/15/10/5 %, mindestens eine Rare); neue kommen in Sammlung und Stapel, eine höhere Stufe ersetzt die alte Version (die wird zu Staub), der Rest wird zu Staub (50/100/200/500/1000). Werkstatt in der Sammlung: Aufwerten kostet 100/200/500/1000 Staub, Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub - Booster: neutral 100 Münzen, je Element 200 (ab seinem Kapitel), einzeln oder fünf und zehn auf einmal; fünf Karten des Elements mit gewürfelter Stufe (45/25/15/10/5 %, mindestens eine Rare); neue kommen in Sammlung und Stapel, eine höhere Stufe ersetzt die alte Version (die wird zu Staub), der Rest wird zu Staub (50/100/200/500/1000). Werkstatt in der Sammlung: Aufwerten kostet 100/200/500/1000 Staub, Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
- 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 drei ausgespielte Karten eines Elements geben den eigenen Wesen dieses Elements +1/+1 (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Die Zähler stehen links neben dem jeweiligen Brett und zeigen nur Elemente, bei denen etwas passiert ist: zwei Symbole, gefüllt je offenem eigenen Wesen, und die Zahl der ausgespielten Karten - Elementpaare, 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); verdeckte zählen nicht. Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte. Die Liste links neben dem jeweiligen Brett zeigt nur Elemente mit offenen Wesen: zwei Symbole, gefüllt je offenem eigenen Wesen, und die Zahl der Wesen
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, schlägt durch den Stapelbonus des Ziels (so viel Schaden mehr, wie das Ziel an +x/+x trägt); wer im Nachteil ist, schlägt ohne seinen eigenen Stapelbonus zu; beim Angriff wie beim Konter, gegen Helden nichts; ohne Stapelbonus bewirkt das Element 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 doppelten Schaden; wer im Nachteil ist, halben (aufgerundet); beim Angriff wie beim Konter, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht, mit Neutral verschmolzen ist es nie im Nachteil; das Boss-Merkmal „kein Elementnachteil“ hebt den Nachteil auf. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn; ü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, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch. Den Zug beendet die Leertaste oder der Rechtsklick ins Leere; Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden) - 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. Den Zug beendet die Leertaste oder der Rechtsklick ins Leere; Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden)
- Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre - Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 7 Karten, das Feld 5 Wesen - Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; keine Grenze für Handkarten, das Feld fasst 5 Wesen
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach - Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 Sekunden; ein Sieg verdoppelt alles; das Ergebnisbild schlüsselt es auf. In der Kampagne ohne Schwierigkeitsfaktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf
- Erfahrung und Skilltree (Menü „Skilltree“): jede verdiente Münze zählt als Erfahrung; Level n braucht 50·n·(n+1) EP (100, 300, 600 …), jedes Level bringt einen Skillpunkt, Münzen kaufen keine Punkte. Drei Äste: Sammeln (Packglück: Common −8, die höheren Stufen +3/+2/+2/+1 Prozentpunkte je Stufe, bis 3; Staubsammler: +25 % Staub je Stufe, bis 3), Wirtschaft (Münzsegen: +10 % Münzen je Stufe, bis 3; Händler: Booster 10 % billiger je Stufe, bis 2), Kampf (Freies Ziehen: auch das zweite Ziehen je Zug gratis; Starker Start: 3 statt 1 Mana im ersten Zug; beide wirken wie die gleichnamigen Merkmale). Jede Skillkarte zeigt die Wirkung jetzt und auf der nächsten Stufe in Zahlen
- Cheats (kleiner Knopf unten rechts im Hauptmenü): Münzen, Staub, Level, Skills, Kapitel und Sammlung direkt setzen, zum Ausprobieren
- Schwierigkeit Leicht/Normal/Schwer (Einzelspieler): der Gegner spielt 30 Karten eines Elements aus den offenen Kapiteln nach Manakurve, seine Karten eine Stufe unter deiner Deckstärke, auf ihr oder eine darüber (Stärke 100 → Common … 500 → Ultimate Rare); Leicht mit einfacher KI, Schwer mit einem zufälligen Merkmal; 30 Leben wie du; Münzen 1/1,5/2-fach
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread - KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen und eigenen Stapeln (Kapitel 1 bis Kosten 3, Kapitel 2 bis 5 als Rare, Kapitel 3 alles als Super Rare), eigenes Merkmal nach Kapitel 1 - Kampagne: fünf Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 spielt neutrale Karten, Kapitel 2 bis 5 Feuer, Wasser, Luft, Erde; jeder Gegner spielt 30 Karten seines Elements nach Manakurve, der Kostendeckel steigt je Stufe von 3 auf 10 (Boss), in Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher (Kapitel 5 Ultra Rare); ein Merkmal je Gegner, ab Kapitel 3 zwei, in Kapitel 5 drei; eigenes Merkmal nach Kapitel 1
- 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; Ton und Musik sind standardmäßig aus - Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen; Ton und Musik sind standardmäßig aus
@@ -74,6 +77,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
- `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Stapel, Booster, Werkstatt, Einstellungen - `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Stapel, Booster, Werkstatt, Einstellungen
- `scripts/campaign.gd` – Kampagnenstufen - `scripts/campaign.gd` – Kampagnenstufen
- `scenes/main.tscn` – Startszene - `scenes/main.tscn` – Startszene
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `element_view.gd` den Elementzähler, `battle_layout.gd` die Aufteilung des Kampfes - `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `element_view.gd` die Elementpaare, `battle_layout.gd` die Aufteilung des Kampfes
- `translations/ui.csv` – englische Oberflächentexte - `translations/ui.csv` – englische Oberflächentexte
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs - `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
+9 -7
View File
@@ -6,15 +6,17 @@
- Kartenspiel wie zum Beispiel Magic - Kartenspiel wie zum Beispiel Magic
- PC-Spiel - PC-Spiel
- Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln - Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit - Einzelspieler-Knopf führt zur Wahl der Schwierigkeit: der Gegner spielt ein Element aus den offenen Kapiteln, Karten eine Stufe unter deiner Deckstärke / auf ihr / eine darüber; Leicht einfache KI, Schwer ein zufälliges Merkmal; Münzen ×1/×1,5/×2
- Kampagne gegen benannte Magier als Fortschritt - Münzen nach jedem Kampf: 5 je eigenem Zug, 5 je ausgespielter Karte, 5 je Zug unter 15 s; ein Sieg verdoppelt; Kampagne ohne Faktor, der erste Sieg einer Stufe bringt ihre Belohnung obendrauf
- Erfahrung = verdiente Münzen; Level n braucht 50·n·(n+1) EP, je Level ein Skillpunkt (nicht kaufbar); Skilltree mit drei Ästen: Sammeln (Packglück, Staubsammler), Wirtschaft (Münzsegen, Händler), Kampf (Freies Ziehen, Starker Start als Merkmale)
- Kampagne: fünf Kapitel mit je zehn Magiern, der zehnte ist der Boss; Kapitel 1 neutral, dann Feuer, Wasser, Luft, Erde; Gegner spielen 30 Karten ihres Elements nach Manakurve, Kostendeckel je Stufe 3 bis 10, Kapitel 1 und 2 Common, dann je Kapitel eine Stufe höher (Kapitel 5 Ultra Rare); ein offenes Kapitel schaltet den Booster seines Elements frei (neutral 100 Münzen, Element 200)
## Sammeln ## Sammeln
- Man startet mit einem von vier Startdecks (40 Karten: 25 eines Elements bis Angriff 4, je fünf billige der anderen); alle Karten sammeln ist das lange Ziel - Man startet mit allen 110 neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel; die Elemente kommen über die Kampagne und ihre Booster; alle Karten sammeln ist das lange Ziel
- Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare); je Stufe +1/+1 bei gleichen Kosten, dazu Optik (Glanz, Holo, Relief, Funkeln); Fusionen tragen die höhere Stufe der Eltern - Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare); je Stufe +1/+1 bei gleichen Kosten, dazu Optik (Glanz, Holo, Relief, Funkeln); Fusionen tragen die höhere Stufe der Eltern
- Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000) - Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000)
- Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub - Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
- Jeder hat sein Deck: 40 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter, wertvollste zuerst; Stärke = mittlerer Kartenwert ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100: 100 bis 500); zu Beginn die obersten fünf Karten daraus; Ziehen bringt eine Karte, die erste im Zug frei, dann 1, 2, 3 … Mana; leerer Stapel: jeder Zug kostet 1, 2, 3 … Leben - Jeder hat sein Deck: 30 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter, wertvollste zuerst; Stärke = mittlerer Kartenwert ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100: 100 bis 500); zu Beginn die obersten fünf Karten daraus; Ziehen bringt eine Karte, die erste im Zug frei, dann 1, 2, 3 … Mana; leerer Stapel: jeder Zug kostet 1, 2, 3 … Leben
- Mana für Ziehen und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für Handkarten - Mana für Ziehen und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für 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
@@ -23,10 +25,10 @@
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen - Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
## Elemente ## Elemente
- Jede Karte gehört zu genau einem Element oder ist neutral; je Element und neutral gibt es alle Werte von 0/1 bis 5/5 (150 Karten). Neutral: nie im Vorteil oder Nachteil, kein Paar, aber ein eigener Zähler (+1/+1 je drei); verschmolzen nimmt das Wesen im schlechten Matchup die neutrale Seite - Jede Karte gehört zu genau einem Element oder ist neutral; je Element und neutral gibt es alle Werte von 0/1 bis 10/10 (550 Karten). Neutral: nie im Vorteil oder Nachteil, kein Paar; verschmolzen nimmt das Wesen im schlechten Matchup die neutrale Seite
- 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)
- Je drei ausgespielte Karten eines Elements: +1/+1 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 - Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte; verdeckte zählen nicht fürs Paar
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, schlägt durch den Stapelbonus des Ziels: so viel Schaden mehr, wie das Ziel an +x/+x trägt; wer im Nachteil ist, schlägt ohne seinen eigenen Stapelbonus zu; beim Angriff wie beim Konter; ohne Stapelbonus bewirkt das Element nichts; 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 doppelten Schaden, wer im Nachteil ist, halben, aufgerundet; beim Angriff wie beim Konter; gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht; Boss-Merkmal „kein Elementnachteil“)
- Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel und wird bestätigt oder abgebrochen (Gamepad: über das Kartenmenü); kostet kein Mana; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand; je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus, dann die höhere Stufe der Eltern (+1/+1 je Stufe); Elemente und Eigenschaften beider - Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel und wird bestätigt oder abgebrochen (Gamepad: über das Kartenmenü); kostet kein Mana; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand; je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus, dann die höhere Stufe der Eltern (+1/+1 je Stufe); Elemente und Eigenschaften beider
- 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
+4000
View File
File diff suppressed because it is too large. Load diff
+26 -15
View File
@@ -27,8 +27,7 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
der Platz erlaubt, unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten der Platz erlaubt, unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten
haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die
Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration, Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration,
gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt), rechts der gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt).
Elementbonus.
- Die Hand sind alle Karten in Feldgröße, ab sechs überlappend; Details kommen aus der schwebenden Vorschau. - Die Hand sind alle Karten in Feldgröße, ab sechs überlappend; Details kommen aus der schwebenden Vorschau.
- Seltenheit nach Yu-Gi-Oh: Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild - Seltenheit nach Yu-Gi-Oh: Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild
(wandernder Regenbogenschimmer im Bildkreis); Ultra Rare golden glänzender Name plus Holo-Bild; (wandernder Regenbogenschimmer im Bildkreis); Ultra Rare golden glänzender Name plus Holo-Bild;
@@ -55,9 +54,9 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
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.
- **Elementzähler** je Spieler als Liste links neben seinem Brett, nur Elemente mit Wesen oder gespielten Karten. - **Elementpaare** je Spieler als Liste links neben seinem Brett, nur Elemente mit offenen Wesen.
Je Element zwei Symbole in seiner Form, gefüllt je offenem eigenen Wesen (beide voll: Sondereffekt), 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. daneben die Zahl der Wesen.
- **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.
@@ -68,8 +67,8 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
und Feldkombo darunter. und Feldkombo darunter.
- **Optionen** per Esc als Überlagerung: Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden; der Verlauf - **Optionen** per Esc als Überlagerung: Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden; der Verlauf
öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig. öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig.
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen - Beim Angreifen zeigt das Ziel seinen Konter mit Elementvorteil (doppelt oder halb), bei
steht der Elementkonter (der ignorierte Stapelbonus) am Ziel, bei Verdeckten nicht. Verdeckten nicht; über dem Ziel zeigt der Angreifer seinen Angriff mit Vorteil.
## Bewegung und Ton ## Bewegung und Ton
@@ -83,17 +82,29 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
## Menüs ## Menüs
- Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Einstellungen, - Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Skilltree,
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und in der Zeile darunter Einstellungen und Beenden, unten rechts der kleine Cheats-Knopf; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
Staub oben rechts. Staub oben rechts.
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter. - Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte - Kampagne als Pfad mit zehn Stationen je Kapitel (fünf Kapitel: Neutral, Feuer, Wasser, Luft,
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale Erde, das Element steht über dem Pfad), die nächste pulsiert; darunter die gewählte Stufe mit
stärker, nicht über Begleiter. Gegner, Leben, Belohnung und Merkmalen. Die Gegner werden über Leben, Merkmale und die
- Sammlung: sechs Spalten, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts Seltenheit ihrer Karten stärker, nicht über Begleiter.
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, nur eigene Karten, - Booster: je offenem Kapitel eine Pack-Kachel (Symbol, Name, Preis, ×1 ×5 ×10), mittig; gesperrte
Stapelkarten hell, die anderen abgedunkelt; Klick wechselt. Rechts die Kartenzahl und die Stärke gibt es nicht zu sehen. Die Karten fliegen aus dem Pack ins Raster (ein Pack fünf große, mehrere
(mittlerer Kartenwert, 100 für Grundkarten bis 500 für Ultimate Rare). zehn kleine je Reihe, beste Stufe zuerst) und liegen verdeckt; Aufdecken gestaffelt, seltene
blitzen auf, ab Ultra Rare mit Pause. Danach das Fazit „n neu · n aufgewertet · +n Staub“, das
gekaufte Pack pulsiert.
- Skilltree: oben Level, Erfahrung bis zum nächsten Level und freie Punkte, darunter drei Spalten
(Sammeln, Wirtschaft, Kampf) mit Skillkarten: Name, Wirkung, „Jetzt“ und „Nächste Stufe“ in
Zahlen, Stufe x / y und „Lernen“.
- Cheats (kleiner Knopf unten rechts im Hauptmenü): Stand oben, darunter Knopfreihen für Währung,
Erfahrung, Kampagne und Sammlung; jeder Knopf speichert sofort.
- Sammlung: sechs Spalten, nur Elemente offener Kapitel, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, Filter nach Element (Symbolknöpfe)
und Mana 0 bis 10, Stapelkarten zuerst und höchste Stufen vorn, Stapelkarten mit grünem Haken, die
anderen abgedunkelt; Klick wechselt. Rechts Kartenzahl, Stärke (mittlerer Kartenwert, 100 für Grundkarten bis 500 für
Ultimate Rare), Manakurve, Elementanteile und die Vorschau der gezeigten Karte.
## Grafik ## Grafik
+51 -58
View File
@@ -1,79 +1,72 @@
class_name Campaign class_name Campaign
## Kampagne: zehn benannte Magier in drei Kapiteln. Die Bosse haben weniger Leben als du, ## Kampagne: fünf Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt
## dafür Merkmale; je Kapitel kommt eins dazu. ## neutrale Karten, Kapitel 2 bis 5 je ein Element; jeder Gegner spielt 30 Karten seines Elements
## nach Manakurve (DeckGen.curve_deck) mit steigendem Kostendeckel (COST_CAP), in der Stufe seines Kapitels (Kapitel 1 und 2 Common, dann je Kapitel eine höher, Kapitel 5
## Ultra Rare). Mit jedem offenen
## Kapitel gibt es den Booster seines Elements zu kaufen (Profile.booster_unlocked).
const STAGES := [ const STAGES_PER_CHAPTER := 10
{"name": "Fenn", "title": "Kobold-Zähmer", "life": 22, "reward": 100, "perk": "feuer_angriff"}, const CHAPTER_ELEMENTS: Array[String] = [Cards.NEUTRAL, "Feuer", "Wasser", "Luft", "Erde"]
{"name": "Mara", "title": "Gezeitenhexe", "life": 22, "reward": 100, "perk": "leben_35"}, const NAMES: Array[String] = ["Fenn", "Mara", "Borin", "Liska", "Vulkar", "Nyx", "Ignis", "Thalassa", "Zephyra", "Archimagier"]
{"name": "Borin", "title": "Steinfaust", "life": 24, "reward": 150, "perk": "wesen_angriff"}, const TITLES := {
{"name": "Liska", "title": "Windläuferin", "life": 24, "reward": 150, "perk": "ziehen_gratis"}, "Neutral": ["Kobold-Zähmer", "Wanderhexe", "Steinfaust", "Pfadfinderin", "Söldnerfürst", "Schattenruferin", "Grauer Priester", "Händlerkönigin", "Stahlkaiserin", "Meister der Lehre"],
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"}, "Feuer": ["Flammenzähmer", "Glutweberin", "Schmiedefaust", "Funkenläuferin", "Herr der Lava", "Aschenruferin", "Inferno-Priester", "Glutkönigin", "Flammenkaiserin", "Meister des Feuers"],
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"}, "Wasser": ["Gezeitenzähmer", "Gezeitenhexe", "Eisfaust", "Nebelläuferin", "Herr der Brandung", "Regenruferin", "Tiefsee-Priester", "Tiefseekönigin", "Flutkaiserin", "Meister des Wassers"],
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"}, "Luft": ["Windzähmer", "Wolkenhexe", "Sturmfaust", "Windläuferin", "Herr der Böen", "Sturmruferin", "Himmelspriester", "Wolkenkönigin", "Blitzkaiserin", "Meister der Lüfte"],
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "alle_flink"}, "Erde": ["Wurzelzähmer", "Moorhexe", "Felsenfaust", "Hügelläuferin", "Herr der Schluchten", "Bebenruferin", "Erzpriester", "Kristallkönigin", "Bergkaiserin", "Meister der Erde"],
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "alle_flink"}, }
{"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"}, const LIFE := [22, 22, 24, 24, 24, 26, 26, 26, 28, 28]
] ## Höchste Kartenkosten im Stapel je Stufe eines Kapitels.
const COST_CAP: Array[int] = [3, 4, 4, 5, 5, 6, 7, 8, 9, 10]
## Merkmale je Stufe: das erste nach dieser Liste, weitere je drei Plätze dahinter.
const PERK_ORDER: Array[String] = ["feuer_angriff", "leben_35", "wesen_angriff", "ziehen_gratis", "wesen_leben", "extra_mana", "kein_nachteil", "alle_flink", "alle_flink", "start_energie"]
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
const STAGES2 := [
{"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
{"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]},
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "alle_flink"]},
{"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "wesen_angriff"]},
{"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
{"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]},
{"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]},
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["alle_flink", "leben_35"]},
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["alle_flink", "start_energie"]},
{"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]},
]
## Kapitel 3: drei Merkmale und volle Tische.
const STAGES3 := [
{"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "alle_flink"]},
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "alle_flink", "leben_35"]},
{"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "wesen_angriff", "extra_mana"]},
{"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]},
{"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]},
{"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]},
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["alle_flink", "leben_35", "element_doppelt"]},
{"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
{"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]},
]
const CHAPTERS := [STAGES, STAGES2, STAGES3]
static func total() -> int: static func total() -> int:
return STAGES.size() * CHAPTERS.size() return STAGES_PER_CHAPTER * CHAPTER_ELEMENTS.size()
static func chapter(stage: int) -> int: static func chapter(stage: int) -> int:
return stage / STAGES.size() + 1 return stage / STAGES_PER_CHAPTER + 1
## Stufe mit einheitlichem Feld "perks". static func element_of(chapter_no: int) -> String:
return CHAPTER_ELEMENTS[chapter_no - 1]
## Merkmale je Gegner: eins, ab Kapitel 3 zwei, in Kapitel 5 drei.
static func perk_count(chapter_no: int) -> int:
return (chapter_no + 1) / 2
## Stufe i: Name, Titel, Leben, Belohnung (Kapitel 1: 100 bis 300, Boss 500; je Kapitel 250 mehr),
## Merkmale und Element.
static func stage(i: int) -> Dictionary: static func stage(i: int) -> Dictionary:
if i < STAGES.size(): var c := chapter(i)
var st: Dictionary = STAGES[i].duplicate() var k := i % STAGES_PER_CHAPTER
st["perks"] = [st.perk] var perks: Array = []
return st for n in perk_count(c):
return CHAPTERS[chapter(i) - 1][i % STAGES.size()] perks.append(PERK_ORDER[(k + 3 * n) % STAGES_PER_CHAPTER])
var base := 100 + 250 * (c - 1)
return {"name": NAMES[k], "title": TITLES[element_of(c)][k], "life": LIFE[k],
"reward": base + (400 if k == STAGES_PER_CHAPTER - 1 else 50 * (k / 2)),
"perks": perks, "element": element_of(c), "max_cost": COST_CAP[k]}
## Boss einer Stufe mit dem Leben, den Merkmalen und einem Stapel der Stufe; ab Kapitel 2 sind ## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis
## seine Karten eine Seltenheitsstufe höher (+1/+1), ab Kapitel 3 zwei. ## max_cost, in der Stufe des Kapitels (rarity_of).
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player: static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player:
var st := stage(stage_index) var st := stage(stage_index)
var boss := Game.Player.new(st.name, false, int(st.life), st.title) var boss := Game.Player.new(st.name, false, int(st.life), st.title)
boss.perks = Array(st.perks) boss.perks = Array(st.perks)
if r == null: if r == null:
r = Game.make_rng() r = Game.make_rng()
var ch := chapter(stage_index) boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
boss.deck = DeckGen.opponent_deck(r, ch)
for id in boss.deck: for id in boss.deck:
boss.looks[id] = ch - 1 boss.looks[id] = rarity_of(chapter(stage_index))
return boss return boss
## Seltenheitsstufe der Gegnerkarten: Kapitel 1 und 2 Common, dann je Kapitel eine höher.
static func rarity_of(chapter_no: int) -> int:
return maxi(0, chapter_no - 2)
+61 -24
View File
@@ -1,7 +1,10 @@
class_name DeckGen class_name DeckGen
## Stapel: die vier Startdecks, Auffüllen aus der Sammlung, zufällige Stapel für Gegner. ## Stapel: Elementstapel nach Manakurve (Startstapel und Gegner), Auffüllen aus der Sammlung,
## zufällige Stapel für Tests und Simulation.
const CHEAP_IN_RANDOM := 12 # so viele im ersten Zug bezahlbare Karten hat ein zufälliger Stapel mindestens const CHEAP_IN_RANDOM := 12 # so viele im ersten Zug bezahlbare Karten hat ein zufälliger Stapel mindestens
## Manakurve eines Stapels mit DECK_MIN Karten: so viele Karten je Kosten 0 bis 10.
const CURVE: Array[int] = [2, 5, 6, 6, 5, 3, 2, 1, 0, 0, 0]
## Zufälliger Stapel aus DECK_MIN verschiedenen Karten bis max_cost, mit genug billigen für den Anfang. ## Zufälliger Stapel aus DECK_MIN verschiedenen Karten bis max_cost, mit genug billigen für den Anfang.
@@ -18,41 +21,75 @@ static func random_deck(r: RandomNumberGenerator, max_cost: int = 99) -> Array:
return out return out
## Stapel eines Kampagnengegners: Kapitel 1 bis Kosten 3, Kapitel 2 bis 5, Kapitel 3 alles. ## Alle Karten eines Elements (oder die neutralen) in Datenreihenfolge: die Startsammlung.
static func opponent_deck(r: RandomNumberGenerator, chapter: int) -> Array: static func element_deck(element: String) -> Array:
return random_deck(r, [3, 5, 99][clampi(chapter - 1, 0, 2)]) return Cards.all().filter(func(c): return c.elements[0] == element).map(func(c): return c.id)
## Startdeck für ein Element: die 25 Karten des Elements bis Angriff 4, dazu je fünf billige der ## Stapel eines Elements nach der Manakurve: DECK_MIN verschiedene Karten des Elements bis max_cost,
## anderen Elemente (DECK_MIN Karten). ## je Kosten so viele wie CURVE vorgibt, zufällig gewählt. Was über dem Deckel fehlt, füllen die
static func start_deck(element: String) -> Array: ## erlaubten Kosten reihum von billig nach teuer auf.
var out := [] static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int = 10) -> Array:
var by_cost := {}
for c in Cards.all(): for c in Cards.all():
if c.elements[0] == element and c.attack <= 4: if c.elements[0] == element and c.cost <= max_cost:
out.append(c.id) if not by_cost.has(c.cost):
for e in Cards.ELEMENTS: by_cost[c.cost] = []
if e == element: by_cost[c.cost].append(c.id)
continue for cost in by_cost:
var cheap := Cards.all().filter(func(c): return c.elements[0] == e and c.cost <= Game.START_MANA) Game.shuffle(by_cost[cost], r)
cheap.sort_custom(func(a, b): return a.cost < b.cost if a.cost != b.cost else a.attack > b.attack) var out := []
for c in cheap.slice(0, 5): for cost in CURVE.size():
out.append(c.id) for k in CURVE[cost]:
if by_cost.has(cost) and not by_cost[cost].is_empty():
out.append(by_cost[cost].pop_back())
while out.size() < Game.DECK_MIN:
var added := false
for cost in range(0, max_cost + 1):
if out.size() >= Game.DECK_MIN:
break
if by_cost.has(cost) and not by_cost[cost].is_empty():
out.append(by_cost[cost].pop_back())
added = true
if not added:
break
return out return out
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der ## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der Stufe
## Stufe aus levels: ID -> Stufe), bei gleichem Wert die teuersten; nichts doppelt. ## aus levels: ID -> Stufe); bei gleichem Wert zuerst, was der Manakurve (CURVE, auf limit
## skaliert) noch fehlt, dann die billigeren, dann nach ID; nichts doppelt.
static func fill(deck: Array, pool: Array, limit: int, levels: Dictionary = {}) -> Array: static func fill(deck: Array, pool: Array, limit: int, levels: Dictionary = {}) -> Array:
var out := deck.duplicate() var out := deck.duplicate()
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id).leveled(int(levels.get(id, 0)))) var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id).leveled(int(levels.get(id, 0))))
cards.sort_custom(func(a, b): cards.sort_custom(func(a, b):
if a.value() != b.value(): if a.value() != b.value():
return a.value() > b.value() return a.value() > b.value()
return a.cost > b.cost if a.cost != b.cost else a.id < b.id) return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
for c in cards: var have := {} # Kosten -> Karten im Stapel
if out.size() >= limit: for id in out:
break var slot := clampi(Cards.by_id(id).cost, 0, CURVE.size() - 1)
out.append(c.id) have[slot] = int(have.get(slot, 0)) + 1
var i := 0
while i < cards.size() and out.size() < limit:
var j := i
while j < cards.size() and cards[j].value() == cards[i].value():
j += 1
var rest := []
for c in cards.slice(i, j):
if out.size() >= limit:
break
var slot := clampi(c.cost, 0, CURVE.size() - 1)
if int(have.get(slot, 0)) < CURVE[slot] * limit / Game.DECK_MIN:
out.append(c.id)
have[slot] = int(have.get(slot, 0)) + 1
else:
rest.append(c)
for c in rest:
if out.size() >= limit:
break
out.append(c.id)
i = j
return out return out
+28 -85
View File
@@ -6,11 +6,10 @@ extends RefCounted
const MAX_BOARD := 5 const MAX_BOARD := 5
const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele) const DECK_MIN := 30 # Karten je Stapel: mindestens so viele (Gegner genau so viele: alle Karten eines Elements)
const DECK_MAX := 60 # und höchstens so viele const DECK_MAX := 60 # und höchstens so viele
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_COUNT := 3 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
# Merkmale der Kampagnen-Bosse (Player.perks). # Merkmale der Kampagnen-Bosse (Player.perks).
@@ -23,7 +22,7 @@ const PERKS := {
"leben_35": "Beginnt mit 5 Leben mehr", "leben_35": "Beginnt mit 5 Leben mehr",
"alle_flink": "Wesen greifen sofort an", "alle_flink": "Wesen greifen sofort an",
"start_energie": "Hat im ersten Zug 2 Mana mehr", "start_energie": "Hat im ersten Zug 2 Mana mehr",
"element_doppelt": "Elementbonus zählt doppelt", "kein_nachteil": "Wesen haben keinen Elementnachteil",
} }
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch). ## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
@@ -50,8 +49,7 @@ class Creature extends RefCounted:
var max_health: int var max_health: int
var attacks_done := 0 # Angriffe in diesem Zug var attacks_done := 0 # Angriffe in diesem Zug
var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementboni var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht fürs Elementpaar
var aura := 0 # Elementbonus (+1 je Element mit vollem Stapel); das Leben ist schon angepasst
func _init(c: Cards.Card, o, id: int) -> void: func _init(c: Cards.Card, o, id: int) -> void:
card = c card = c
@@ -90,9 +88,9 @@ class Creature extends RefCounted:
out.append(k) out.append(k)
return out return out
## Angriffswert inklusive Elementbonus und Boss-Merkmalen. ## Angriffswert inklusive Boss-Merkmalen.
func power() -> int: func power() -> int:
var v := attack + aura var v := attack
var o = owner() var o = owner()
if o != null: if o != null:
if "feuer_angriff" in o.perks and "Feuer" in card.elements: if "feuer_angriff" in o.perks and "Feuer" in card.elements:
@@ -118,7 +116,6 @@ class Creature extends RefCounted:
c.attacks_done = attacks_done c.attacks_done = attacks_done
c.ready = ready c.ready = ready
c.hidden = hidden c.hidden = hidden
c.aura = aura
return c return c
@@ -137,7 +134,7 @@ 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 -> bisher ausgespielte Karten (je ELEMENT_COUNT: +1/+1) var cards_played := 0 # ausgespielte Karten (Münzen nach dem Kampf)
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
@@ -147,8 +144,6 @@ 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
@@ -178,7 +173,7 @@ 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.cards_played = cards_played
p.deck = deck.duplicate() p.deck = deck.duplicate()
p.draws = draws p.draws = draws
p.fatigue = fatigue p.fatigue = fatigue
@@ -406,14 +401,12 @@ 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.cards_played += 1
p.played[e] = int(p.played.get(e, 0)) + 1
if face_down: if face_down:
say("%s legt ein Wesen verdeckt." % p.name) say("%s legt ein Wesen verdeckt." % p.name)
else: else:
say("%s spielt %s." % [p.name, card.name]) say("%s spielt %s." % [p.name, card.name])
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down}) emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down})
apply_auras()
cleanup() cleanup()
check_end() check_end()
return "" return ""
@@ -463,12 +456,11 @@ func reveal(p: Player, cr: Creature) -> String:
return "" return ""
## Deckt ein Wesen auf; die Elementboni gelten sofort. ## Deckt ein Wesen auf; das Elementpaar gilt sofort.
func _flip(cr: Creature) -> void: func _flip(cr: Creature) -> void:
cr.hidden = false cr.hidden = false
say("%s wird aufgedeckt." % cr.name) say("%s wird aufgedeckt." % cr.name)
emit({"type": "reveal", "uid": cr.uid, "card": cr.card}) emit({"type": "reveal", "uid": cr.uid, "card": cr.card})
apply_auras()
# --- Verschmelzen ------------------------------------------------------------ # --- Verschmelzen ------------------------------------------------------------
@@ -523,49 +515,6 @@ func fuse(p: Player, a, b) -> String:
return "" return ""
# --- Elementboni ---------------------------------------------------------------
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente
## zählen alle, auch Neutral (eigener Zähler, aber kein Paar).
static func aura_of(elements: Array, played: Dictionary) -> int:
var a := 0
for e in elements:
a += int(played.get(e, 0)) / ELEMENT_COUNT
return a
## Bonus eines offenen Wesens aus den ausgespielten Karten seines Besitzers; verdeckte bekommen nichts.
func aura_for(c: Creature) -> int:
var o = c.owner()
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
return a
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er
## (beim Verdecken), sinkt nur das Höchstleben, das Leben wird gekappt.
func apply_auras() -> void:
for p in players:
for c in p.board:
var a := 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 (sie zählt selbst mit).
func forecast(p: Player, card: Cards.Card) -> int:
var played := p.played.duplicate()
for e in card.elements:
played[e] = int(played.get(e, 0)) + 1
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
# --- Kampf ------------------------------------------------------------------- # --- Kampf -------------------------------------------------------------------
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott ## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott
@@ -582,7 +531,7 @@ func can_attack(cr: Creature, target) -> Array:
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 if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff macht auch im Vorteil nichts
return [false, "%s würde keinen Schaden machen" % cr.name] 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"]
@@ -609,17 +558,18 @@ func attack(cr: Creature, target) -> String:
return "" return ""
## Elementvorteil beim Zuschlagen und beim Konter: im Vorteil zählt der Stapelbonus des Ziels ## Elementvorteil beim Zuschlagen und beim Konter, als Verschiebung des Schadens: im Vorteil
## nicht (so viel Schaden mehr), im Nachteil der eigene nicht (so viel weniger). Gegen Helden ## doppelt (+ Angriff), im Nachteil halb, aufgerundet (− Angriff / 2). Gegen Helden gibt es keinen;
## gibt es keinen. ## mit dem Merkmal kein_nachteil entfällt der Nachteil.
static func matchup_bonus(source: Creature, target) -> int: static func matchup_bonus(source: Creature, target) -> int:
if not (target is Creature): if not (target is Creature):
return 0 return 0
var m := Cards.matchup(source.card.elements, target.card.elements) var m := Cards.matchup(source.card.elements, target.card.elements)
if m > 0: if m > 0:
return target.aura return source.power()
if m < 0: if m < 0:
return -source.aura var o = source.owner()
return 0 if o != null and "kein_nachteil" in o.perks else -(source.power() / 2)
return 0 return 0
@@ -634,24 +584,17 @@ func damage(target, amount: int) -> void:
emit({"type": "damage", "target": ref_of(target), "amount": amount}) emit({"type": "damage", "target": ref_of(target), "amount": amount})
## Räumt Tote ab; danach gelten die Elementboni neu, was weitere Tote nach sich ziehen kann. ## Räumt Tote ab; wer besiegt ist, nimmt seine Wesen mit vom Tisch.
func cleanup() -> void: func cleanup() -> void:
for round in 8: for p in players:
var died := false for c in p.board.duplicate():
for p in players: if c.health <= 0 or not p.alive():
for c in p.board.duplicate(): p.board.erase(c)
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch. if c.health > 0:
if c.health <= 0 or not p.alive(): say("%s verschwindet mit %s." % [c.name, p.name])
p.board.erase(c) else:
if c.health > 0: say("%s stirbt." % c.name)
say("%s verschwindet mit %s." % [c.name, p.name]) emit({"type": "death", "target": ref_of(c)})
else:
say("%s stirbt." % c.name)
emit({"type": "death", "target": ref_of(c)})
died = true
if not died:
return
apply_auras()
## Ende: einer von beiden hat kein Leben mehr. ## Ende: einer von beiden hat kein Leben mehr.
@@ -676,11 +619,11 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
arr[j] = tmp arr[j] = tmp
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}, "deck": [ids]} ## Duell gegen einen zufälligen Gegner (Tests, Simulation). rules: {"human_perks": [...], "looks": {...}, "deck": [ids]}
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game: static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
if r == null: if r == null:
r = make_rng() r = make_rng()
return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), r, false, rules) return new_battle_with(DeckGen.random_opponent(), r, false, rules)
static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game: static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
+185 -46
View File
@@ -1,24 +1,30 @@
class_name Profile class_name Profile
extends RefCounted extends RefCounted
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen. ## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die ## Man startet mit allen neutralen Karten (Stapel: 30 nach Manakurve); die Elemente schaltet die Kampagne frei (je Kapitel einen
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe ## Booster), die Werkstatt bringt den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird. ## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird.
const SAVE_VERSION := 7 const SAVE_VERSION := 8
const CRAFT_COST := 200 # Staub für eine neue Karte (Common) const CRAFT_COST := 200 # Staub für eine neue Karte (Common)
const SAVE_PATH := "user://spielstand.json" const SAVE_PATH := "user://spielstand.json"
const BOOSTER_COST := 100 const BOOSTER_COST := 100 # neutraler Booster
const ELEMENT_BOOSTER_COST := 200 # Booster eines Elements (ab seinem Kapitel)
const BOOSTER_SIZE := 5 const BOOSTER_SIZE := 5
const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0} const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0}
const START_COINS := 150 const START_COINS := 150
const REWARD_WIN := 100 # Münzen nach dem Kampf: je eigener Zug, je ausgespielte Karte und je Zug unter FAST_TURN_SEC
const REWARD_LOSS := 50 # Sekunden; ein Sieg verdoppelt alles, die Schwierigkeit multipliziert (siehe battle_coins).
# Schwierigkeit: Leben der Gegner, KI und Münzen. const COINS_TURN := 5
const COINS_CARD := 5
const COINS_FAST := 5
const FAST_TURN_SEC := 15.0
# Schwierigkeit im Einzelspieler: Stufe der Gegnerkarten relativ zu deiner Deckstärke (level),
# KI (smart: Vorausschau, sonst gierig), ein zufälliges Merkmal (perk) und der Münzfaktor.
const DIFFICULTIES := { const DIFFICULTIES := {
"leicht": {"name": "Leicht", "life": 25, "reward": 0.75, "smart": false}, "leicht": {"name": "Leicht", "level": -1, "smart": false, "perk": false, "reward": 1.0},
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true}, "normal": {"name": "Normal", "level": 0, "smart": true, "perk": false, "reward": 1.5},
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true}, "schwer": {"name": "Schwer", "level": 1, "smart": true, "perk": true, "reward": 2.0},
} }
# Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub. # Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub.
const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000} const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
@@ -26,6 +32,8 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
var coins := 0 var coins := 0
var dust := 0 var dust := 0
var xp := 0 # Erfahrung: alle je verdienten Münzen (Skills.level_for)
var skills := {} # Skill-ID -> gelernte Stufe (Skills.SKILLS)
var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES) var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES)
var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX) var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
@@ -34,24 +42,20 @@ var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfort
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1) var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start). ## Neues Profil: alle neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel.
static func starter() -> Profile: static func starter() -> Profile:
var p := Profile.new() var p := Profile.new()
p.coins = START_COINS p.coins = START_COINS
p.give_start()
return p return p
## Hat der Spieler schon ein Startdeck gewählt? ## Alle neutralen Karten in Common werden die Sammlung, ein Kurvenstapel daraus der Stapel.
func started() -> bool: func give_start() -> void:
return not collection.is_empty()
## Das Startdeck eines Elements wird Sammlung und Stapel.
func choose_start(element: String) -> void:
collection = {} collection = {}
deck = DeckGen.start_deck(element) for id in DeckGen.element_deck(Cards.NEUTRAL):
for id in deck:
collection[id] = 0 collection[id] = 0
deck = DeckGen.curve_deck(Cards.NEUTRAL, Game.make_rng())
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil. ## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
@@ -74,6 +78,11 @@ static func from_dict(d: Dictionary) -> Profile:
var p := Profile.new() var p := Profile.new()
p.coins = int(d.get("coins", 0)) p.coins = int(d.get("coins", 0))
p.dust = int(d.get("dust", 0)) p.dust = int(d.get("dust", 0))
p.xp = int(d.get("xp", 0))
var sk = d.get("skills", {})
for id in sk:
if Skills.SKILLS.has(id):
p.skills[id] = clampi(int(sk[id]), 0, int(Skills.SKILLS[id].max))
p.campaign_stage = int(d.get("campaign_stage", 0)) p.campaign_stage = int(d.get("campaign_stage", 0))
if int(d.get("version", 1)) >= SAVE_VERSION: if int(d.get("version", 1)) >= SAVE_VERSION:
var coll = d.get("collection", {}) var coll = d.get("collection", {})
@@ -85,6 +94,8 @@ static func from_dict(d: Dictionary) -> Profile:
if p.collection.has(cid) and not (cid in ids): if p.collection.has(cid) and not (cid in ids):
ids.append(cid) ids.append(cid)
p.deck = ids.slice(0, Game.DECK_MAX) p.deck = ids.slice(0, Game.DECK_MAX)
if p.collection.is_empty():
p.give_start()
var se = d.get("settings", {}) var se = d.get("settings", {})
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)), 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)),
@@ -99,8 +110,8 @@ static func from_dict(d: Dictionary) -> Profile:
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck, return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection,
"campaign_stage": campaign_stage, "settings": settings, "perk": perk} "deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk}
## Speichert und sichert den vorherigen Stand als .bak. ## Speichert und sichert den vorherigen Stand als .bak.
@@ -182,7 +193,7 @@ func dismantle(cid: String) -> String:
if not owns(cid): if not owns(cid):
return "Karte nicht in der Sammlung" return "Karte nicht in der Sammlung"
var lvl := level(cid) var lvl := level(cid)
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]] dust += dust_gain(DUST_DISMANTLE[Cards.RARITIES[lvl]])
if lvl == 0: if lvl == 0:
collection.erase(cid) collection.erase(cid)
deck.erase(cid) deck.erase(cid)
@@ -204,11 +215,21 @@ func craft(cid: String) -> String:
# --- Booster ---------------------------------------------------------------------- # --- Booster ----------------------------------------------------------------------
## Fünf Karten mit gewürfelter Seltenheit. ## Preis eines Boosters: neutral BOOSTER_COST, je Element ELEMENT_BOOSTER_COST.
static func roll_booster(r: RandomNumberGenerator) -> Array: static func booster_cost(element: String) -> int:
return BOOSTER_COST if element == Cards.NEUTRAL else ELEMENT_BOOSTER_COST
## Der Booster eines Elements ist zu haben, sobald sein Kampagnenkapitel offen ist (neutral immer).
func booster_unlocked(element: String) -> bool:
return campaign_stage >= Campaign.STAGES_PER_CHAPTER * Campaign.CHAPTER_ELEMENTS.find(element)
## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen.
static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0) -> Array:
var rarities := RARITY_WEIGHTS.keys() var rarities := RARITY_WEIGHTS.keys()
var weights := PackedFloat32Array(RARITY_WEIGHTS.values()) var weights := Skills.pack_weights(luck)
var pool := Cards.all() var pool := Cards.all().filter(func(c): return c.elements[0] == element)
var out := [] var out := []
for i in BOOSTER_SIZE: for i in BOOSTER_SIZE:
var rar: String = rarities[r.rand_weighted(weights)] var rar: String = rarities[r.rand_weighted(weights)]
@@ -218,16 +239,29 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
return out return out
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder ## Öffnet einen Booster des Elements. Neue Karten kommen in Sammlung und Stapel (solange Platz ist).
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche ## Von jeder Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen. ## gleiche oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn der Booster
func open_booster(r: RandomNumberGenerator = null) -> Array: ## noch gesperrt ist oder die Münzen nicht reichen.
if coins < BOOSTER_COST: func open_booster(element: String, r: RandomNumberGenerator = null) -> Array:
return open_boosters(element, 1, r)
## Öffnet n Booster des Elements auf einmal; leer, wenn gesperrt oder die Münzen für alle nicht reichen.
func open_boosters(element: String, n: int, r: RandomNumberGenerator = null) -> Array:
if not booster_unlocked(element) or coins < pack_price(element) * n:
return [] return []
if r == null: if r == null:
r = Game.make_rng() r = Game.make_rng()
coins -= BOOSTER_COST var out := []
var out := roll_booster(r) for i in n:
out += _open_one(element, r)
return out
func _open_one(element: String, r: RandomNumberGenerator) -> Array:
coins -= pack_price(element)
var out := roll_booster(r, element, skill("packglueck"))
for res in out: for res in out:
var lvl: int = Cards.RARITIES.find(res.rarity) var lvl: int = Cards.RARITIES.find(res.rarity)
res["new"] = not owns(res.card.id) res["new"] = not owns(res.card.id)
@@ -239,33 +273,138 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
deck.append(res.card.id) deck.append(res.card.id)
elif lvl > level(res.card.id): elif lvl > level(res.card.id):
res["upgraded"] = true res["upgraded"] = true
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)] res["dust"] = dust_gain(DUST_DISMANTLE[rarity(res.card.id)])
collection[res.card.id] = lvl collection[res.card.id] = lvl
else: else:
res["dust"] = DUST_DISMANTLE[res.rarity] res["dust"] = dust_gain(DUST_DISMANTLE[res.rarity])
dust += res.dust dust += res.dust
return out return out
# --- Kampf und Kampagne ----------------------------------------------------------- # --- Kampf und Kampagne -----------------------------------------------------------
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück. ## Münzen eines Kampfes: COINS_TURN je eigenem Zug, COINS_CARD je ausgespielter Karte, COINS_FAST
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei. ## je schnellem Zug; ein Sieg verdoppelt, factor ist der Münzfaktor der Schwierigkeit.
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int: static func battle_coins(won: bool, turns: int, cards: int, fast: int, factor: float = 1.0) -> int:
var reward := REWARD_LOSS var pts := COINS_TURN * turns + COINS_CARD * cards + COINS_FAST * fast
if won: if won:
reward = int(round(REWARD_WIN * reward_factor)) pts *= 2
if stage >= 0 and stage == campaign_stage: return int(round(pts * factor))
campaign_stage += 1
reward = Campaign.stage(stage).reward
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name) ## Verbucht ein Kampfergebnis mit den verdienten Münzen (battle_coins, mal Münzsegen) und gibt die
## Gutschrift zurück; sie zählt auch als Erfahrung, ein neues Level meldet sich. stage >= 0:
## Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei und bringt ihre Belohnung obendrauf.
func record_battle(won: bool, earned: int, stage: int = -1) -> int:
var reward := int(round(earned * coin_factor()))
if won and stage >= 0 and stage == campaign_stage:
campaign_stage += 1
reward += Campaign.stage(stage).reward
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
coins += reward coins += reward
var before := player_level()
xp += reward
if player_level() > before:
var gained := player_level() - before
notices.append(("Level %d erreicht: +%d Skillpunkt" if gained == 1 else "Level %d erreicht: +%d Skillpunkte") % [player_level(), gained])
return reward return reward
# --- Skilltree ----------------------------------------------------------------------
## Spielerlevel aus der Erfahrung (nicht die Kartenstufe level(cid)).
func player_level() -> int:
return Skills.level_for(xp)
## Gelernte Stufe eines Skills.
func skill(id: String) -> int:
return int(skills.get(id, 0))
## Freie Skillpunkte: ein Punkt je Level, abzüglich der gelernten Stufen.
func skill_points() -> int:
var spent := 0
for id in skills:
spent += int(skills[id])
return player_level() - spent
## Lernt die nächste Stufe eines Skills für einen Punkt. Gibt "" zurück oder eine Fehlermeldung.
func learn(id: String) -> String:
if skill(id) >= int(Skills.SKILLS[id].max):
return "Schon auf der höchsten Stufe"
if skill_points() <= 0:
return "Kein Skillpunkt frei"
skills[id] = skill(id) + 1
return ""
## Preis eines Boosters mit Händler-Rabatt.
func pack_price(element: String) -> int:
return int(round(booster_cost(element) * (1.0 - 0.1 * skill("packpreis"))))
func coin_factor() -> float:
return 1.0 + 0.1 * skill("muenzen")
## Staub mit Staubsammler-Bonus.
func dust_gain(base: int) -> int:
return int(round(base * (1.0 + 0.25 * skill("staub"))))
## Merkmale im Kampf: das gewählte (nach Kapitel 1) und die der Kampfskills.
func battle_perks() -> Array:
var out := []
if perk != "" and perk_unlocked():
out.append(perk)
for id in Skills.PERK_OF:
if skill(id) > 0 and not (Skills.PERK_OF[id] in out):
out.append(Skills.PERK_OF[id])
return out
func difficulty() -> Dictionary: func difficulty() -> Dictionary:
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal) return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
## Deine Stufe: die Deckstärke des Kampfstapels (100 Common … 500 Ultimate Rare) als Seltenheitsstufe.
func deck_level() -> int:
return clampi(roundi((DeckGen.strength(deck_cards(), collection) - 100) / 100.0), 0, Cards.RARITIES.size() - 1)
## Stufe der Gegnerkarten im Einzelspieler: deine Stufe plus den Versatz der Schwierigkeit.
func opponent_level() -> int:
return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1)
## Elemente der offenen Kampagnenkapitel (Neutral immer).
func unlocked_elements() -> Array:
return Campaign.CHAPTER_ELEMENTS.slice(0, campaign_stage / Campaign.STAGES_PER_CHAPTER + 1)
## Alle Karten der offenen Elemente: was Sammlung, Deck und Booster zeigen.
func unlocked_cards() -> Array:
var open := unlocked_elements()
return Cards.all().filter(func(c): return c.elements[0] in open)
## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level;
## auf Schwer ein zufälliges Merkmal. Leben wie deins.
func single_opponent(r: RandomNumberGenerator) -> Game.Player:
var elements := unlocked_elements()
var element: String = elements[r.randi_range(0, elements.size() - 1)]
var opp := Game.Player.new("Gegner", false, Game.START_LIFE, element)
opp.deck = DeckGen.curve_deck(element, r)
var level := opponent_level()
for id in opp.deck:
opp.looks[id] = level
if bool(difficulty().perk):
var keys: Array = Game.PERKS.keys()
opp.perks = [keys[r.randi_range(0, keys.size() - 1)]]
return opp
func perk_unlocked() -> bool: func perk_unlocked() -> bool:
return campaign_stage >= Campaign.STAGES.size() return campaign_stage >= Campaign.STAGES_PER_CHAPTER
+62
View File
@@ -0,0 +1,62 @@
class_name Skills
## Skilltree: Erfahrung sind die verdienten Münzen, je Level ein Skillpunkt, Punkte kaufen Stufen
## der Skills in drei Ästen. Münzen kaufen keine Punkte.
const BRANCHES: Array[String] = ["Sammeln", "Wirtschaft", "Kampf"]
## id -> Name, Ast, Höchststufe, Text.
const SKILLS := {
"packglueck": {"name": "Packglück", "branch": "Sammeln", "max": 3, "text": "Seltenere Karten in Boostern"},
"staub": {"name": "Staubsammler", "branch": "Sammeln", "max": 3, "text": "Mehr Staub aus Doppelten und Zerlegen"},
"muenzen": {"name": "Münzsegen", "branch": "Wirtschaft", "max": 3, "text": "Mehr Münzen nach jedem Kampf"},
"packpreis": {"name": "Händler", "branch": "Wirtschaft", "max": 2, "text": "Booster werden billiger"},
"gratisziehen": {"name": "Freies Ziehen", "branch": "Kampf", "max": 1, "text": "Das zweite Ziehen je Zug ist auch gratis"},
"startmana": {"name": "Starker Start", "branch": "Kampf", "max": 1, "text": "Mehr Mana im ersten Zug"},
}
## Kampfskills geben ein Merkmal (Game.PERKS) im Kampf.
const PERK_OF := {"gratisziehen": "ziehen_gratis", "startmana": "start_energie"}
## Packglück je Stufe: Common gibt ab, die höheren Stufen gewinnen.
const LUCK_SHIFT := {"Common": -8.0, "Rare": 3.0, "Super Rare": 2.0, "Ultra Rare": 2.0, "Ultimate Rare": 1.0}
## Erfahrung, die Level n verlangt: 100, 300, 600, 1000, 1500 … (50 · n · (n + 1)).
static func xp_for_level(n: int) -> int:
return 50 * n * (n + 1)
## Level zu einer Erfahrung.
static func level_for(xp: int) -> int:
var n := 0
while xp >= xp_for_level(n + 1):
n += 1
return n
static func in_branch(branch: String) -> Array:
return SKILLS.keys().filter(func(id): return SKILLS[id].branch == branch)
## Boosterchancen je Stufe (Reihenfolge Cards.RARITIES) mit Packglück luck.
static func pack_weights(luck: int) -> PackedFloat32Array:
var out := PackedFloat32Array()
for rar in Cards.RARITIES:
out.append(float(Profile.RARITY_WEIGHTS[rar]) + luck * float(LUCK_SHIFT[rar]))
return out
## Konkrete Wirkung eines Skills auf einer Stufe, als Text.
static func effect(id: String, rank: int) -> String:
match id:
"packglueck":
var w := pack_weights(rank)
return TranslationServer.translate("Chancen %d/%d/%d/%d/%d %%") % [int(w[0]), int(w[1]), int(w[2]), int(w[3]), int(w[4])]
"staub":
return TranslationServer.translate("+%d %% Staub") % (25 * rank)
"muenzen":
return TranslationServer.translate("+%d %% Münzen") % (10 * rank)
"packpreis":
return TranslationServer.translate("Neutral %d, Element %d Münzen") % [roundi(Profile.BOOSTER_COST * (1.0 - 0.1 * rank)), roundi(Profile.ELEMENT_BOOSTER_COST * (1.0 - 0.1 * rank))]
"gratisziehen":
return TranslationServer.translate("zwei Karten je Zug gratis" if rank > 0 else "eine Karte je Zug gratis")
"startmana":
return TranslationServer.translate("erster Zug: %d Mana") % (Game.START_MANA + (2 if rank > 0 else 0))
return ""
+1
View File
@@ -0,0 +1 @@
uid://bbcmk1gtfjoim
+1 -1
View File
@@ -68,6 +68,6 @@ static func preview_rect(_anchor: Rect2) -> Rect2:
return Rect2(Vector2(1070, 278), PREVIEW_SIZE) return Rect2(Vector2(1070, 278), PREVIEW_SIZE)
## Zusatzzeile (Elementbonus, Eigenschaften) links unter den Elementzählern. ## Zusatzzeile (Merkmale, Eigenschaften) links unter den Elementpaaren.
static func info_rect(_preview: Rect2) -> Rect2: static func info_rect(_preview: Rect2) -> Rect2:
return INFO return INFO
+53 -27
View File
@@ -31,7 +31,10 @@ var ai_timer := 0.0
var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung
var ai_copy: Game # Spielkopie, auf der die KI rechnet var ai_copy: Game # Spielkopie, auf der die KI rechnet
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt) var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
var reward := -1 var reward := -1 # gutgeschriebene Münzen (mit Kampagnenbonus)
var earned := 0 # Münzen aus Zügen, Karten und Tempo (Profile.battle_coins)
var fast_turns := 0 # eigene Züge unter Profile.FAST_TURN_SEC
var turn_start := 0 # Beginn des laufenden eigenen Zugs (ms)
var ended := false var ended := false
var pad: BattleGamepad var pad: BattleGamepad
var gp_zone: String: var gp_zone: String:
@@ -72,7 +75,7 @@ var energy_label: Label
var msg_label: Label var msg_label: Label
var element_view: ElementView # eigene Elementleiste var element_view: ElementView # eigene Elementleiste
var opp_element_view: ElementView # die des Gegners darüber var opp_element_view: ElementView # die des Gegners darüber
var info_label: Label # Vorschau: Elementbonus, Merkmale var info_label: Label # Vorschau: Merkmale, Eigenschaften
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
@@ -81,6 +84,7 @@ var banner_turn := -1 # Zug, für den die Zugansage schon lief
var overlay: Control var overlay: Control
var result_label: Label var result_label: Label
var reward_label: Label var reward_label: Label
var detail_label: Label # Aufschlüsselung der Münzen
var notice_label: Label var notice_label: Label
var confirm: Control var confirm: Control
var cursor_layer: Control var cursor_layer: Control
@@ -99,23 +103,20 @@ func _ready() -> void:
pad = BattleGamepad.new(self) pad = BattleGamepad.new(self)
ai_delay = 0.75 / float(s.ai_speed) ai_delay = 0.75 / float(s.ai_speed)
_build_static() _build_static()
if stage >= 0: var r := Game.make_rng()
var r := Game.make_rng() var opp: Game.Player = Campaign.opponent(stage) if stage >= 0 else app.profile.single_opponent(r)
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules()) game = Game.new_battle_with(opp, r, false, own_rules())
else:
game = Game.new_battle(Game.make_rng(), own_rules())
smart_ai = bool(app.profile.difficulty().smart) smart_ai = bool(app.profile.difficulty().smart)
_begin() _begin()
## Eigener Stapel und Kartenstufen, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1). ## Eigener Stapel und Kartenstufen und das eigene Merkmal (nach Kapitel 1).
func own_rules() -> Dictionary: func own_rules() -> Dictionary:
var p: Profile = app.profile var p: Profile = app.profile
var rules := {"looks": p.looks(), "deck": p.deck_cards()} var rules := {"looks": p.looks(), "deck": p.deck_cards()}
if stage < 0: var perks := p.battle_perks()
rules["opponent_life"] = int(p.difficulty().life) if not perks.is_empty():
if p.perk != "" and p.perk_unlocked(): rules["human_perks"] = perks
rules["human_perks"] = [p.perk]
return rules return rules
@@ -224,7 +225,8 @@ func _build_static() -> void:
overlay = _overlay() overlay = _overlay()
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true) result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center") reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center") detail_label = Style.label(overlay, "", 17, Style.MUTED, Rect2(0, 372, 1280, 28), "center")
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 405, 1280, 70), "center")
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c")) Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen") confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
@@ -459,8 +461,11 @@ func _exit_tree() -> void:
func _finish() -> void: func _finish() -> void:
var won := game.winner == "human" var won := game.winner == "human"
if game.current() == game.human:
_close_turn() # der Zug, der das Spiel entschied
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0 var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, stage, factor) earned = Profile.battle_coins(won, game.human.turns, game.human.cards_played, fast_turns, factor)
reward = app.profile.record_battle(won, earned, stage)
app.save() app.save()
app.sfx.play("win" if won else "lose") app.sfx.play("win" if won else "lose")
refresh() refresh()
@@ -777,6 +782,7 @@ func end_turn() -> void:
return return
cancel() cancel()
clear_undo() clear_undo()
_close_turn()
game.end_turn() game.end_turn()
ai_timer = ai_delay ai_timer = ai_delay
after_action() after_action()
@@ -839,11 +845,36 @@ func on_tile_click(cr: Game.Creature) -> void:
on_click("enemy", cr) on_click("enemy", cr)
## Zugansage, wenn der eigene Zug beginnt. ## Zählt den laufenden eigenen Zug als schnell, wenn er unter Profile.FAST_TURN_SEC blieb.
func _close_turn() -> void:
if Time.get_ticks_msec() - turn_start < Profile.FAST_TURN_SEC * 1000:
fast_turns += 1
## Aufschlüsselung der Münzen fürs Ergebnisbild.
func reward_text() -> String:
var h := game.human
var s := "%s · %s · %s = %d" % [tr("%d Züge") % h.turns, tr("%d Karten") % h.cards_played, tr("%d schnelle Züge") % fast_turns,
Profile.COINS_TURN * h.turns + Profile.COINS_CARD * h.cards_played + Profile.COINS_FAST * fast_turns]
if game.winner == "human":
s += " · " + tr("Sieg ×2")
var d: Dictionary = app.profile.difficulty()
if stage < 0 and float(d.reward) != 1.0:
s += " · %s %s" % [tr(d.name), Style.factor(float(d.reward))]
var boosted := int(round(earned * app.profile.coin_factor()))
if boosted != earned:
s += " · %s %s" % [tr("Münzsegen"), Style.factor(app.profile.coin_factor())]
if reward > boosted:
s += " · " + tr("Kampagne +%d") % (reward - boosted)
return s
## Zugansage, wenn der eigene Zug beginnt; ab hier läuft die Zeit für den schnellen Zug.
func announce_turn() -> void: func announce_turn() -> void:
if game == null or not my_turn() or banner_turn == game.turn: if game == null or not my_turn() or banner_turn == game.turn:
return return
banner_turn = game.turn banner_turn = game.turn
turn_start = Time.get_ticks_msec()
fx.banner(tr("Dein Zug"), Style.GOLD) fx.banner(tr("Dein Zug"), Style.GOLD)
fx.sfx("turn") fx.sfx("turn")
@@ -871,23 +902,17 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
info_label.size = ir.size info_label.size = ir.size
## Eigene Handkarte: zeigt den Elementbonus, den sie offen gelegt bekäme. ## Eigene Handkarte in der Vorschau.
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void: func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
if drag_ghost != null: if drag_ghost != null:
return return
var a := game.forecast(perspective, card) show_preview(card, entered, perspective.looks, "", anchor)
var info := ""
if a > 0:
info = tr("Elementbonus +%d") % a
show_preview(card, entered, perspective.looks, info, anchor)
## Was ein Wesen gerade hebt, was es kann und ob es schon angreift. ## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
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: var perk := cr.power() - cr.attack
parts.append(tr("Elementbonus +%d") % 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)
if cr.card.is_neutral(): if cr.card.is_neutral():
@@ -902,10 +927,11 @@ func creature_info(cr: Game.Creature) -> String:
return " · ".join(parts) return " · ".join(parts)
## Elementvorteil des gewählten Angreifers gegen ein mögliches Ziel; gegen Verdeckte unbekannt. ## Konter eines möglichen Ziels gegen den gewählten Angreifer (Verschiebung durch sein Element);
## gegen Verdeckte unbekannt.
func matchup_for(cr: Game.Creature) -> int: func matchup_for(cr: Game.Creature) -> int:
if selected != null and not cr.hidden and game.can_attack(selected, cr)[0]: if selected != null and not cr.hidden and game.can_attack(selected, cr)[0]:
return Game.matchup_bonus(selected, cr) return Game.matchup_bonus(cr, selected)
return 0 return 0
@@ -1139,7 +1165,6 @@ func refresh() -> void:
fx.glide(v, hand_arrivals[card]) fx.glide(v, hand_arrivals[card])
hand_arrivals.erase(card) hand_arrivals.erase(card)
v.lift = mine v.lift = mine
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]:
v.border = Style.PURPLE v.border = Style.PURPLE
elif card == fuse_first: elif card == fuse_first:
@@ -1185,6 +1210,7 @@ func refresh() -> void:
result_label.text = "Sieg!" if won else "Niederlage" result_label.text = "Sieg!" if won else "Niederlage"
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED) result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
reward_label.text = "+%d Münzen" % maxi(reward, 0) reward_label.text = "+%d Münzen" % maxi(reward, 0)
detail_label.text = reward_text()
var notes: Array[String] = app.profile.take_notices() var notes: Array[String] = app.profile.take_notices()
if not notes.is_empty(): if not notes.is_empty():
notice_label.text = "\n".join(notes) notice_label.text = "\n".join(notes)
+165 -36
View File
@@ -1,19 +1,32 @@
class_name BoosterScreen class_name BoosterScreen
extends Control extends Control
## Booster kaufen und öffnen: unten die Pack-Kacheln der offenen Kapitel (Neutral und die Elemente in
## Kapitelreihenfolge, gesperrte sind unsichtbar) mit ×1, ×5, ×10; die Karten fliegen aus dem Pack in ein Raster
## (ein Pack: fünf große, mehrere: zehn je Reihe, beste Stufe zuerst) und liegen verdeckt. Klick
## oder „Alle aufdecken“ dreht sie gestaffelt um, seltene blitzen auf; danach steht das Fazit.
const CARD_W := 200 const CARD_W := 200
const CARD_H := 280 const CARD_H := 280
const SMALL_W := 108
const SMALL_H := 151
const AREA := Rect2(40, 100, 1200, 460)
const QUANTITIES := [1, 5, 10]
var app var app
var cards: Array = [] # {"card", "rarity", "upgraded", "dust"} je Karte var cards: Array = [] # {"card", "rarity", "new", "upgraded", "dust"} je Karte
var revealed := {} var revealed := {}
var coins_label: Label var coins_label: Label
var msg_label: Label var summary_label: Label
var b_buy: Button var b_buy: Button # Neutral ×1 (Tests)
var b_all: Button var b_all: Button
var scroll: ScrollContainer
var slots: Control var slots: Control
var fx_layer: Control var pack_tiles := {} # Element -> PackTile (Kapitelreihenfolge)
var fresh := {} # gerade aufgedeckt: wird umgedreht var pack_buttons := {} # Element -> der ×1-Knopf
var fresh := {} # gerade aufgedeckt: Index -> Verzögerung fürs Umdrehen
var fly_from := Vector2.ZERO # woher die Karten einfliegen (Mitte der gekauften Kachel)
var fly := false # beim nächsten refresh einfliegen
var last_pack := "" # zuletzt gekauftes Pack (pulsiert)
class CardBack extends Control: class CardBack extends Control:
@@ -39,28 +52,56 @@ class CardBack extends Control:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect) draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect)
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6) Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true) Style.text(self, "?", 48 if size.x >= 180 else 30, Style.GOLD, size / 2, "center", true)
func slot_rect(i: int) -> Rect2: ## Pack-Kachel eines Elements: Symbol, Name, Preis; die Kaufknöpfe hängt der Bildschirm an.
var gap := 20 class PackTile extends Control:
var x0 := (1280 - Profile.BOOSTER_SIZE * CARD_W - (Profile.BOOSTER_SIZE - 1) * gap) / 2 var element: String
return Rect2(x0 + i * (CARD_W + gap), 230, CARD_W, CARD_H) var locked := false
var hint := ""
var pulse := false
var buy_buttons: Array = []
func _init(e: String, rect: Rect2) -> void:
element = e
position = rect.position
size = rect.size
pivot_offset = rect.size / 2
func _draw() -> void:
var col: Color = Cards.ELEMENT_COLORS[element]
draw_style_box(Style.box(Style.darker(col, 0.5), 10, Style.GOLD if pulse else Color(col, 0.7), 2), Rect2(Vector2.ZERO, size))
Style.element_icon(self, element, Vector2(32, 40), 34, Style.lighter(col, 0.4))
Style.text(self, tr(element), 16, Style.TEXT, Vector2(60, 30), "midleft", true)
Style.text(self, hint, 13, Style.MUTED, Vector2(60, 52), "midleft")
func _ready() -> void: func _ready() -> void:
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38)) coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
fx_layer = Control.new() scroll = ScrollContainer.new()
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE scroll.position = AREA.position
add_child(fx_layer) scroll.size = AREA.size
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
slots = Control.new() slots = Control.new()
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE slots.mouse_filter = Control.MOUSE_FILTER_PASS
add_child(slots) slots.custom_minimum_size = AREA.size
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center") scroll.add_child(slots)
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628")) summary_label = Style.label(self, "", 16, Style.GOLD, Rect2(0, 566, 1280, 24), "center", true)
b_buy.add_theme_font_size_override("font_size", 20) b_all = Style.button(self, "Alle aufdecken", Rect2(540, 594, 200, 40), reveal_all)
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all) for i in Campaign.CHAPTER_ELEMENTS.size():
var e: String = Campaign.CHAPTER_ELEMENTS[i]
var tile := PackTile.new(e, Rect2(70 + i * 230, 644, 220, 136))
add_child(tile)
for k in QUANTITIES.size():
var b := Style.button(tile, "×%d" % QUANTITIES[k], Rect2(10 + k * 68, 88, 64, 36), buy.bind(e, QUANTITIES[k]))
b.add_theme_font_size_override("font_size", 14)
tile.buy_buttons.append(b)
pack_tiles[e] = tile
pack_buttons[e] = tile.buy_buttons[0]
b_buy = pack_buttons[Cards.NEUTRAL]
refresh() refresh()
@@ -69,19 +110,34 @@ func _input(event: InputEvent) -> void:
app.show_screen(MenuScreen.new()) app.show_screen(MenuScreen.new())
func buy() -> void: ## Kauft n Packs eines Elements; die Karten liegen verdeckt, mehrere Packs nach Stufe sortiert.
func buy(element: String, n: int) -> void:
var p: Profile = app.profile var p: Profile = app.profile
var got: Array = p.open_booster() var before: int = p.coins
var got: Array = p.open_boosters(element, n)
if got.is_empty(): if got.is_empty():
msg_label.text = "Nicht genug Münzen"
msg_label.add_theme_color_override("font_color", Style.RED)
return return
app.save() app.save()
app.sfx.play("play") app.sfx.play("play")
if n > 1:
got.sort_custom(func(a, b): return Cards.RARITIES.find(a.rarity) > Cards.RARITIES.find(b.rarity))
cards = got cards = got
revealed = {} revealed = {}
msg_label.text = "" fresh = {}
fly = true
var tile: PackTile = pack_tiles[element]
fly_from = tile.position + tile.size / 2
last_pack = element
scroll.scroll_vertical = 0
refresh() refresh()
_count_coins(before, p.coins)
## Die Münzen zählen sichtbar herunter.
func _count_coins(from: int, to: int) -> void:
if not Style.animations or from == to:
return
create_tween().tween_method(func(v: float): coins_label.text = "Münzen: %d" % int(round(v)), float(from), float(to), 0.5)
func reveal(i: int) -> void: func reveal(i: int) -> void:
@@ -91,13 +147,17 @@ func reveal(i: int) -> void:
refresh() refresh()
## Deckt alles auf, von vorn nach hinten gestaffelt; ab Ultra Rare bleibt die Karte kurz allein.
func reveal_all() -> void: func reveal_all() -> void:
fresh = {} fresh = {}
var k := 0 var delay := 0.0
for i in Profile.BOOSTER_SIZE: var step := 0.12 if cards.size() <= Profile.BOOSTER_SIZE else 0.06
for i in cards.size():
if not revealed.has(i): if not revealed.has(i):
fresh[i] = k * 0.12 fresh[i] = delay
k += 1 delay += step
if Cards.RARITIES.find(cards[i].rarity) >= 3:
delay += 0.35
revealed[i] = true revealed[i] = true
app.sfx.play("play") app.sfx.play("play")
refresh() refresh()
@@ -113,7 +173,7 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
if rarity == "Common": if rarity == "Common":
return return
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12) var burst := Style.panel(slots, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
burst.pivot_offset = burst.size / 2 burst.pivot_offset = burst.size / 2
burst.modulate.a = 0.0 burst.modulate.a = 0.0
var bt := create_tween() var bt := create_tween()
@@ -124,13 +184,71 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
bt.tween_callback(burst.queue_free) bt.tween_callback(burst.queue_free)
## Einflug einer verdeckten Karte aus dem Pack auf ihren Platz, reihenweise gestaffelt.
func fly_in(back: Control, target: Vector2, i: int, per_row: int) -> void:
if not Style.animations:
return
back.position = fly_from - AREA.position - back.size / 2
back.scale = Vector2(0.4, 0.4)
var tw := create_tween().set_parallel()
var delay := (i / per_row) * 0.1 + (i % per_row) * 0.03
tw.tween_property(back, "position", target, 0.3).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.tween_property(back, "scale", Vector2.ONE, 0.3).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
## Platz der i-ten Karte: ein Pack fünf große in einer Reihe, sonst zehn kleine je Reihe.
func slot_rect(i: int) -> Rect2:
var single := cards.size() <= Profile.BOOSTER_SIZE
var w := CARD_W if single else SMALL_W
var h := CARD_H if single else SMALL_H
var per_row := 5 if single else 10
var gap := 20 if single else 12
var x0 := (AREA.size.x - per_row * w - (per_row - 1) * gap) / 2
var row_h := h + (60 if single else 44)
return Rect2(x0 + (i % per_row) * (w + gap), 10 + (i / per_row) * row_h, w, h)
## Fazit nach dem Aufdecken: neue Karten, Aufwertungen, Staub.
func summary() -> String:
if cards.is_empty() or revealed.size() < cards.size():
return ""
var fresh_n := 0
var up := 0
var dust := 0
for res in cards:
if res.get("new", false):
fresh_n += 1
elif res.upgraded:
up += 1
dust += int(res.dust)
return tr("%d neu · %d aufgewertet · +%d Staub") % [fresh_n, up, dust]
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
coins_label.text = "Münzen: %d" % p.coins coins_label.text = "Münzen: %d" % p.coins
b_buy.disabled = p.coins < Profile.BOOSTER_COST var open: Array = p.unlocked_elements()
b_all.disabled = cards.is_empty() or revealed.size() >= Profile.BOOSTER_SIZE var x0 := (1280 - open.size() * 220 - (open.size() - 1) * 10) / 2
for e in pack_tiles:
var tile: PackTile = pack_tiles[e]
tile.locked = not p.booster_unlocked(e)
tile.visible = not tile.locked
if not tile.locked:
tile.position = Vector2(x0 + open.find(e) * 230, 644)
tile.hint = tr("%d Münzen") % p.pack_price(e)
for k in QUANTITIES.size():
tile.buy_buttons[k].disabled = tile.locked or p.coins < p.pack_price(e) * QUANTITIES[k]
tile.pulse = e == last_pack and not cards.is_empty() and revealed.size() >= cards.size()
tile.queue_redraw()
b_all.disabled = cards.is_empty() or revealed.size() >= cards.size()
summary_label.text = summary()
Style.clear(slots) Style.clear(slots)
for i in Profile.BOOSTER_SIZE: var single := cards.size() <= Profile.BOOSTER_SIZE
var per_row := 5 if single else 10
var n := maxi(cards.size(), Profile.BOOSTER_SIZE)
var rows := (n + per_row - 1) / per_row
slots.custom_minimum_size = Vector2(AREA.size.x, maxf(AREA.size.y, slot_rect(n - 1).end.y + 60))
for i in n:
var r := slot_rect(i) var r := slot_rect(i)
if cards.is_empty(): if cards.is_empty():
Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2) Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2)
@@ -142,16 +260,27 @@ func refresh() -> void:
slots.add_child(v) slots.add_child(v)
if fresh.has(i): if fresh.has(i):
flip(v, fresh[i], res.rarity) flip(v, fresh[i], res.rarity)
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true) var fs := 16 if single else 12
Style.label(slots, tr(res.rarity), fs, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x - 10, r.end.y + 6, r.size.x + 20, 20), "center", true)
var y2 := r.end.y + (30 if single else 24)
if res.get("new", false): if res.get("new", false):
Style.label(slots, "Neu!", 16, Style.GOLD, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true) Style.label(slots, "Neu!", fs, Style.GOLD, Rect2(r.position.x, y2, r.size.x, 20), "center", true)
elif res.upgraded: elif res.upgraded:
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true) Style.label(slots, "Aufgewertet", fs, Style.GREEN, Rect2(r.position.x - 10, y2, r.size.x + 20, 20), "center", true)
else: else:
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center") Style.label(slots, "+%d Staub" % res.dust, fs - 2, Style.MUTED, Rect2(r.position.x, y2, r.size.x, 20), "center")
else: else:
var back := CardBack.new(r.size) var back := CardBack.new(r.size)
back.position = r.position back.position = r.position
back.pivot_offset = r.size / 2
back.clicked.connect(reveal.bind(i)) back.clicked.connect(reveal.bind(i))
slots.add_child(back) slots.add_child(back)
if fly:
fly_in(back, r.position, i, per_row)
fly = false
fresh = {} fresh = {}
if rows > 0 and last_pack != "" and pack_tiles[last_pack].pulse and Style.animations:
var tile: PackTile = pack_tiles[last_pack]
var tw := tile.create_tween().set_loops(3)
tw.tween_property(tile, "scale", Vector2(1.04, 1.04), 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
tw.tween_property(tile, "scale", Vector2.ONE, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
+11 -8
View File
@@ -1,6 +1,7 @@
class_name CampaignScreen class_name CampaignScreen
extends Control extends Control
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail. ## Kampagne als Pfad: zehn Stationen je Kapitel (fünf Kapitel: Neutral, dann je ein Element),
## darunter die gewählte Stufe im Detail.
var app var app
var chapter := 1 var chapter := 1
@@ -11,7 +12,7 @@ var node_buttons: Array = []
func _ready() -> void: func _ready() -> void:
var p: Profile = app.profile var p: Profile = app.profile
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size()) chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTER_ELEMENTS.size())
selected = mini(p.campaign_stage, Campaign.total() - 1) selected = mini(p.campaign_stage, Campaign.total() - 1)
build() build()
@@ -21,14 +22,16 @@ func build() -> void:
node_buttons = [] node_buttons = []
var p: Profile = app.profile var p: Profile = app.profile
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
var n := Campaign.STAGES.size() var n := Campaign.STAGES_PER_CHAPTER
var x := 40 var x := 40
for c in Campaign.CHAPTERS.size(): for c in Campaign.CHAPTER_ELEMENTS.size():
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 150, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null) var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 130, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
b.disabled = p.campaign_stage < c * n b.disabled = p.campaign_stage < c * n
x += 160 x += 138
var element := Campaign.element_of(chapter)
Style.label(self, tr(element), 20, Cards.ELEMENT_COLORS[element], Rect2(0, 136, 1280, 30), "center", true)
if p.perk_unlocked(): if p.perk_unlocked():
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right") Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(740, 96, 150, 24), "right")
perk_box = OptionButton.new() perk_box = OptionButton.new()
perk_box.position = Vector2(900, 88) perk_box.position = Vector2(900, 88)
perk_box.size = Vector2(340, 38) perk_box.size = Vector2(340, 38)
@@ -109,7 +112,7 @@ func build() -> void:
func set_chapter(c: int) -> void: func set_chapter(c: int) -> void:
chapter = c chapter = c
var p: Profile = app.profile var p: Profile = app.profile
var first := (c - 1) * Campaign.STAGES.size() var first := (c - 1) * Campaign.STAGES_PER_CHAPTER
selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first
build() build()
+6 -4
View File
@@ -14,10 +14,10 @@ var card: Cards.Card
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe) var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
var rarity := "Common" # nur Optik var rarity := "Common" # nur Optik
var dim := false var dim := false
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme
var lift := false # hebt sich beim Zeigen an (Hand) var lift := false # hebt sich beim Zeigen an (Hand)
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
var drop := false # Ziel der gerade gezogenen Karte var drop := false # Ziel der gerade gezogenen Karte
var marked := false # Haken: die Karte ist im Stapel (Deck)
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick. ## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
@@ -92,15 +92,17 @@ func _draw() -> void:
var pic_bottom := size.y - r_stat - 4 var pic_bottom := size.y - r_stat - 4
var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2) var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2)
Style.picture(self, card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rarity, seed) Style.picture(self, card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rarity, seed)
Style.rarity_sparkle(self, rect, rarity, seed) # vor den Werten, damit nichts die Zahlen überlagert
# Unten: Angriff links, Leben rechts # Unten: Angriff links, Leben rechts
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat, Color.WHITE, shiny) Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat, Color.WHITE, shiny)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat, Color.WHITE, shiny) Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat, Color.WHITE, shiny)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, 13 if big else 11, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
Style.card_sheen(self, rect, rarity, seed) Style.card_sheen(self, rect, rarity, 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 + 4, r_cost + 4), r_cost, card.cost, f_stat, shiny) Style.cost_crystal(self, Vector2(r_cost + 4, r_cost + 4), r_cost, card.cost, f_stat, shiny)
Style.rarity_sparkle(self, rect, rarity, seed) if marked:
var mc := Vector2(size.x / 2, size.y - r_stat + 2)
draw_circle(mc, r_stat * 0.6, Style.GREEN)
Style.text(self, "✓", f_stat - 4, Color.WHITE, mc, "center", true)
if dim: if dim:
draw_rect(rect, Color(0, 0, 0, 0.47)) draw_rect(rect, Color(0, 0, 0, 0.47))
if has_focus(): if has_focus():
+69
View File
@@ -0,0 +1,69 @@
class_name CheatScreen
extends Control
## Cheats zum Ausprobieren: Münzen, Staub, Level, Skills, Kampagnenkapitel und Sammlung direkt setzen.
## Jeder Knopf speichert sofort; oben steht der aktuelle Stand.
var app
var status_label: Label
var msg_label: Label
func _ready() -> void:
Style.header(self, "Cheats", func(): app.show_screen(MenuScreen.new()))
status_label = Style.label(self, "", 16, Style.GOLD, Rect2(40, 90, 1200, 48), "left", true)
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD
var groups := [
["Währung", [["+1.000 Münzen", func(): app.profile.coins += 1000], ["+10.000 Münzen", func(): app.profile.coins += 10000],
["+1.000 Staub", func(): app.profile.dust += 1000], ["+10.000 Staub", func(): app.profile.dust += 10000]]],
["Erfahrung", [["+1 Level", func(): app.profile.xp = Skills.xp_for_level(app.profile.player_level() + 1)],
["+5 Level", func(): app.profile.xp = Skills.xp_for_level(app.profile.player_level() + 5)],
["Skills zurücksetzen", func(): app.profile.skills = {}], ["Erfahrung auf 0", func():
app.profile.xp = 0
app.profile.skills = {}]]],
["Kampagne", [["Kapitel 2 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 10)],
["Kapitel 3 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 20)],
["Kapitel 4 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 30)],
["Kapitel 5 frei", func(): app.profile.campaign_stage = maxi(app.profile.campaign_stage, 40)],
["Kampagne zurücksetzen", func(): app.profile.campaign_stage = 0]]],
["Sammlung", [["Alle Karten (Common)", func(): _give_all(0)], ["Alle Karten Ultimate Rare", func(): _give_all(4)],
["Eigene Karten +1 Stufe", func():
for id in app.profile.collection:
app.profile.collection[id] = mini(int(app.profile.collection[id]) + 1, Cards.RARITIES.size() - 1)],
["Sammlung zurücksetzen", func(): app.profile.give_start()]]],
]
var y := 160
for g in groups:
Style.label(self, g[0], 18, Style.TEXT, Rect2(40, y, 200, 30), "left", true)
var x := 250
for entry in g[1]:
var b := Style.button(self, entry[0], Rect2(x, y - 4, 190, 40), _apply.bind(entry[1]))
b.add_theme_font_size_override("font_size", 14)
x += 198
y += 70
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(40, y + 10, 1200, 24), "left")
refresh()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())
## Alle Karten der Sammlung geben, in Stufe level; der Stapel bleibt.
func _give_all(level: int) -> void:
for c in Cards.all():
app.profile.collection[c.id] = level
func _apply(action: Callable) -> void:
action.call()
app.save()
app.sfx.play("click")
msg_label.text = tr("Gespeichert")
refresh()
func refresh() -> void:
var p: Profile = app.profile
status_label.text = tr("Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d") % [
p.coins, p.dust, p.xp, p.player_level(), p.skill_points(), p.campaign_stage, Campaign.total(), p.collection.size(), p.deck.size()]
+1
View File
@@ -0,0 +1 @@
uid://dluwr50n0sd17
+4 -3
View File
@@ -21,7 +21,7 @@ var b_craft: Button
func _ready() -> void: func _ready() -> void:
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind("")) Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL] var filters: Array = app.profile.unlocked_elements() # nur Elemente offener Kapitel
for i in filters.size(): for i in filters.size():
var e: String = filters[i] var e: String = filters[i]
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8)) var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
@@ -60,9 +60,10 @@ func set_filter(e: String) -> void:
refresh() refresh()
## Eigene Karten zuerst (höhere Stufen vorn), dann die fehlenden, sonst in Kartenreihenfolge. ## Karten der offenen Elemente: eigene zuerst (höhere Stufen vorn), dann die fehlenden, sonst in
## Kartenreihenfolge.
func cards() -> Array: func cards() -> Array:
var out: Array = Cards.all().duplicate() var out: Array = app.profile.unlocked_cards()
if filter != "": if filter != "":
out = out.filter(func(c): return filter in c.elements) out = out.filter(func(c): return filter in c.elements)
if search != "": if search != "":
+4 -10
View File
@@ -12,7 +12,7 @@ var width := 2
var flash := false # kurz rot: wurde gerade angegriffen var flash := false # kurz rot: wurde gerade angegriffen
var slot: Vector2 # Zielposition im Raster var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren) var busy := false # wird gerade animiert (kein Neupositionieren)
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (Game.matchup_bonus); die Werte zeigen dann, was ihn erwartet var matchup := 0 # fremdes Wesen: sein Konter gegen den gewählten Angreifer (Game.matchup_bonus); die Werte zeigen, was ihn erwartet
var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus
var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
@@ -78,7 +78,7 @@ func _draw() -> void:
var pic_bottom := size.y - r_stat - 4 var pic_bottom := size.y - r_stat - 4
var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2) var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2)
Style.picture(self, creature.card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rar, seed) Style.picture(self, creature.card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rar, seed)
# Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt), rechts der Elementbonus # Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt)
var icon := 6.0 if small else 7.0 var icon := 6.0 if small else 7.0
var y := top_h + 6 + icon var y := top_h + 6 + icon
for k in creature.keywords(): for k in creature.keywords():
@@ -88,11 +88,7 @@ func _draw() -> void:
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45) col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col) Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col)
y += icon * 2 + 6 y += icon * 2 + 6
if creature.aura != 0: Style.rarity_sparkle(self, rect, rar, seed) # vor den Werten, damit nichts die Zahlen überlagert
var badge := "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
var bw := Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
draw_rect(Rect2(size.x - bw - 4, top_h + 5, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, top_h + 13), "center", true)
_draw_stats(15 if small else 17, r_stat, shiny) _draw_stats(15 if small else 17, r_stat, shiny)
Style.card_sheen(self, rect, rar, seed) Style.card_sheen(self, rect, rar, seed)
Style.border(self, rect, bcol, 4 if flash else width, 6) Style.border(self, rect, bcol, 4 if flash else width, 6)
@@ -103,7 +99,6 @@ func _draw() -> void:
if doomed: if doomed:
draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8)) draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8))
Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3) Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3)
Style.rarity_sparkle(self, rect, rar, seed)
_draw_drop() _draw_drop()
@@ -128,8 +123,7 @@ func _draw_back(rect: Rect2) -> void:
## 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 := maxi(0, creature.power() - matchup) return [maxi(0, creature.power() + matchup), 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]
return [creature.power(), creature.health] return [creature.power(), creature.health]
+190 -25
View File
@@ -1,38 +1,139 @@
class_name DeckScreen class_name DeckScreen
extends Control extends Control
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den ## Deck zusammenstellen: alle eigenen Karten im Raster (Filter nach Element und Mana, Stapelkarten
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält ## zuerst, dann höchste Stufen vorn), ein Klick nimmt eine Karte in den Stapel oder wieder heraus. Stapelkarten tragen einen Haken, die anderen sind
## 40 bis 60 Karten; „Auffüllen“ nimmt die wertvollsten eigenen Karten des gewählten Filters. ## abgedunkelt. Rechts Stapelgröße, Stärke (mittlerer Kartenwert, 100 Grundkarten … 500 Ultimate
## „Stärke“ ist der mittlere Kartenwert (100 Grundkarten … 500 Ultimate Rare). ## Rare), Manakurve, Elementanteile und die Vorschau der gezeigten Karte. Der Stapel hält DECK_MIN
## bis DECK_MAX Karten; „Auffüllen“ nimmt die wertvollsten eigenen Karten des gewählten Filters.
const PREVIEW := Rect2(940, 362, 180, 252)
const MAX_COST := 10
var app var app
var filter := "" var filter := "" # Element oder ""
var cost_filter := -1 # Kosten 0 bis 10 oder -1
var grid: CardGrid var grid: CardGrid
var all_button: Button
var element_buttons := {} # Element -> Knopf mit Symbol
var cost_buttons: Array = [] # je Kosten ein Knopf
var count_label: Label var count_label: Label
var strength_label: Label var strength_label: Label
var curve: CurveView
var share: ElementShare
var preview_holder: Control
var shown_card: Cards.Card = null # Karte in der Vorschau
var msg_label: Label var msg_label: Label
## Filterknopf mit dem Symbol eines Elements.
class ElementButton extends Button:
var element: String
func _init(e: String) -> void:
element = e
tooltip_text = tr(e)
func _draw() -> void:
Style.element_icon(self, element, size / 2, size.y * 0.6, Style.lighter(Cards.ELEMENT_COLORS[element], 0.4))
## Manakurve: ein Balken je Kosten 0 bis 10 mit der Zahl der Stapelkarten.
class CurveView extends Control:
var counts: Array = []
func _init(rect: Rect2) -> void:
position = rect.position
size = rect.size
mouse_filter = Control.MOUSE_FILTER_IGNORE
func set_counts(c: Array) -> void:
counts = c
queue_redraw()
func _draw() -> void:
var n := 11
var gap := 4.0
var bw := (size.x - gap * (n - 1)) / n
var top := 16.0
var base := size.y - 16.0
var mx := 1
for v in counts:
mx = maxi(mx, int(v))
for i in n:
var v: int = int(counts[i]) if i < counts.size() else 0
var x := i * (bw + gap)
var h := (base - top) * v / mx
draw_rect(Rect2(x, base - 2, bw, 2), Style.PANEL_LIGHT)
if v > 0:
draw_rect(Rect2(x, base - h, bw, h), Style.BLUE)
Style.text(self, str(v), 11, Style.TEXT, Vector2(x + bw / 2, base - h - 8), "center", true)
Style.text(self, str(i), 11, Style.MUTED, Vector2(x + bw / 2, base + 8), "center")
## Elementanteile des Stapels: je Element eine farbige Kachel mit der Kartenzahl.
class ElementShare extends Control:
var counts := {}
func _init(rect: Rect2) -> void:
position = rect.position
size = rect.size
mouse_filter = Control.MOUSE_FILTER_IGNORE
func set_counts(c: Dictionary) -> void:
counts = c
queue_redraw()
func shown() -> Array:
return ElementView.all().filter(func(e): return int(counts.get(e, 0)) > 0)
func _draw() -> void:
var items := shown()
if items.is_empty():
return
var w := size.x / items.size()
for i in items.size():
var e: String = items[i]
var col: Color = Cards.ELEMENT_COLORS[e]
var r := Rect2(i * w + 2, 0, w - 4, size.y)
draw_style_box(Style.box(Color(col, 0.25), 6, Color(col, 0.7), 1), r)
Style.text(self, "%s %d" % [tr(e), int(counts[e])], 12, Style.TEXT, r.get_center(), "center", true)
func _ready() -> void: func _ready() -> void:
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind("")) all_button = Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL] var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
for i in filters.size(): for i in filters.size():
var e: String = filters[i] var e: String = filters[i]
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8)) var fb := ElementButton.new(e)
fb.add_theme_font_size_override("font_size", 14) fb.position = Vector2(108 + i * 40, 86)
fb.size = Vector2(34, 34)
fb.pressed.connect(set_filter.bind(e))
Style.juice(fb)
add_child(fb)
element_buttons[e] = fb
for c in MAX_COST + 1:
var cb := Style.button(self, str(c), Rect2(330 + c * 40, 86, 36, 34), set_cost.bind(c))
cb.add_theme_font_size_override("font_size", 14)
cost_buttons.append(cb)
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650)) grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
grid.card_clicked.connect(toggle) grid.card_clicked.connect(toggle)
add_child(grid) add_child(grid)
Style.panel(self, Rect2(820, 86, 420, 696)) Style.panel(self, Rect2(820, 86, 420, 696))
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE) count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
strength_label = Style.chip(self, "", Rect2(860, 144, 340, 36), Style.GOLD) strength_label = Style.chip(self, "", Rect2(860, 144, 340, 36), Style.GOLD)
var hint := Style.label(self, "", 15, Style.MUTED, Rect2(840, 188, 380, 66), "center") Style.label(self, "Manakurve", 13, Style.MUTED, Rect2(850, 190, 360, 20), "center")
hint.autowrap_mode = TextServer.AUTOWRAP_WORD # vor dem Text, sonst wächst die Breite auf eine Zeile curve = CurveView.new(Rect2(850, 212, 360, 100))
hint.text = "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters." add_child(curve)
Style.button(self, "Auffüllen", Rect2(840, 262, 380, 46), fill_up) share = ElementShare.new(Rect2(850, 322, 360, 30))
Style.button(self, "Leeren", Rect2(840, 318, 380, 46), clear_deck, Color("5a3c28")) add_child(share)
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 382, 380, 48), "center") preview_holder = Control.new()
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(preview_holder)
Style.button(self, "Auffüllen", Rect2(840, 626, 380, 44), fill_up, Style.PRIMARY)
Style.button(self, "Leeren", Rect2(840, 678, 380, 40), clear_deck)
msg_label = Style.label(self, "", 14, Style.MUTED, Rect2(840, 726, 380, 48), "center")
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD
refresh() refresh()
@@ -46,15 +147,34 @@ func set_filter(e: String) -> void:
refresh() refresh()
## Manafilter: derselbe Knopf noch einmal hebt ihn auf.
func set_cost(c: int) -> void:
cost_filter = -1 if cost_filter == c else c
refresh()
## Hebt den aktiven Filterknopf hervor.
func _mark(b: Button, active: bool, base: Color) -> void:
b.add_theme_stylebox_override("normal", Style.box(Style.lighter(base) if active else base, 8, Style.GOLD if active else Style.MUTED, 2 if active else 1))
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 8, Style.GOLD if active else Style.MUTED, 2))
func toggle(card: Cards.Card) -> void: func toggle(card: Cards.Card) -> void:
var err: String = app.profile.toggle_deck(card.id) var err: String = app.profile.toggle_deck(card.id)
msg_label.text = err say(err)
if err == "": if err == "":
app.sfx.play("click") app.sfx.play("click")
app.save() app.save()
refresh() refresh()
## Fehler rot, sonst der Hinweis.
func say(err: String) -> void:
msg_label.text = tr(err) if err != "" else tr("Klick auf eine Karte: rein oder raus.")
msg_label.add_theme_color_override("font_color", Style.RED if err != "" else Style.MUTED)
## Füllt mit den wertvollsten eigenen Karten des gewählten Filters bis zum Höchstwert auf. ## Füllt mit den wertvollsten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
func fill_up() -> void: func fill_up() -> void:
var p: Profile = app.profile var p: Profile = app.profile
@@ -72,30 +192,75 @@ func clear_deck() -> void:
refresh() refresh()
## Eigene Karten, die im Stapel zuerst, dann höchste Stufen vorn, dann nach Kosten. ## Sortierschlüssel einer Karte: im Stapel zuerst, dann höchste Stufen vorn, dann Kosten, dann ID.
func sort_key(c: Cards.Card) -> Array:
var p: Profile = app.profile
return [0 if c.id in p.deck else 1, -p.level(c.id), c.cost, c.id]
## Eigene Karten nach Element- und Manafilter, sortiert (sort_key).
func cards() -> Array: func cards() -> Array:
var p: Profile = app.profile var p: Profile = app.profile
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id)) var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
if filter != "": if filter != "":
out = out.filter(func(c): return filter in c.elements) out = out.filter(func(c): return filter in c.elements)
out.sort_custom(func(a, b): if cost_filter >= 0:
var ia: bool = a.id in p.deck out = out.filter(func(c): return c.cost == cost_filter)
var ib: bool = b.id in p.deck out.sort_custom(func(a, b): return sort_key(a) < sort_key(b))
if ia != ib:
return ia
if p.level(a.id) != p.level(b.id):
return p.level(a.id) > p.level(b.id)
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
return out return out
## Vorschau der gezeigten Karte in ihrer Stufe.
func show_card(card: Cards.Card) -> void:
var p: Profile = app.profile
shown_card = card
Style.clear(preview_holder)
var v := CardView.new(card.leveled(p.level(card.id)), PREVIEW.size, false, p.rarity(card.id))
v.position = PREVIEW.position
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v)
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX] count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE) count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
strength_label.text = tr("Stärke %d") % DeckGen.strength(p.deck, p.collection) strength_label.text = tr("Stärke %d") % DeckGen.strength(p.deck, p.collection)
_mark(all_button, filter == "", Style.PANEL_LIGHT)
var open: Array = p.unlocked_elements() # nur Elemente offener Kapitel
for e in element_buttons:
var eb: Button = element_buttons[e]
eb.visible = e in open
if eb.visible:
eb.position = Vector2(108 + open.find(e) * 40, 86)
_mark(eb, filter == e, Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
for c in cost_buttons.size():
_mark(cost_buttons[c], cost_filter == c, Style.PANEL_LIGHT)
var counts := []
counts.resize(11)
counts.fill(0)
var per_element := {}
for id in p.deck:
var c := Cards.by_id(id)
counts[clampi(c.cost, 0, 10)] += 1
per_element[c.elements[0]] = int(per_element.get(c.elements[0], 0)) + 1
curve.set_counts(counts)
share.set_counts(per_element)
if msg_label.text == "":
say("")
var shown := cards() var shown := cards()
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look]) grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
var views := grid.views() var views := grid.views()
for i in views.size(): for i in views.size():
views[i].dim = not (shown[i].id in p.deck) var in_deck: bool = shown[i].id in p.deck
views[i].dim = not in_deck
views[i].marked = in_deck
views[i].hover.connect(func(c, entered):
if entered:
show_card(c))
if shown_card != null and p.owns(shown_card.id):
show_card(shown_card)
elif shown.is_empty():
Style.clear(preview_holder)
else:
show_card(shown[0])
+10 -22
View File
@@ -1,14 +1,12 @@
class_name ElementView class_name ElementView
extends Control extends Control
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen ## Elementpaare einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente mit offenen Wesen
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je ## da: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je offenem Wesen, beide:
## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten ## Effekt aktiv; Neutral hat keinen) und die Zahl der offenen Wesen.
## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
const ROW := 32.0 const ROW := 32.0
var own := {} # offene eigene Wesen je Element var own := {} # offene eigene Wesen je Element
var played := {} # ausgespielte Karten je Element
func _init(rect: Rect2) -> void: func _init(rect: Rect2) -> void:
@@ -17,7 +15,6 @@ func _init(rect: Rect2) -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE mouse_filter = Control.MOUSE_FILTER_IGNORE
for e in all(): for e in all():
own[e] = 0 own[e] = 0
played[e] = 0
static func all() -> Array: static func all() -> Array:
@@ -27,7 +24,6 @@ static func all() -> Array:
func update_from(_g: Game, me: Game.Player) -> void: func update_from(_g: Game, me: Game.Player) -> void:
for e in all(): for e in all():
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()
@@ -35,24 +31,18 @@ func paired(e: String) -> bool:
return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR
func bonus(e: String) -> int: ## Elemente mit offenen Wesen.
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
func shown() -> Array: func shown() -> Array:
return all().filter(func(e): return int(own.get(e, 0)) > 0 or int(played.get(e, 0)) > 0) return all().filter(func(e): return int(own.get(e, 0)) > 0)
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus. ## Der Inhalt als Text (Tests, Protokoll): Element und offene Wesen, dazu der Effekt.
func text() -> String: func text() -> String:
var parts: Array[String] = [] var parts: Array[String] = []
for e in shown(): for e in shown():
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))] var s := "%s %d" % [tr(e), int(own.get(e, 0))]
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/+%d)" % [bonus(e), bonus(e)]
parts.append(s) parts.append(s)
return " · ".join(parts) return " · ".join(parts)
@@ -61,7 +51,7 @@ func _draw() -> void:
var y := ROW / 2 var y := ROW / 2
for e in shown(): for e in shown():
var col: Color = Cards.ELEMENT_COLORS[e] var col: Color = Cards.ELEMENT_COLORS[e]
var active := paired(e) or bonus(e) > 0 var active := paired(e)
draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4)) draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4))
var px := 10.0 var px := 10.0
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active) Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
@@ -71,7 +61,5 @@ func _draw() -> void:
var filled := k < int(own.get(e, 0)) var filled := k < int(own.get(e, 0))
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3)) Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
px += Game.ELEMENT_PAIR * 24 + 8 px += Game.ELEMENT_PAIR * 24 + 8
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true) Style.text(self, str(int(own.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
if bonus(e) > 0:
Style.text(self, "+%d/+%d" % [bonus(e), bonus(e)], 12, Style.GREEN, Vector2(size.x - 6, y), "midright", true)
y += ROW y += ROW
+3 -3
View File
@@ -18,7 +18,7 @@ func _ready() -> void:
add_child(sfx) add_child(sfx)
profile = Profile.load_or_new(save_path) profile = Profile.load_or_new(save_path)
apply_settings() apply_settings()
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new()) show_screen(MenuScreen.new())
func apply_settings() -> void: func apply_settings() -> void:
@@ -30,12 +30,12 @@ func apply_settings() -> void:
theme = Style.make_theme() theme = Style.make_theme()
## Neues Profil: Sammlung und Fortschritt werden verworfen, das Startdeck wird neu gewählt. ## Neues Profil: Sammlung und Fortschritt werden verworfen, zurück bleiben die neutralen Karten.
func reset_profile() -> void: func reset_profile() -> void:
profile = Profile.starter() profile = Profile.starter()
save() save()
apply_settings() apply_settings()
show_screen(StartDeckScreen.new()) show_screen(MenuScreen.new())
func set_language(code: String) -> void: func set_language(code: String) -> void:
+12 -4
View File
@@ -1,6 +1,7 @@
class_name MenuScreen class_name MenuScreen
extends Control extends Control
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster. ## Hauptmenü: links Titel und Spielarten (darunter Booster, Sammlung, Skilltree, dann Einstellungen und
## Beenden), rechts drei Karten der eigenen Sammlung als Schaufenster, unten rechts die Cheats.
var app var app
var coins_label: Label var coins_label: Label
@@ -25,10 +26,17 @@ func _ready() -> void:
var x := 80 var x := 80
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())], for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
["Sammlung", func(): app.show_screen(CollectionScreen.new())], ["Sammlung", func(): app.show_screen(CollectionScreen.new())],
["Einstellungen", func(): app.show_screen(SettingsScreen.new())], ["Skilltree", func(): app.show_screen(SkillScreen.new())]]:
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
x += 176
y += 56
x = 80
for entry in [["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
["Beenden", func(): get_tree().quit()]]: ["Beenden", func(): get_tree().quit()]]:
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1]) Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
x += 176 x += 176
var cheats := Style.button(self, "Cheats", Rect2(1120, 744, 120, 36), func(): app.show_screen(CheatScreen.new()))
cheats.add_theme_font_size_override("font_size", 14)
notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left") notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
_showcase(p) _showcase(p)
@@ -55,8 +63,8 @@ func _showcase(p: Profile) -> void:
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust] coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d") % [ coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d · Level %d") % [
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()] p.collection.size(), p.unlocked_cards().size(), p.campaign_stage, Campaign.total(), p.player_level()]
var notes := p.take_notices() var notes := p.take_notices()
notice_label.text = " · ".join(notes) notice_label.text = " · ".join(notes)
if not notes.is_empty(): if not notes.is_empty():
+7 -3
View File
@@ -1,7 +1,7 @@
class_name OpponentSelectScreen class_name OpponentSelectScreen
extends Control extends Control
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf ## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Der Gegner spielt ein Element aus den
## vom gemeinsamen Stapel (siehe Game.start). ## offenen Kapiteln in einer Stufe relativ zu deiner Deckstärke (Profile.single_opponent).
var app var app
var diff_buttons := {} var diff_buttons := {}
@@ -46,7 +46,11 @@ func refresh() -> void:
for key in diff_buttons: for key in diff_buttons:
_mark(diff_buttons[key], key == p.settings.difficulty) _mark(diff_buttons[key], key == p.settings.difficulty)
var d := p.difficulty() var d := p.difficulty()
info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))] var parts: Array[String] = [tr("Gegnerkarten: %s") % tr(Cards.RARITIES[p.opponent_level()]), tr("Vorausschau-KI") if bool(d.smart) else tr("einfache KI")]
if bool(d.perk):
parts.append(tr("ein Merkmal"))
parts.append(tr("Münzen %s") % Style.factor(float(d.reward)))
info_label.text = " · ".join(parts)
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void: func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
+70
View File
@@ -0,0 +1,70 @@
class_name SkillScreen
extends Control
## Skilltree: oben Level, Erfahrung bis zum nächsten Level und freie Skillpunkte; darunter drei
## Äste (Skills.BRANCHES) mit ihren Skills als Karten: Name, Wirkung, aktuelle und nächste Stufe
## in Zahlen (Skills.effect), Stufe und „Lernen“.
var app
var level_label: Label
var xp_label: Label
var points_label: Label
var msg_label: Label
var learn_buttons := {} # Skill-ID -> Knopf
var rank_labels := {} # Skill-ID -> Label
var now_labels := {} # Skill-ID -> Wirkung jetzt
var next_labels := {} # Skill-ID -> Wirkung der nächsten Stufe
func _ready() -> void:
Style.header(self, "Skilltree", func(): app.show_screen(MenuScreen.new()))
level_label = Style.chip(self, "", Rect2(40, 86, 220, 38))
xp_label = Style.chip(self, "", Rect2(276, 86, 400, 38), Style.BLUE)
points_label = Style.chip(self, "", Rect2(692, 86, 300, 38), Style.GREEN)
Style.label(self, "Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt.", 14, Style.MUTED, Rect2(40, 134, 1200, 22), "left")
for b in Skills.BRANCHES.size():
var branch: String = Skills.BRANCHES[b]
var x := 40 + b * 410
Style.panel(self, Rect2(x, 170, 390, 580))
Style.label(self, branch, 22, Style.GOLD, Rect2(x, 184, 390, 32), "center", true)
var y := 236
for id in Skills.in_branch(branch):
var sk: Dictionary = Skills.SKILLS[id]
Style.panel(self, Rect2(x + 16, y, 358, 176), Style.PANEL_LIGHT, 8)
Style.label(self, sk.name, 18, Style.TEXT, Rect2(x + 32, y + 10, 326, 26), "left", true)
Style.label(self, sk.text, 14, Style.MUTED, Rect2(x + 32, y + 38, 326, 22), "left")
now_labels[id] = Style.label(self, "", 14, Style.TEXT, Rect2(x + 32, y + 64, 326, 22), "left")
next_labels[id] = Style.label(self, "", 14, Style.GREEN, Rect2(x + 32, y + 86, 326, 22), "left")
rank_labels[id] = Style.label(self, "", 15, Style.GOLD, Rect2(x + 32, y + 126, 150, 30), "left", true)
learn_buttons[id] = Style.button(self, "Lernen", Rect2(x + 214, y + 122, 144, 40), learn.bind(id), Style.PRIMARY)
y += 192
msg_label = Style.label(self, "", 15, Style.RED, Rect2(0, 760, 1280, 24), "center")
refresh()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())
func learn(id: String) -> void:
var err: String = app.profile.learn(id)
msg_label.text = tr(err) if err != "" else ""
if err == "":
app.sfx.play("combo")
app.save()
refresh()
func refresh() -> void:
var p: Profile = app.profile
var lvl := p.player_level()
var base := Skills.xp_for_level(lvl)
level_label.text = tr("Level %d") % lvl
xp_label.text = tr("%d / %d EP bis Level %d") % [p.xp - base, Skills.xp_for_level(lvl + 1) - base, lvl + 1]
points_label.text = tr("Skillpunkte: %d") % p.skill_points()
for id in Skills.SKILLS:
var sk: Dictionary = Skills.SKILLS[id]
rank_labels[id].text = tr("Stufe %d / %d") % [p.skill(id), int(sk.max)]
now_labels[id].text = tr("Jetzt: %s") % Skills.effect(id, p.skill(id))
next_labels[id].text = tr("Nächste Stufe: %s") % Skills.effect(id, p.skill(id) + 1) if p.skill(id) < int(sk.max) else tr("Höchste Stufe erreicht")
learn_buttons[id].disabled = p.skill_points() <= 0 or p.skill(id) >= int(sk.max)
+1
View File
@@ -0,0 +1 @@
uid://b0dgryn2kpic5
-37
View File
@@ -1,37 +0,0 @@
class_name StartDeckScreen
extends Control
## Erster Start: eines von vier Startdecks wählen, je eines mit einem Element im Mittelpunkt
## (25 Karten davon, je fünf der anderen). Danach geht es ins Menü.
var app
func _ready() -> void:
Style.label(self, tr("Wähle dein Startdeck"), 40, Style.GOLD, Rect2(0, 60, 1280, 60), "center", true)
Style.label(self, tr("25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest."), 17, Style.MUTED, Rect2(140, 124, 1000, 30), "center")
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var x := 70 + i * 290
var col: Color = Cards.ELEMENT_COLORS[e]
Style.panel(self, Rect2(x, 190, 260, 470), Style.darker(col, 0.75), 12, col, 2)
var ids := DeckGen.start_deck(e)
var sample: Array = ids.filter(func(id): return Cards.by_id(id).elements[0] == e)
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
for k in 3:
var c: Cards.Card = Cards.by_id(sample[k * 7])
var v := CardView.new(c, Vector2(120, 152), false, "Common")
v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
v.rotation_degrees = -6 + k * 6
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(v)
Style.label(self, tr(e), 28, Style.TEXT, Rect2(x, 400, 260, 40), "center", true)
Style.label(self, tr(Cards.KEYWORD_TEXT[Cards.ELEMENT_KEYWORD[e]]), 15, Style.MUTED, Rect2(x + 16, 446, 228, 60), "center")
var b := Style.button(self, tr("%s wählen") % tr(e), Rect2(x + 30, 590, 200, 50), choose.bind(e), Style.PRIMARY)
b.add_theme_font_size_override("font_size", 18)
func choose(element: String) -> void:
app.profile.choose_start(element)
app.save()
app.sfx.play("combo")
app.show_screen(MenuScreen.new())
-1
View File
@@ -1 +0,0 @@
uid://bxl6isdrgqb2b
+10
View File
@@ -26,6 +26,11 @@ const DANGER := Color("5a2828")
## Schriftgröße mit der Einstellung „Große Schrift“. ## Schriftgröße mit der Einstellung „Große Schrift“.
## Faktor als Text: ×2, ×1,5 (Dezimaltrenner der Sprache).
static func factor(f: float) -> String:
return "×" + String.num(f, 1).trim_suffix(".0").replace(".", TranslationServer.translate(","))
static func fs(size: int) -> int: static func fs(size: int) -> int:
return int(round(size * font_scale)) return int(round(size * font_scale))
@@ -630,6 +635,11 @@ static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tin
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2) ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
## Elementsymbol in einer Farbe (Filterknöpfe, Listen), Grundvariante.
static func element_icon(ci: CanvasItem, element: String, c: Vector2, size: float, col: Color) -> void:
_element_glyph(ci, element, c, size, 0, col)
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void: static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
var h := size / 2 var h := size / 2
match element: match element:
+2 -2
View File
@@ -16,7 +16,7 @@ func _initialize() -> void:
print("Beide mit Vorausschau:") print("Beide mit Vorausschau:")
_run(games / 2, r, false, false) _run(games / 2, r, false, false)
print("Kampagne (du mit Vorausschau), je Stufe:") print("Kampagne (du mit Vorausschau), je Stufe:")
for stage in [0, 4, 9, 14, 19, 24, 29]: for stage in [0, 4, 9, 19, 29, 39, 49]:
_campaign(stage, games / 4, r) _campaign(stage, games / 4, r)
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0)) print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
quit() quit()
@@ -25,7 +25,7 @@ func _initialize() -> void:
func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> void: func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> void:
var wins := 0 var wins := 0
for i in games: for i in games:
var g := Game.new_battle_with(Campaign.opponent(stage), r) var g := Game.new_battle_with(Campaign.opponent(stage), r, false, {"deck": DeckGen.element_deck(Cards.NEUTRAL)}) # du mit der Startsammlung
while g.winner == "": while g.winner == "":
AI.take_turn(g, g.current(), false) AI.take_turn(g, g.current(), false)
assert(g.turn < 300, "Spiel endet nicht") assert(g.turn < 300, "Spiel endet nicht")
+198 -134
View File
@@ -89,6 +89,8 @@ func _initialize() -> void:
test_merkmale() test_merkmale()
test_ki_zwei_schritte() test_ki_zwei_schritte()
test_kapitel_zwei_und_merkmalwahl() test_kapitel_zwei_und_merkmalwahl()
test_einzelspieler_gegner()
test_skilltree()
test_ki_zeitbudget() test_ki_zeitbudget()
test_englische_karten() test_englische_karten()
test_uebersetzung_vollstaendig() test_uebersetzung_vollstaendig()
@@ -96,20 +98,21 @@ func _initialize() -> void:
quit(1 if failed > 0 else 0) quit(1 if failed > 0 else 0)
## 150 Karten: je Element und für Neutral alle Werte von 0/1 bis 5/5, die ID verrät die Werte. ## 550 Karten: je Element und für Neutral alle Werte von 0/1 bis 10/10, die ID verrät die Werte.
func test_kartenpool() -> void: func test_kartenpool() -> void:
check(Cards.all().size() == 150, "150 Karten") check(Cards.all().size() == 550, "550 Karten")
check(Cards.all().all(func(c): return c.elements.size() == 1 and (c.elements[0] in Cards.ELEMENTS or c.elements[0] == Cards.NEUTRAL)), "jede Karte hat genau ein Element oder ist neutral") check(Cards.all().all(func(c): return c.elements.size() == 1 and (c.elements[0] in Cards.ELEMENTS or c.elements[0] == Cards.NEUTRAL)), "jede Karte hat genau ein Element oder ist neutral")
for e in Cards.ELEMENTS + [Cards.NEUTRAL]: for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
check(Cards.all().filter(func(c): return c.elements == [e]).size() == 30, "30 Karten je Element: " + e) check(Cards.all().filter(func(c): return c.elements == [e]).size() == 110, "110 Karten je Element: " + e)
var names := {} var names := {}
for c in Cards.all(): for c in Cards.all():
var parts: PackedStringArray = c.id.split("_") var parts: PackedStringArray = c.id.split("_")
check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 5 and c.health >= 1 and c.health <= 5, "ID passt zu den Werten: " + c.id) check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 10 and c.health >= 1 and c.health <= 10, "ID passt zu den Werten: " + c.id)
check(c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id) check(c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.keywords.is_empty() and c.text() == ("Neutral." if c.is_neutral() else "") and c.name_en != "", "Karten kommen ohne Eigenschaften (außer Neutral), mit englischem Namen: " + c.id) check(c.keywords.is_empty() and c.text() == ("Neutral." if c.is_neutral() else "") and c.name_en != "", "Karten kommen ohne Eigenschaften (außer Neutral), mit englischem Namen: " + c.id)
names[c.name] = true names[c.name] = true
check(names.size() == 150, "alle Namen verschieden") check(names.size() == 550, "alle Namen verschieden")
check(C("f_10_10").cost == 10 and C("n_6_1").name == "Eiserner Knappe" and C("w_0_10").name == "Stiller Urleviathan", "10/10 kostet 10; neue Eigenschaften und Wesen ab 6")
check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral") check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral")
check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts") check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts")
check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10") check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10")
@@ -146,12 +149,13 @@ func test_mana_und_kosten() -> void:
check(h.energy == Game.START_MANA + 21, "keine Obergrenze") check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
h.energy = 5 h.energy = 5
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_3_2").cost, "Ausspielen kostet die Kartenkosten") check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_3_2").cost, "Ausspielen kostet die Kartenkosten")
check(h.cards_played == 1 and g.clone().human.cards_played == 1, "ausgespielte Karte gezählt, auch in der Kopie")
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) == 4 and Cards.fusion_value(8, 1) == 6 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 3 and Cards.fusion_value(8, 8) == 9, "Fusionswert: aufgerundete halbe Summe plus eins") check(Cards.fusion_value(4, 2) == 4 and Cards.fusion_value(8, 1) == 6 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 3 and Cards.fusion_value(8, 8) == 9, "Fusionswert: aufgerundete halbe Summe plus eins")
## Stapel: 40 verschiedene Karten, zwölf davon im ersten Zug bezahlbar; Kampagnengegner bekommen ## Stapel: 40 verschiedene Karten, zwölf davon im ersten Zug bezahlbar; Kampagnengegner bekommen
## Stapel mit Kostendeckel und ab Kapitel 2 höhere Stufen; Startdecks je Element; Auffüllen. ## Zufällige Stapel; Elementstapel der Kampagne in der Stufe des Kapitels; Auffüllen.
func test_stapel() -> void: func test_stapel() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 5 r.seed = 5
@@ -160,29 +164,45 @@ func test_stapel() -> void:
var uniq := {} var uniq := {}
for id in d: for id in d:
uniq[id] = true uniq[id] = true
check(d.size() == Game.DECK_MIN and uniq.size() == Game.DECK_MIN, "40 verschiedene Karten") check(d.size() == Game.DECK_MIN and uniq.size() == Game.DECK_MIN, "30 verschiedene Karten")
check(d.filter(func(id): return Cards.by_id(id).cost <= Game.START_MANA).size() >= DeckGen.CHEAP_IN_RANDOM, "mindestens zwölf billige") check(d.filter(func(id): return Cards.by_id(id).cost <= Game.START_MANA).size() >= DeckGen.CHEAP_IN_RANDOM, "mindestens zwölf billige")
var easy := DeckGen.opponent_deck(r, 1) var neutral := DeckGen.element_deck(Cards.NEUTRAL)
check(easy.all(func(id): return Cards.by_id(id).cost <= 3), "Kapitel 1: nur bis Kosten 3") check(neutral.size() == 110 and neutral.all(func(id): return Cards.by_id(id).is_neutral()), "alle 110 neutralen Karten")
var start := DeckGen.curve_deck(Cards.NEUTRAL, r)
var per_cost := {}
var uniq2 := {}
for id in start:
per_cost[Cards.by_id(id).cost] = int(per_cost.get(Cards.by_id(id).cost, 0)) + 1
uniq2[id] = true
check(start.size() == Game.DECK_MIN and start.all(func(id): return Cards.by_id(id).is_neutral()) and per_cost.get(0, 0) == 2 and per_cost.get(3, 0) == 6 and per_cost.get(7, 0) == 1 and not per_cost.has(8), "Kurvenstapel: 30 Neutrale nach Manakurve (2 zu 0, 6 zu 3, 1 zu 7, nichts darüber)")
check(uniq2.size() == Game.DECK_MIN and DeckGen.curve_deck(Cards.NEUTRAL, r) != start, "nichts doppelt, jedes Mal anders gewürfelt")
var capped := DeckGen.curve_deck("Feuer", r, 3)
check(capped.size() == Game.DECK_MIN and capped.all(func(id): return Cards.by_id(id).cost <= 3 and Cards.by_id(id).elements[0] == "Feuer"), "Kostendeckel 3: 30 Feuerkarten bis Kosten 3")
var fenn := Campaign.opponent(0, r)
check(fenn.deck.size() == Game.DECK_MIN and fenn.deck.all(func(id): return Cards.by_id(id).is_neutral() and Cards.by_id(id).cost <= 3) and fenn.looks.values().all(func(l): return l == 0), "Kapitel 1, Stufe 1: 30 Neutrale bis Kosten 3 in Common")
var boss := Campaign.opponent(15, r) var boss := Campaign.opponent(15, r)
check(boss.deck.size() == Game.DECK_MIN and boss.deck.all(func(id): return Cards.by_id(id).cost <= 5) and boss.looks.values().all(func(l): return l == 1), "Kapitel 2: bis Kosten 5, Stufe Rare") check(boss.deck.size() == Game.DECK_MIN and boss.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer" and Cards.by_id(id).cost <= 6) and boss.looks.values().all(func(l): return l == 0), "Kapitel 2, Stufe 6: Feuer bis Kosten 6 in Common")
for e in Cards.ELEMENTS: check(Campaign.stage(0).max_cost == 3 and Campaign.stage(9).max_cost == 10 and Campaign.stage(14).max_cost == 5, "Kostendeckel steigt je Stufe von 3 auf 10")
var sd := DeckGen.start_deck(e) for c in Campaign.CHAPTER_ELEMENTS.size():
var main := sd.filter(func(id): return Cards.by_id(id).elements[0] == e) var e: String = Campaign.CHAPTER_ELEMENTS[c]
check(sd.size() == Game.DECK_MIN and main.size() == 25 and main.all(func(id): return Cards.by_id(id).attack <= 4), "Startdeck %s: 25 Karten des Elements bis Angriff 4" % e) var o := Campaign.opponent(c * Campaign.STAGES_PER_CHAPTER + 9, r)
for o in Cards.ELEMENTS: check(o.deck.all(func(id): return Cards.by_id(id).elements[0] == e) and o.looks.values().all(func(l): return l == maxi(0, c - 1)), "Boss von Kapitel %d: nur %s in Stufe %d" % [c + 1, e, maxi(0, c - 1)])
if o != e:
var others := sd.filter(func(id): return Cards.by_id(id).elements[0] == o)
check(others.size() == 5 and others.all(func(id): return Cards.by_id(id).cost <= Game.START_MANA), "Startdeck %s: fünf billige %s" % [e, o])
var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3) var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3)
check(filled == ["f_0_1", "w_5_5", "e_0_4"], "Auffüllen ohne Stufen: teuerste zuerst, dann nach ID, nichts doppelt, bis zur Grenze") check(filled == ["f_0_1", "e_0_4", "f_2_1"], "Auffüllen ohne Stufen: gleicher Wert, dann die billigeren, nach ID, nichts doppelt, bis zur Grenze")
var full := DeckGen.fill([], neutral, Game.DECK_MIN)
var curve_counts: Array[int] = []
curve_counts.resize(11)
curve_counts.fill(0)
for id in full:
curve_counts[Cards.by_id(id).cost] += 1
check(full.size() == Game.DECK_MIN and curve_counts == DeckGen.CURVE, "Auffüllen aus 110 Neutralen folgt der Manakurve")
filled = DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 4, {"l_1_1": 2, "e_0_4": 1, "f_2_1": 1}) filled = DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 4, {"l_1_1": 2, "e_0_4": 1, "f_2_1": 1})
check(filled == ["f_0_1", "l_1_1", "e_0_4", "f_2_1"], "Auffüllen: wertvollste zuerst, bei gleichem Wert die teuerste") check(filled == ["f_0_1", "l_1_1", "e_0_4", "f_2_1"], "Auffüllen: wertvollste zuerst, bei gleichem Wert die teuerste")
# Kartenwert: Grundkarten 100, je Stufe +100; Fusionen aus ihren Werten # Kartenwert: Grundkarten 100, je Stufe +100; Fusionen aus ihren Werten
check(Cards.all().all(func(c): return c.value() == 100), "jede Grundkarte ist 100 wert") check(Cards.all().all(func(c): return c.value() == 100), "jede Grundkarte ist 100 wert")
check(C("l_1_1").leveled(4).value() == 500 and C("w_4_5").leveled(2).value() == 300, "Ultimate Rare 500, Super Rare 300") check(C("l_1_1").leveled(4).value() == 500 and C("w_4_5").leveled(2).value() == 300, "Ultimate Rare 500, Super Rare 300")
check(Cards.fused(C("f_3_2"), C("w_4_5")).value() == 100 and Cards.fused(C("f_3_2"), C("w_4_5"), 3).value() == 400, "Dampf: 100, in Stufe 3 400") check(Cards.fused(C("f_3_2"), C("w_4_5")).value() == 100 and Cards.fused(C("f_3_2"), C("w_4_5"), 3).value() == 400, "Dampf: 100, in Stufe 3 400")
check(DeckGen.strength(["f_0_1", "l_1_1", "w_4_5"], {"l_1_1": 4, "w_4_5": 2}) == 300 and DeckGen.strength([], {}) == 0 and DeckGen.strength(DeckGen.start_deck("Feuer"), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startdeck 100") check(DeckGen.strength(["f_0_1", "l_1_1", "w_4_5"], {"l_1_1": 4, "w_4_5": 2}) == 300 and DeckGen.strength([], {}) == 0 and DeckGen.strength(DeckGen.element_deck(Cards.NEUTRAL), {}) == 100, "Stärke: Mittelwert 300, leer 0, Startsammlung 100")
## Start: die obersten fünf Karten vom eigenen Stapel, ungefiltert. Ziehen bringt eine Karte; ## Start: die obersten fünf Karten vom eigenen Stapel, ungefiltert. Ziehen bringt eine Karte;
@@ -269,51 +289,42 @@ func test_elementvorteil() -> void:
check(Cards.matchup(["Neutral"], ["Feuer"]) == 0 and Cards.matchup(["Wasser"], ["Neutral"]) == 0, "Neutral: nie im Vorteil oder Nachteil") check(Cards.matchup(["Neutral"], ["Feuer"]) == 0 and Cards.matchup(["Wasser"], ["Neutral"]) == 0, "Neutral: nie im Vorteil oder Nachteil")
var feuerritter := ["Feuer", "Neutral"] var feuerritter := ["Feuer", "Neutral"]
check(Cards.matchup(feuerritter, ["Luft"]) == 1 and Cards.matchup(feuerritter, ["Wasser"]) == 0 and Cards.matchup(["Wasser"], feuerritter) == 0, "Feuerritter: Vorteil bleibt, die Schwäche ist weg") check(Cards.matchup(feuerritter, ["Luft"]) == 1 and Cards.matchup(feuerritter, ["Wasser"]) == 0 and Cards.matchup(["Wasser"], feuerritter) == 0, "Feuerritter: Vorteil bleibt, die Schwäche ist weg")
# Ohne Stapelbonus bewirkt das Element nichts # Schaden: im Vorteil doppelt, im Nachteil halb (aufgerundet), gegen Helden nichts
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2] var o: Game.Player = s[2]
var tiefen := put(h, "w_4_5") # Wasser 4/5 var tiefen := put(h, "w_4_5") # Wasser 4/5
var wolf := put(o, "f_3_2") # Feuer 3/2 var wolf := put(o, "f_3_2") # Feuer 3/2
check(Game.matchup_bonus(tiefen, wolf) == 4 and Game.matchup_bonus(wolf, tiefen) == -1, "Wasser schlägt Feuer doppelt (4 → 8), Feuer kontert halb (3 → 2)")
var life: int = o.life var life: int = o.life
g.attack(tiefen, wolf) g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life and tiefen.health == 5 - 3, "Wasser gegen Feuer ohne Stapelbonus: 4 Schaden, Konter 3, nichts schlägt durch") check(wolf.health <= 0 and o.life == life and tiefen.health == 5 - 2, "8 Schaden töten den Wolf, der Konter bringt 2, nichts schlägt durch")
# Im Vorteil zählt der Stapelbonus des Ziels nicht; beim Konter fehlt dem Ziel sein Angriffsbonus # Nachteil beim Angriff: halb aufgerundet; der Konter des Ziels kommt doppelt
var s3 := setup()
var w3 := put(s3[1], "w_4_5")
var f3 := put(s3[2], "f_3_2")
s3[2].played["Feuer"] = 6
s3[0].apply_auras()
check(f3.aura == 2 and f3.power() == 5 and f3.max_health == 4 and Game.matchup_bonus(w3, f3) == 2 and Game.matchup_bonus(f3, w3) == -2, "Feuer mit +2/+2: Wasser schlägt 2 mehr, Feuer 2 weniger")
s3[0].attack(w3, f3)
check(f3.health <= 0 and w3.health == 5 - 3, "Wasser trifft für 4 + 2 durch den Bonus, der Konter kommt ohne Bonus: 3")
# Im Nachteil fehlt der eigene Angriffsbonus; der Konter geht durch den eigenen Lebensbonus
var s4 := setup() var s4 := setup()
var f4 := put(s4[1], "f_3_2") var f4 := put(s4[1], "f_3_2")
var w4 := put(s4[2], "w_4_5") var w4 := put(s4[2], "w_4_5")
s4[1].played["Feuer"] = 6
s4[0].apply_auras()
s4[0].attack(f4, w4) s4[0].attack(f4, w4)
check(w4.health == 5 - 3 and f4.health <= 0, "Feuer mit +2/+2 greift Wasser an: schlägt ohne Bonus für 3, der Konter 4 + 2 geht durch den Bonus") check(w4.health == 5 - 2 and f4.health <= 0, "Feuer trifft Wasser für 2 (3 halbiert, aufgerundet), der Konter 4 × 2 tötet")
# 0 Angriff: nur der Stapelbonus des Ziels gibt Schaden # Rundung und Null
var s5 := setup() var s5 := setup()
var funke := put(s5[1], "f_0_2") # Feuer 0/2 var funke := put(s5[1], "f_0_2") # Feuer 0/2
var hauch := put(s5[2], "l_0_2") # Luft 0/2 var hauch := put(s5[2], "l_0_2") # Luft 0/2
check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff, Ziel ohne Bonus: kein Angriff") check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff: auch im Vorteil kein Schaden")
s5[2].played["Luft"] = 3 var wall := put(s5[2], "w_5_5")
s5[0].apply_auras() check(Game.matchup_bonus(put(s5[1], "f_1_1"), wall) == 0 and Game.matchup_bonus(put(s5[1], "f_2_1"), wall) == -1 and Game.matchup_bonus(put(s5[1], "f_5_1"), wall) == -2, "halbiert aufgerundet: 1 bleibt 1, 2 wird 1, 5 wird 3")
check(hauch.power() == 1 and hauch.health == 3 and s5[0].attack(funke, hauch) == "" and hauch.health == 2 and funke.health == 2, "Ziel mit +1/+1: 0 Angriff trifft durch den Bonus für 1, der Konter 1 − 1 bleibt aus") check(not s5[0].can_attack(put(s5[1], "l_0_2"), put(s5[2], "e_0_4"))[0], "0 Angriff im Vorteil (Luft gegen Erde): kein Angriff")
var wall := put(s5[2], "e_0_4") # Helden haben kein Element
var geist5 := put(s5[1], "w_2_2") # Wasser 2/2
check(s5[0].attack(geist5, wall) == "" and wall.health == 2 and geist5.health == 2, "Erdhüter ohne Bonus kontert nicht")
var s2 := setup() var s2 := setup()
var g2: Game = s2[0]
var geist := put(s2[1], "w_2_2") var geist := put(s2[1], "w_2_2")
s2[1].played["Wasser"] = 3 s2[0].attack(geist, s2[2])
g2.apply_auras() check(s2[2].life == 15 - 2, "gegen Helden gibt es keinen Vorteil")
g2.attack(geist, s2[2]) # Merkmal: kein Nachteil
check(s2[2].life == 12, "gegen Helden gibt es keinen Vorteil, nur den eigenen Bonus") var s6 := setup()
var f6 := put(s6[1], "f_3_2")
var w6 := put(s6[2], "w_4_5")
s6[1].perks = ["kein_nachteil"]
check(Game.matchup_bonus(f6, w6) == 0 and Game.matchup_bonus(w6, f6) == 4, "Merkmal: kein Nachteil für die eigenen Wesen, der Gegner behält seinen Vorteil")
## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt ## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt
@@ -344,9 +355,8 @@ func test_verdeckt() -> void:
check(seen.card.id == "?" and seen.attack == 2, "die KI-Kopie kennt verdeckte Wesen nicht") check(seen.card.id == "?" and seen.attack == 2, "die KI-Kopie kennt verdeckte Wesen nicht")
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft ## Elementpaare: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
## Flink, Wasser Regeneration, Feuer Doppelschlag); je drei ausgespielte Karten eines Elements geben ## Flink, Wasser Regeneration, Feuer Doppelschlag). Jeder Spieler zählt für sich, verdeckte nicht.
## den eigenen Wesen des Elements +1/+1. Jeder Spieler zählt für sich.
func test_elementboni() -> void: func test_elementboni() -> void:
var s := setup(["f_4_3", "f_4_3"]) var s := setup(["f_4_3", "f_4_3"])
var g: Game = s[0] var g: Game = s[0]
@@ -354,39 +364,17 @@ func test_elementboni() -> void:
var o: Game.Player = s[2] var o: Game.Player = s[2]
var fire := [put(h, "f_3_2"), put(h, "f_2_1")] var fire := [put(h, "f_3_2"), put(h, "f_2_1")]
var bird := put(o, "l_1_1") # Luft 1/1 var bird := put(o, "l_1_1") # Luft 1/1
g.apply_auras()
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")
check(not bird.has("Flink") and bird.power() == 1 and bird.health == 1, "der Gegner zählt für sich: nichts")
var hidden_fire := put(o, "f_4_3", true, true) var hidden_fire := put(o, "f_4_3", true, true)
g.apply_auras()
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar") check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
# Ausgespielte Karten zählen; je drei gibt es +1/+1 für die eigenen Wesen des Elements g.play_card(h, h.hand[0]) # Flammender Wolf
h.played["Feuer"] = 2 check(h.board[2].has("Doppelschlag") and h.board[2].power() == 4 and h.board[2].health == 3, "ausgespielt: Werte wie auf der Karte, dazu der Effekt")
g.play_card(h, h.hand[0]) # Flammender Wolf: die dritte Feuerkarte
check(int(h.played["Feuer"]) == 3 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and h.board[2].aura == 1, "drei Feuerkarten ausgespielt: +1/+1 für alle eigenen Feuerwesen")
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)
check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt
var secret: Game.Creature = h.board.back() var secret: Game.Creature = h.board.back()
check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 4, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen") check(secret.hidden and not secret.has("Doppelschlag") and h.element_count("Feuer") == 3, "verdeckt gelegt: ohne Effekt, zählt nicht")
g._flip(secret) g._flip(secret)
check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus") check(secret.has("Doppelschlag") and h.element_count("Feuer") == 4, "aufgedeckt zählt es und bekommt den Effekt")
# Vorschau
h.played["Luft"] = 2
check(g.forecast(h, C("l_1_1")) == 1 and g.forecast(h, C("w_2_2")) == 0, "Vorschau: der Hauch wäre die dritte Luftkarte und bekäme +1/+1")
# Merkmal: doppelt
h.perks = ["element_doppelt"]
g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "Elementbonus zählt doppelt")
h.perks = []
h.played["Feuer"] = 6
g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "sechs Feuerkarten: +2/+2")
fire[0].health = 1
h.played["Feuer"] = 0
g.apply_auras()
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1, "Bonus weg: nur das Höchstleben sinkt")
# Die anderen Paare # Die anderen Paare
var s5 := setup() var s5 := setup()
var walls := [put(s5[1], "e_0_4"), put(s5[1], "e_0_4")] var walls := [put(s5[1], "e_0_4"), put(s5[1], "e_0_4")]
@@ -396,33 +384,22 @@ func test_elementboni() -> void:
var s6 := setup() var s6 := setup()
var geister := [put(s6[1], "w_2_2"), put(s6[1], "w_2_2")] var geister := [put(s6[1], "w_2_2"), put(s6[1], "w_2_2")]
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
var s2 := setup() var s2 := setup()
var g2: Game = s2[0]
var h2: Game.Player = s2[1] var h2: Game.Player = s2[1]
var dampf := put(h2, "f_3_2") var dampf := put(h2, "f_3_2")
dampf.card = Cards.fused(C("f_3_2"), C("w_2_2")) dampf.card = Cards.fused(C("f_3_2"), C("w_2_2"))
var fw := put(h2, "f_2_1") var fw := put(h2, "f_2_1")
var ww := put(h2, "w_2_2") var ww := put(h2, "w_2_2")
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")
h2.played["Feuer"] = 3 # Neutral: kein Paar
h2.played["Wasser"] = 3
g2.apply_auras()
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
# Neutral: kein Paar, aber ein eigener Zähler
var s7 := setup(["n_2_2", "n_2_2"]) var s7 := setup(["n_2_2", "n_2_2"])
var g7: Game = s7[0] var g7: Game = s7[0]
var h7: Game.Player = s7[1] var h7: Game.Player = s7[1]
g7.play_card(h7, h7.hand[0]) g7.play_card(h7, h7.hand[0])
g7.play_card(h7, h7.hand[0]) g7.play_card(h7, h7.hand[0])
check(h7.played["Neutral"] == 2 and h7.board[0].keywords().is_empty() and h7.board[0].aura == 0, "zwei Neutrale: kein Paar, der Zähler steht auf 2, noch kein Bonus") check(h7.board.size() == 2 and h7.board[0].keywords().is_empty() and h7.board[0].health == 2, "zwei Neutrale: kein Paar, nichts ändert sich")
check(g7.forecast(h7, C("n_5_5")) == 1 and g7.forecast(h7, C("f_5_5")) == 0, "Vorschau: die dritte Neutrale bringt +1/+1, nur für Neutrale")
h7.played["Neutral"] = 3
g7.apply_auras()
check(h7.board.all(func(c): return c.aura == 1 and c.health == 3), "drei Neutrale: +1/+1 für die neutralen Wesen")
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana. ## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
func test_abwerfen() -> void: func test_abwerfen() -> void:
@@ -493,7 +470,6 @@ func test_verschmelzen() -> void:
check(f.name == "Dampf" and f.attack == 5 and f.health == 5 and f.cost == 5, "Dampf: ⌈7/2⌉ + 1 = 5 Angriff und Leben, Kosten (10 + 5) / 3 = 5, die Verletzung ist weg") check(f.name == "Dampf" and f.attack == 5 and f.health == 5 and f.cost == 5, "Dampf: ⌈7/2⌉ + 1 = 5 Angriff und Leben, Kosten (10 + 5) / 3 = 5, die Verletzung ist weg")
check("Spott" in f.keywords and f.elements == ["Feuer", "Wasser"] and f.look == 0, "Eigenschaften und Elemente beider, Stufe Common") check("Spott" in f.keywords and f.elements == ["Feuer", "Wasser"] and f.look == 0, "Eigenschaften und Elemente beider, Stufe Common")
check(g.play_card(h, f) == "" and h.energy == 5 and h.board[0].health == 5 and not h.board[0].ready, "ausgespielt für die neuen Kosten, unverletzt, erst nächsten Zug bereit") check(g.play_card(h, f) == "" and h.energy == 5 and h.board[0].health == 5 and not h.board[0].ready, "ausgespielt für die neuen Kosten, unverletzt, erst nächsten Zug bereit")
check(h.played["Feuer"] == 1 and h.played["Wasser"] == 1, "zählt beim Legen für beide Elemente")
# Wesen mit Handkarte, auch verdeckt; stark plus schwach ergibt weniger als das starke # Wesen mit Handkarte, auch verdeckt; stark plus schwach ergibt weniger als das starke
var dampf: Game.Creature = h.board[0] var dampf: Game.Creature = h.board[0]
var secret := put(h, "e_0_4", false, true) var secret := put(h, "e_0_4", false, true)
@@ -639,27 +615,35 @@ func test_booster() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 3 r.seed = 3
for i in 50: for i in 50:
var rolled := Profile.roll_booster(r) var rolled := Profile.roll_booster(r, "Feuer")
check(rolled.size() == 5, "5 Karten") check(rolled.size() == 5 and rolled.all(func(x): return x.card.elements[0] == "Feuer"), "5 Feuerkarten")
check(rolled.any(func(x): return x.rarity != "Common"), "mindestens eine Rare") check(rolled.any(func(x): return x.rarity != "Common"), "mindestens eine Rare")
var p := Profile.new() var p := Profile.new()
p.coins = Profile.BOOSTER_COST p.coins = Profile.ELEMENT_BOOSTER_COST
var got := p.open_booster(r) check(p.open_booster("Feuer", r).is_empty() and p.coins == Profile.ELEMENT_BOOSTER_COST and not p.booster_unlocked("Feuer") and p.booster_unlocked(Cards.NEUTRAL), "Feuer-Booster erst ab Kapitel 2, neutral immer")
check(p.coins == 0 and got.size() == 5, "Booster kostet %d" % Profile.BOOSTER_COST) p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.booster_unlocked("Feuer") and not p.booster_unlocked("Wasser"), "Kapitel 2 offen: Feuer ja, Wasser nein")
var got := p.open_booster("Feuer", r)
check(p.coins == 0 and got.size() == 5 and got.all(func(x): return x.card.elements[0] == "Feuer"), "Feuer-Booster kostet %d" % Profile.ELEMENT_BOOSTER_COST)
check(got.all(func(x): return p.owns(x.card.id) and x.card.id in p.deck), "alle fünf sind jetzt in Sammlung und Stapel") check(got.all(func(x): return p.owns(x.card.id) and x.card.id in p.deck), "alle fünf sind jetzt in Sammlung und Stapel")
var fresh := got.filter(func(x): return x.new) var fresh := got.filter(func(x): return x.new)
check(not fresh.is_empty() and fresh.all(func(x): return x.dust == 0 and not x.upgraded), "neue Karten: kein Staub, kein Aufwerten") check(not fresh.is_empty() and fresh.all(func(x): return x.dust == 0 and not x.upgraded), "neue Karten: kein Staub, kein Aufwerten")
check(p.dust == got.reduce(func(a, x): return a + int(x.dust), 0), "Doppelte werden zu Staub") check(p.dust == got.reduce(func(a, x): return a + int(x.dust), 0), "Doppelte werden zu Staub")
check(p.open_booster(r).is_empty(), "ohne Münzen kein Booster") check(p.open_booster("Feuer", r).is_empty(), "ohne Münzen kein Booster")
p.coins = Profile.BOOSTER_COST
check(p.open_booster(Cards.NEUTRAL, r).size() == 5 and p.coins == 0, "neutraler Booster kostet %d" % Profile.BOOSTER_COST)
p.coins = Profile.ELEMENT_BOOSTER_COST * 3
check(p.open_boosters("Feuer", 4, r).is_empty() and p.coins == Profile.ELEMENT_BOOSTER_COST * 3, "vier Packs: zu teuer, nichts passiert")
check(p.open_boosters("Feuer", 3, r).size() == 15 and p.coins == 0, "drei Packs auf einmal: 15 Karten, alles bezahlt")
p.collection = {"f_3_2": 0} p.collection = {"f_3_2": 0}
p.deck = [] p.deck = []
for i in Game.DECK_MAX: for i in Game.DECK_MAX:
p.deck.append("x%d" % i) # voll: neue Karten passen nicht mehr hinein p.deck.append("x%d" % i) # voll: neue Karten passen nicht mehr hinein
p.coins = Profile.BOOSTER_COST * 20 p.coins = Profile.ELEMENT_BOOSTER_COST * 20
var dust_before := p.dust var dust_before := p.dust
var upgraded := false var upgraded := false
for i in 20: for i in 20:
for res in p.open_booster(r): for res in p.open_booster("Feuer", r):
if res.card.id == "f_3_2" and res.upgraded: if res.card.id == "f_3_2" and res.upgraded:
upgraded = true upgraded = true
check(res.dust == Profile.DUST_DISMANTLE[Cards.RARITIES[Cards.RARITIES.find(res.rarity) - 1]] or res.dust > 0, "die alte Version wird zu Staub") check(res.dust == Profile.DUST_DISMANTLE[Cards.RARITIES[Cards.RARITIES.find(res.rarity) - 1]] or res.dust > 0, "die alte Version wird zu Staub")
@@ -691,13 +675,11 @@ func test_booster() -> void:
check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "die Stufe bleibt über zwei Fusionen") check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "die Stufe bleibt über zwei Fusionen")
## Sammlung und Deck: Start mit einem Startdeck, Herstellen für Staub, Zerlegen entfernt ## Sammlung und Deck: Start mit den neutralen Karten, Herstellen für Staub, Zerlegen entfernt
## Common-Karten, der Stapel hält 40 bis 60 eigene Karten. ## Common-Karten, der Stapel hält 30 bis 60 eigene Karten.
func test_sammlung_und_deck() -> void: func test_sammlung_und_deck() -> void:
var p := Profile.starter() var p := Profile.starter()
check(not p.started() and p.collection.is_empty() and p.deck.is_empty(), "neues Profil: noch kein Startdeck") check(p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN and p.deck.all(func(id): return p.owns(id) and p.level(id) == 0 and Cards.by_id(id).is_neutral()), "neues Profil: alle 110 Neutralen in Common in der Sammlung, 30 im Stapel")
p.choose_start("Feuer")
check(p.started() and p.collection.size() == Game.DECK_MIN and p.deck == DeckGen.start_deck("Feuer") and p.deck.all(func(id): return p.owns(id)), "Startdeck Feuer: 40 Karten in Sammlung und Stapel")
check(p.toggle_deck("f_5_5") == "Karte nicht in der Sammlung", "fremde Karte nicht in den Stapel") check(p.toggle_deck("f_5_5") == "Karte nicht in der Sammlung", "fremde Karte nicht in den Stapel")
var first: String = p.deck[0] var first: String = p.deck[0]
check(p.toggle_deck(first) == "" and p.deck.size() == Game.DECK_MIN - 1, "Karte aus dem Stapel genommen") check(p.toggle_deck(first) == "" and p.deck.size() == Game.DECK_MIN - 1, "Karte aus dem Stapel genommen")
@@ -708,7 +690,7 @@ func test_sammlung_und_deck() -> void:
p.deck = DeckGen.fill(p.deck, p.collection.keys(), Game.DECK_MAX) p.deck = DeckGen.fill(p.deck, p.collection.keys(), Game.DECK_MAX)
var outside: Array = p.collection.keys().filter(func(id): return not (id in p.deck)) var outside: Array = p.collection.keys().filter(func(id): return not (id in p.deck))
check(p.toggle_deck(outside[0]) == "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX, "60 ist voll") check(p.toggle_deck(outside[0]) == "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX, "60 ist voll")
p.choose_start("Feuer") p = Profile.starter()
p.dust = Profile.CRAFT_COST - 1 p.dust = Profile.CRAFT_COST - 1
check(p.craft("f_5_5") == "Nicht genug Staub (%d nötig)" % Profile.CRAFT_COST, "Herstellen braucht Staub") check(p.craft("f_5_5") == "Nicht genug Staub (%d nötig)" % Profile.CRAFT_COST, "Herstellen braucht Staub")
p.dust = Profile.CRAFT_COST p.dust = Profile.CRAFT_COST
@@ -717,7 +699,7 @@ func test_sammlung_und_deck() -> void:
var in_deck: String = p.deck[0] var in_deck: String = p.deck[0]
check(p.dismantle(in_deck) == "" and not p.owns(in_deck) and not (in_deck in p.deck) and p.dust == Profile.DUST_DISMANTLE["Common"], "Common zerlegt: raus aus Sammlung und Stapel, Staub dafür") check(p.dismantle(in_deck) == "" and not p.owns(in_deck) and not (in_deck in p.deck) and p.dust == Profile.DUST_DISMANTLE["Common"], "Common zerlegt: raus aus Sammlung und Stapel, Staub dafür")
var filled := p.deck_cards() var filled := p.deck_cards()
check(filled.size() == Game.DECK_MIN and filled.all(func(id): return p.owns(id)) and filled.slice(0, p.deck.size()) == p.deck and "f_5_5" in filled, "zum Kampf wird der Stapel mit eigenen Karten bis 40 aufgefüllt") check(filled.size() == Game.DECK_MIN and filled.all(func(id): return p.owns(id)) and filled.slice(0, p.deck.size()) == p.deck, "zum Kampf wird der Stapel mit eigenen Karten bis 30 aufgefüllt")
p.deck = [] p.deck = []
check(p.deck_cards().size() == Game.DECK_MIN, "leerer Stapel: Standard aus der Sammlung") check(p.deck_cards().size() == Game.DECK_MIN, "leerer Stapel: Standard aus der Sammlung")
var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict()))) var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
@@ -731,12 +713,11 @@ func test_spielstand() -> void:
if FileAccess.file_exists(path + suffix): if FileAccess.file_exists(path + suffix):
DirAccess.remove_absolute(abs + suffix) DirAccess.remove_absolute(abs + suffix)
var p := Profile.load_or_new(path) var p := Profile.load_or_new(path)
check(p.coins == Profile.START_COINS and not p.started(), "Starterprofil: Startdeck noch zu wählen") check(p.coins == Profile.START_COINS and p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN, "Starterprofil: neutrale Karten")
p.choose_start("Wasser")
check(p.collection.size() == Game.DECK_MIN and p.deck.size() == Game.DECK_MIN, "Startdeck gewählt")
check(FileAccess.file_exists(path), "Datei angelegt") check(FileAccess.file_exists(path), "Datei angelegt")
p.coins = 999 p.coins = 999
p.collection["f_3_2"] = 2 p.collection["f_3_2"] = 2
p.collection["f_0_1"] = 0
p.deck = ["f_3_2", "f_0_1"] p.deck = ["f_3_2", "f_0_1"]
p.save(path) p.save(path)
var p2 := Profile.load_or_new(path) var p2 := Profile.load_or_new(path)
@@ -829,7 +810,7 @@ func test_spielstand_migration() -> void:
"collection": {"f_kobold": 2}, "decks": [], "settings": {"volume": 0.3}, "perk": "resonanz_doppelt"} "collection": {"f_kobold": 2}, "decks": [], "settings": {"volume": 0.3}, "perk": "resonanz_doppelt"}
var p := Profile.from_dict(alt) var p := Profile.from_dict(alt)
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(not p.started() and p.deck.is_empty() and p.perk == "", "alte Sammlung verworfen, Startdeck wird neu gewählt, unbekanntes Merkmal verworfen") check(p.deck.size() == Game.DECK_MIN and p.collection.size() == 110 and p.perk == "", "alte Sammlung verworfen, neutrale Startsammlung, 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() var fresh := Profile.new()
check(fresh.settings.volume == 0.0 and fresh.settings.music == 0.0, "Ton und Musik sind standardmäßig aus") check(fresh.settings.volume == 0.0 and fresh.settings.music == 0.0, "Ton und Musik sind standardmäßig aus")
@@ -845,23 +826,26 @@ func test_spielstand_migration() -> void:
func test_kampagne() -> void: func test_kampagne() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 4 r.seed = 4
check(Campaign.STAGES.size() == 10, "zehn Stufen") check(Campaign.STAGES_PER_CHAPTER == 10 and Campaign.total() == 50, "fünf Kapitel mit je zehn Stufen")
var boss := Campaign.opponent(4, r) var boss := Campaign.opponent(4)
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben") check(boss.name == "Vulkar" and boss.title == "Söldnerfürst" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben")
check(boss.deck.size() == Game.DECK_MIN and boss.looks.values().all(func(l): return l == 0), "Kapitel 1: Stapel in Common") check(boss.deck.size() == Game.DECK_MIN and boss.looks.values().all(func(l): return l == 0), "Kapitel 1: Stapel in Common")
var g := Game.new_battle_with(boss, r) var g := Game.new_battle_with(boss, r)
check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet") check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
check(Profile.battle_coins(false, 10, 20, 8) == 190 and Profile.battle_coins(true, 10, 20, 8) == 380, "Münzen: 5 je Zug, Karte und schnellem Zug, Sieg verdoppelt")
check(Profile.battle_coins(true, 10, 20, 8, 1.5) == 570 and Profile.battle_coins(true, 10, 20, 8, 2.0) == 760 and Profile.battle_coins(false, 5, 10, 4, 2.0) == 190, "Schwierigkeit ×1,5 und ×2")
check(Profile.DIFFICULTIES.leicht.reward == 1.0 and Profile.DIFFICULTIES.normal.reward == 1.5 and Profile.DIFFICULTIES.schwer.reward == 2.0, "Münzfaktoren 1 / 1,5 / 2")
var p := Profile.starter() var p := Profile.starter()
var reward := p.record_battle(true, 0) var reward := p.record_battle(true, 380, 0)
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen") check(p.campaign_stage == 1 and reward == 380 + Campaign.stage(0).reward and p.coins == Profile.START_COINS + reward, "Stufe 1 abgeschlossen: Münzen plus Stufenbelohnung")
reward = p.record_battle(true, 0) reward = p.record_battle(true, 380, 0)
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung") check(p.campaign_stage == 1 and reward == 380, "Wiederholung nur die Münzen des Kampfs")
reward = p.record_battle(false, 1) reward = p.record_battle(false, 190, 1)
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei") check(p.campaign_stage == 1 and reward == 190, "Niederlage schaltet nichts frei")
p.campaign_stage = 9 p.campaign_stage = 9
p.record_battle(true, 9) p.record_battle(true, 380, 9)
check(p.campaign_stage == 10, "Kampagne abgeschlossen") check(p.campaign_stage == 10, "Kampagne abgeschlossen")
var sim := Game.new_battle_with(Campaign.opponent(0, r), r) var sim := Game.new_battle_with(Campaign.opponent(0), r)
var guard := 0 var guard := 0
while sim.winner == "" and guard < 300: while sim.winner == "" and guard < 300:
AI.take_turn(sim, sim.current(), true) AI.take_turn(sim, sim.current(), true)
@@ -918,17 +902,17 @@ func test_merkmale() -> void:
var mana := h.energy var mana := h.energy
check(g.draw_cost(h) == 0 and g.draw(h) == "" and h.energy == mana and g.draw_cost(h) == 1, "Merkmal: ein Ziehen mehr umsonst") check(g.draw_cost(h) == 0 and g.draw(h) == "" and h.energy == mana and g.draw_cost(h) == 1, "Merkmal: ein Ziehen mehr umsonst")
for perk in Game.PERKS: for perk in Game.PERKS:
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk) check(range(Campaign.total()).any(func(i): return perk in Campaign.stage(i).perks), "Merkmal vergeben: " + perk)
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 2 r.seed = 2
var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1, r), r).opponent var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1), r).opponent
check(mara.life == 22 + 5 and mara.max_life == 27 and mara.perks == ["leben_35"], "Mara hat mit ihrem Merkmal 5 Leben mehr") check(mara.life == 22 + 5 and mara.max_life == 27 and mara.perks == ["leben_35"], "Mara hat mit ihrem Merkmal 5 Leben mehr")
var g3 := Game.new_battle_with(Campaign.opponent(9, r), r, true) var g3 := Game.new_battle_with(Campaign.opponent(9), r, true)
var archi: Game.Player = g3.opponent var archi: Game.Player = g3.opponent
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt") check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
g3.end_turn() g3.end_turn()
check(g3.current() == archi and archi.energy == Game.START_MANA + 2, "Archimagier hat im ersten Zug 2 Mana mehr") check(g3.current() == archi and archi.energy == Game.START_MANA + 2, "Archimagier hat im ersten Zug 2 Mana mehr")
var nyx: Game.Player = Campaign.opponent(5, r) var nyx: Game.Player = Campaign.opponent(5)
var g4 := Game.new_battle_with(nyx, r, true) var g4 := Game.new_battle_with(nyx, r, true)
g4.end_turn() g4.end_turn()
check(nyx.max_energy == Game.START_MANA and nyx.energy == Game.START_MANA + 1, "Nyx bekommt 1 Mana mehr je Zug") check(nyx.max_energy == Game.START_MANA and nyx.energy == Game.START_MANA + 1, "Nyx bekommt 1 Mana mehr je Zug")
@@ -956,17 +940,19 @@ func test_ki_zwei_schritte() -> void:
func test_kapitel_zwei_und_merkmalwahl() -> void: func test_kapitel_zwei_und_merkmalwahl() -> void:
check(Campaign.total() == 30 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(29) == 3, "dreißig Stufen in drei Kapiteln") check(Campaign.total() == 50 and Campaign.chapter(9) == 1 and Campaign.chapter(10) == 2 and Campaign.chapter(49) == 5, "fünfzig Stufen in fünf Kapiteln")
check(Campaign.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx", "Kapitel 3 mit drei Merkmalen") check(Campaign.stage(25).perks.size() == 2 and Campaign.stage(25).name == "Nyx" and Campaign.stage(25).element == "Wasser", "Kapitel 3: Wasser, zwei Merkmale")
check(Campaign.stage(45).perks.size() == 3 and Campaign.stage(40).element == "Erde" and Campaign.stage(49).reward == 1500 and Campaign.stage(10).reward == 350 and Campaign.stage(9).reward == 500, "Kapitel 5: Erde, drei Merkmale; Belohnungen 350 bis 1500, Boss 1 gibt 500")
check(Campaign.stage(0).perks == ["feuer_angriff"] and Campaign.stage(23).perks == ["ziehen_gratis", "kein_nachteil"] and Campaign.stage(49).perks == ["start_energie", "wesen_angriff", "extra_mana"], "Merkmale je drei Plätze versetzt, nichts doppelt")
for i in Campaign.total(): for i in Campaign.total():
var st := Campaign.stage(i) var st := Campaign.stage(i)
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1)) check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 8 r.seed = 8
var archi2 := Campaign.opponent(19, r) var archi2 := Campaign.opponent(19)
check(archi2.name == "Archimagier" and archi2.perks.size() == 2 and archi2.life == 28, "letzte Stufe von Kapitel 2: Archimagier mit zwei Merkmalen und 28 Leben") check(archi2.name == "Archimagier" and archi2.perks.size() == 1 and archi2.life == 28, "letzte Stufe von Kapitel 2: Archimagier mit einem Merkmal und 28 Leben")
check(archi2.looks.values().all(func(l): return l == 1), "Kapitel 2: seine Karten sind Rare") check(archi2.looks.values().all(func(l): return l == 0) and archi2.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer"), "Kapitel 2: seine Karten sind Feuer in Common")
check(Campaign.opponent(25, r).looks.values().all(func(l): return l == 2), "Kapitel 3: Super Rare") check(Campaign.opponent(25).looks.values().all(func(l): return l == 1) and Campaign.opponent(45).looks.values().all(func(l): return l == 3), "Kapitel 3 Rare, Kapitel 5 Ultra Rare")
var p := Profile.starter() var p := Profile.starter()
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1") check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
p.campaign_stage = 10 p.campaign_stage = 10
@@ -975,12 +961,90 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
var g := Game.new_battle(r, {"human_perks": [p.perk]}) var g := Game.new_battle(r, {"human_perks": [p.perk]})
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf") check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
p.campaign_stage = 19 p.campaign_stage = 19
p.record_battle(true, 19) p.record_battle(true, 380, 19)
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen") check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict()))) var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "gespeichert und gelesen") check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "gespeichert und gelesen")
## Einzelspieler: der Gegner spielt ein offenes Element in einer Stufe relativ zu deiner Deckstärke.
func test_einzelspieler_gegner() -> void:
var r := RandomNumberGenerator.new()
r.seed = 21
var p := Profile.starter()
check(p.deck_level() == 0 and p.unlocked_elements() == [Cards.NEUTRAL] and p.unlocked_cards().size() == 110 and p.unlocked_cards().all(func(c): return c.is_neutral()), "Start: Stufe Common, nur Neutral offen, 110 Karten sichtbar")
check(Profile.DIFFICULTIES.leicht.level == -1 and Profile.DIFFICULTIES.normal.level == 0 and Profile.DIFFICULTIES.schwer.level == 1, "Versatz -1 / 0 / +1")
p.settings.difficulty = "schwer"
var o := p.single_opponent(r)
check(o.deck.size() == Game.DECK_MIN and o.deck.all(func(id): return Cards.by_id(id).is_neutral()) and o.looks.values().all(func(l): return l == 1) and o.life == Game.START_LIFE, "Schwer: 30 Neutrale, eine Stufe über dir, 30 Leben")
check(o.perks.size() == 1 and Game.PERKS.has(o.perks[0]) and o.title == Cards.NEUTRAL, "Schwer: ein zufälliges Merkmal, Element als Titel")
p.settings.difficulty = "leicht"
o = p.single_opponent(r)
check(o.looks.values().all(func(l): return l == 0) and o.perks.is_empty(), "Leicht: nicht unter Common, kein Merkmal")
for id in p.collection:
p.collection[id] = 3
check(p.deck_level() == 3 and p.opponent_level() == 2, "Ultra-Rare-Deck: Stufe 3, Leicht eine darunter")
p.settings.difficulty = "normal"
check(p.opponent_level() == 3 and p.single_opponent(r).looks.values().all(func(l): return l == 3), "Normal: deine Stufe")
for id in p.deck.slice(0, 3):
p.collection[id] = 4
check(DeckGen.strength(p.deck_cards(), p.collection) == 410 and p.deck_level() == 3, "410 rundet auf Stufe 3")
p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.unlocked_elements() == [Cards.NEUTRAL, "Feuer"] and p.unlocked_cards().size() == 220, "Kapitel 2 offen: Feuer dazu, 220 Karten sichtbar")
var seen := {}
for i in 30:
var e: String = Cards.by_id(p.single_opponent(r).deck[0]).elements[0]
seen[e] = true
check(seen.keys().size() == 2 and seen.has("Feuer") and seen.has(Cards.NEUTRAL), "Gegner würfelt aus Neutral und Feuer")
p.campaign_stage = Campaign.total()
check(p.unlocked_elements() == Array(Campaign.CHAPTER_ELEMENTS), "alles offen: alle fünf")
var g := Game.new_battle_with(p.single_opponent(r), r, false, {"deck": p.deck_cards(), "looks": p.looks()})
check(g.opponent.deck.size() + g.opponent.hand.size() == Game.DECK_MIN and g.opponent.hand.all(func(c): return c.look == 3), "im Kampf: 30 Karten in Stufe 3")
## Skilltree: Münzen sind Erfahrung, je Level ein Punkt, Skills wirken auf Booster, Staub, Münzen und Kampf.
func test_skilltree() -> void:
check(Skills.xp_for_level(1) == 100 and Skills.xp_for_level(2) == 300 and Skills.xp_for_level(5) == 1500 and Skills.level_for(0) == 0 and Skills.level_for(299) == 1 and Skills.level_for(300) == 2, "Level brauchen 100, 300, 600 … Erfahrung")
var p := Profile.starter()
check(p.player_level() == 0 and p.skill_points() == 0 and p.learn("packglueck") == "Kein Skillpunkt frei", "Start: Level 0, kein Punkt")
var reward := p.record_battle(true, 380)
check(reward == 380 and p.xp == 380 and p.player_level() == 2 and p.skill_points() == 2 and p.take_notices() == ["Level 2 erreicht: +2 Skillpunkte"], "380 Münzen: Level 2, zwei Punkte, Meldung")
check(p.learn("packglueck") == "" and p.skill("packglueck") == 1 and p.skill_points() == 1, "Packglück gelernt")
check(p.learn("gratisziehen") == "" and p.learn("gratisziehen") == "Schon auf der höchsten Stufe" and p.skill_points() == 0, "Freies Ziehen hat nur eine Stufe")
check(p.battle_perks() == ["ziehen_gratis"], "Kampfskill wird im Kampf zum Merkmal")
p.perk = "leben_35"
p.campaign_stage = Campaign.STAGES_PER_CHAPTER
check(p.battle_perks() == ["leben_35", "ziehen_gratis"], "das gewählte Merkmal kommt dazu")
p.xp = 100000
for i in 3:
p.learn("muenzen")
p.learn("staub")
p.learn("packpreis")
p.learn("packpreis")
check(is_equal_approx(p.coin_factor(), 1.3) and p.pack_price(Cards.NEUTRAL) == 80 and p.pack_price("Feuer") == 160 and p.dust_gain(100) == 175, "Münzsegen ×1,3, Händler −20 %, Staubsammler +75 %")
var coins := p.coins
check(p.record_battle(false, 200) == 260 and p.coins == coins + 260, "260 statt 200 Münzen")
p.coins = 80
check(p.open_booster(Cards.NEUTRAL).size() == 5 and p.coins == 0, "neutraler Booster kostet mit Händler 80")
var rr := RandomNumberGenerator.new()
rr.seed = 9
var commons := 0
var commons0 := 0
for i in 200:
commons += Profile.roll_booster(rr, Cards.NEUTRAL, 3).filter(func(x): return x.rarity == "Common").size()
commons0 += Profile.roll_booster(rr, Cards.NEUTRAL, 0).filter(func(x): return x.rarity == "Common").size()
check(commons < commons0 * 0.7, "Packglück 3: deutlich weniger Common (%d statt %d)" % [commons, commons0])
p.collection["n_0_1"] = 1
var dust := p.dust
check(p.dismantle("n_0_1") == "" and p.dust == dust + 175, "Zerlegen einer Rare mit Staubsammler 3: 175 statt 100")
var d := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(d.xp == p.xp and d.skills == p.skills and d.skill_points() == p.skill_points(), "Erfahrung und Skills gespeichert")
var bad := Profile.from_dict({"version": Profile.SAVE_VERSION, "xp": 100, "skills": {"gibt_es_nicht": 2, "packglueck": 9}})
check(bad.skill("packglueck") == 3 and bad.skills.size() == 1 and bad.player_level() == 1, "unbekannte Skills fliegen raus, Stufen gedeckelt")
check(Skills.pack_weights(0) == PackedFloat32Array([45, 25, 15, 10, 5]) and Skills.pack_weights(3) == PackedFloat32Array([21, 34, 21, 16, 8]), "Packglück 3: Chancen 21/34/21/16/8")
check(Skills.effect("packglueck", 1) == "Chancen 37/28/17/12/6 %" and Skills.effect("packpreis", 2) == "Neutral 80, Element 160 Münzen" and Skills.effect("startmana", 1) == "erster Zug: 3 Mana" and Skills.effect("gratisziehen", 0) == "eine Karte je Zug gratis", "Wirkungstexte in Zahlen")
func test_ki_zeitbudget() -> void: func test_ki_zeitbudget() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 12 r.seed = 12
+157 -68
View File
@@ -133,11 +133,8 @@ func screen():
return main.screen return main.screen
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist; ohne Startdeck wird ## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
## Feuer gewählt.
func ensure_menu() -> void: func ensure_menu() -> void:
if screen() is StartDeckScreen:
await click(find_button("Feuer wählen"))
if TranslationServer.get_locale().begins_with("en"): if TranslationServer.get_locale().begins_with("en"):
main.set_language("de") main.set_language("de")
if not (screen() is MenuScreen): if not (screen() is MenuScreen):
@@ -188,7 +185,7 @@ func _ready() -> void:
main.sfx.muted = true # Tests bleiben still main.sfx.muted = true # Tests bleiben still
await frames(3) await frames(3)
var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen, var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen] test_kampagne, test_sammlung, test_skilltree, test_cheats, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
suite += [test_kampf.bind(3), test_kampf_abbrechen] suite += [test_kampf.bind(3), test_kampf_abbrechen]
suite.append(test_profil_zuruecksetzen) suite.append(test_profil_zuruecksetzen)
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf
@@ -204,16 +201,14 @@ func _ready() -> void:
get_tree().quit(1 if failed > 0 else 0) get_tree().quit(1 if failed > 0 else 0)
## Erster Start: eines von vier Startdecks wählen (ensure_menu hat schon eins gewählt, darum frisch). ## Neues Profil: die neutralen Karten als Sammlung und Stapel, direkt ins Menü.
func test_start() -> void: func test_start() -> void:
main.reset_profile() main.reset_profile()
await frames(2) await frames(2)
check(screen() is StartDeckScreen and not main.profile.started(), "neues Profil: Startdeck wählen")
await shot("00_startdeck")
await click(find_button("Feuer wählen"))
var p: Profile = main.profile var p: Profile = main.profile
check(screen() is MenuScreen and p.started() and p.deck.size() == Game.DECK_MIN and p.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Feuer").size() == 25, "Feuer gewählt: 40 Karten, 25 davon Feuer") check(screen() is MenuScreen and p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN and p.deck.all(func(id): return Cards.by_id(id).is_neutral()), "neues Profil: 110 Neutrale, 30 im Stapel")
check(Profile.load_or_new(SAVE).started(), "gespeichert") await shot("00_start")
check(Profile.load_or_new(SAVE).collection.size() == 110, "gespeichert")
func test_menu() -> void: func test_menu() -> void:
@@ -244,6 +239,32 @@ func test_booster() -> void:
else: else:
check(res.dust > 0, "sonst gibt es Staub") check(res.dust > 0, "sonst gibt es Staub")
check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand") check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand")
check(not scr.pack_tiles["Feuer"].visible and scr.pack_tiles[Cards.NEUTRAL].visible and scr.pack_tiles[Cards.NEUTRAL].position.x == 530, "Feuer-Pack bis Kapitel 2 unsichtbar, Neutral mittig")
check(scr.summary_label.text.ends_with("Staub") and scr.pack_tiles[Cards.NEUTRAL].pulse, "Fazit steht, das gekaufte Pack pulsiert")
var saved_coins: int = main.profile.coins
main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
main.profile.coins = Profile.ELEMENT_BOOSTER_COST
scr.refresh()
check(scr.pack_tiles["Feuer"].visible and not scr.pack_buttons["Feuer"].disabled and "200" in scr.pack_tiles["Feuer"].hint and not scr.pack_tiles["Wasser"].visible, "Kapitel 2 offen: Feuer kaufbar, Wasser unsichtbar")
await click(scr.pack_buttons["Feuer"])
check(main.profile.coins == 0 and scr.cards.size() == 5 and scr.cards.all(func(res): return res.card.elements[0] == "Feuer"), "Feuer-Booster: fünf Feuerkarten")
await click(scr.b_all)
await shot("04b_booster_feuer")
main.profile.coins = Profile.BOOSTER_COST * 5
scr.refresh()
check(not scr.pack_tiles[Cards.NEUTRAL].buy_buttons[1].disabled and scr.pack_tiles[Cards.NEUTRAL].buy_buttons[2].disabled, "×5 bezahlbar, ×10 nicht")
await click(scr.pack_tiles[Cards.NEUTRAL].buy_buttons[1])
check(scr.cards.size() == 25 and main.profile.coins == 0 and scr.revealed.is_empty() and scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack).size() == 25, "fünf Packs auf einmal: 25 verdeckte Karten")
check(Cards.RARITIES.find(scr.cards[0].rarity) >= Cards.RARITIES.find(scr.cards[24].rarity), "beste Stufe zuerst")
await frames(30)
await shot("04c_booster_fuenf_zu")
await click(scr.b_all)
check(scr.revealed.size() == 25 and scr.summary_label.text.begins_with(str(scr.cards.filter(func(x): return x.new).size())) and "Staub" in scr.summary_label.text, "alle aufgedeckt, Fazit zählt neu und Staub")
await frames(40)
await shot("04d_booster_fuenf_offen")
main.profile.campaign_stage = 0
main.profile.coins = saved_coins
main.save()
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
@@ -395,13 +416,13 @@ func test_gegnerauswahl() -> void:
check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten") check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
await click(find_button("Schwer")) await click(find_button("Schwer"))
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert") check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
check(str(Profile.DIFFICULTIES.schwer.life) in sel.info_label.text and str(int(Profile.REWARD_WIN * 1.5)) in sel.info_label.text, "Leben des Gegners und Belohnung stehen dabei") check("Rare" in sel.info_label.text and "Merkmal" in sel.info_label.text and "×2" in sel.info_label.text, "Schwer: Gegnerkarten Rare, Merkmal und Münzfaktor stehen dabei: " + sel.info_label.text)
await shot("07_gegnerauswahl") await shot("07_gegnerauswahl")
await click(find_button("Starten")) await click(find_button("Starten"))
var hard = screen() var hard = screen()
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet") check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
var opp: Game.Player = hard.game.opponent var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life) check(opp.max_life == Game.START_LIFE and hard.smart_ai and opp.perks.size() == 1 and opp.looks.values().all(func(l): return l == 1) and opp.title == Cards.NEUTRAL, "Schwer: 30 Leben, Vorausschau-KI, ein Merkmal, Neutral in Rare")
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")
var mine: Array = main.profile.deck_cards() var mine: Array = main.profile.deck_cards()
check(hard.game.human.hand.all(func(c): return c.id in mine), "die Starthand kommt ungefiltert vom eigenen Stapel") check(hard.game.human.hand.all(func(c): return c.id in mine), "die Starthand kommt ungefiltert vom eigenen Stapel")
@@ -414,7 +435,7 @@ func test_gegnerauswahl() -> void:
await click(find_button("Starten")) await click(find_button("Starten"))
var easy = screen() var easy = screen()
opp = easy.game.opponent opp = easy.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life) check(opp.max_life == Game.START_LIFE and not easy.smart_ai and opp.perks.is_empty() and opp.looks.values().all(func(l): return l == 0), "Leicht: 30 Leben, einfache KI, kein Merkmal, Common")
check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide") check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
@@ -473,28 +494,89 @@ func test_einstellungen() -> void:
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit dem frischen Startdeck. ## Skilltree: Level und Punkte aus der Erfahrung, Lernen verbraucht Punkte, Knöpfe folgen dem Stand.
func test_deck() -> void: func test_skilltree() -> void:
main.profile.choose_start("Feuer") main.profile.xp = Skills.xp_for_level(3)
main.profile.skills = {}
main.save() main.save()
await click(find_button("Skilltree"))
var scr = screen()
check(scr is SkillScreen and scr.level_label.text == "Level 3" and scr.points_label.text == "Skillpunkte: 3", "Skilltree: Level 3, drei Punkte")
check(scr.learn_buttons.size() == Skills.SKILLS.size() and not scr.learn_buttons["packglueck"].disabled and scr.xp_label.text == "0 / 400 EP bis Level 4", "alle Skills mit Lernen-Knopf, Fortschritt bis Level 4")
check(scr.now_labels["packglueck"].text == "Jetzt: Chancen 45/25/15/10/5 %" and scr.next_labels["packglueck"].text == "Nächste Stufe: Chancen 37/28/17/12/6 %", "Wirkung jetzt und nächste Stufe in Zahlen")
await click(scr.learn_buttons["packglueck"])
check(main.profile.skill("packglueck") == 1 and scr.points_label.text == "Skillpunkte: 2" and scr.rank_labels["packglueck"].text == "Stufe 1 / 3", "Packglück gelernt, Punkt verbraucht")
await click(scr.learn_buttons["gratisziehen"])
check(scr.learn_buttons["gratisziehen"].disabled and scr.rank_labels["gratisziehen"].text == "Stufe 1 / 1" and scr.next_labels["gratisziehen"].text == "Höchste Stufe erreicht", "Freies Ziehen voll: Knopf aus")
await shot("28_skilltree")
check(Profile.load_or_new(SAVE).skill("packglueck") == 1, "gespeichert")
await click(find_button("Zurück"))
check(screen() is MenuScreen and screen().coll_label.text.ends_with("Level 3"), "zurück im Menü, Level steht dabei")
main.profile.xp = 0
main.profile.skills = {}
main.save()
## Cheats: Knöpfe setzen Münzen, Level, Kapitel und Sammlung sofort und speichern.
func test_cheats() -> void:
await click(find_button("Cheats"))
var scr = screen()
check(scr is CheatScreen and scr.status_label.text.begins_with("Münzen %d" % main.profile.coins), "Cheats geöffnet, Stand steht oben")
var coins: int = main.profile.coins
await click(find_button("+1.000 Münzen"))
check(main.profile.coins == coins + 1000 and Profile.load_or_new(SAVE).coins == coins + 1000 and scr.msg_label.text == "Gespeichert", "+1.000 Münzen, gespeichert")
await click(find_button("+1 Level"))
check(main.profile.player_level() == 1 and main.profile.skill_points() == 1, "+1 Level: ein Skillpunkt")
await click(find_button("Kapitel 3 frei"))
check(main.profile.campaign_stage == 20 and main.profile.unlocked_elements().size() == 3, "Kapitel 3 frei: Neutral, Feuer, Wasser")
await click(find_button("Alle Karten Ultimate Rare"))
check(main.profile.collection.size() == Cards.all().size() and main.profile.level("f_5_5") == 4, "alle 550 Karten in Ultimate Rare")
await shot("29_cheats")
await click(find_button("Sammlung zurücksetzen"))
check(main.profile.collection.size() == 110 and main.profile.deck.size() == Game.DECK_MIN, "Sammlung wieder am Start")
await click(find_button("Kampagne zurücksetzen"))
await click(find_button("Erfahrung auf 0"))
check(main.profile.campaign_stage == 0 and main.profile.xp == 0, "Kampagne und Erfahrung zurück")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit einem frischen Profil.
func test_deck() -> void:
main.reset_profile()
await frames(2)
await click(find_button("Deck")) await click(find_button("Deck"))
var scr = screen() var scr = screen()
check(scr is DeckScreen, "Deck geöffnet") check(scr is DeckScreen, "Deck geöffnet")
var p: Profile = main.profile var p: Profile = main.profile
check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt") check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt")
check(scr.strength_label.text == "Stärke 100", "Startdeck aus Grundkarten: Stärke 100") check(scr.strength_label.text == "Stärke 100", "Startsammlung aus Grundkarten: Stärke 100")
p.collection[p.deck[0]] = 4 p.collection[p.deck[0]] = 4
scr.refresh() scr.refresh()
await frames(2) await frames(2)
check(scr.strength_label.text == "Stärke %d" % roundi((39 * 100 + 500) / 40.0), "eine Ultimate Rare hebt die Stärke: " + scr.strength_label.text) check(scr.strength_label.text == "Stärke %d" % roundi((29 * 100 + 500) / 30.0), "eine Ultimate Rare hebt die Stärke: " + scr.strength_label.text)
check(scr.grid.views()[0].card.id == p.deck[0] and scr.grid.views()[0].rarity == "Ultimate Rare", "höchste Stufe steht vorn")
p.collection[p.deck[0]] = 0 p.collection[p.deck[0]] = 0
scr.refresh() scr.refresh()
await frames(2) await frames(2)
var views: Array = scr.grid.views() var views: Array = scr.grid.views()
check(views.all(func(v): return not v.dim) and p.deck.size() == Game.DECK_MIN, "nach dem Start sind alle eigenen Karten im Stapel") check(views.size() == 110 and views.slice(0, Game.DECK_MIN).all(func(v): return not v.dim and v.marked) and views[Game.DECK_MIN].dim and not views[Game.DECK_MIN].marked and p.deck.size() == Game.DECK_MIN, "Stapelkarten vorn mit Haken, der Rest dunkel")
check(scr.curve.counts.size() == 11 and scr.curve.counts.reduce(func(a, v): return a + v, 0) == Game.DECK_MIN and scr.curve.counts[0] == 2, "Manakurve: 30 Karten verteilt, zwei zu 0 Mana")
check(scr.share.counts == {Cards.NEUTRAL: Game.DECK_MIN} and scr.share.shown() == [Cards.NEUTRAL], "Elementanteile: nur Neutral")
check(views[0].card.cost <= views[1].card.cost and views[Game.DECK_MIN].card.cost <= views[Game.DECK_MIN + 1].card.cost, "innerhalb von Stapel und Rest nach Kosten")
await click(scr.cost_buttons[3])
views = scr.grid.views()
check(scr.cost_filter == 3 and views.size() == 15 and views.all(func(v): return v.card.cost == 3), "Manafilter 3: nur die 15 Karten zu 3 Mana")
await click(scr.cost_buttons[3])
views = scr.grid.views()
check(scr.cost_filter == -1 and views.size() == 110, "derselbe Knopf hebt den Manafilter auf")
check(scr.element_buttons.size() == 5 and scr.element_buttons["Feuer"].tooltip_text == "Feuer" and not scr.element_buttons["Feuer"].visible and scr.element_buttons[Cards.NEUTRAL].visible and scr.element_buttons[Cards.NEUTRAL].position.x == 108, "Elementfilter als Symbolknöpfe, nur offene Elemente")
await hover(views[2])
check(scr.preview_holder.get_child_count() == 1 and scr.preview_holder.get_child(0).card.id == views[2].card.id, "Zeigen stellt die Karte groß in die Vorschau")
var first_id: String = views[0].card.id var first_id: String = views[0].card.id
await click(views[0]) await click(views[0])
check(not (first_id in p.deck) and p.deck.size() == Game.DECK_MIN - 1 and scr.grid.views().any(func(v): return v.card.id == first_id and v.dim), "Klick nimmt die Karte aus dem Stapel, sie wird dunkel") check(not (first_id in p.deck) and p.deck.size() == Game.DECK_MIN - 1 and scr.grid.views().any(func(v): return v.card.id == first_id and v.dim and not v.marked), "Klick nimmt die Karte aus dem Stapel, sie wird dunkel und verliert den Haken")
check(scr.curve.counts.reduce(func(a, v): return a + v, 0) == Game.DECK_MIN - 1 and scr.msg_label.text == "Klick auf eine Karte: rein oder raus.", "Manakurve folgt, Hinweis steht")
var back_view = scr.grid.views().filter(func(v): return v.card.id == first_id)[0] var back_view = scr.grid.views().filter(func(v): return v.card.id == first_id)[0]
scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs
await frames(2) await frames(2)
@@ -503,12 +585,21 @@ func test_deck() -> void:
await shot("19_deck") await shot("19_deck")
await click(find_button("Leeren")) await click(find_button("Leeren"))
check(p.deck.is_empty(), "geleert") check(p.deck.is_empty(), "geleert")
await click(find_button("Wasser")) for id in ["w_0_1", "w_0_2", "w_0_3", "w_0_4", "w_0_5"]:
p.collection[id] = 0
main.profile.campaign_stage = 2 * Campaign.STAGES_PER_CHAPTER # Wasser offen
scr.refresh()
await frames(1)
check(scr.element_buttons["Wasser"].visible and scr.element_buttons["Wasser"].position.x == 108 + 2 * 40, "Kapitel 3 offen: Wasserfilter an dritter Stelle")
await click(scr.element_buttons["Wasser"])
await click(find_button("Auffüllen")) await click(find_button("Auffüllen"))
check(p.deck.size() == 5 and p.deck.all(func(id): return Cards.by_id(id).elements[0] == "Wasser"), "Auffüllen hält sich an den Filter: nur die fünf Wasserkarten") check(p.deck.size() == 5 and p.deck.all(func(id): return Cards.by_id(id).elements[0] == "Wasser"), "Auffüllen hält sich an den Filter: nur die fünf Wasserkarten")
check(scr.share.shown() == ["Wasser"] and scr.share.counts["Wasser"] == 5, "Elementanteile: fünf Wasser")
await click(find_button("Alle")) await click(find_button("Alle"))
await click(find_button("Auffüllen")) await click(find_button("Auffüllen"))
check(p.deck.size() == Game.DECK_MIN and Profile.load_or_new(SAVE).deck == p.deck, "ohne Filter alles, gespeichert") check(p.deck.size() == Game.DECK_MAX and Profile.load_or_new(SAVE).deck == p.deck, "ohne Filter bis 60, gespeichert")
main.profile.campaign_stage = 0
main.save()
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
@@ -518,30 +609,38 @@ func test_sammlung() -> void:
var scr = screen() var scr = screen()
check(scr is CollectionScreen, "Sammlung geöffnet") check(scr is CollectionScreen, "Sammlung geöffnet")
var p: Profile = main.profile var p: Profile = main.profile
check(scr.grid.views().size() == Cards.all().size(), "alle Karten des Spiels sind zu sehen") check(scr.grid.views().size() == 110 and scr.grid.views().all(func(v): return v.card.is_neutral()), "nur die Karten offener Elemente: zu Beginn die 110 Neutralen")
check(scr.grid.views().slice(0, p.collection.size()).all(func(v): return not v.dim) and scr.grid.views().back().dim, "eigene Karten vorn, fehlende abgedunkelt") check(find_button("Feuer") == null and find_button("Neutral") != null, "Filter nur für offene Elemente")
check(scr.grid.views().all(func(v): return not v.dim), "zu Beginn gehören dir alle gezeigten Karten")
p.dust = 5000 p.dust = 5000
scr.search_box.text = "Lodernder Funke" scr.search_box.text = "Tapferer Knappe"
scr.search_box.text_changed.emit("Lodernder Funke") scr.search_box.text_changed.emit("Tapferer Knappe")
await frames(1) await frames(1)
var funke = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0] var funke = scr.grid.views().filter(func(v): return v.card.id == "n_2_1")[0]
await click(funke) await click(funke)
check(scr.selected.id == "f_2_1" and p.rarity("f_2_1") == "Common" and scr.b_upgrade.visible, "eigene Karte gewählt, noch Common") check(scr.selected.id == "n_2_1" and p.rarity("n_2_1") == "Common" and scr.b_upgrade.visible, "eigene Karte gewählt, noch Common")
await click(scr.b_upgrade) await click(scr.b_upgrade)
check(p.level("f_2_1") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet") check(p.level("n_2_1") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
scr.search_box.text = "" scr.search_box.text = ""
scr.search_box.text_changed.emit("") scr.search_box.text_changed.emit("")
await frames(1) await frames(1)
var front = scr.grid.views().filter(func(v): return v.card.id == "f_2_1")[0] var front = scr.grid.views().filter(func(v): return v.card.id == "n_2_1")[0]
check(front.rarity == "Rare" and front.card.attack == 3 and front.card.health == 2 and scr.grid.views()[0].rarity != "Common", "die Rare zeigt +1/+1, aufgewertete stehen vorn: %s %d/%d" % [front.rarity, front.card.attack, front.card.health]) check(front.rarity == "Rare" and front.card.attack == 3 and front.card.health == 2 and scr.grid.views()[0].rarity != "Common", "die Rare zeigt +1/+1, aufgewertete stehen vorn: %s %d/%d" % [front.rarity, front.card.attack, front.card.health])
check(Profile.load_or_new(SAVE).level("f_2_1") == 1, "Sammlung gespeichert") check(Profile.load_or_new(SAVE).level("n_2_1") == 1, "Sammlung gespeichert")
scr.search_box.text = "Lodernder Funke" scr.search_box.text = "Tapferer Knappe"
scr.search_box.text_changed.emit("Lodernder Funke") scr.search_box.text_changed.emit("Tapferer Knappe")
await frames(1) await frames(1)
var dust: int = p.dust var dust: int = p.dust
await click(scr.b_dismantle) await click(scr.b_dismantle)
check(p.level("f_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt") check(p.level("n_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
# Herstellen einer fehlenden Karte # Herstellen einer fehlenden Karte (Feuer: erst ab Kapitel 2 zu sehen)
scr.search_box.text = ""
scr.search_box.text_changed.emit("")
main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
scr.refresh()
await frames(1)
var owned_shown: int = scr.grid.views().filter(func(v): return not v.dim).size()
check(scr.grid.views().size() == 220 and owned_shown < 220 and scr.grid.views().slice(0, owned_shown).all(func(v): return not v.dim) and scr.grid.views().slice(owned_shown).all(func(v): return v.dim), "Kapitel 2 offen: auch Feuer zu sehen, eigene vorn, fehlende abgedunkelt")
scr.search_box.text = "Infernalischer Drache" scr.search_box.text = "Infernalischer Drache"
scr.search_box.text_changed.emit("Infernalischer Drache") scr.search_box.text_changed.emit("Infernalischer Drache")
await frames(1) await frames(1)
@@ -551,6 +650,8 @@ func test_sammlung() -> void:
await click(scr.b_craft) await click(scr.b_craft)
check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt") check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt")
await shot("20_sammlung") await shot("20_sammlung")
main.profile.campaign_stage = 0
main.save()
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
@@ -594,9 +695,7 @@ func test_profil_zuruecksetzen() -> void:
check(not scr.confirm.visible, "abgebrochen") check(not scr.confirm.visible, "abgebrochen")
await click(find_button("Profil zurücksetzen")) await click(find_button("Profil zurücksetzen"))
await click(find_button("Ja, zurücksetzen")) await click(find_button("Ja, zurücksetzen"))
check(screen() is StartDeckScreen and main.profile.coins == Profile.START_COINS and not main.profile.started(), "Profil zurückgesetzt: Startdeck neu wählen") check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and main.profile.collection.size() == 110 and main.profile.campaign_stage == 0, "Profil zurückgesetzt: neutrale Startsammlung, zurück im Menü")
await click(find_button("Luft wählen"))
check(screen() is MenuScreen and main.profile.deck.filter(func(id): return Cards.by_id(id).elements[0] == "Luft").size() == 25, "Luft gewählt")
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert") check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
@@ -745,9 +844,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
await wait_for_human(scr) await wait_for_human(scr)
var g: Game = scr.game var g: Game = scr.game
g.human.board.clear() g.human.board.clear()
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 Paare
for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
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
@@ -765,12 +862,11 @@ func test_kampf_elemente_und_verschmelzen() -> void:
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()] g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
g.human.energy = 20 g.human.energy = 20
g.human.looks = {"w_3_3": 3} g.human.looks = {"w_3_3": 3}
g.apply_auras()
scr.refresh() scr.refresh()
await frames(2) await frames(2)
check(scr.element_view.text() == "Feuer 2/0 (Doppelschlag) · Wasser 1/0", "Zähler: nur Elemente mit Wesen oder gespielten Karten, Feuerpaar: " + scr.element_view.text()) check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1", "Liste: nur Elemente mit offenen Wesen, Feuerpaar: " + scr.element_view.text())
check(scr.opp_element_view.text() == "Luft 1/0", "die Liste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text()) check(scr.opp_element_view.text() == "Luft 1", "die Liste 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") check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken")
# 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())
@@ -786,7 +882,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
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/1"), "mittlere Maustaste legt verdeckt: zählt als ausgespielt, nicht als offen") check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2 (Doppelschlag)"), "mittlere Maustaste legt verdeckt: zählt 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")
@@ -797,26 +893,15 @@ func test_kampf_elemente_und_verschmelzen() -> void:
check(mine_hidden.hidden and scr.msg_label.text == "Lodernder Funke 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 == "Lodernder Funke 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 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text()) check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (Doppelschlag)"), "aufgedeckt: drei offene Feuer: " + scr.element_view.text())
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter drei Karten kein Bonus, aufgedeckt greift sofort an") check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "Werte bleiben die der Karten, aufgedeckt greift sofort an")
g.human.played["Feuer"] = 3 await shot("23b_drei_feuer")
g.apply_auras() # Angriffsziel: Feuer gegen Luft schlägt doppelt, der Falke kontert halb; verdeckte ohne Anzeige, Held gesperrt
scr.refresh()
await frames(1)
check(scr.element_view.text().begins_with("Feuer 3/3 (Doppelschlag) (+1/+1)") and wolf.power() == 4 and falke.power() == 3, "drei Feuerkarten: +1/+1 für die eigenen Feuerwesen: " + scr.element_view.text())
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
await shot("23b_vier_feuer")
# Angriffsziel: Elementvorteil schlägt durch den Stapelbonus des Ziels, verdeckte ohne Anzeige, Held gesperrt
scr.opponent().played["Luft"] = 3
g.apply_auras()
scr.refresh()
await frames(1)
check(falke.aura == 1 and falke.power() == 4 and scr.opp_element_view.text() == "Luft 1/3 (+1/+1)", "der Gegner hat drei Luftkarten gespielt, der Falke trägt +1/+1: " + scr.opp_element_view.text())
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 den Falken (sein Bonus zählt nicht), nichts gegen Verdeckte") check(scr.selected == wolf and scr.tiles[falke.uid].matchup == -1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: der Falke kontert halbiert, nichts gegen Verdeckte")
check(scr.tiles[falke.uid].shown_stats() == [falke.power() - 1, falke.health], "der Falke zeigt seinen Konter gegen den Wolf (ohne seinen Bonus) und sein echtes Leben") check(scr.tiles[falke.uid].shown_stats() == [2, falke.health], "der Falke zeigt seinen Konter gegen den Wolf: 3 halbiert, aufgerundet 2")
await hover(scr.tiles[falke.uid]) await hover(scr.tiles[falke.uid])
check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() + 1, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt mit +1 zu") check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() * 2, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt doppelt zu")
await shot("23c_angreifer_gewaehlt") await shot("23c_angreifer_gewaehlt")
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen") check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
await click(scr.tiles[secret.uid]) await click(scr.tiles[secret.uid])
@@ -866,7 +951,8 @@ func test_kampagne() -> void:
var scr = screen() var scr = screen()
check(scr is CampaignScreen, "Kampagne geöffnet") check(scr is CampaignScreen, "Kampagne geöffnet")
await shot("15_kampagne") await shot("15_kampagne")
check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES.size() - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen") check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES_PER_CHAPTER - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen")
check(find_button("Kapitel 5") != null and find_button("Kapitel 5").disabled, "fünf Kapitel, das letzte gesperrt")
await click(scr.node_buttons[3]) await click(scr.node_buttons[3])
check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen") check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen")
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt") check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
@@ -878,7 +964,7 @@ func test_kampagne() -> void:
await shot("16_kampagne_kampf") await shot("16_kampagne_kampf")
await give_up() await give_up()
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl # Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
main.profile.campaign_stage = Campaign.STAGES.size() main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
await click(find_button("Kampagne")) await click(find_button("Kampagne"))
var scr2 = screen() var scr2 = screen()
check(scr2.chapter == 2 and scr2.perk_box != null, "Kapitel 2 offen, Merkmal wählbar") check(scr2.chapter == 2 and scr2.perk_box != null, "Kapitel 2 offen, Merkmal wählbar")
@@ -888,7 +974,8 @@ func test_kampagne() -> void:
await shot("27_kampagne_kapitel2") await shot("27_kampagne_kapitel2")
await click(find_button("Kämpfen")) await click(find_button("Kämpfen"))
var b2 = screen() var b2 = screen()
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 22, "Kapitel-2-Boss mit zwei Merkmalen und 22 Leben") check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 1 and b2.game.opponent.life == 22, "Kapitel-2-Gegner mit einem Merkmal und 22 Leben")
check(b2.game.opponent.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer") and b2.game.opponent.looks.values().all(func(l): return l == 0), "Kapitel 2: nur Feuer in Common")
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf") check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
await give_up() await give_up()
main.profile.campaign_stage = 0 main.profile.campaign_stage = 0
@@ -934,6 +1021,8 @@ func test_kampf(shot_turn: int) -> void:
await frames(3) await frames(3)
await shot("09_kampf_ende") await shot("09_kampf_ende")
check(scr.reward > 0 and main.profile.coins >= coins + scr.reward, "Belohnung gutgeschrieben") check(scr.reward > 0 and main.profile.coins >= coins + scr.reward, "Belohnung gutgeschrieben")
check(scr.fast_turns > 0 and scr.reward == Profile.battle_coins(g.winner == "human", g.human.turns, g.human.cards_played, scr.fast_turns, main.profile.difficulty().reward), "Münzen aus Zügen, Karten und schnellen Zügen")
check(scr.detail_label.visible and tr("%d Züge") % g.human.turns in scr.detail_label.text and "= " in scr.detail_label.text, "Aufschlüsselung im Ergebnisbild")
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Belohnung gespeichert") check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Belohnung gespeichert")
await click(find_button("Zurück zum Menü")) await click(find_button("Zurück zum Menü"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
+75 -9
View File
@@ -27,7 +27,7 @@ KI-Tempo,AI speed
Animationen,Animations Animationen,Animations
Sprache,Language Sprache,Language
Booster,Boosters Booster,Boosters
Booster kaufen (100 Münzen),Buy booster (100 coins) %d neu · %d aufgewertet · +%d Staub,%d new · %d upgraded · +%d dust
Alle aufdecken,Reveal all Alle aufdecken,Reveal all
Zerlegen,Dismantle Zerlegen,Dismantle
Kämpfen,Fight Kämpfen,Fight
@@ -39,6 +39,9 @@ Du bist dran,Your turn
Rückgängig,Undo Rückgängig,Undo
Kapitel 1,Chapter 1 Kapitel 1,Chapter 1
Kapitel 2,Chapter 2 Kapitel 2,Chapter 2
Kapitel 3,Chapter 3
Kapitel 4,Chapter 4
Kapitel 5,Chapter 5
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d" "Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
Wähle ein Angriffsziel,Choose an attack target Wähle ein Angriffsziel,Choose an attack target
Abgebrochen,Cancelled Abgebrochen,Cancelled
@@ -82,7 +85,63 @@ Sammlung,Collection
Verschmelzen,Fuse Verschmelzen,Fuse
Nur eigene Wesen,Only your own creatures Nur eigene Wesen,Only your own creatures
Nur eigene Karten,Only your own cards Nur eigene Karten,Only your own cards
Sammlung: %d von %d Karten · Kampagne: %d/%d,Collection: %d of %d cards · Campaign: %d/%d Sammlung: %d von %d Karten · Kampagne: %d/%d · Level %d,Collection: %d of %d cards · Campaign: %d/%d · Level %d
Skilltree,Skill tree
Level %d,Level %d
%d / %d EP bis Level %d,%d / %d XP to level %d
Skillpunkte: %d,Skill points: %d
Lernen,Learn
Stufe %d / %d,Rank %d / %d
Sammeln,Collecting
Wirtschaft,Economy
Kampf,Battle
Packglück,Pack luck
Staubsammler,Dust collector
Münzsegen,Coin blessing
Händler,Merchant
Freies Ziehen,Free draw
Starker Start,Strong start
Seltenere Karten in Boostern,Rarer cards in boosters
Mehr Staub aus Doppelten und Zerlegen,More dust from duplicates and dismantling
Mehr Münzen nach jedem Kampf,More coins after every battle
Booster werden billiger,Boosters get cheaper
Das zweite Ziehen je Zug ist auch gratis,The second draw each turn is free as well
Mehr Mana im ersten Zug,More mana in the first turn
Chancen %d/%d/%d/%d/%d %%,Odds %d/%d/%d/%d/%d %%
+%d %% Staub,+%d %% dust
+%d %% Münzen,+%d %% coins
"Neutral %d, Element %d Münzen","Neutral %d, element %d coins"
zwei Karten je Zug gratis,two cards per turn for free
eine Karte je Zug gratis,one card per turn for free
erster Zug: %d Mana,first turn: %d mana
Jetzt: %s,Now: %s
Nächste Stufe: %s,Next rank: %s
Höchste Stufe erreicht,Highest rank reached
Cheats,Cheats
Währung,Currency
Erfahrung,Experience
+1.000 Münzen,"+1,000 coins"
+10.000 Münzen,"+10,000 coins"
+1.000 Staub,"+1,000 dust"
+10.000 Staub,"+10,000 dust"
+1 Level,+1 level
+5 Level,+5 levels
Skills zurücksetzen,Reset skills
Erfahrung auf 0,Experience to 0
Kapitel 2 frei,Unlock chapter 2
Kapitel 3 frei,Unlock chapter 3
Kapitel 4 frei,Unlock chapter 4
Kapitel 5 frei,Unlock chapter 5
Kampagne zurücksetzen,Reset campaign
Alle Karten (Common),All cards (Common)
Alle Karten Ultimate Rare,All cards Ultimate Rare
Eigene Karten +1 Stufe,Own cards +1 rank
Sammlung zurücksetzen,Reset collection
Gespeichert,Saved
"Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d","Coins %d · Dust %d · XP %d (level %d, %d skill points free) · Campaign %d/%d · Collection %d cards, deck %d"
Schon auf der höchsten Stufe,Already at the highest rank
Kein Skillpunkt frei,No skill point available
Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt.,Experience is the coins you earn. Every level grants one skill point.
%s ist jetzt %s,%s is now %s %s ist jetzt %s,%s is now %s
Alle Kapitel abgeschlossen,All chapters completed Alle Kapitel abgeschlossen,All chapters completed
Elemente,Elements Elemente,Elements
@@ -101,14 +160,23 @@ Nicht genug Mana,Not enough mana
Profil wirklich zurücksetzen?,Really reset your profile? Profil wirklich zurücksetzen?,Really reset your profile?
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost." "Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
%d Leben,%d life %d Leben,%d life
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win Gegnerkarten: %s,Opponent cards: %s
Vorausschau-KI,lookahead AI
einfache KI,simple AI
ein Merkmal,one perk
Münzen %s,coins %s
",","."
%d Züge,%d turns
%d Karten,%d cards
%d schnelle Züge,%d fast turns
Sieg ×2,Victory ×2
Kampagne +%d,Campaign +%d
Start gegen die KI,Start against the AI Start gegen die KI,Start against the AI
Ziehen,Draw Ziehen,Draw
Verdeckt legen ◆ %d,Set face-down ◆ %d Verdeckt legen ◆ %d,Set face-down ◆ %d
Aufdecken,Reveal Aufdecken,Reveal
verdeckt,face-down verdeckt,face-down
Aufgedeckt!,Revealed! Aufgedeckt!,Revealed!
Elementbonus +%d,Element bonus +%d
Merkmal +%d,Perk +%d Merkmal +%d,Perk +%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
@@ -117,7 +185,7 @@ Unbekannte Eigenschaft,Unknown trait
%s hat schon %s,%s already has %s %s hat schon %s,%s already has %s
Wesen kommen mit +1 Angriff,Creatures enter with +1 attack Wesen kommen mit +1 Angriff,Creatures enter with +1 attack
Hat im ersten Zug 2 Mana mehr,Has 2 extra mana on the first turn Hat im ersten Zug 2 Mana mehr,Has 2 extra mana on the first turn
Elementbonus zählt doppelt,Element bonus counts twice Wesen haben keinen Elementnachteil,Creatures have no element weakness
Beginnt mit 5 Leben mehr,Starts with 5 extra life Beginnt mit 5 Leben mehr,Starts with 5 extra life
Greift zweimal je Zug,Attacks twice per turn Greift zweimal je Zug,Attacks twice per turn
Verdeckt,Face-down Verdeckt,Face-down
@@ -162,10 +230,8 @@ Neu!,New!
Auffüllen,Fill up Auffüllen,Fill up
Leeren,Clear Leeren,Clear
Dein Stapel ist leer,Your deck is empty Dein Stapel ist leer,Your deck is empty
Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters.,Click adds a card to the deck or removes it. Fill up takes the most valuable cards of the filter. Klick auf eine Karte: rein oder raus.,Click a card: in or out.
Wähle dein Startdeck,Choose your starting deck Manakurve,Mana curve
"25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest.","25 cards of the element plus five of each other one. Boosters and the workshop bring the rest later."
%s wählen,Choose %s
Optionen,Options Optionen,Options
Hauptmenü,Main menu Hauptmenü,Main menu
Neutral,Neutral Neutral,Neutral
1 keys en
27 Animationen Animations
28 Sprache Language
29 Booster Boosters
30 Booster kaufen (100 Münzen) %d neu · %d aufgewertet · +%d Staub Buy booster (100 coins) %d new · %d upgraded · +%d dust
31 Alle aufdecken Reveal all
32 Zerlegen Dismantle
33 Kämpfen Fight
39 Rückgängig Undo
40 Kapitel 1 Chapter 1
41 Kapitel 2 Chapter 2
42 Kapitel 3 Chapter 3
43 Kapitel 4 Chapter 4
44 Kapitel 5 Chapter 5
45 Münzen: %d · Staub: %d Coins: %d · Dust: %d
46 Wähle ein Angriffsziel Choose an attack target
47 Abgebrochen Cancelled
85 Verschmelzen Fuse
86 Nur eigene Wesen Only your own creatures
87 Nur eigene Karten Only your own cards
88 Sammlung: %d von %d Karten · Kampagne: %d/%d Sammlung: %d von %d Karten · Kampagne: %d/%d · Level %d Collection: %d of %d cards · Campaign: %d/%d Collection: %d of %d cards · Campaign: %d/%d · Level %d
89 Skilltree Skill tree
90 Level %d Level %d
91 %d / %d EP bis Level %d %d / %d XP to level %d
92 Skillpunkte: %d Skill points: %d
93 Lernen Learn
94 Stufe %d / %d Rank %d / %d
95 Sammeln Collecting
96 Wirtschaft Economy
97 Kampf Battle
98 Packglück Pack luck
99 Staubsammler Dust collector
100 Münzsegen Coin blessing
101 Händler Merchant
102 Freies Ziehen Free draw
103 Starker Start Strong start
104 Seltenere Karten in Boostern Rarer cards in boosters
105 Mehr Staub aus Doppelten und Zerlegen More dust from duplicates and dismantling
106 Mehr Münzen nach jedem Kampf More coins after every battle
107 Booster werden billiger Boosters get cheaper
108 Das zweite Ziehen je Zug ist auch gratis The second draw each turn is free as well
109 Mehr Mana im ersten Zug More mana in the first turn
110 Chancen %d/%d/%d/%d/%d %% Odds %d/%d/%d/%d/%d %%
111 +%d %% Staub +%d %% dust
112 +%d %% Münzen +%d %% coins
113 Neutral %d, Element %d Münzen Neutral %d, element %d coins
114 zwei Karten je Zug gratis two cards per turn for free
115 eine Karte je Zug gratis one card per turn for free
116 erster Zug: %d Mana first turn: %d mana
117 Jetzt: %s Now: %s
118 Nächste Stufe: %s Next rank: %s
119 Höchste Stufe erreicht Highest rank reached
120 Cheats Cheats
121 Währung Currency
122 Erfahrung Experience
123 +1.000 Münzen +1,000 coins
124 +10.000 Münzen +10,000 coins
125 +1.000 Staub +1,000 dust
126 +10.000 Staub +10,000 dust
127 +1 Level +1 level
128 +5 Level +5 levels
129 Skills zurücksetzen Reset skills
130 Erfahrung auf 0 Experience to 0
131 Kapitel 2 frei Unlock chapter 2
132 Kapitel 3 frei Unlock chapter 3
133 Kapitel 4 frei Unlock chapter 4
134 Kapitel 5 frei Unlock chapter 5
135 Kampagne zurücksetzen Reset campaign
136 Alle Karten (Common) All cards (Common)
137 Alle Karten Ultimate Rare All cards Ultimate Rare
138 Eigene Karten +1 Stufe Own cards +1 rank
139 Sammlung zurücksetzen Reset collection
140 Gespeichert Saved
141 Münzen %d · Staub %d · EP %d (Level %d, %d Skillpunkte frei) · Kampagne %d/%d · Sammlung %d Karten, Stapel %d Coins %d · Dust %d · XP %d (level %d, %d skill points free) · Campaign %d/%d · Collection %d cards, deck %d
142 Schon auf der höchsten Stufe Already at the highest rank
143 Kein Skillpunkt frei No skill point available
144 Erfahrung sind deine verdienten Münzen. Jedes Level bringt einen Skillpunkt. Experience is the coins you earn. Every level grants one skill point.
145 %s ist jetzt %s %s is now %s
146 Alle Kapitel abgeschlossen All chapters completed
147 Elemente Elements
160 Profil wirklich zurücksetzen? Really reset your profile?
161 Sammlung, Münzen und Fortschritt gehen verloren. Collection, coins and progress are lost.
162 %d Leben %d life
163 Gegner mit %d Leben · %d Münzen bei Sieg Gegnerkarten: %s Opponent with %d life · %d coins for a win Opponent cards: %s
164 Vorausschau-KI lookahead AI
165 einfache KI simple AI
166 ein Merkmal one perk
167 Münzen %s coins %s
168 , .
169 %d Züge %d turns
170 %d Karten %d cards
171 %d schnelle Züge %d fast turns
172 Sieg ×2 Victory ×2
173 Kampagne +%d Campaign +%d
174 Start gegen die KI Start against the AI
175 Ziehen Draw
176 Verdeckt legen ◆ %d Set face-down ◆ %d
177 Aufdecken Reveal
178 verdeckt face-down
179 Aufgedeckt! Revealed!
Elementbonus +%d Element bonus +%d
180 Merkmal +%d Perk +%d
181 Erst die Wesen Creatures first
182 %s liegt verdeckt %s is face-down
185 %s hat schon %s %s already has %s
186 Wesen kommen mit +1 Angriff Creatures enter with +1 attack
187 Hat im ersten Zug 2 Mana mehr Has 2 extra mana on the first turn
188 Elementbonus zählt doppelt Wesen haben keinen Elementnachteil Element bonus counts twice Creatures have no element weakness
189 Beginnt mit 5 Leben mehr Starts with 5 extra life
190 Greift zweimal je Zug Attacks twice per turn
191 Verdeckt Face-down
230 Auffüllen Fill up
231 Leeren Clear
232 Dein Stapel ist leer Your deck is empty
233 Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters. Klick auf eine Karte: rein oder raus. Click adds a card to the deck or removes it. Fill up takes the most valuable cards of the filter. Click a card: in or out.
234 Wähle dein Startdeck Manakurve Choose your starting deck Mana curve
25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest. 25 cards of the element plus five of each other one. Boosters and the workshop bring the rest later.
%s wählen Choose %s
235 Optionen Options
236 Hauptmenü Main menu
237 Neutral Neutral
Binary file not shown.