update
This commit is contained in:
1 parent
f2c609a08b
commit
1d563b9eec
34 files changed
+5486
-643
No files matched your search
@@ -20,22 +20,25 @@ Der Spielstand liegt unter `user://spielstand.json`
|
||||
|
||||
## 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)
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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“
|
||||
- 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
|
||||
- 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: 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
|
||||
- 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, 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)
|
||||
- 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
|
||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; keine Grenze für Handkarten, das Feld fasst 5 Wesen
|
||||
- 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
|
||||
- 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`)
|
||||
- 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
|
||||
@@ -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/campaign.gd` – Kampagnenstufen
|
||||
- `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
|
||||
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
||||
+9
-7
@@ -6,15 +6,17 @@
|
||||
- Kartenspiel wie zum Beispiel Magic
|
||||
- PC-Spiel
|
||||
- Duell: Spieler gegen einen KI-Gegner, beide mit gleichen Regeln
|
||||
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
|
||||
- Kampagne gegen benannte Magier als Fortschritt
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
@@ -23,10 +25,10 @@
|
||||
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
||||
|
||||
## 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)
|
||||
- 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
|
||||
- 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)
|
||||
- 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, 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
|
||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||
+4000
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
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,
|
||||
gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt), rechts der
|
||||
Elementbonus.
|
||||
gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt).
|
||||
- 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
|
||||
(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).
|
||||
- **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.
|
||||
- **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),
|
||||
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
|
||||
oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
|
||||
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.
|
||||
- **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.
|
||||
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||
steht der Elementkonter (der ignorierte Stapelbonus) am Ziel, bei Verdeckten nicht.
|
||||
- Beim Angreifen zeigt das Ziel seinen Konter mit Elementvorteil (doppelt oder halb), bei
|
||||
Verdeckten nicht; über dem Ziel zeigt der Angreifer seinen Angriff mit Vorteil.
|
||||
|
||||
## Bewegung und Ton
|
||||
|
||||
@@ -83,17 +82,29 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||
|
||||
## Menüs
|
||||
|
||||
- Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Einstellungen,
|
||||
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
|
||||
- Hauptmenü: Titel, Einzelspieler, Kampagne und Deck links, darunter Booster, Sammlung, Skilltree,
|
||||
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.
|
||||
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
|
||||
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte
|
||||
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale
|
||||
stärker, nicht über Begleiter.
|
||||
- Sammlung: sechs Spalten, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts
|
||||
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, nur eigene Karten,
|
||||
Stapelkarten hell, die anderen abgedunkelt; Klick wechselt. Rechts die Kartenzahl und die Stärke
|
||||
(mittlerer Kartenwert, 100 für Grundkarten bis 500 für Ultimate Rare).
|
||||
- Kampagne als Pfad mit zehn Stationen je Kapitel (fünf Kapitel: Neutral, Feuer, Wasser, Luft,
|
||||
Erde, das Element steht über dem Pfad), die nächste pulsiert; darunter die gewählte Stufe mit
|
||||
Gegner, Leben, Belohnung und Merkmalen. Die Gegner werden über Leben, Merkmale und die
|
||||
Seltenheit ihrer Karten stärker, nicht über Begleiter.
|
||||
- Booster: je offenem Kapitel eine Pack-Kachel (Symbol, Name, Preis, ×1 ×5 ×10), mittig; gesperrte
|
||||
gibt es nicht zu sehen. Die Karten fliegen aus dem Pack ins Raster (ein Pack fünf große, mehrere
|
||||
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
|
||||
|
||||
|
||||
+51
-58
@@ -1,79 +1,72 @@
|
||||
class_name Campaign
|
||||
## Kampagne: zehn benannte Magier in drei Kapiteln. Die Bosse haben weniger Leben als du,
|
||||
## dafür Merkmale; je Kapitel kommt eins dazu.
|
||||
## Kampagne: fünf Kapitel mit je zehn benannten Magiern, der zehnte ist der Boss. Kapitel 1 spielt
|
||||
## 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 := [
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "life": 22, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "life": 22, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "life": 24, "reward": 150, "perk": "wesen_angriff"},
|
||||
{"name": "Liska", "title": "Windläuferin", "life": 24, "reward": 150, "perk": "ziehen_gratis"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "alle_flink"},
|
||||
{"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 STAGES_PER_CHAPTER := 10
|
||||
const CHAPTER_ELEMENTS: Array[String] = [Cards.NEUTRAL, "Feuer", "Wasser", "Luft", "Erde"]
|
||||
const NAMES: Array[String] = ["Fenn", "Mara", "Borin", "Liska", "Vulkar", "Nyx", "Ignis", "Thalassa", "Zephyra", "Archimagier"]
|
||||
const TITLES := {
|
||||
"Neutral": ["Kobold-Zähmer", "Wanderhexe", "Steinfaust", "Pfadfinderin", "Söldnerfürst", "Schattenruferin", "Grauer Priester", "Händlerkönigin", "Stahlkaiserin", "Meister der Lehre"],
|
||||
"Feuer": ["Flammenzähmer", "Glutweberin", "Schmiedefaust", "Funkenläuferin", "Herr der Lava", "Aschenruferin", "Inferno-Priester", "Glutkönigin", "Flammenkaiserin", "Meister des Feuers"],
|
||||
"Wasser": ["Gezeitenzähmer", "Gezeitenhexe", "Eisfaust", "Nebelläuferin", "Herr der Brandung", "Regenruferin", "Tiefsee-Priester", "Tiefseekönigin", "Flutkaiserin", "Meister des Wassers"],
|
||||
"Luft": ["Windzähmer", "Wolkenhexe", "Sturmfaust", "Windläuferin", "Herr der Böen", "Sturmruferin", "Himmelspriester", "Wolkenkönigin", "Blitzkaiserin", "Meister der Lüfte"],
|
||||
"Erde": ["Wurzelzähmer", "Moorhexe", "Felsenfaust", "Hügelläuferin", "Herr der Schluchten", "Bebenruferin", "Erzpriester", "Kristallkönigin", "Bergkaiserin", "Meister der Erde"],
|
||||
}
|
||||
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:
|
||||
return STAGES.size() * CHAPTERS.size()
|
||||
return STAGES_PER_CHAPTER * CHAPTER_ELEMENTS.size()
|
||||
|
||||
|
||||
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:
|
||||
if i < STAGES.size():
|
||||
var st: Dictionary = STAGES[i].duplicate()
|
||||
st["perks"] = [st.perk]
|
||||
return st
|
||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||
var c := chapter(i)
|
||||
var k := i % STAGES_PER_CHAPTER
|
||||
var perks: Array = []
|
||||
for n in perk_count(c):
|
||||
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
|
||||
## seine Karten eine Seltenheitsstufe höher (+1/+1), ab Kapitel 3 zwei.
|
||||
## Gegner einer Stufe: Leben und Merkmale der Stufe, 30 Karten seines Elements nach Manakurve bis
|
||||
## max_cost, in der Stufe des Kapitels (rarity_of).
|
||||
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player:
|
||||
var st := stage(stage_index)
|
||||
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
|
||||
boss.perks = Array(st.perks)
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
var ch := chapter(stage_index)
|
||||
boss.deck = DeckGen.opponent_deck(r, ch)
|
||||
boss.deck = DeckGen.curve_deck(st.element, r, int(st.max_cost))
|
||||
for id in boss.deck:
|
||||
boss.looks[id] = ch - 1
|
||||
boss.looks[id] = rarity_of(chapter(stage_index))
|
||||
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
@@ -1,7 +1,10 @@
|
||||
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
|
||||
## 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.
|
||||
@@ -18,41 +21,75 @@ static func random_deck(r: RandomNumberGenerator, max_cost: int = 99) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Stapel eines Kampagnengegners: Kapitel 1 bis Kosten 3, Kapitel 2 bis 5, Kapitel 3 alles.
|
||||
static func opponent_deck(r: RandomNumberGenerator, chapter: int) -> Array:
|
||||
return random_deck(r, [3, 5, 99][clampi(chapter - 1, 0, 2)])
|
||||
## Alle Karten eines Elements (oder die neutralen) in Datenreihenfolge: die Startsammlung.
|
||||
static func element_deck(element: String) -> Array:
|
||||
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
|
||||
## anderen Elemente (DECK_MIN Karten).
|
||||
static func start_deck(element: String) -> Array:
|
||||
var out := []
|
||||
## Stapel eines Elements nach der Manakurve: DECK_MIN verschiedene Karten des Elements bis max_cost,
|
||||
## je Kosten so viele wie CURVE vorgibt, zufällig gewählt. Was über dem Deckel fehlt, füllen die
|
||||
## erlaubten Kosten reihum von billig nach teuer auf.
|
||||
static func curve_deck(element: String, r: RandomNumberGenerator, max_cost: int = 10) -> Array:
|
||||
var by_cost := {}
|
||||
for c in Cards.all():
|
||||
if c.elements[0] == element and c.attack <= 4:
|
||||
out.append(c.id)
|
||||
for e in Cards.ELEMENTS:
|
||||
if e == element:
|
||||
continue
|
||||
var cheap := Cards.all().filter(func(c): return c.elements[0] == e and c.cost <= Game.START_MANA)
|
||||
cheap.sort_custom(func(a, b): return a.cost < b.cost if a.cost != b.cost else a.attack > b.attack)
|
||||
for c in cheap.slice(0, 5):
|
||||
out.append(c.id)
|
||||
if c.elements[0] == element and c.cost <= max_cost:
|
||||
if not by_cost.has(c.cost):
|
||||
by_cost[c.cost] = []
|
||||
by_cost[c.cost].append(c.id)
|
||||
for cost in by_cost:
|
||||
Game.shuffle(by_cost[cost], r)
|
||||
var out := []
|
||||
for cost in CURVE.size():
|
||||
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
|
||||
|
||||
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der
|
||||
## Stufe aus levels: ID -> Stufe), bei gleichem Wert die teuersten; nichts doppelt.
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der Stufe
|
||||
## 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:
|
||||
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))))
|
||||
cards.sort_custom(func(a, b):
|
||||
if a.value() != b.value():
|
||||
return a.value() > b.value()
|
||||
return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||
for c in cards:
|
||||
if out.size() >= limit:
|
||||
break
|
||||
out.append(c.id)
|
||||
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||
var have := {} # Kosten -> Karten im Stapel
|
||||
for id in out:
|
||||
var slot := clampi(Cards.by_id(id).cost, 0, CURVE.size() - 1)
|
||||
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
|
||||
|
||||
|
||||
|
||||
+28
-85
@@ -6,11 +6,10 @@ extends RefCounted
|
||||
const MAX_BOARD := 5
|
||||
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 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 START_LIFE := 30
|
||||
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
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
@@ -23,7 +22,7 @@ const PERKS := {
|
||||
"leben_35": "Beginnt mit 5 Leben mehr",
|
||||
"alle_flink": "Wesen greifen sofort an",
|
||||
"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).
|
||||
@@ -50,8 +49,7 @@ class Creature extends RefCounted:
|
||||
var max_health: int
|
||||
var attacks_done := 0 # Angriffe in diesem Zug
|
||||
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 aura := 0 # Elementbonus (+1 je Element mit vollem Stapel); das Leben ist schon angepasst
|
||||
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht fürs Elementpaar
|
||||
|
||||
func _init(c: Cards.Card, o, id: int) -> void:
|
||||
card = c
|
||||
@@ -90,9 +88,9 @@ class Creature extends RefCounted:
|
||||
out.append(k)
|
||||
return out
|
||||
|
||||
## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
|
||||
## Angriffswert inklusive Boss-Merkmalen.
|
||||
func power() -> int:
|
||||
var v := attack + aura
|
||||
var v := attack
|
||||
var o = owner()
|
||||
if o != null:
|
||||
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.ready = ready
|
||||
c.hidden = hidden
|
||||
c.aura = aura
|
||||
return c
|
||||
|
||||
|
||||
@@ -137,7 +134,7 @@ class Player extends RefCounted:
|
||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||
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 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
|
||||
life = l
|
||||
max_life = l
|
||||
for e in Cards.ELEMENTS:
|
||||
played[e] = 0
|
||||
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
@@ -178,7 +173,7 @@ class Player extends RefCounted:
|
||||
p.perks = perks.duplicate()
|
||||
p.looks = looks
|
||||
p.turns = turns
|
||||
p.played = played.duplicate()
|
||||
p.cards_played = cards_played
|
||||
p.deck = deck.duplicate()
|
||||
p.draws = draws
|
||||
p.fatigue = fatigue
|
||||
@@ -406,14 +401,12 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
cr.health += 1
|
||||
cr.max_health += 1
|
||||
p.board.append(cr)
|
||||
for e in card.elements:
|
||||
p.played[e] = int(p.played.get(e, 0)) + 1
|
||||
p.cards_played += 1
|
||||
if face_down:
|
||||
say("%s legt ein Wesen verdeckt." % p.name)
|
||||
else:
|
||||
say("%s spielt %s." % [p.name, card.name])
|
||||
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down})
|
||||
apply_auras()
|
||||
cleanup()
|
||||
check_end()
|
||||
return ""
|
||||
@@ -463,12 +456,11 @@ func reveal(p: Player, cr: Creature) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Deckt ein Wesen auf; die Elementboni gelten sofort.
|
||||
## Deckt ein Wesen auf; das Elementpaar gilt sofort.
|
||||
func _flip(cr: Creature) -> void:
|
||||
cr.hidden = false
|
||||
say("%s wird aufgedeckt." % cr.name)
|
||||
emit({"type": "reveal", "uid": cr.uid, "card": cr.card})
|
||||
apply_auras()
|
||||
|
||||
|
||||
# --- Verschmelzen ------------------------------------------------------------
|
||||
@@ -523,49 +515,6 @@ func fuse(p: Player, a, b) -> String:
|
||||
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 -------------------------------------------------------------------
|
||||
|
||||
## 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
|
||||
if not (tgt_owner in enemies_of(cr.owner())):
|
||||
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]
|
||||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||
return [false, "Erst die Wesen"]
|
||||
@@ -609,17 +558,18 @@ func attack(cr: Creature, target) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Elementvorteil beim Zuschlagen und beim Konter: im Vorteil zählt der Stapelbonus des Ziels
|
||||
## nicht (so viel Schaden mehr), im Nachteil der eigene nicht (so viel weniger). Gegen Helden
|
||||
## gibt es keinen.
|
||||
## Elementvorteil beim Zuschlagen und beim Konter, als Verschiebung des Schadens: im Vorteil
|
||||
## doppelt (+ Angriff), im Nachteil halb, aufgerundet (− Angriff / 2). Gegen Helden gibt es keinen;
|
||||
## mit dem Merkmal kein_nachteil entfällt der Nachteil.
|
||||
static func matchup_bonus(source: Creature, target) -> int:
|
||||
if not (target is Creature):
|
||||
return 0
|
||||
var m := Cards.matchup(source.card.elements, target.card.elements)
|
||||
if m > 0:
|
||||
return target.aura
|
||||
return source.power()
|
||||
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
|
||||
|
||||
|
||||
@@ -634,24 +584,17 @@ func damage(target, amount: int) -> void:
|
||||
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:
|
||||
for round in 8:
|
||||
var died := false
|
||||
for p in players:
|
||||
for c in p.board.duplicate():
|
||||
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||||
if c.health <= 0 or not p.alive():
|
||||
p.board.erase(c)
|
||||
if c.health > 0:
|
||||
say("%s verschwindet mit %s." % [c.name, p.name])
|
||||
else:
|
||||
say("%s stirbt." % c.name)
|
||||
emit({"type": "death", "target": ref_of(c)})
|
||||
died = true
|
||||
if not died:
|
||||
return
|
||||
apply_auras()
|
||||
for p in players:
|
||||
for c in p.board.duplicate():
|
||||
if c.health <= 0 or not p.alive():
|
||||
p.board.erase(c)
|
||||
if c.health > 0:
|
||||
say("%s verschwindet mit %s." % [c.name, p.name])
|
||||
else:
|
||||
say("%s stirbt." % c.name)
|
||||
emit({"type": "death", "target": ref_of(c)})
|
||||
|
||||
|
||||
## Ende: einer von beiden hat kein Leben mehr.
|
||||
@@ -676,11 +619,11 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
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:
|
||||
if r == null:
|
||||
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:
|
||||
|
||||
+185
-46
@@ -1,24 +1,30 @@
|
||||
class_name Profile
|
||||
extends RefCounted
|
||||
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
|
||||
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die
|
||||
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
|
||||
## Man startet mit allen neutralen Karten (Stapel: 30 nach Manakurve); die Elemente schaltet die Kampagne frei (je Kapitel einen
|
||||
## 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.
|
||||
|
||||
const SAVE_VERSION := 7
|
||||
const SAVE_VERSION := 8
|
||||
const CRAFT_COST := 200 # Staub für eine neue Karte (Common)
|
||||
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 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 REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||
# Münzen nach dem Kampf: je eigener Zug, je ausgespielte Karte und je Zug unter FAST_TURN_SEC
|
||||
# Sekunden; ein Sieg verdoppelt alles, die Schwierigkeit multipliziert (siehe battle_coins).
|
||||
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 := {
|
||||
"leicht": {"name": "Leicht", "life": 25, "reward": 0.75, "smart": false},
|
||||
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true},
|
||||
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
|
||||
"leicht": {"name": "Leicht", "level": -1, "smart": false, "perk": false, "reward": 1.0},
|
||||
"normal": {"name": "Normal", "level": 0, "smart": true, "perk": false, "reward": 1.5},
|
||||
"schwer": {"name": "Schwer", "level": 1, "smart": true, "perk": true, "reward": 2.0},
|
||||
}
|
||||
# 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}
|
||||
@@ -26,6 +32,8 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
|
||||
|
||||
var coins := 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 deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
|
||||
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)
|
||||
|
||||
|
||||
## 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:
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
p.give_start()
|
||||
return p
|
||||
|
||||
|
||||
## Hat der Spieler schon ein Startdeck gewählt?
|
||||
func started() -> bool:
|
||||
return not collection.is_empty()
|
||||
|
||||
|
||||
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||
func choose_start(element: String) -> void:
|
||||
## Alle neutralen Karten in Common werden die Sammlung, ein Kurvenstapel daraus der Stapel.
|
||||
func give_start() -> void:
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
for id in DeckGen.element_deck(Cards.NEUTRAL):
|
||||
collection[id] = 0
|
||||
deck = DeckGen.curve_deck(Cards.NEUTRAL, Game.make_rng())
|
||||
|
||||
|
||||
## 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()
|
||||
p.coins = int(d.get("coins", 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))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
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):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
if p.collection.is_empty():
|
||||
p.give_start()
|
||||
var se = d.get("settings", {})
|
||||
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)),
|
||||
@@ -99,8 +110,8 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection,
|
||||
"deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
@@ -182,7 +193,7 @@ func dismantle(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
dust += dust_gain(DUST_DISMANTLE[Cards.RARITIES[lvl]])
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
@@ -204,11 +215,21 @@ func craft(cid: String) -> String:
|
||||
|
||||
# --- Booster ----------------------------------------------------------------------
|
||||
|
||||
## Fünf Karten mit gewürfelter Seltenheit.
|
||||
static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
## Preis eines Boosters: neutral BOOSTER_COST, je Element ELEMENT_BOOSTER_COST.
|
||||
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 weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
var weights := Skills.pack_weights(luck)
|
||||
var pool := Cards.all().filter(func(c): return c.elements[0] == element)
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
@@ -218,16 +239,29 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder
|
||||
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche
|
||||
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
if coins < BOOSTER_COST:
|
||||
## Öffnet einen Booster des Elements. Neue Karten kommen in Sammlung und Stapel (solange Platz ist).
|
||||
## Von jeder Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine
|
||||
## gleiche oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn der Booster
|
||||
## noch gesperrt ist oder die Münzen nicht reichen.
|
||||
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 []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
var out := []
|
||||
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:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
@@ -239,33 +273,138 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
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
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
res["dust"] = dust_gain(DUST_DISMANTLE[res.rarity])
|
||||
dust += res.dust
|
||||
return out
|
||||
|
||||
|
||||
# --- Kampf und Kampagne -----------------------------------------------------------
|
||||
|
||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
|
||||
var reward := REWARD_LOSS
|
||||
## Münzen eines Kampfes: COINS_TURN je eigenem Zug, COINS_CARD je ausgespielter Karte, COINS_FAST
|
||||
## je schnellem Zug; ein Sieg verdoppelt, factor ist der Münzfaktor der Schwierigkeit.
|
||||
static func battle_coins(won: bool, turns: int, cards: int, fast: int, factor: float = 1.0) -> int:
|
||||
var pts := COINS_TURN * turns + COINS_CARD * cards + COINS_FAST * fast
|
||||
if won:
|
||||
reward = int(round(REWARD_WIN * reward_factor))
|
||||
if stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
|
||||
pts *= 2
|
||||
return int(round(pts * factor))
|
||||
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
|
||||
# --- 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:
|
||||
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:
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
return campaign_stage >= Campaign.STAGES_PER_CHAPTER
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbcmk1gtfjoim
|
||||
@@ -68,6 +68,6 @@ static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||
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:
|
||||
return INFO
|
||||
+53
-27
@@ -31,7 +31,10 @@ var ai_timer := 0.0
|
||||
var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung
|
||||
var ai_copy: Game # Spielkopie, auf der die KI rechnet
|
||||
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 pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
@@ -72,7 +75,7 @@ var energy_label: Label
|
||||
var msg_label: Label
|
||||
var element_view: ElementView # eigene Elementleiste
|
||||
var opp_element_view: ElementView # die des Gegners darüber
|
||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
var info_label: Label # Vorschau: Merkmale, Eigenschaften
|
||||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||||
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
|
||||
@@ -81,6 +84,7 @@ var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
var reward_label: Label
|
||||
var detail_label: Label # Aufschlüsselung der Münzen
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var cursor_layer: Control
|
||||
@@ -99,23 +103,20 @@ func _ready() -> void:
|
||||
pad = BattleGamepad.new(self)
|
||||
ai_delay = 0.75 / float(s.ai_speed)
|
||||
_build_static()
|
||||
if stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||
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(opp, r, false, own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart)
|
||||
_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:
|
||||
var p: Profile = app.profile
|
||||
var rules := {"looks": p.looks(), "deck": p.deck_cards()}
|
||||
if stage < 0:
|
||||
rules["opponent_life"] = int(p.difficulty().life)
|
||||
if p.perk != "" and p.perk_unlocked():
|
||||
rules["human_perks"] = [p.perk]
|
||||
var perks := p.battle_perks()
|
||||
if not perks.is_empty():
|
||||
rules["human_perks"] = perks
|
||||
return rules
|
||||
|
||||
|
||||
@@ -224,7 +225,8 @@ func _build_static() -> void:
|
||||
overlay = _overlay()
|
||||
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")
|
||||
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"))
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
|
||||
@@ -459,8 +461,11 @@ func _exit_tree() -> void:
|
||||
|
||||
func _finish() -> void:
|
||||
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
|
||||
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.sfx.play("win" if won else "lose")
|
||||
refresh()
|
||||
@@ -777,6 +782,7 @@ func end_turn() -> void:
|
||||
return
|
||||
cancel()
|
||||
clear_undo()
|
||||
_close_turn()
|
||||
game.end_turn()
|
||||
ai_timer = ai_delay
|
||||
after_action()
|
||||
@@ -839,11 +845,36 @@ func on_tile_click(cr: Game.Creature) -> void:
|
||||
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:
|
||||
if game == null or not my_turn() or banner_turn == game.turn:
|
||||
return
|
||||
banner_turn = game.turn
|
||||
turn_start = Time.get_ticks_msec()
|
||||
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||||
fx.sfx("turn")
|
||||
|
||||
@@ -871,23 +902,17 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
|
||||
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:
|
||||
if drag_ghost != null:
|
||||
return
|
||||
var a := game.forecast(perspective, card)
|
||||
var info := ""
|
||||
if a > 0:
|
||||
info = tr("Elementbonus +%d") % a
|
||||
show_preview(card, entered, perspective.looks, info, anchor)
|
||||
show_preview(card, entered, perspective.looks, "", anchor)
|
||||
|
||||
|
||||
## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
|
||||
func creature_info(cr: Game.Creature) -> String:
|
||||
var parts: Array[String] = []
|
||||
if cr.aura > 0:
|
||||
parts.append(tr("Elementbonus +%d") % cr.aura)
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
var perk := cr.power() - cr.attack
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
if cr.card.is_neutral():
|
||||
@@ -902,10 +927,11 @@ func creature_info(cr: Game.Creature) -> String:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
@@ -1139,7 +1165,6 @@ func refresh() -> void:
|
||||
fx.glide(v, hand_arrivals[card])
|
||||
hand_arrivals.erase(card)
|
||||
v.lift = mine
|
||||
v.bonus = g.forecast(h, card)
|
||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||
v.border = Style.PURPLE
|
||||
elif card == fuse_first:
|
||||
@@ -1185,6 +1210,7 @@ func refresh() -> void:
|
||||
result_label.text = "Sieg!" if won else "Niederlage"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||
detail_label.text = reward_text()
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
if not notes.is_empty():
|
||||
notice_label.text = "\n".join(notes)
|
||||
|
||||
+165
-36
@@ -1,19 +1,32 @@
|
||||
class_name BoosterScreen
|
||||
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_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 cards: Array = [] # {"card", "rarity", "upgraded", "dust"} je Karte
|
||||
var cards: Array = [] # {"card", "rarity", "new", "upgraded", "dust"} je Karte
|
||||
var revealed := {}
|
||||
var coins_label: Label
|
||||
var msg_label: Label
|
||||
var b_buy: Button
|
||||
var summary_label: Label
|
||||
var b_buy: Button # Neutral ×1 (Tests)
|
||||
var b_all: Button
|
||||
var scroll: ScrollContainer
|
||||
var slots: Control
|
||||
var fx_layer: Control
|
||||
var fresh := {} # gerade aufgedeckt: wird umgedreht
|
||||
var pack_tiles := {} # Element -> PackTile (Kapitelreihenfolge)
|
||||
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:
|
||||
@@ -39,28 +52,56 @@ class CardBack extends Control:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
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.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:
|
||||
var gap := 20
|
||||
var x0 := (1280 - Profile.BOOSTER_SIZE * CARD_W - (Profile.BOOSTER_SIZE - 1) * gap) / 2
|
||||
return Rect2(x0 + i * (CARD_W + gap), 230, CARD_W, CARD_H)
|
||||
## Pack-Kachel eines Elements: Symbol, Name, Preis; die Kaufknöpfe hängt der Bildschirm an.
|
||||
class PackTile extends Control:
|
||||
var element: String
|
||||
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:
|
||||
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
|
||||
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
|
||||
fx_layer = Control.new()
|
||||
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(fx_layer)
|
||||
scroll = ScrollContainer.new()
|
||||
scroll.position = AREA.position
|
||||
scroll.size = AREA.size
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(scroll)
|
||||
slots = Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628"))
|
||||
b_buy.add_theme_font_size_override("font_size", 20)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
slots.custom_minimum_size = AREA.size
|
||||
scroll.add_child(slots)
|
||||
summary_label = Style.label(self, "", 16, Style.GOLD, Rect2(0, 566, 1280, 24), "center", true)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(540, 594, 200, 40), 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()
|
||||
|
||||
|
||||
@@ -69,19 +110,34 @@ func _input(event: InputEvent) -> void:
|
||||
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 got: Array = p.open_booster()
|
||||
var before: int = p.coins
|
||||
var got: Array = p.open_boosters(element, n)
|
||||
if got.is_empty():
|
||||
msg_label.text = "Nicht genug Münzen"
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
return
|
||||
app.save()
|
||||
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
|
||||
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()
|
||||
_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:
|
||||
@@ -91,13 +147,17 @@ func reveal(i: int) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Deckt alles auf, von vorn nach hinten gestaffelt; ab Ultra Rare bleibt die Karte kurz allein.
|
||||
func reveal_all() -> void:
|
||||
fresh = {}
|
||||
var k := 0
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
var delay := 0.0
|
||||
var step := 0.12 if cards.size() <= Profile.BOOSTER_SIZE else 0.06
|
||||
for i in cards.size():
|
||||
if not revealed.has(i):
|
||||
fresh[i] = k * 0.12
|
||||
k += 1
|
||||
fresh[i] = delay
|
||||
delay += step
|
||||
if Cards.RARITIES.find(cards[i].rarity) >= 3:
|
||||
delay += 0.35
|
||||
revealed[i] = true
|
||||
app.sfx.play("play")
|
||||
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)
|
||||
if rarity == "Common":
|
||||
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.modulate.a = 0.0
|
||||
var bt := create_tween()
|
||||
@@ -124,13 +184,71 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||
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:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= Profile.BOOSTER_SIZE
|
||||
var open: Array = p.unlocked_elements()
|
||||
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)
|
||||
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)
|
||||
if cards.is_empty():
|
||||
Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2)
|
||||
@@ -142,16 +260,27 @@ func refresh() -> void:
|
||||
slots.add_child(v)
|
||||
if fresh.has(i):
|
||||
flip(v, fresh[i], res.rarity)
|
||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
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):
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
var back := CardBack.new(r.size)
|
||||
back.position = r.position
|
||||
back.pivot_offset = r.size / 2
|
||||
back.clicked.connect(reveal.bind(i))
|
||||
slots.add_child(back)
|
||||
if fly:
|
||||
fly_in(back, r.position, i, per_row)
|
||||
fly = false
|
||||
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)
|
||||
@@ -1,6 +1,7 @@
|
||||
class_name CampaignScreen
|
||||
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 chapter := 1
|
||||
@@ -11,7 +12,7 @@ var node_buttons: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
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)
|
||||
build()
|
||||
|
||||
@@ -21,14 +22,16 @@ func build() -> void:
|
||||
node_buttons = []
|
||||
var p: Profile = app.profile
|
||||
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
|
||||
var n := Campaign.STAGES.size()
|
||||
var n := Campaign.STAGES_PER_CHAPTER
|
||||
var x := 40
|
||||
for c in Campaign.CHAPTERS.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)
|
||||
for c in Campaign.CHAPTER_ELEMENTS.size():
|
||||
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
|
||||
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():
|
||||
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.position = Vector2(900, 88)
|
||||
perk_box.size = Vector2(340, 38)
|
||||
@@ -109,7 +112,7 @@ func build() -> void:
|
||||
func set_chapter(c: int) -> void:
|
||||
chapter = c
|
||||
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
|
||||
build()
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ var card: Cards.Card
|
||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||
var rarity := "Common" # nur Optik
|
||||
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 drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
|
||||
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.
|
||||
@@ -92,15 +92,17 @@ func _draw() -> void:
|
||||
var pic_bottom := size.y - r_stat - 4
|
||||
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.rarity_sparkle(self, rect, rarity, seed) # vor den Werten, damit nichts die Zahlen überlagert
|
||||
# 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(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.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.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:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
|
||||
@@ -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()]
|
||||
@@ -0,0 +1 @@
|
||||
uid://dluwr50n0sd17
|
||||
@@ -21,7 +21,7 @@ var b_craft: Button
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||
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():
|
||||
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))
|
||||
@@ -60,9 +60,10 @@ func set_filter(e: String) -> void:
|
||||
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:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
var out: Array = app.profile.unlocked_cards()
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
|
||||
@@ -12,7 +12,7 @@ var width := 2
|
||||
var flash := false # kurz rot: wurde gerade angegriffen
|
||||
var slot: Vector2 # Zielposition im Raster
|
||||
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 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
|
||||
@@ -78,7 +78,7 @@ func _draw() -> void:
|
||||
var pic_bottom := size.y - r_stat - 4
|
||||
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)
|
||||
# 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 y := top_h + 6 + icon
|
||||
for k in creature.keywords():
|
||||
@@ -88,11 +88,7 @@ func _draw() -> void:
|
||||
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
|
||||
Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col)
|
||||
y += icon * 2 + 6
|
||||
if creature.aura != 0:
|
||||
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)
|
||||
Style.rarity_sparkle(self, rect, rar, seed) # vor den Werten, damit nichts die Zahlen überlagert
|
||||
_draw_stats(15 if small else 17, r_stat, shiny)
|
||||
Style.card_sheen(self, rect, rar, seed)
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
@@ -103,7 +99,6 @@ func _draw() -> void:
|
||||
if doomed:
|
||||
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.rarity_sparkle(self, rect, rar, seed)
|
||||
_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.
|
||||
func shown_stats() -> Array:
|
||||
if matchup != 0:
|
||||
var atk := maxi(0, creature.power() - matchup)
|
||||
return [atk, creature.health]
|
||||
return [maxi(0, creature.power() + matchup), creature.health]
|
||||
if attack_shift != 0:
|
||||
return [maxi(0, creature.power() + attack_shift), creature.health]
|
||||
return [creature.power(), creature.health]
|
||||
|
||||
+190
-25
@@ -1,38 +1,139 @@
|
||||
class_name DeckScreen
|
||||
extends Control
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
|
||||
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
|
||||
## 40 bis 60 Karten; „Auffüllen“ nimmt die wertvollsten eigenen Karten des gewählten Filters.
|
||||
## „Stärke“ ist der mittlere Kartenwert (100 Grundkarten … 500 Ultimate Rare).
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster (Filter nach Element und Mana, Stapelkarten
|
||||
## zuerst, dann höchste Stufen vorn), ein Klick nimmt eine Karte in den Stapel oder wieder heraus. Stapelkarten tragen einen Haken, die anderen sind
|
||||
## abgedunkelt. Rechts Stapelgröße, Stärke (mittlerer Kartenwert, 100 Grundkarten … 500 Ultimate
|
||||
## 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 filter := ""
|
||||
var filter := "" # Element oder ""
|
||||
var cost_filter := -1 # Kosten 0 bis 10 oder -1
|
||||
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 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
|
||||
|
||||
|
||||
## 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:
|
||||
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]
|
||||
for i in filters.size():
|
||||
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))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
var fb := ElementButton.new(e)
|
||||
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.card_clicked.connect(toggle)
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
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")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD # vor dem Text, sonst wächst die Breite auf eine Zeile
|
||||
hint.text = "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters."
|
||||
Style.button(self, "Auffüllen", Rect2(840, 262, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 318, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 382, 380, 48), "center")
|
||||
Style.label(self, "Manakurve", 13, Style.MUTED, Rect2(850, 190, 360, 20), "center")
|
||||
curve = CurveView.new(Rect2(850, 212, 360, 100))
|
||||
add_child(curve)
|
||||
share = ElementShare.new(Rect2(850, 322, 360, 30))
|
||||
add_child(share)
|
||||
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()
|
||||
|
||||
|
||||
@@ -46,15 +147,34 @@ func set_filter(e: String) -> void:
|
||||
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:
|
||||
var err: String = app.profile.toggle_deck(card.id)
|
||||
msg_label.text = err
|
||||
say(err)
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
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.
|
||||
func fill_up() -> void:
|
||||
var p: Profile = app.profile
|
||||
@@ -72,30 +192,75 @@ func clear_deck() -> void:
|
||||
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:
|
||||
var p: Profile = app.profile
|
||||
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
out.sort_custom(func(a, b):
|
||||
var ia: bool = a.id in p.deck
|
||||
var ib: bool = b.id in p.deck
|
||||
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)
|
||||
if cost_filter >= 0:
|
||||
out = out.filter(func(c): return c.cost == cost_filter)
|
||||
out.sort_custom(func(a, b): return sort_key(a) < sort_key(b))
|
||||
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:
|
||||
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.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)
|
||||
_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()
|
||||
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||
var views := grid.views()
|
||||
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
@@ -1,14 +1,12 @@
|
||||
class_name ElementView
|
||||
extends Control
|
||||
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
|
||||
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
|
||||
## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten
|
||||
## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
## Elementpaare einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente mit offenen Wesen
|
||||
## da: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je offenem Wesen, beide:
|
||||
## Effekt aktiv; Neutral hat keinen) und die Zahl der offenen Wesen.
|
||||
|
||||
const ROW := 32.0
|
||||
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
var played := {} # ausgespielte Karten je Element
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
@@ -17,7 +15,6 @@ func _init(rect: Rect2) -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in all():
|
||||
own[e] = 0
|
||||
played[e] = 0
|
||||
|
||||
|
||||
static func all() -> Array:
|
||||
@@ -27,7 +24,6 @@ static func all() -> Array:
|
||||
func update_from(_g: Game, me: Game.Player) -> void:
|
||||
for e in all():
|
||||
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()
|
||||
|
||||
|
||||
@@ -35,24 +31,18 @@ func paired(e: String) -> bool:
|
||||
return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
|
||||
|
||||
func bonus(e: String) -> int:
|
||||
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
|
||||
|
||||
|
||||
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||
## Elemente mit offenen Wesen.
|
||||
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:
|
||||
var parts: Array[String] = []
|
||||
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):
|
||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||
if bonus(e) > 0:
|
||||
s += " (+%d/+%d)" % [bonus(e), bonus(e)]
|
||||
parts.append(s)
|
||||
return " · ".join(parts)
|
||||
|
||||
@@ -61,7 +51,7 @@ func _draw() -> void:
|
||||
var y := ROW / 2
|
||||
for e in shown():
|
||||
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))
|
||||
var px := 10.0
|
||||
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))
|
||||
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
|
||||
Style.text(self, str(int(played.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)
|
||||
Style.text(self, str(int(own.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||
y += ROW
|
||||
+3
-3
@@ -18,7 +18,7 @@ func _ready() -> void:
|
||||
add_child(sfx)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
apply_settings()
|
||||
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new())
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func apply_settings() -> void:
|
||||
@@ -30,12 +30,12 @@ func apply_settings() -> void:
|
||||
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:
|
||||
profile = Profile.starter()
|
||||
save()
|
||||
apply_settings()
|
||||
show_screen(StartDeckScreen.new())
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_language(code: String) -> void:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class_name MenuScreen
|
||||
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 coins_label: Label
|
||||
@@ -25,10 +26,17 @@ func _ready() -> void:
|
||||
var x := 80
|
||||
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.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()]]:
|
||||
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||
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.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
_showcase(p)
|
||||
@@ -55,8 +63,8 @@ func _showcase(p: Profile) -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
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") % [
|
||||
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d · Level %d") % [
|
||||
p.collection.size(), p.unlocked_cards().size(), p.campaign_stage, Campaign.total(), p.player_level()]
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||
## vom gemeinsamen Stapel (siehe Game.start).
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Der Gegner spielt ein Element aus den
|
||||
## offenen Kapiteln in einer Stufe relativ zu deiner Deckstärke (Profile.single_opponent).
|
||||
|
||||
var app
|
||||
var diff_buttons := {}
|
||||
@@ -46,7 +46,11 @@ func refresh() -> void:
|
||||
for key in diff_buttons:
|
||||
_mark(diff_buttons[key], key == p.settings.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:
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0dgryn2kpic5
|
||||
@@ -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 +0,0 @@
|
||||
uid://bxl6isdrgqb2b
|
||||
@@ -26,6 +26,11 @@ const DANGER := Color("5a2828")
|
||||
|
||||
|
||||
## 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:
|
||||
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)
|
||||
|
||||
|
||||
## 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:
|
||||
var h := size / 2
|
||||
match element:
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ func _initialize() -> void:
|
||||
print("Beide mit Vorausschau:")
|
||||
_run(games / 2, r, false, false)
|
||||
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)
|
||||
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
|
||||
quit()
|
||||
@@ -25,7 +25,7 @@ func _initialize() -> void:
|
||||
func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> void:
|
||||
var wins := 0
|
||||
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 == "":
|
||||
AI.take_turn(g, g.current(), false)
|
||||
assert(g.turn < 300, "Spiel endet nicht")
|
||||
|
||||
+198
-134
@@ -89,6 +89,8 @@ func _initialize() -> void:
|
||||
test_merkmale()
|
||||
test_ki_zwei_schritte()
|
||||
test_kapitel_zwei_und_merkmalwahl()
|
||||
test_einzelspieler_gegner()
|
||||
test_skilltree()
|
||||
test_ki_zeitbudget()
|
||||
test_englische_karten()
|
||||
test_uebersetzung_vollstaendig()
|
||||
@@ -96,20 +98,21 @@ func _initialize() -> void:
|
||||
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:
|
||||
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")
|
||||
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 := {}
|
||||
for c in Cards.all():
|
||||
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.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
|
||||
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("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")
|
||||
@@ -146,12 +149,13 @@ func test_mana_und_kosten() -> void:
|
||||
check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
|
||||
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(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(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 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:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 5
|
||||
@@ -160,29 +164,45 @@ func test_stapel() -> void:
|
||||
var uniq := {}
|
||||
for id in d:
|
||||
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")
|
||||
var easy := DeckGen.opponent_deck(r, 1)
|
||||
check(easy.all(func(id): return Cards.by_id(id).cost <= 3), "Kapitel 1: nur bis Kosten 3")
|
||||
var neutral := DeckGen.element_deck(Cards.NEUTRAL)
|
||||
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)
|
||||
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")
|
||||
for e in Cards.ELEMENTS:
|
||||
var sd := DeckGen.start_deck(e)
|
||||
var main := sd.filter(func(id): return Cards.by_id(id).elements[0] == e)
|
||||
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)
|
||||
for o in Cards.ELEMENTS:
|
||||
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])
|
||||
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")
|
||||
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")
|
||||
for c in Campaign.CHAPTER_ELEMENTS.size():
|
||||
var e: String = Campaign.CHAPTER_ELEMENTS[c]
|
||||
var o := Campaign.opponent(c * Campaign.STAGES_PER_CHAPTER + 9, r)
|
||||
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)])
|
||||
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})
|
||||
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
|
||||
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(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;
|
||||
@@ -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")
|
||||
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")
|
||||
# Ohne Stapelbonus bewirkt das Element nichts
|
||||
# Schaden: im Vorteil doppelt, im Nachteil halb (aufgerundet), gegen Helden nichts
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
var tiefen := put(h, "w_4_5") # Wasser 4/5
|
||||
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
|
||||
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")
|
||||
# Im Vorteil zählt der Stapelbonus des Ziels nicht; beim Konter fehlt dem Ziel sein Angriffsbonus
|
||||
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
|
||||
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")
|
||||
# Nachteil beim Angriff: halb aufgerundet; der Konter des Ziels kommt doppelt
|
||||
var s4 := setup()
|
||||
var f4 := put(s4[1], "f_3_2")
|
||||
var w4 := put(s4[2], "w_4_5")
|
||||
s4[1].played["Feuer"] = 6
|
||||
s4[0].apply_auras()
|
||||
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")
|
||||
# 0 Angriff: nur der Stapelbonus des Ziels gibt Schaden
|
||||
check(w4.health == 5 - 2 and f4.health <= 0, "Feuer trifft Wasser für 2 (3 halbiert, aufgerundet), der Konter 4 × 2 tötet")
|
||||
# Rundung und Null
|
||||
var s5 := setup()
|
||||
var funke := put(s5[1], "f_0_2") # Feuer 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")
|
||||
s5[2].played["Luft"] = 3
|
||||
s5[0].apply_auras()
|
||||
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")
|
||||
var wall := put(s5[2], "e_0_4")
|
||||
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")
|
||||
check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff: auch im Vorteil kein Schaden")
|
||||
var wall := put(s5[2], "w_5_5")
|
||||
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(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")
|
||||
# Helden haben kein Element
|
||||
var s2 := setup()
|
||||
var g2: Game = s2[0]
|
||||
var geist := put(s2[1], "w_2_2")
|
||||
s2[1].played["Wasser"] = 3
|
||||
g2.apply_auras()
|
||||
g2.attack(geist, s2[2])
|
||||
check(s2[2].life == 12, "gegen Helden gibt es keinen Vorteil, nur den eigenen Bonus")
|
||||
s2[0].attack(geist, s2[2])
|
||||
check(s2[2].life == 15 - 2, "gegen Helden gibt es keinen Vorteil")
|
||||
# Merkmal: kein Nachteil
|
||||
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
|
||||
@@ -344,9 +355,8 @@ func test_verdeckt() -> void:
|
||||
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
|
||||
## Flink, Wasser Regeneration, Feuer Doppelschlag); je drei ausgespielte Karten eines Elements geben
|
||||
## den eigenen Wesen des Elements +1/+1. Jeder Spieler zählt für sich.
|
||||
## Elementpaare: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
|
||||
## Flink, Wasser Regeneration, Feuer Doppelschlag). Jeder Spieler zählt für sich, verdeckte nicht.
|
||||
func test_elementboni() -> void:
|
||||
var s := setup(["f_4_3", "f_4_3"])
|
||||
var g: Game = s[0]
|
||||
@@ -354,39 +364,17 @@ func test_elementboni() -> void:
|
||||
var o: Game.Player = s[2]
|
||||
var fire := [put(h, "f_3_2"), put(h, "f_2_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(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)
|
||||
g.apply_auras()
|
||||
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
|
||||
h.played["Feuer"] = 2
|
||||
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]) # Flammender Wolf
|
||||
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], true) # zweiter Wolf verdeckt
|
||||
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)
|
||||
check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus")
|
||||
# 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")
|
||||
check(secret.has("Doppelschlag") and h.element_count("Feuer") == 4, "aufgedeckt zählt es und bekommt den Effekt")
|
||||
# Die anderen Paare
|
||||
var s5 := setup()
|
||||
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 geister := [put(s6[1], "w_2_2"), put(s6[1], "w_2_2")]
|
||||
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 g2: Game = s2[0]
|
||||
var h2: Game.Player = s2[1]
|
||||
var dampf := put(h2, "f_3_2")
|
||||
dampf.card = Cards.fused(C("f_3_2"), C("w_2_2"))
|
||||
var fw := put(h2, "f_2_1")
|
||||
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(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
|
||||
h2.played["Feuer"] = 3
|
||||
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
|
||||
# Neutral: kein Paar
|
||||
var s7 := setup(["n_2_2", "n_2_2"])
|
||||
var g7: Game = s7[0]
|
||||
var h7: Game.Player = s7[1]
|
||||
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(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")
|
||||
|
||||
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")
|
||||
|
||||
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
|
||||
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("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(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
|
||||
var dampf: Game.Creature = h.board[0]
|
||||
var secret := put(h, "e_0_4", false, true)
|
||||
@@ -639,27 +615,35 @@ func test_booster() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 3
|
||||
for i in 50:
|
||||
var rolled := Profile.roll_booster(r)
|
||||
check(rolled.size() == 5, "5 Karten")
|
||||
var rolled := Profile.roll_booster(r, "Feuer")
|
||||
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")
|
||||
var p := Profile.new()
|
||||
p.coins = Profile.BOOSTER_COST
|
||||
var got := p.open_booster(r)
|
||||
check(p.coins == 0 and got.size() == 5, "Booster kostet %d" % Profile.BOOSTER_COST)
|
||||
p.coins = Profile.ELEMENT_BOOSTER_COST
|
||||
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")
|
||||
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")
|
||||
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(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.deck = []
|
||||
for i in Game.DECK_MAX:
|
||||
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 upgraded := false
|
||||
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:
|
||||
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")
|
||||
@@ -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")
|
||||
|
||||
|
||||
## Sammlung und Deck: Start mit einem Startdeck, Herstellen für Staub, Zerlegen entfernt
|
||||
## Common-Karten, der Stapel hält 40 bis 60 eigene Karten.
|
||||
## Sammlung und Deck: Start mit den neutralen Karten, Herstellen für Staub, Zerlegen entfernt
|
||||
## Common-Karten, der Stapel hält 30 bis 60 eigene Karten.
|
||||
func test_sammlung_und_deck() -> void:
|
||||
var p := Profile.starter()
|
||||
check(not p.started() and p.collection.is_empty() and p.deck.is_empty(), "neues Profil: noch kein Startdeck")
|
||||
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.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")
|
||||
check(p.toggle_deck("f_5_5") == "Karte nicht in der Sammlung", "fremde Karte nicht in den Stapel")
|
||||
var first: String = p.deck[0]
|
||||
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)
|
||||
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")
|
||||
p.choose_start("Feuer")
|
||||
p = Profile.starter()
|
||||
p.dust = Profile.CRAFT_COST - 1
|
||||
check(p.craft("f_5_5") == "Nicht genug Staub (%d nötig)" % Profile.CRAFT_COST, "Herstellen braucht Staub")
|
||||
p.dust = Profile.CRAFT_COST
|
||||
@@ -717,7 +699,7 @@ func test_sammlung_und_deck() -> void:
|
||||
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")
|
||||
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 = []
|
||||
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())))
|
||||
@@ -731,12 +713,11 @@ func test_spielstand() -> void:
|
||||
if FileAccess.file_exists(path + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
var p := Profile.load_or_new(path)
|
||||
check(p.coins == Profile.START_COINS and not p.started(), "Starterprofil: Startdeck noch zu wählen")
|
||||
p.choose_start("Wasser")
|
||||
check(p.collection.size() == Game.DECK_MIN and p.deck.size() == Game.DECK_MIN, "Startdeck gewählt")
|
||||
check(p.coins == Profile.START_COINS and p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN, "Starterprofil: neutrale Karten")
|
||||
check(FileAccess.file_exists(path), "Datei angelegt")
|
||||
p.coins = 999
|
||||
p.collection["f_3_2"] = 2
|
||||
p.collection["f_0_1"] = 0
|
||||
p.deck = ["f_3_2", "f_0_1"]
|
||||
p.save(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"}
|
||||
var p := Profile.from_dict(alt)
|
||||
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")
|
||||
var fresh := Profile.new()
|
||||
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:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 4
|
||||
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
||||
var boss := Campaign.opponent(4, r)
|
||||
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 24, "Stufe 5: Vulkar mit 24 Leben")
|
||||
check(Campaign.STAGES_PER_CHAPTER == 10 and Campaign.total() == 50, "fünf Kapitel mit je zehn Stufen")
|
||||
var boss := Campaign.opponent(4)
|
||||
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")
|
||||
var g := Game.new_battle_with(boss, r)
|
||||
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 reward := p.record_battle(true, 0)
|
||||
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
|
||||
reward = p.record_battle(true, 0)
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung")
|
||||
reward = p.record_battle(false, 1)
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
||||
var reward := p.record_battle(true, 380, 0)
|
||||
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, 380, 0)
|
||||
check(p.campaign_stage == 1 and reward == 380, "Wiederholung nur die Münzen des Kampfs")
|
||||
reward = p.record_battle(false, 190, 1)
|
||||
check(p.campaign_stage == 1 and reward == 190, "Niederlage schaltet nichts frei")
|
||||
p.campaign_stage = 9
|
||||
p.record_battle(true, 9)
|
||||
p.record_battle(true, 380, 9)
|
||||
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
|
||||
while sim.winner == "" and guard < 300:
|
||||
AI.take_turn(sim, sim.current(), true)
|
||||
@@ -918,17 +902,17 @@ func test_merkmale() -> void:
|
||||
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")
|
||||
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()
|
||||
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")
|
||||
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
|
||||
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
|
||||
g3.end_turn()
|
||||
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)
|
||||
g4.end_turn()
|
||||
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:
|
||||
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.stage(25).perks.size() == 3 and Campaign.stage(25).name == "Nyx", "Kapitel 3 mit drei Merkmalen")
|
||||
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() == 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():
|
||||
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))
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 8
|
||||
var archi2 := Campaign.opponent(19, r)
|
||||
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.looks.values().all(func(l): return l == 1), "Kapitel 2: seine Karten sind Rare")
|
||||
check(Campaign.opponent(25, r).looks.values().all(func(l): return l == 2), "Kapitel 3: Super Rare")
|
||||
var archi2 := Campaign.opponent(19)
|
||||
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 == 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).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()
|
||||
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
|
||||
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]})
|
||||
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.record_battle(true, 19)
|
||||
p.record_battle(true, 380, 19)
|
||||
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
||||
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")
|
||||
|
||||
|
||||
## 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:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 12
|
||||
|
||||
+157
-68
@@ -133,11 +133,8 @@ func screen():
|
||||
return main.screen
|
||||
|
||||
|
||||
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist; ohne Startdeck wird
|
||||
## Feuer gewählt.
|
||||
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
|
||||
func ensure_menu() -> void:
|
||||
if screen() is StartDeckScreen:
|
||||
await click(find_button("Feuer wählen"))
|
||||
if TranslationServer.get_locale().begins_with("en"):
|
||||
main.set_language("de")
|
||||
if not (screen() is MenuScreen):
|
||||
@@ -188,7 +185,7 @@ func _ready() -> void:
|
||||
main.sfx.muted = true # Tests bleiben still
|
||||
await frames(3)
|
||||
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.append(test_profil_zuruecksetzen)
|
||||
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)
|
||||
|
||||
|
||||
## 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:
|
||||
main.reset_profile()
|
||||
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
|
||||
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(Profile.load_or_new(SAVE).started(), "gespeichert")
|
||||
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")
|
||||
await shot("00_start")
|
||||
check(Profile.load_or_new(SAVE).collection.size() == 110, "gespeichert")
|
||||
|
||||
|
||||
func test_menu() -> void:
|
||||
@@ -244,6 +239,32 @@ func test_booster() -> void:
|
||||
else:
|
||||
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(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"))
|
||||
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")
|
||||
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(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 click(find_button("Starten"))
|
||||
var hard = screen()
|
||||
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
||||
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")
|
||||
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")
|
||||
@@ -414,7 +435,7 @@ func test_gegnerauswahl() -> void:
|
||||
await click(find_button("Starten"))
|
||||
var easy = screen()
|
||||
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")
|
||||
await give_up()
|
||||
await click(find_button("Einzelspieler"))
|
||||
@@ -473,28 +494,89 @@ func test_einstellungen() -> void:
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit dem frischen Startdeck.
|
||||
func test_deck() -> void:
|
||||
main.profile.choose_start("Feuer")
|
||||
## Skilltree: Level und Punkte aus der Erfahrung, Lernen verbraucht Punkte, Knöpfe folgen dem Stand.
|
||||
func test_skilltree() -> void:
|
||||
main.profile.xp = Skills.xp_for_level(3)
|
||||
main.profile.skills = {}
|
||||
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"))
|
||||
var scr = screen()
|
||||
check(scr is DeckScreen, "Deck geöffnet")
|
||||
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.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
|
||||
scr.refresh()
|
||||
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
|
||||
scr.refresh()
|
||||
await frames(2)
|
||||
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
|
||||
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]
|
||||
scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs
|
||||
await frames(2)
|
||||
@@ -503,12 +585,21 @@ func test_deck() -> void:
|
||||
await shot("19_deck")
|
||||
await click(find_button("Leeren"))
|
||||
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"))
|
||||
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("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"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
@@ -518,30 +609,38 @@ func test_sammlung() -> void:
|
||||
var scr = screen()
|
||||
check(scr is CollectionScreen, "Sammlung geöffnet")
|
||||
var p: Profile = main.profile
|
||||
check(scr.grid.views().size() == Cards.all().size(), "alle Karten des Spiels sind zu sehen")
|
||||
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(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(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
|
||||
scr.search_box.text = "Lodernder Funke"
|
||||
scr.search_box.text_changed.emit("Lodernder Funke")
|
||||
scr.search_box.text = "Tapferer Knappe"
|
||||
scr.search_box.text_changed.emit("Tapferer Knappe")
|
||||
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)
|
||||
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)
|
||||
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_changed.emit("")
|
||||
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(Profile.load_or_new(SAVE).level("f_2_1") == 1, "Sammlung gespeichert")
|
||||
scr.search_box.text = "Lodernder Funke"
|
||||
scr.search_box.text_changed.emit("Lodernder Funke")
|
||||
check(Profile.load_or_new(SAVE).level("n_2_1") == 1, "Sammlung gespeichert")
|
||||
scr.search_box.text = "Tapferer Knappe"
|
||||
scr.search_box.text_changed.emit("Tapferer Knappe")
|
||||
await frames(1)
|
||||
var dust: int = p.dust
|
||||
await click(scr.b_dismantle)
|
||||
check(p.level("f_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
|
||||
# Herstellen einer fehlenden Karte
|
||||
check(p.level("n_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
|
||||
# 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_changed.emit("Infernalischer Drache")
|
||||
await frames(1)
|
||||
@@ -551,6 +650,8 @@ func test_sammlung() -> void:
|
||||
await click(scr.b_craft)
|
||||
check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt")
|
||||
await shot("20_sammlung")
|
||||
main.profile.campaign_stage = 0
|
||||
main.save()
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
@@ -594,9 +695,7 @@ func test_profil_zuruecksetzen() -> void:
|
||||
check(not scr.confirm.visible, "abgebrochen")
|
||||
await click(find_button("Profil 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")
|
||||
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(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ü")
|
||||
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)
|
||||
var g: Game = scr.game
|
||||
g.human.board.clear()
|
||||
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
|
||||
for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
|
||||
scr.opponent().played[e] = 0
|
||||
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Paare
|
||||
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)
|
||||
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.energy = 20
|
||||
g.human.looks = {"w_3_3": 3}
|
||||
g.apply_auras()
|
||||
scr.refresh()
|
||||
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.opp_element_view.text() == "Luft 1/0", "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(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", "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, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken")
|
||||
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
|
||||
await hover(scr.hand_views[0])
|
||||
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
|
||||
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]
|
||||
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ü
|
||||
await right_click_at(scr.own_views[0].get_global_rect().get_center())
|
||||
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)
|
||||
mine_hidden.ready = true
|
||||
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(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter drei Karten kein Bonus, aufgedeckt greift sofort an")
|
||||
g.human.played["Feuer"] = 3
|
||||
g.apply_auras()
|
||||
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())
|
||||
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(), "Werte bleiben die der Karten, aufgedeckt greift sofort an")
|
||||
await shot("23b_drei_feuer")
|
||||
# Angriffsziel: Feuer gegen Luft schlägt doppelt, der Falke kontert halb; verdeckte ohne Anzeige, Held gesperrt
|
||||
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.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.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() == [2, falke.health], "der Falke zeigt seinen Konter gegen den Wolf: 3 halbiert, aufgerundet 2")
|
||||
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")
|
||||
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
|
||||
await click(scr.tiles[secret.uid])
|
||||
@@ -866,7 +951,8 @@ func test_kampagne() -> void:
|
||||
var scr = screen()
|
||||
check(scr is CampaignScreen, "Kampagne geöffnet")
|
||||
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])
|
||||
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")
|
||||
@@ -878,7 +964,7 @@ func test_kampagne() -> void:
|
||||
await shot("16_kampagne_kampf")
|
||||
await give_up()
|
||||
# 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"))
|
||||
var scr2 = screen()
|
||||
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 click(find_button("Kämpfen"))
|
||||
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")
|
||||
await give_up()
|
||||
main.profile.campaign_stage = 0
|
||||
@@ -934,6 +1021,8 @@ func test_kampf(shot_turn: int) -> void:
|
||||
await frames(3)
|
||||
await shot("09_kampf_ende")
|
||||
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")
|
||||
await click(find_button("Zurück zum Menü"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
+75
-9
@@ -27,7 +27,7 @@ KI-Tempo,AI speed
|
||||
Animationen,Animations
|
||||
Sprache,Language
|
||||
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
|
||||
Zerlegen,Dismantle
|
||||
Kämpfen,Fight
|
||||
@@ -39,6 +39,9 @@ Du bist dran,Your turn
|
||||
Rückgängig,Undo
|
||||
Kapitel 1,Chapter 1
|
||||
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"
|
||||
Wähle ein Angriffsziel,Choose an attack target
|
||||
Abgebrochen,Cancelled
|
||||
@@ -82,7 +85,63 @@ Sammlung,Collection
|
||||
Verschmelzen,Fuse
|
||||
Nur eigene Wesen,Only your own creatures
|
||||
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
|
||||
Alle Kapitel abgeschlossen,All chapters completed
|
||||
Elemente,Elements
|
||||
@@ -101,14 +160,23 @@ Nicht genug Mana,Not enough mana
|
||||
Profil wirklich zurücksetzen?,Really reset your profile?
|
||||
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
||||
%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
|
||||
Ziehen,Draw
|
||||
Verdeckt legen ◆ %d,Set face-down ◆ %d
|
||||
Aufdecken,Reveal
|
||||
verdeckt,face-down
|
||||
Aufgedeckt!,Revealed!
|
||||
Elementbonus +%d,Element bonus +%d
|
||||
Merkmal +%d,Perk +%d
|
||||
Erst die Wesen,Creatures first
|
||||
%s liegt verdeckt,%s is face-down
|
||||
@@ -117,7 +185,7 @@ Unbekannte Eigenschaft,Unknown trait
|
||||
%s hat schon %s,%s already has %s
|
||||
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
|
||||
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
|
||||
Greift zweimal je Zug,Attacks twice per turn
|
||||
Verdeckt,Face-down
|
||||
@@ -162,10 +230,8 @@ Neu!,New!
|
||||
Auffüllen,Fill up
|
||||
Leeren,Clear
|
||||
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.
|
||||
Wähle dein Startdeck,Choose your starting deck
|
||||
"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
|
||||
Klick auf eine Karte: rein oder raus.,Click a card: in or out.
|
||||
Manakurve,Mana curve
|
||||
Optionen,Options
|
||||
Hauptmenü,Main menu
|
||||
Neutral,Neutral
|
||||
|
||||
|
Binary file not shown.
Reference in new issue
Block a user