update
Some checks are pending
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Waiting to run
Tests / balance (push) Waiting to run

This commit is contained in:
team3
2026-10-13 15:39:55 +02:00
parent b91f475a6e
commit f2c609a08b
25 changed files with 1338 additions and 776 deletions

View File

@@ -20,19 +20,19 @@ Der Spielstand liegt unter `user://spielstand.json`
## Spielsysteme ## Spielsysteme
- 120 Karten in `data/karten.json`: je Element alle Werte von 0/1 bis 5/5, die ID verrät sie (`f_3_2` = Feuer 3/2), Namen aus Eigenschaft und Wesen; Kombos sind die Namen verschmolzener Wesen - 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
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts) - Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts)
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen - 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 teuersten des gewählten Filters). Zu Beginn fünf im ersten Zug spielbare Karten. 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 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)
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst - Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf - Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar 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 - 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 - 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 fünf 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 - 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, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; so greift und kontert auch ein Wesen mit 0 Angriff, nur nicht ohne Vorteil; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“ - 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“
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch. Den Zug beendet die Leertaste oder der Rechtsklick ins Leere; Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden) - Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar, per Doppelklick auf das eigene Wesen direkt. Nichts schlägt durch. Den Zug beendet die Leertaste oder der Rechtsklick ins Leere; Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden)
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten aus den neuen Werten, Elemente und Eigenschaften beider; es kostet die halbe Manasumme beider Karten (aufgerundet); drei oder mehr Elemente ergeben eine Chimäre - Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen - 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 - Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread - KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
- 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: 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

View File

@@ -14,8 +14,8 @@
- Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare); je Stufe +1/+1 bei gleichen Kosten, dazu Optik (Glanz, Holo, Relief, Funkeln); Fusionen tragen die höhere Stufe der Eltern - Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare); je Stufe +1/+1 bei gleichen Kosten, dazu Optik (Glanz, Holo, Relief, Funkeln); Fusionen tragen die höhere Stufe der Eltern
- Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000) - Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000)
- Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub - Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
- Jeder hat sein Deck: 40 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter); zu Beginn fünf im ersten Zug spielbare 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: 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
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0) und Verschmelzen (halbe Manasumme beider Karten), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten - Mana für Ziehen und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für Handkarten
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug) - Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst - Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf - Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
@@ -23,10 +23,10 @@
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen - Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
## Elemente ## Elemente
- Jede Karte gehört zu genau einem Element; je Element gibt es alle Werte von 0/1 bis 5/5 (120 Karten) - 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
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) - Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag) - Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
- Je fünf 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 - 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, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht) - Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, 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)
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung - Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel und wird bestätigt oder abgebrochen (Gamepad: über das Kartenmenü); kostet kein Mana; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand; je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus, dann die höhere Stufe der Eltern (+1/+1 je Stufe); Elemente und Eigenschaften beider
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus - Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus

View File

@@ -1199,6 +1199,306 @@
], ],
"attack": 5, "attack": 5,
"health": 5 "health": 5
},
{
"id": "n_0_1",
"name": "Junger Knappe",
"name_en": "Young Squire",
"elements": [
"Neutral"
],
"attack": 0,
"health": 1
},
{
"id": "n_0_2",
"name": "Junger Späher",
"name_en": "Young Scout",
"elements": [
"Neutral"
],
"attack": 0,
"health": 2
},
{
"id": "n_0_3",
"name": "Junger Söldner",
"name_en": "Young Mercenary",
"elements": [
"Neutral"
],
"attack": 0,
"health": 3
},
{
"id": "n_0_4",
"name": "Junger Ritter",
"name_en": "Young Knight",
"elements": [
"Neutral"
],
"attack": 0,
"health": 4
},
{
"id": "n_0_5",
"name": "Junger Champion",
"name_en": "Young Champion",
"elements": [
"Neutral"
],
"attack": 0,
"health": 5
},
{
"id": "n_1_1",
"name": "Wachsamer Knappe",
"name_en": "Watchful Squire",
"elements": [
"Neutral"
],
"attack": 1,
"health": 1
},
{
"id": "n_1_2",
"name": "Wachsamer Späher",
"name_en": "Watchful Scout",
"elements": [
"Neutral"
],
"attack": 1,
"health": 2
},
{
"id": "n_1_3",
"name": "Wachsamer Söldner",
"name_en": "Watchful Mercenary",
"elements": [
"Neutral"
],
"attack": 1,
"health": 3
},
{
"id": "n_1_4",
"name": "Wachsamer Ritter",
"name_en": "Watchful Knight",
"elements": [
"Neutral"
],
"attack": 1,
"health": 4
},
{
"id": "n_1_5",
"name": "Wachsamer Champion",
"name_en": "Watchful Champion",
"elements": [
"Neutral"
],
"attack": 1,
"health": 5
},
{
"id": "n_2_1",
"name": "Tapferer Knappe",
"name_en": "Brave Squire",
"elements": [
"Neutral"
],
"attack": 2,
"health": 1
},
{
"id": "n_2_2",
"name": "Tapferer Späher",
"name_en": "Brave Scout",
"elements": [
"Neutral"
],
"attack": 2,
"health": 2
},
{
"id": "n_2_3",
"name": "Tapferer Söldner",
"name_en": "Brave Mercenary",
"elements": [
"Neutral"
],
"attack": 2,
"health": 3
},
{
"id": "n_2_4",
"name": "Tapferer Ritter",
"name_en": "Brave Knight",
"elements": [
"Neutral"
],
"attack": 2,
"health": 4
},
{
"id": "n_2_5",
"name": "Tapferer Champion",
"name_en": "Brave Champion",
"elements": [
"Neutral"
],
"attack": 2,
"health": 5
},
{
"id": "n_3_1",
"name": "Erfahrener Knappe",
"name_en": "Seasoned Squire",
"elements": [
"Neutral"
],
"attack": 3,
"health": 1
},
{
"id": "n_3_2",
"name": "Erfahrener Späher",
"name_en": "Seasoned Scout",
"elements": [
"Neutral"
],
"attack": 3,
"health": 2
},
{
"id": "n_3_3",
"name": "Erfahrener Söldner",
"name_en": "Seasoned Mercenary",
"elements": [
"Neutral"
],
"attack": 3,
"health": 3
},
{
"id": "n_3_4",
"name": "Erfahrener Ritter",
"name_en": "Seasoned Knight",
"elements": [
"Neutral"
],
"attack": 3,
"health": 4
},
{
"id": "n_3_5",
"name": "Erfahrener Champion",
"name_en": "Seasoned Champion",
"elements": [
"Neutral"
],
"attack": 3,
"health": 5
},
{
"id": "n_4_1",
"name": "Grimmiger Knappe",
"name_en": "Grim Squire",
"elements": [
"Neutral"
],
"attack": 4,
"health": 1
},
{
"id": "n_4_2",
"name": "Grimmiger Späher",
"name_en": "Grim Scout",
"elements": [
"Neutral"
],
"attack": 4,
"health": 2
},
{
"id": "n_4_3",
"name": "Grimmiger Söldner",
"name_en": "Grim Mercenary",
"elements": [
"Neutral"
],
"attack": 4,
"health": 3
},
{
"id": "n_4_4",
"name": "Grimmiger Ritter",
"name_en": "Grim Knight",
"elements": [
"Neutral"
],
"attack": 4,
"health": 4
},
{
"id": "n_4_5",
"name": "Grimmiger Champion",
"name_en": "Grim Champion",
"elements": [
"Neutral"
],
"attack": 4,
"health": 5
},
{
"id": "n_5_1",
"name": "Legendärer Knappe",
"name_en": "Legendary Squire",
"elements": [
"Neutral"
],
"attack": 5,
"health": 1
},
{
"id": "n_5_2",
"name": "Legendärer Späher",
"name_en": "Legendary Scout",
"elements": [
"Neutral"
],
"attack": 5,
"health": 2
},
{
"id": "n_5_3",
"name": "Legendärer Söldner",
"name_en": "Legendary Mercenary",
"elements": [
"Neutral"
],
"attack": 5,
"health": 3
},
{
"id": "n_5_4",
"name": "Legendärer Ritter",
"name_en": "Legendary Knight",
"elements": [
"Neutral"
],
"attack": 5,
"health": 4
},
{
"id": "n_5_5",
"name": "Legendärer Champion",
"name_en": "Legendary Champion",
"elements": [
"Neutral"
],
"attack": 5,
"health": 5
} }
] ]
} }

View File

@@ -21,17 +21,22 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
## Karte ## Karte
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter - Oben links der Kostenkristall, daneben der Name (immer eine Zeile, nie abgeschnitten: die
Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration, gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt), unten Angriff (gelb) und Leben (blau) als Kreise, dazwischen der Edelstein der Schrift schrumpft etwas, dann wird der Text schmaler gezeichnet; das Feld ist so hoch wie der
Stufe. Karten haben keinen Text: nur Element, Angriff, Leben. Kristall), in der Mitte das Bild als Elementsymbol im dunklen Kreis, so groß wie
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol. der Platz erlaubt, unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten
- Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau. haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die
- Seltenheit nach Yu-Gi-Oh: Common matt; Rare silberner Name und Rahmen, stehender Glanzstreifen, Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration,
Edelstein; Super Rare Holo-Bild (wandernder Regenbogenschimmer über dem Symbol); Ultra Rare gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt), rechts der
goldener Name und Rahmen plus Holo-Bild; Ultimate Rare Relief am Symbol, Holo auf Bild und Elementbonus.
Rahmen, Funkeln, schillernde Farben. Fusionen tragen die höhere Stufe ihrer Eltern. - Die Hand sind alle Karten in Feldgröße, ab sechs überlappend; Details kommen aus der schwebenden Vorschau.
- Mehrere Elemente: der Hintergrund trägt das erste Element, ein rundes Medaillon hinter dem - Seltenheit nach Yu-Gi-Oh: Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild
Symbol zeigt alle Elemente als Tortenstücke. (wandernder Regenbogenschimmer im Bildkreis); Ultra Rare golden glänzender Name plus Holo-Bild;
Ultimate Rare goldener Name, goldenes Bild, glänzende Kosten und Werte, Schimmer über die ganze
Karte, Funkeln, schillernder Rahmen. Der Rahmen trägt ab Rare die Stufenfarbe.
Fusionen tragen die höhere Stufe ihrer Eltern.
- Mehrere Elemente: der Hintergrund trägt je Element einen senkrechten Streifen; das Bild kommt
vom stärkeren Elternteil (Angriff plus Leben).
- Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit - Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit
dem Vermerk „verdeckt“. dem Vermerk „verdeckt“.
@@ -39,12 +44,14 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
Nur, was gerade zählt, ist zu sehen. Von unten nach oben: Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
- **Hand** als bis zu fünf Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners - **Hand** als alle Karten in Feldgröße unten, in einem leichten Bogen, die des Gegners
gleich groß als Rückseiten oben; der gleich groß als Rückseiten oben; der
eigener Stapel rechts unten als Kartenstoß mit den Kosten des nächsten Ziehens (das erste im Zug eigener Stapel rechts unten als Kartenstoß mit den Kosten des nächsten Ziehens (das erste im Zug
frei), darüber der eigene Kasten; der Stapel des Gegners rechts oben, sein Kasten darunter. frei), darüber der eigene Kasten; der Stapel des Gegners rechts oben, sein Kasten darunter.
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein - **Verschmelzen** per Ziehen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über (golden gerahmt) und loslassen, auch gemischt. Die neue Handkarte schwebt über dem Ziel, beide
Ausgangskarten bleiben sichtbar; darunter „Verschmelzen“ und „Abbrechen“. Es kostet nichts;
Wesen verlassen das Brett, die neue Karte wird wie jede andere ausgespielt. Mit Gamepad über
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner). das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana, - **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist. Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
@@ -62,7 +69,7 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
- **Optionen** per Esc als Überlagerung: Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden; der Verlauf - **Optionen** per Esc als Überlagerung: Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden; der Verlauf
öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig. öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig.
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen - Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht. steht der Elementkonter (der ignorierte Stapelbonus) am Ziel, bei Verdeckten nicht.
## Bewegung und Ton ## Bewegung und Ton
@@ -85,7 +92,8 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
stärker, nicht über Begleiter. stärker, nicht über Begleiter.
- Sammlung: sechs Spalten, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts - 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, die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, nur eigene Karten,
Stapelkarten hell, die anderen abgedunkelt; Klick wechselt. 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).
## Grafik ## Grafik

View File

@@ -101,14 +101,7 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
for cr in p.board: for cr in p.board:
if cr.hidden and game.can_reveal(p, cr)[0]: if cr.hidden and game.can_reveal(p, cr)[0]:
out.append({"type": "reveal", "creature": cr}) out.append({"type": "reveal", "creature": cr})
var pairs := [] for pr in fusion_pairs(game, p, prune):
for i in p.board.size():
for j in range(i + 1, p.board.size()):
if game.can_fuse(p, p.board[i], p.board[j])[0]:
pairs.append([p.board[i], p.board[j]])
if prune and pairs.size() > MAX_TARGETS:
pairs = _top(pairs, func(pr): return _value(pr[0]) + _value(pr[1]), MAX_TARGETS)
for pr in pairs:
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]}) out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
for cr in p.board: for cr in p.board:
if not cr.can_attack(): if not cr.can_attack():
@@ -126,6 +119,39 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
return out return out
## Verschmelzungen, die sich lohnen könnten: Paare eigener Wesen (mit prune die schwächsten, die
## Plätze frei machen), jedes angeschlagene Wesen mit der billigsten Handkarte, die zwei
## billigsten Handkarten. Das Ergebnis kommt auf die Hand, die Vorausschau legt es im Folgeschritt.
static func fusion_pairs(game: Game, p: Game.Player, prune: bool) -> Array:
var pairs := []
for i in p.board.size():
for j in range(i + 1, p.board.size()):
pairs.append([p.board[i], p.board[j]])
if prune and pairs.size() > MAX_TARGETS:
pairs = _top(pairs, func(pr): return -(_value(pr[0]) + _value(pr[1])), MAX_TARGETS)
var cheap := _cheapest(p)
if not cheap.is_empty():
for cr in p.board:
if cr.health < cr.max_health:
pairs.append([cr, cheap[0]])
if cheap.size() >= 2:
pairs.append([cheap[0], cheap[1]])
return pairs.filter(func(pr): return game.can_fuse(p, pr[0], pr[1])[0])
## Handkarten nach Kosten aufsteigend.
static func _cheapest(p: Game.Player) -> Array:
var cheap: Array = p.hand.duplicate()
cheap.sort_custom(func(a, b): return a.cost < b.cost)
return cheap
## Teil einer Verschmelzung in einem anderen Spiel: Handkarten sind in Kopien dieselben Objekte,
## Wesen werden über ihre uid gefunden.
static func _part(g: Game, x):
return x if x is Cards.Card else g.find_creature(x.uid)
## Führt eine Aktion des Originals auf der Kopie aus. ## Führt eine Aktion des Originals auf der Kopie aus.
static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void: static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
var p2: Game.Player = g2.players[game.current_idx] var p2: Game.Player = g2.players[game.current_idx]
@@ -135,7 +161,7 @@ static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
"reveal": "reveal":
g2.reveal(p2, g2.find_creature(action.creature.uid)) g2.reveal(p2, g2.find_creature(action.creature.uid))
"fuse": "fuse":
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid)) g2.fuse(p2, _part(g2, action.creature), _part(g2, action.other))
_: _:
g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target)) g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target))
@@ -150,9 +176,9 @@ static func map_target(g2: Game, game: Game, t):
static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary: static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary:
var out := action.duplicate() var out := action.duplicate()
if action.has("creature"): if action.has("creature"):
out["creature"] = game.find_creature(action.creature.uid) out["creature"] = _part(game, action.creature)
if action.has("other"): if action.has("other"):
out["other"] = game.find_creature(action.other.uid) out["other"] = _part(game, action.other)
if action.get("target") != null: if action.get("target") != null:
out["target"] = map_target(game, copy, action.target) out["target"] = map_target(game, copy, action.target)
return out return out
@@ -211,7 +237,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
static func _kills(attacker, defender) -> bool: static func _kills(attacker, defender) -> bool:
if defender is Game.Player: if defender is Game.Player:
return attacker.power() >= defender.life return attacker.power() >= defender.life
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health return attacker.power() + Game.matchup_bonus(attacker, defender) >= defender.health
static func _value(c) -> int: static func _value(c) -> int:
@@ -234,8 +260,14 @@ static func _best(list: Array, key: Callable):
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary: static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if game.winner != "" or game.enemies_of(p).is_empty(): if game.winner != "" or game.enemies_of(p).is_empty():
return {"type": "end"} return {"type": "end"}
# Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen # Gierig: ein Wesen mit höchstens halbem Leben heilen (mit der billigsten Handkarte
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen. # verschmelzen, wenn das Ergebnis bezahlbar bleibt), dann das teuerste Spielbare legen (bei
# vollem Feld die zwei schwächsten Wesen verschmelzen), dann aufdecken und angreifen, dann ziehen.
var cheap := _cheapest(p)
if not cheap.is_empty():
for cr in p.board:
if cr.health * 2 <= cr.max_health and game.can_fuse(p, cr, cheap[0])[0] and Game.fusion_result(p, cr, cheap[0]).cost <= p.energy:
return {"type": "fuse", "creature": cr, "other": cheap[0]}
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0]) var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
if not playable.is_empty(): if not playable.is_empty():
Game.shuffle(playable, game.rng) Game.shuffle(playable, game.rng)
@@ -244,7 +276,7 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
if c.cost > card.cost: if c.cost > card.cost:
card = c card = c
return {"type": "play", "card": card, "face_down": false} return {"type": "play", "card": card, "face_down": false}
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy - 1): if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy):
var weakest: Array = p.board.duplicate() var weakest: Array = p.board.duplicate()
weakest.sort_custom(func(a, b): return _value(a) < _value(b)) weakest.sort_custom(func(a, b): return _value(a) < _value(b))
if game.can_fuse(p, weakest[0], weakest[1])[0]: if game.can_fuse(p, weakest[0], weakest[1])[0]:

View File

@@ -2,14 +2,24 @@ class_name Cards
## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool. ## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool.
const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"] const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"]
# Neutral ist kein Element: nie im Vorteil oder Nachteil, zählt nicht für Paare und Zähler.
# Verschmolzen mit einem Element nimmt das Wesen im schlechten Matchup die neutrale Seite.
const NEUTRAL := "Neutral"
const NEUTRAL_TEXT := "Nie im Vorteil oder Nachteil"
const ELEMENT_COLORS := { const ELEMENT_COLORS := {
"Feuer": Color("be3c23"), "Feuer": Color("be3c23"),
"Wasser": Color("2d64be"), "Wasser": Color("2d64be"),
"Erde": Color("73552d"), "Erde": Color("73552d"),
"Luft": Color("78aac8"), "Luft": Color("78aac8"),
"Neutral": Color("6e7382"),
} }
# Jede Karte hat genau ein Element. Kombos sind die Namen verschmolzener Wesen. # Jede Karte hat genau ein Element (oder ist neutral). Kombos sind die Namen verschmolzener Wesen.
const COMBOS := { const COMBOS := {
"Neutral+Neutral": "Veteran",
"Feuer+Neutral": "Feuerritter",
"Wasser+Neutral": "Wasserritter",
"Erde+Neutral": "Erdritter",
"Luft+Neutral": "Luftritter",
"Feuer+Feuer": "Inferno", "Feuer+Feuer": "Inferno",
"Wasser+Wasser": "Tiefsee", "Wasser+Wasser": "Tiefsee",
"Erde+Erde": "Berg", "Erde+Erde": "Berg",
@@ -47,9 +57,15 @@ const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regenerat
const DATA_PATH := "res://data/karten.json" const DATA_PATH := "res://data/karten.json"
## Reihenfolge der Elemente in Namen und Darstellung: wie ELEMENTS, Neutral zuletzt.
static func element_order(e: String) -> int:
var i := ELEMENTS.find(e)
return i if i >= 0 else ELEMENTS.size()
static func combo_key(elements: Array) -> String: static func combo_key(elements: Array) -> String:
var sorted := elements.duplicate() var sorted := elements.duplicate()
sorted.sort_custom(func(a, b): return ELEMENTS.find(a) < ELEMENTS.find(b)) sorted.sort_custom(func(a, b): return element_order(a) < element_order(b))
return "+".join(sorted) return "+".join(sorted)
@@ -98,11 +114,10 @@ static func cost_of(attack: int, health: int) -> int:
return mini(10, (2 * attack + health) / 3) return mini(10, (2 * attack + health) / 3)
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1), ## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter ## Summe plus eins. Ein starker und ein schwacher Partner ergeben also weniger als der starke.
## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
static func fusion_value(a: int, b: int) -> int: static func fusion_value(a: int, b: int) -> int:
return maxi(a, b) + maxi(1, ceili(mini(a, b) / 3.0)) return ceili((a + b) / 2.0) + 1
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt). ## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
@@ -110,18 +125,18 @@ static func fusion_name(elements: Array) -> String:
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]]) return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
## Karte des verschmolzenen Wesens: Elemente und Eigenschaften beider Karten, Werte nach ## Verschmelzung zweier Karten: Elemente (sortiert) und Eigenschaften beider, Angriff und Leben
## fusion_value (oder die übergebenen), eine Verschmelzung mehr als der erfahrenere Elternteil. ## nach fusion_value aus den Grundwerten, Kosten daraus wie bei jeder Karte; dann die Stufe level
static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card: ## mit ihrem +1/+1 (ohne Angabe die höhere der Eltern). Das Bild trägt der stärkere Elternteil
if attack < 0: ## (Angriff plus Leben).
attack = fusion_value(a.attack, b.attack) static func fused(a: Card, b: Card, level: int = -1) -> Card:
if health < 0: var attack := fusion_value(a.base_attack(), b.base_attack())
health = fusion_value(a.health, b.health) var health := fusion_value(a.base_health(), b.base_health())
var els := [] var els := []
for e in a.elements + b.elements: for e in a.elements + b.elements:
if not (e in els): if not (e in els):
els.append(e) els.append(e)
els.sort_custom(func(x, y): return ELEMENTS.find(x) < ELEMENTS.find(y)) els.sort_custom(func(x, y): return element_order(x) < element_order(y))
var c := Card.new() var c := Card.new()
c.id = "fus_%s_%s" % [a.id, b.id] c.id = "fus_%s_%s" % [a.id, b.id]
c.name = fusion_name(els) c.name = fusion_name(els)
@@ -132,8 +147,9 @@ static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
for k in KEYWORDS: for k in KEYWORDS:
if k in a.keywords or k in b.keywords: if k in a.keywords or k in b.keywords:
c.keywords.append(k) c.keywords.append(k)
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt var strong := a if a.attack + a.health >= b.attack + b.health else b
return c c.art = strong.art_card().id
return c.leveled(maxi(level, maxi(maxi(a.look, b.look), 0)))
## Platzhalter für ein verdecktes gegnerisches Wesen: unbekanntes Element, geschätzte Werte. ## Platzhalter für ein verdecktes gegnerisches Wesen: unbekanntes Element, geschätzte Werte.
@@ -159,6 +175,7 @@ class Card extends RefCounted:
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten) var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten)
var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit) var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit)
var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool
var art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil)
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health} var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
@@ -166,10 +183,24 @@ class Card extends RefCounted:
d["keywords"] = keywords d["keywords"] = keywords
if look >= 0: if look >= 0:
d["look"] = look d["look"] = look
if art != "":
d["art"] = art
if cost != Cards.cost_of(attack, health): if cost != Cards.cost_of(attack, health):
d["cost"] = cost d["cost"] = cost
return d return d
## Wert der Karte: was sie über ihre Manakosten hinaus bringt, mal 100. Grundkarten liegen
## bei 100, jede Seltenheitsstufe gibt 100 dazu (Ultimate Rare 500); Fusionen aus ihren Werten.
func value() -> int:
return (Cards.cost_of(attack, health) - cost + 1) * 100
## Grundwerte ohne den Stufenbonus.
func base_attack() -> int:
return attack - maxi(look, 0)
func base_health() -> int:
return health - maxi(look, 0)
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks). ## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
func rarity_in(looks: Dictionary) -> String: func rarity_in(looks: Dictionary) -> String:
return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))] return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))]
@@ -185,8 +216,13 @@ class Card extends RefCounted:
c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health))) c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health)))
c.keywords = Array(d.get("keywords", [])) c.keywords = Array(d.get("keywords", []))
c.look = int(d.get("look", -1)) c.look = int(d.get("look", -1))
c.art = str(d.get("art", ""))
return c return c
## Karte, deren Bild gezeichnet wird: bei Verschmelzungen der stärkere Elternteil, sonst sie selbst.
func art_card() -> Card:
return Cards.by_id(art) if art != "" and Cards._by_id.has(art) else self
## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool. ## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool.
func copy() -> Card: func copy() -> Card:
return Card.from_dict(to_dict()) return Card.from_dict(to_dict())
@@ -212,14 +248,19 @@ class Card extends RefCounted:
func combo() -> String: func combo() -> String:
return Cards.combo_name(elements) return Cards.combo_name(elements)
## Kartentext: die festen Eigenschaften. func is_neutral() -> bool:
return Cards.NEUTRAL in elements
## Kartentext: Neutral und die festen Eigenschaften.
func text() -> String: func text() -> String:
if keywords.is_empty():
return ""
var kws: Array[String] = [] var kws: Array[String] = []
if is_neutral():
kws.append(tr(Cards.NEUTRAL))
for k in Cards.KEYWORDS: for k in Cards.KEYWORDS:
if k in keywords: if k in keywords:
kws.append(tr(k)) kws.append(tr(k))
if kws.is_empty():
return ""
return ", ".join(kws) + "." return ", ".join(kws) + "."

View File

@@ -40,11 +40,15 @@ static func start_deck(element: String) -> Array:
return out return out
## Füllt deck aus pool (Karten-IDs) bis limit auf, die teuersten zuerst; nichts doppelt. ## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der
static func fill(deck: Array, pool: Array, limit: int) -> Array: ## Stufe aus levels: ID -> Stufe), bei gleichem Wert die teuersten; nichts doppelt.
static func fill(deck: Array, pool: Array, limit: int, levels: Dictionary = {}) -> Array:
var out := deck.duplicate() var out := deck.duplicate()
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id)) 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): return a.cost > b.cost if a.cost != b.cost else a.id < b.id) 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: for c in cards:
if out.size() >= limit: if out.size() >= limit:
break break
@@ -52,5 +56,16 @@ static func fill(deck: Array, pool: Array, limit: int) -> Array:
return out return out
## Stärke eines Stapels: der mittlere Kartenwert (Card.value) in den Stufen aus levels,
## 100 für lauter Grundkarten bis 500 für lauter Ultimate Rare; leer: 0.
static func strength(deck: Array, levels: Dictionary) -> int:
if deck.is_empty():
return 0
var sum := 0
for id in deck:
sum += Cards.by_id(id).leveled(int(levels.get(id, 0))).value()
return roundi(float(sum) / deck.size())
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player: static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
return Game.Player.new("Gegner", false, life) return Game.Player.new("Gegner", false, life)

View File

@@ -4,14 +4,13 @@ extends RefCounted
## Elementboni und Verschmelzen. ## Elementboni und Verschmelzen.
const MAX_BOARD := 5 const MAX_BOARD := 5
const MAX_HAND := 5 const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele) const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele)
const DECK_MAX := 60 # und höchstens so viele const DECK_MAX := 60 # und höchstens so viele
const START_LIFE := 30 const START_LIFE := 30
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
const ELEMENT_COUNT := 5 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements const ELEMENT_COUNT := 3 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
# Merkmale der Kampagnen-Bosse (Player.perks). # Merkmale der Kampagnen-Bosse (Player.perks).
@@ -154,23 +153,11 @@ class Player extends RefCounted:
func alive() -> bool: func alive() -> bool:
return life > 0 return life > 0
## Eine Karte vom eigenen Stapel als Exemplar in der eigenen Stufe; null, wenn er leer ist. ## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe; null, wenn er leer ist.
## Mit max_cost kommt die oberste bezahlbare, zu teure wandern nach unten (Starthand). func draw_from_deck() -> Cards.Card:
func draw_from_deck(max_cost: int = 99) -> Cards.Card: if deck.is_empty():
var skipped := []
var id := ""
while not deck.is_empty():
var top: String = deck.pop_back()
if Cards.by_id(top).cost <= max_cost:
id = top
break
skipped.append(top)
if id == "" and not skipped.is_empty(): # nichts Bezahlbares: dann eben die oberste
id = skipped.pop_front()
for sk in skipped:
deck.insert(0, sk)
if id == "":
return null return null
var id: String = deck.pop_back()
return Cards.by_id(id).leveled(int(looks.get(id, 0))) return Cards.by_id(id).leveled(int(looks.get(id, 0)))
## Offene eigene Wesen mit diesem Element. ## Offene eigene Wesen mit diesem Element.
@@ -295,8 +282,8 @@ func masked_clone(viewer: Player) -> Game:
# --- Zugablauf --------------------------------------------------------------- # --- Zugablauf ---------------------------------------------------------------
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind fünf im ## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind die
## ersten Zug bezahlbare Karten daraus. ## obersten fünf Karten daraus, wie gemischt.
func start(fixed_order: bool = false) -> void: func start(fixed_order: bool = false) -> void:
if not fixed_order: if not fixed_order:
shuffle(players, rng) # zufällige Zugreihenfolge shuffle(players, rng) # zufällige Zugreihenfolge
@@ -307,7 +294,7 @@ func start(fixed_order: bool = false) -> void:
p.deck = DeckGen.random_deck(rng) p.deck = DeckGen.random_deck(rng)
shuffle(p.deck, rng) shuffle(p.deck, rng)
for k in START_HAND: for k in START_HAND:
var c := p.draw_from_deck(START_MANA) var c := p.draw_from_deck()
if c != null: if c != null:
p.hand.append(c) p.hand.append(c)
turn = 1 turn = 1
@@ -324,8 +311,6 @@ func start(fixed_order: bool = false) -> void:
func can_draw(p: Player) -> Array: func can_draw(p: Player) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if p.hand.size() >= MAX_HAND:
return [false, "Deine Hand ist voll"]
if p.deck.is_empty(): if p.deck.is_empty():
return [false, "Dein Stapel ist leer"] return [false, "Dein Stapel ist leer"]
if p.energy < draw_cost(p): if p.energy < draw_cost(p):
@@ -497,75 +482,51 @@ static func look_of(p: Player, c: Cards.Card) -> int:
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0)) return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
## Kosten: die Hälfte der Manasumme beider Karten, aufgerundet. ## a und b: zwei verschiedene eigene Karten, auf der Hand oder als Wesen auf dem Brett, auch
static func fusion_cost(a, b) -> int: ## gemischt. Verschmelzen kostet kein Mana.
return ceili((card_of(a).cost + card_of(b).cost) / 2.0)
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
func can_fuse(p: Player, a, b) -> Array: func can_fuse(p: Player, a, b) -> Array:
if winner != "" or p != current(): if winner != "" or p != current():
return [false, "Nicht dein Zug"] return [false, "Nicht dein Zug"]
if a == null or b == null or a == b: if a == null or b == null or a == b:
return [false, "Zwei verschiedene Karten wählen"] return [false, "Zwei verschiedene Karten wählen"]
if a is Creature and b is Creature: for x in [a, b]:
if not (a in p.board) or not (b in p.board): if not ((x in p.board) if x is Creature else (x in p.hand)):
return [false, "Nur eigene Wesen"] return [false, "Nur eigene Karten"]
elif a is Creature or b is Creature:
return [false, "Nur Hand mit Hand oder Brett mit Brett"]
elif not (a in p.hand) or not (b in p.hand):
return [false, "Nicht auf der Hand"]
if p.energy < fusion_cost(a, b):
return [false, "Nicht genug Mana"]
return [true, ""] return [true, ""]
## Verschmilzt zwei Handkarten zu einer oder zwei eigene Wesen zu einem offenen: Elemente und ## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
## Eigenschaften beider, Werte nach Cards.fusion_value, Kosten folgen den Werten. static func fusion_result(p: Player, a, b) -> Cards.Card:
return Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b))))
## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue
## Karte ist unverletzt und wird wie jede andere ausgespielt. Sie landet am Platz der vorderen
## Handkarte, sonst hinten.
func fuse(p: Player, a, b) -> String: func fuse(p: Player, a, b) -> String:
var ok := can_fuse(p, a, b) var ok := can_fuse(p, a, b)
if not ok[0]: if not ok[0]:
return ok[1] return ok[1]
p.energy -= fusion_cost(a, b) var card := fusion_result(p, a, b)
if a is Creature: var at := p.hand.size()
return _fuse_board(p, a, b) var gone := []
var card := Cards.fused(a, b) for x in [a, b]:
card.look = maxi(look_of(p, a), look_of(p, b)) if x is Creature:
var at := p.hand.find(a) p.board.erase(x)
p.hand.erase(a) gone.append(x.uid)
p.hand.erase(b) else:
at = mini(at, p.hand.find(x))
p.hand.erase(x)
p.hand.insert(mini(at, p.hand.size()), card) p.hand.insert(mini(at, p.hand.size()), card)
say("%s und %s verschmelzen auf der Hand zu %s (%d/%d)." % [a.name, b.name, card.name, card.attack, card.health]) say("%s und %s verschmelzen zu %s (%d/%d) auf der Hand." % [a.name, b.name, card.name, card.attack, card.health])
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": []}) emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": gone})
return ""
## Auf dem Brett zählen die Werte der Wesen (ohne Elementbonus, den bekommt das neue selbst);
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
var cr := Creature.new(card, p, next_uid)
next_uid += 1
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
cr.ready = a.ready and b.ready
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
var at := p.board.find(a)
p.board.erase(a)
p.board.erase(b)
p.board.insert(mini(at, p.board.size()), cr)
say("%s und %s verschmelzen zu %s (%d/%d)." % [a.name, b.name, cr.name, cr.attack, cr.health])
emit({"type": "fuse", "player": players.find(p), "uid": cr.uid, "gone": [a.uid, b.uid]})
apply_auras()
cleanup()
check_end()
return "" return ""
# --- Elementboni --------------------------------------------------------------- # --- Elementboni ---------------------------------------------------------------
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente zählen alle. ## +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: static func aura_of(elements: Array, played: Dictionary) -> int:
var a := 0 var a := 0
for e in elements: for e in elements:
@@ -648,12 +609,18 @@ func attack(cr: Creature, target) -> String:
return "" return ""
## Elementvorteil beim Zuschlagen und beim Konter: +1 im Vorteil, -1 im Nachteil, ## Elementvorteil beim Zuschlagen und beim Konter: im Vorteil zählt der Stapelbonus des Ziels
## gegen Helden gibt es keinen. ## nicht (so viel Schaden mehr), im Nachteil der eigene nicht (so viel weniger). Gegen Helden
## gibt es keinen.
static func matchup_bonus(source: Creature, target) -> int: static func matchup_bonus(source: Creature, target) -> int:
if not (target is Creature): if not (target is Creature):
return 0 return 0
return Cards.matchup(source.card.elements, target.card.elements) var m := Cards.matchup(source.card.elements, target.card.elements)
if m > 0:
return target.aura
if m < 0:
return -source.aura
return 0
func damage(target, amount: int) -> void: func damage(target, amount: int) -> void:

View File

@@ -145,7 +145,7 @@ func looks() -> Dictionary:
## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt, ## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt,
## falls er zu klein ist (Karten können durch Zerlegen wegfallen). ## falls er zu klein ist (Karten können durch Zerlegen wegfallen).
func deck_cards() -> Array: func deck_cards() -> Array:
return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN) return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN, collection)
## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung. ## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung.

View File

@@ -139,14 +139,31 @@ func pop_in(tile: CreatureView) -> void:
func fade_out(tile: CreatureView) -> void: func fade_out(tile: CreatureView) -> void:
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE fade_view(tile)
## Blendet eine Ansicht aus und entfernt sie (Wesen, die sterben; Handkarten, die verschmelzen).
func fade_view(view: Control) -> void:
view.mouse_filter = Control.MOUSE_FILTER_IGNORE
if not enabled: if not enabled:
tile.queue_free() view.queue_free()
return return
var tw := tile.create_tween().set_parallel(true) var tw := view.create_tween().set_parallel(true)
tw.tween_property(tile, "modulate:a", 0.0, 0.3) tw.tween_property(view, "modulate:a", 0.0, 0.3)
tw.tween_property(tile, "scale", Vector2(0.6, 0.6), 0.3) tw.tween_property(view, "scale", Vector2(0.6, 0.6), 0.3)
tw.chain().tween_callback(tile.queue_free) tw.chain().tween_callback(view.queue_free)
## Eine Ansicht gleitet von from an ihren Platz (die neue Handkarte nach dem Verschmelzen).
func glide(view: Control, from: Rect2) -> void:
if not enabled:
return
var to := view.position
view.position = from.position
view.scale = from.size / view.size
var tw := view.create_tween().set_parallel(true)
tw.tween_property(view, "position", to, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.tween_property(view, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
func move_to(tile: CreatureView, pos: Vector2) -> void: func move_to(tile: CreatureView, pos: Vector2) -> void:

View File

@@ -8,7 +8,7 @@ const B_UNDO := Rect2(20, 10, 210, 32) # links oben: Rückgängig, d
const B_SKIP := Rect2(20, 48, 210, 32) const B_SKIP := Rect2(20, 48, 210, 32)
const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben
const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter
const OPP_HAND_Y := 11 const OPP_HAND_Y := 22 # Bogen hebt die äußeren Rückseiten um bis zu 12 Pixel
const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett
const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau
@@ -16,11 +16,11 @@ const MSG := Rect2(20, 650, 210, 80) # Meldungen
const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel
const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc) const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
const OWN_X := 345 const OWN_X := 295
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld const HAND_W := 130 # Handkarten so groß wie die Wesen auf dem Feld
const HAND_H := 140 const HAND_H := 165
const HAND_GAP := 10 const HAND_GAP := 6
const HAND_Y := 610 const HAND_Y := 605
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen) const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
const LOG_PANEL := Rect2(240, 60, 800, 560) const LOG_PANEL := Rect2(240, 60, 800, 560)
const PREVIEW_SIZE := Vector2(170, 238) const PREVIEW_SIZE := Vector2(170, 238)
@@ -29,19 +29,23 @@ const CENTER_W := 800
static func own_slot(j: int) -> Rect2: static func own_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 440, 110, 140) return Rect2(OWN_X + j * 140, 410, HAND_W, HAND_H)
## Brett des Gegners über seiner Elementleiste. ## Brett des Gegners über seiner Elementleiste.
static func opp_slot(j: int) -> Rect2: static func opp_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 220, 110, 140) return Rect2(OWN_X + j * 140, 205, HAND_W, HAND_H)
## Hand: bis zu fünf Karten unten, mittig; den Bogen legt hand_arc dazu. ## Hand: alle Karten unten, mittig; passen sie nicht nebeneinander, überlappen sie sich
## gleichmäßig. Den Bogen legt hand_arc dazu.
static func hand_rect(count: int, i: int) -> Rect2: static func hand_rect(count: int, i: int) -> Rect2:
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP var step: float = HAND_W + HAND_GAP
if count > 1:
step = minf(step, float(CENTER_W - HAND_W) / (count - 1))
var total: float = HAND_W + (count - 1) * step
var x := CENTER_X + (CENTER_W - total) / 2 var x := CENTER_X + (CENTER_W - total) / 2
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H) return Rect2(x + i * step, HAND_Y, HAND_W, HAND_H)
## Hand des Gegners oben, gleich groß, nur die Rückseiten. ## Hand des Gegners oben, gleich groß, nur die Rückseiten.

View File

@@ -5,8 +5,9 @@ extends Control
## die Leertaste oder der Rechtsklick ins Leere. ## die Leertaste oder der Rechtsklick ins Leere.
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder ## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen. ## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit ## Verschmelzen: eine Handkarte oder ein eigenes Wesen auf ein anderes ziehen und loslassen; das
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“. ## Ergebnis (eine neue Handkarte) erscheint über dem Ziel, darunter Bestätigen oder Abbrechen.
## Mit Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
## Modi: Einzelspieler und Kampagne. ## Modi: Einzelspieler und Kampagne.
var app var app
@@ -17,15 +18,13 @@ var fx: BattleFx
var selected = null # Game.Creature var selected = null # Game.Creature
var hover_target = null # fremdes Wesen unter der Maus: Vorschau des Angriffs mit dem gewählten Wesen var hover_target = null # fremdes Wesen unter der Maus: Vorschau des Angriffs mit dem gewählten Wesen
var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
var hold_time := 1.0 # so lange still über dem Ziel halten, dann verschmelzen die beiden var fuse_pending = null # [a, b] nach dem Loslassen über einem Ziel, wartet auf Bestätigung
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
var press_view: Control = null var press_view: Control = null
var press_pos := Vector2.ZERO var press_pos := Vector2.ZERO
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
var drag_target_view: Control = null var drag_target_view: Control = null
var hold_t := 0.0
var hold_pos := Vector2.ZERO
var msg_t := 0.0 var msg_t := 0.0
var ai_delay := 0.75 var ai_delay := 0.75
var ai_timer := 0.0 var ai_timer := 0.0
@@ -77,6 +76,7 @@ var info_label: Label # Vorschau: Elementbonus, Merkmale
var menu_layer: Control # Kartenmenü (Rechtsklick) var menu_layer: Control # Kartenmenü (Rechtsklick)
var menu_target = null # Karte oder Wesen, für das das Menü offen ist var menu_target = null # Karte oder Wesen, für das das Menü offen ist
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
var hand_arrivals := {} # Karte -> Rect2: neue Handkarte, die aus der Verschmelzung in die Hand gleitet
var banner_turn := -1 # Zug, für den die Zugansage schon lief var banner_turn := -1 # Zug, für den die Zugansage schon lief
var overlay: Control var overlay: Control
var result_label: Label var result_label: Label
@@ -408,15 +408,6 @@ func _process(delta: float) -> void:
undo_t -= delta undo_t -= delta
if undo_t <= 0.0: if undo_t <= 0.0:
clear_undo() clear_undo()
if drag_ghost != null and drag_target != null:
_set_hold(hold_t + delta)
if hold_t >= hold_time:
var a = press_target
var b = drag_target
press_target = null
_end_drag()
do_fuse(a, b)
after_action()
if game.winner != "": if game.winner != "":
if not ended: if not ended:
ended = true ended = true
@@ -487,7 +478,7 @@ func cancel() -> void:
_end_drag() _end_drag()
# --- Ziehen mit der Maus: Klick beim Loslassen, Verschmelzen nach einer Sekunde über dem Ziel --- # --- Ziehen mit der Maus: Klick beim Loslassen; losgelassen über einem Ziel fragt nach dem Verschmelzen ---
func _press(obj, view: Control, pos: Vector2) -> void: func _press(obj, view: Control, pos: Vector2) -> void:
_end_drag() _end_drag()
@@ -505,22 +496,27 @@ func _drag_motion(pos: Vector2) -> void:
_start_drag(pos) _start_drag(pos)
drag_ghost.position = pos - drag_ghost.size / 2 drag_ghost.position = pos - drag_ghost.size / 2
var t = _fusion_target_at(pos) var t = _fusion_target_at(pos)
if t != drag_target or pos.distance_to(hold_pos) > 6.0: if t != drag_target:
_set_hold(0.0) _mark_target(false)
drag_target = t drag_target = t
drag_target_view = _view_of(t) drag_target_view = _view_of(t)
hold_pos = pos _mark_target(true)
## Loslassen ohne Ziehen ist der Klick. ## Loslassen ohne Ziehen ist der Klick; über einem Ziel öffnet es die Verschmelzungsfrage.
func _release() -> void: func _release() -> void:
var obj = press_target var obj = press_target
var target = drag_target
var target_rect := _rect_of(drag_target_view) if drag_target_view != null else Rect2()
var dragged := drag_ghost != null var dragged := drag_ghost != null
press_target = null press_target = null
_end_drag() _end_drag()
if obj == null: if obj == null:
return return
if dragged: if dragged:
if target != null:
open_fuse_confirm(obj, target, target_rect)
else:
refresh() refresh()
elif obj is Cards.Card: elif obj is Cards.Card:
on_click("hand", obj) on_click("hand", obj)
@@ -549,24 +545,27 @@ func _end_drag() -> void:
drag_ghost = null drag_ghost = null
if is_instance_valid(press_view): if is_instance_valid(press_view):
press_view.modulate.a = 1.0 press_view.modulate.a = 1.0
_set_hold(0.0) _mark_target(false)
drag_target = null drag_target = null
drag_target_view = null drag_target_view = null
func _set_hold(v: float) -> void: func _mark_target(on: bool) -> void:
hold_t = v
if is_instance_valid(drag_target_view): if is_instance_valid(drag_target_view):
drag_target_view.hold = clampf(v / hold_time, 0.0, 1.0) if v > 0.0 else 0.0 drag_target_view.drop = on
drag_target_view.queue_redraw() drag_target_view.queue_redraw()
## Karte oder Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt. ## Platz einer Ansicht: Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten.
func _rect_of(v: Control) -> Rect2:
return Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect()
## Handkarte oder eigenes Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
func _fusion_target_at(pos: Vector2): func _fusion_target_at(pos: Vector2):
for v in (hand_views if press_target is Cards.Card else own_views): for v in hand_views + own_views:
var obj = v.card if v is CardView else v.creature var obj = v.card if v is CardView else v.creature
var r: Rect2 = Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect() # Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten if obj != press_target and _rect_of(v).has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
if obj != press_target and r.has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
return obj return obj
return null return null
@@ -581,9 +580,9 @@ func _view_of(obj) -> Control:
return null return null
## Mögliche Partner für „Verschmelzen mit …“: andere Handkarten bzw. andere eigene Wesen. ## Mögliche Partner für „Verschmelzen mit …“: alle anderen Handkarten und eigenen Wesen.
func fusion_partners(target) -> Array: func fusion_partners(target) -> Array:
var pool: Array = perspective.hand if target is Cards.Card else perspective.board var pool: Array = perspective.hand + perspective.board
return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0]) return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0])
@@ -675,6 +674,37 @@ func close_menu() -> void:
menu_layer.visible = false menu_layer.visible = false
Style.clear(menu_layer) Style.clear(menu_layer)
menu_target = null menu_target = null
fuse_pending = null
## Nach dem Loslassen über einem Ziel: die neue Handkarte schwebt über dem Ziel, darunter
## Bestätigen und Abbrechen; beide Ausgangskarten bleiben sichtbar. Klick daneben oder Esc bricht ab.
func open_fuse_confirm(a, b, target: Rect2) -> void:
cancel()
Style.clear(menu_layer)
fuse_pending = [a, b]
var card := Game.fusion_result(perspective, a, b)
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H)
var bw := 150.0
var x := clampf(target.get_center().x - size.x / 2, 10 + bw - size.x / 2, 1270 - bw - size.x / 2)
var y := maxf(10.0, target.position.y - size.y - 48)
var v := CardView.new(card, size, false, Cards.RARITIES[card.look])
v.position = Vector2(x, y)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
menu_layer.add_child(v)
var by := y + size.y + 6
var bx := x + size.x / 2 - bw - 3
var yes := Style.button(menu_layer, tr("Verschmelzen"), Rect2(bx, by, bw, 36), func():
var pair: Array = fuse_pending
close_menu()
do_fuse(pair[0], pair[1], Rect2(v.position, v.size))
after_action(), Color("285a3c"))
yes.add_theme_font_size_override("font_size", 15)
var no := Style.button(menu_layer, tr("Abbrechen"), Rect2(bx + bw + 6, by, bw, 36), func():
close_menu()
refresh())
no.add_theme_font_size_override("font_size", 15)
menu_layer.visible = true
## Abwerfen: eine Handkarte gegen Mana loswerden. ## Abwerfen: eine Handkarte gegen Mana loswerden.
@@ -696,13 +726,26 @@ func do_draw() -> void:
fx.sfx("buy") fx.sfx("buy")
## Verschmilzt zwei Handkarten oder zwei eigene Wesen (siehe Game.fuse). ## Verschmilzt zwei eigene Karten zur neuen Handkarte (siehe Game.fuse). from: von dort gleitet
func do_fuse(a, b) -> void: ## die neue Karte in die Hand, Eltern auf der Hand blenden aus (Wesen tut das der Abgleich).
func do_fuse(a, b, from: Rect2 = Rect2()) -> void:
cancel() cancel()
clear_undo() clear_undo()
var before: Array = perspective.hand.duplicate()
var err := game.fuse(perspective, a, b) var err := game.fuse(perspective, a, b)
if err != "": if err != "":
say(tr(err)) say(tr(err))
return
if from.size.x <= 0:
return
for x in [a, b]:
var pv := _view_of(x)
if pv is CardView:
pv.reparent(fx_layer)
fx.fade_view(pv)
for c in perspective.hand:
if not (c in before):
hand_arrivals[c] = from
func clear_undo() -> void: func clear_undo() -> void:
@@ -747,7 +790,8 @@ func on_click(kind: String, obj) -> void:
var first = fuse_first var first = fuse_first
fuse_first = null fuse_first = null
if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]: if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]:
do_fuse(first, obj) var tv := _view_of(obj)
do_fuse(first, obj, _rect_of(tv) if tv != null else Rect2())
else: else:
say(tr("Abgebrochen")) say(tr("Abgebrochen"))
after_action() after_action()
@@ -846,6 +890,8 @@ func creature_info(cr: Game.Creature) -> String:
var perk := cr.power() - cr.attack - cr.aura var perk := cr.power() - cr.attack - cr.aura
if perk > 0: if perk > 0:
parts.append(tr("Merkmal +%d") % perk) parts.append(tr("Merkmal +%d") % perk)
if cr.card.is_neutral():
parts.append(tr(Cards.NEUTRAL_TEXT))
for k in cr.keywords(): for k in cr.keywords():
if not (k in cr.card.keywords): if not (k in cr.card.keywords):
parts.append(tr("%s durch das Element") % tr(k)) parts.append(tr("%s durch das Element") % tr(k))
@@ -1089,6 +1135,9 @@ func refresh() -> void:
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i) var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
v.rotation_degrees = arc[0] v.rotation_degrees = arc[0]
v.position.y += arc[1] v.position.y += arc[1]
if hand_arrivals.has(card):
fx.glide(v, hand_arrivals[card])
hand_arrivals.erase(card)
v.lift = mine v.lift = mine
v.bonus = g.forecast(h, card) v.bonus = g.forecast(h, card)
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]: if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:

View File

@@ -5,9 +5,9 @@ extends ScrollContainer
signal card_clicked(card: Cards.Card) signal card_clicked(card: Cards.Card)
const COLS := 6 const COLS := 6
const CW := 112 const CW := 118
const CH := 157 const CH := 165
const GAP := 14 const GAP := 10
var grid: GridContainer var grid: GridContainer

View File

@@ -1,10 +1,10 @@
class_name CardView class_name CardView
extends Control extends Control
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau). ## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Eigenschaften als ## Oben Kosten (links) und Name, in der Mitte das Bild im Kreis, unten Angriff (links) und
## Symbole, Angriff und Leben als Kreise unten. Das erste Element färbt den Hintergrund, weitere ## Leben (rechts). Die Elemente färben den Hintergrund (verschmolzen: ein Streifen je Element),
## Elemente zeigt ein Medaillon als Tortenstücke hinter dem Bild; die Seltenheit steckt in Name, ## das Bild trägt bei Verschmelzungen der stärkere Elternteil; die Seltenheit steckt in Name,
## Rahmen, Glanz und Edelstein (siehe Style). Sehr flache Karten zeigen nur das Nötigste. ## Bild, Werten und Rahmen (siehe Style).
signal clicked(view: CardView) signal clicked(view: CardView)
signal pressed(view: CardView, pos: Vector2) # nur mit drag_enabled: der Kampf macht daraus Klick oder Ziehen signal pressed(view: CardView, pos: Vector2) # nur mit drag_enabled: der Kampf macht daraus Klick oder Ziehen
@@ -17,7 +17,7 @@ var dim := false
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme
var lift := false # hebt sich beim Zeigen an (Hand) var lift := false # hebt sich beim Zeigen an (Hand)
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1) var drop := false # Ziel der gerade gezogenen Karte
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick. ## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
@@ -75,120 +75,35 @@ func _draw() -> void:
if rarity != "Common": if rarity != "Common":
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6) Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
Style.fill_elements(self, rect, card.elements, 8) Style.fill_elements(self, rect, card.elements, 8)
if size.y < 90: var big := size.x >= 180
_draw_compact(rect, frame, seed) var mid := size.x >= 140
else: var f_name := 17 if big else (14 if mid else 11)
_draw_full(rect, frame, seed) var f_stat := 20 if big else (17 if mid else 14)
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
var pad := 8.0 if mid else 6.0
var shiny := Style.rarity_stats_shiny(rarity)
# Oben: Kosten links, daneben der Name; das Namensfeld ist genau so hoch wie der Kristall
var top_h := r_cost * 2 + 4
draw_rect(Rect2(2, 2, size.x - 4, top_h), Color(0.05, 0.05, 0.08, 0.6))
Style.card_name(self, card.title(), f_name, rarity, Rect2(r_cost * 2 + 10, 2, size.x - r_cost * 2 - pad - 10, top_h), seed)
# Mitte: das Bild im Kreis, so groß wie der Platz bis zu den Werten erlaubt
var pic_top := top_h + 4
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)
# 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 dim: if dim:
draw_rect(rect, Color(0, 0, 0, 0.47)) draw_rect(rect, Color(0, 0, 0, 0.47))
if has_focus(): if has_focus():
Style.border(self, rect, Style.GOLD, 4, 8) Style.border(self, rect, Style.GOLD, 4, 8)
if hold > 0.0: if drop:
var c := size / 2 Style.border(self, rect, Style.GOLD, 5, 8)
var r := minf(size.x, size.y) * 0.3
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
## Volle Karte: Sammlung, Booster, Vorschau.
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
var big := size.x >= 180
var mid := size.x >= 140
var f_name := 17 if big else (14 if mid else 11)
var f_text := 13 if big else (11 if mid else 10)
var f_stat := 20 if big else (17 if mid else 14)
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
var icon := 11.0 if big else (9.0 if mid else 7.0)
# Bild: das Elementsymbol füllt den oberen Teil
var band_top: float = size.y * 0.36
var gc := Vector2(size.x / 2, band_top * 0.56)
var gs := minf(size.x * 0.62, band_top * 0.95)
if card.is_combo():
Style.element_medallion(self, gc, gs * 0.6, card.elements)
Style.glyph(self, card, gc, gs, 0.4, Style.rarity_relief(rarity))
Style.rarity_sheen(self, rect, rarity, seed, Rect2(0, 0, size.x, band_top))
# Namensband über die volle Breite
var band_h: float = Style.fs(f_name) + 10
var band := Rect2(0, band_top, size.x, band_h)
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.rarity_name_color(rarity, seed), Vector2(size.x / 2, band.get_center().y), "center", true)
var y := band.end.y + 4
# Kombo eines verschmolzenen Wesens
if card.is_combo():
Style.text(self, tr(card.combo()), f_text, Color("e6e6e6"), Vector2(size.x / 2, y), "midtop")
y += Style.fs(f_text) + 4
# Schlüsselwörter als Symbole, auf großen Karten mit Wort
var kws: Array[String] = []
for k in Cards.KEYWORDS:
if k in card.keywords:
kws.append(k)
if not kws.is_empty():
var with_words := big
var widths: Array[float] = []
var total := 0.0
for k in kws:
var w: float = icon * 2 + 6 + (Style.FONT.get_string_size(tr(k), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(f_text)).x + 8 if with_words else 0)
widths.append(w)
total += w + 6
if with_words and total > size.x - 12:
with_words = false
total = kws.size() * (icon * 2 + 12)
var x := (size.x - total + 6) / 2
for i in kws.size():
var k := kws[i]
Style.keyword_icon(self, k, Vector2(x + icon, y + icon + 2), icon)
if with_words:
Style.text(self, tr(k), f_text, Style.TEXT, Vector2(x + icon * 2 + 6, y + icon + 2), "midleft")
x += widths[i] + 6
else:
x += icon * 2 + 12
y += icon * 2 + 8
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
var box := Rect2()
var fx := card.text()
var box_bottom: float = size.y - r_stat * 2 - 4
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
var tsize := Style.fs(f_text)
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
var ty := box.position.y + 4
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, ty + Style.FONT.get_ascent(tsize)),
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
Style.border(self, rect, frame, 3 if big else 2, 8)
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
Style.rarity_sparkle(self, rect, rarity, seed, box)
## Flache Handkachel: Kosten, Name, Symbole, Werte.
func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
Style.rarity_sheen(self, rect, rarity, seed)
Style.border(self, rect, frame, 2, 8)
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
var name_w: float = size.x - 30 - (12 if rarity != "Common" else 0)
var nf := Style.fit_size(card.title(), true, 12, name_w)
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
var kws: Array[String] = []
for k in Cards.KEYWORDS:
if k in card.keywords:
kws.append(k)
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
for k in kws:
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
x += 18
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.LIFE, 12)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
Style.rarity_sparkle(self, rect, rarity, seed)

View File

@@ -21,13 +21,14 @@ var b_craft: Button
func _ready() -> void: func _ready() -> void:
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind("")) Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
for i in Cards.ELEMENTS.size(): var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
var e: String = Cards.ELEMENTS[i] 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)) 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) fb.add_theme_font_size_override("font_size", 14)
search_box = LineEdit.new() search_box = LineEdit.new()
search_box.position = Vector2(444, 86) search_box.position = Vector2(526, 86)
search_box.size = Vector2(220, 34) search_box.size = Vector2(190, 34)
search_box.placeholder_text = "Suche" search_box.placeholder_text = "Suche"
search_box.text_changed.connect(func(t: String): search_box.text_changed.connect(func(t: String):
search = t search = t

View File

@@ -12,11 +12,11 @@ var width := 2
var flash := false # kurz rot: wurde gerade angegriffen var flash := false # kurz rot: wurde gerade angegriffen
var slot: Vector2 # Zielposition im Raster var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren) var busy := false # wird gerade animiert (kein Neupositionieren)
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (+1/-1); die Werte zeigen dann, was ihn erwartet var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (Game.matchup_bonus); die Werte zeigen dann, was ihn erwartet
var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus
var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1) var drop := false # Ziel der gerade gezogenen Karte
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void: func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
@@ -54,7 +54,7 @@ func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
if secret: if secret:
_draw_back(rect) _draw_back(rect)
_draw_hold() _draw_drop()
return return
var small := size.x < 100 var small := size.x < 100
var rar := rarity() var rar := rarity()
@@ -62,44 +62,41 @@ func _draw() -> void:
if rar != "Common": if rar != "Common":
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6) Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6)
Style.fill_elements(self, rect, creature.card.elements, 6) Style.fill_elements(self, rect, creature.card.elements, 6)
var gc := Vector2(size.x / 2, size.y * 0.5)
if creature.card.is_combo():
Style.element_medallion(self, gc, size.x * 0.42, creature.card.elements)
Style.glyph(self, creature.card, gc, size.x * 0.75, 0.3, Style.rarity_relief(rar))
Style.rarity_sheen(self, rect, rar, seed, rect)
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed) var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
if flash: if flash:
bcol = Style.RED bcol = Style.RED
Style.border(self, rect, bcol, 4 if flash else width, 6) var shiny := Style.rarity_stats_shiny(rar)
var fs := 11 if small else 13 # Oben: Kosten links, daneben der Name; das Namensfeld ist genau so hoch wie der Kristall
var name_w: float = size.x - 6 var fs := 12 if small else 13
var nf := Style.fit_size(creature.card.title(), true, fs, name_w) var r_cost := 10.0 if small else 11.0
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7)) var top_h := r_cost * 2 + 4
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.rarity_name_color(rar, seed), Vector2(3 + name_w / 2, 6), "midtop", true) draw_rect(Rect2(2, 2, size.x - 4, top_h), Color(0.05, 0.05, 0.08, 0.6))
# Elementbonus als ein Abzeichen rechts unter dem Namen; die Symbole rücken dafür nach links Style.card_name(self, creature.card.title(), fs, rar, Rect2(r_cost * 2 + 10, 2, size.x - r_cost * 2 - 16, top_h), seed)
var badge := "" # Mitte: das Bild im Kreis; unten Angriff links, Leben rechts
var bw := 0.0 var r_stat := 12.0 if small else 14.0
if creature.aura != 0: var pic_top := top_h + 4
badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura var pic_bottom := size.y - r_stat - 4
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8 var r_pic := minf(size.x * 0.42, (pic_bottom - pic_top) / 2)
var by := Style.fs(fs) + 13 Style.picture(self, creature.card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rar, seed)
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75)) # Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt), rechts der Elementbonus
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, by + 8), "center", true) var icon := 6.0 if small else 7.0
# Eigenschaften als Symbole in der Farbe des Elements, das sie gibt var y := top_h + 6 + icon
var icon := 7.0 if small else 9.0 for k in creature.keywords():
var kws := creature.keywords()
var avail := size.x - (bw + 8 if badge != "" else 0.0)
var x := avail / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
var y := Style.fs(fs) + 14 + icon
for k in kws:
var col := Color("f2f2f6") var col := Color("f2f2f6")
for e in creature.card.elements: for e in creature.card.elements:
if Cards.ELEMENT_KEYWORD.get(e, "") == k: if Cards.ELEMENT_KEYWORD.get(e, "") == k:
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45) col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col) Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col)
x += icon * 2 + 6 y += icon * 2 + 6
_draw_stats(15 if small else 17, 12.0 if small else 14.0) if creature.aura != 0:
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed) var badge := "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
var bw := Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
draw_rect(Rect2(size.x - bw - 4, top_h + 5, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, top_h + 13), "center", true)
_draw_stats(15 if small else 17, r_stat, shiny)
Style.card_sheen(self, rect, rar, seed)
Style.border(self, rect, bcol, 4 if flash else width, 6)
Style.cost_crystal(self, Vector2(r_cost + 4, r_cost + 4), r_cost, creature.card.cost, 13, shiny)
if creature.hidden: if creature.hidden:
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55)) draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3) Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
@@ -107,17 +104,13 @@ func _draw() -> void:
draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8)) draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8))
Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3) Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3)
Style.rarity_sparkle(self, rect, rar, seed) Style.rarity_sparkle(self, rect, rar, seed)
_draw_hold() _draw_drop()
## Ring, der sich füllt, solange eine Karte zum Verschmelzen darüber gehalten wird. ## Goldener Rahmen, solange eine Karte zum Verschmelzen darüber schwebt.
func _draw_hold() -> void: func _draw_drop() -> void:
if hold <= 0.0: if drop:
return Style.border(self, Rect2(Vector2.ZERO, size), Style.GOLD, 5, 6)
var c := size / 2
var r := minf(size.x, size.y) * 0.3
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
## Verdecktes fremdes Wesen: ein Kartenrücken. ## Verdecktes fremdes Wesen: ein Kartenrücken.
@@ -143,11 +136,11 @@ func shown_stats() -> Array:
## Angriff gelb, Leben blau; die Zahl wird grün, wenn sie für den aktuellen Angriff steigt, rot, wenn sie sinkt. ## Angriff gelb, Leben blau; die Zahl wird grün, wenn sie für den aktuellen Angriff steigt, rot, wenn sie sinkt.
func _draw_stats(f: int, r: float) -> void: func _draw_stats(f: int, r: float, shiny: bool) -> void:
var shown := shown_stats() var shown := shown_stats()
var atk: int = shown[0] var atk: int = shown[0]
var hp: int = shown[1] var hp: int = shown[1]
var atk_text := Style.GREEN if atk > creature.power() else (Style.RED if atk < creature.power() else Color.WHITE) var atk_text := Style.GREEN if atk > creature.power() else (Style.RED if atk < creature.power() else Color.WHITE)
var hp_text := Style.GREEN if hp > creature.health else (Style.RED if hp < creature.health else Color.WHITE) var hp_text := Style.GREEN if hp > creature.health else (Style.RED if hp < creature.health else Color.WHITE)
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(atk), Style.AMBER, f, atk_text) Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(atk), Style.AMBER, f, atk_text, shiny)
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_text) Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_text, shiny)

View File

@@ -2,20 +2,23 @@ class_name DeckScreen
extends Control extends Control
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den ## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält ## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
## 40 bis 60 Karten; „Auffüllen“ nimmt die teuersten eigenen Karten des gewählten Filters. ## 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).
var app var app
var filter := "" var filter := ""
var grid: CardGrid var grid: CardGrid
var count_label: Label var count_label: Label
var strength_label: Label
var msg_label: Label var msg_label: Label
func _ready() -> void: func _ready() -> void:
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new())) Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind("")) Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
for i in Cards.ELEMENTS.size(): var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
var e: String = Cards.ELEMENTS[i] 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)) 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) fb.add_theme_font_size_override("font_size", 14)
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650)) grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
@@ -23,11 +26,13 @@ func _ready() -> void:
add_child(grid) add_child(grid)
Style.panel(self, Rect2(820, 86, 420, 696)) Style.panel(self, Rect2(820, 86, 420, 696))
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE) count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
var hint := Style.label(self, "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.", 15, Style.MUTED, Rect2(840, 160, 380, 60), "center") strength_label = Style.chip(self, "", Rect2(860, 144, 340, 36), Style.GOLD)
hint.autowrap_mode = TextServer.AUTOWRAP_WORD var hint := Style.label(self, "", 15, Style.MUTED, Rect2(840, 188, 380, 66), "center")
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up) hint.autowrap_mode = TextServer.AUTOWRAP_WORD # vor dem Text, sonst wächst die Breite auf eine Zeile
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28")) hint.text = "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters."
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center") 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")
refresh() refresh()
@@ -50,13 +55,13 @@ func toggle(card: Cards.Card) -> void:
refresh() refresh()
## Füllt mit den teuersten eigenen Karten des gewählten Filters bis zum Höchstwert auf. ## Füllt mit den wertvollsten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
func fill_up() -> void: func fill_up() -> void:
var p: Profile = app.profile var p: Profile = app.profile
var pool: Array = p.collection.keys() var pool: Array = p.collection.keys()
if filter != "": if filter != "":
pool = pool.filter(func(id): return filter in Cards.by_id(id).elements) pool = pool.filter(func(id): return filter in Cards.by_id(id).elements)
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX) p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX, p.collection)
app.save() app.save()
refresh() refresh()
@@ -67,7 +72,7 @@ func clear_deck() -> void:
refresh() refresh()
## Eigene Karten, die im Stapel zuerst, sonst nach Kosten. ## Eigene Karten, die im Stapel zuerst, dann höchste Stufen vorn, dann nach Kosten.
func cards() -> Array: func cards() -> Array:
var p: Profile = app.profile var p: Profile = app.profile
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id)) var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
@@ -78,6 +83,8 @@ func cards() -> Array:
var ib: bool = b.id in p.deck var ib: bool = b.id in p.deck
if ia != ib: if ia != ib:
return ia return ia
if p.level(a.id) != p.level(b.id):
return p.level(a.id) > p.level(b.id)
return a.cost < b.cost if a.cost != b.cost else a.id < b.id) return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
return out return out
@@ -86,6 +93,7 @@ func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX] count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE) count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
strength_label.text = tr("Stärke %d") % DeckGen.strength(p.deck, p.collection)
var shown := cards() var shown := cards()
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look]) grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
var views := grid.views() var views := grid.views()

View File

@@ -2,8 +2,8 @@ class_name ElementView
extends Control extends Control
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen ## 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 ## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
## offenem Wesen, beide: Effekt aktiv), die Zahl der ausgespielten Karten und ab ## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten
## Game.ELEMENT_COUNT der Bonus +1/+1. ## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
const ROW := 32.0 const ROW := 32.0
@@ -15,20 +15,24 @@ func _init(rect: Rect2) -> void:
position = rect.position position = rect.position
size = rect.size size = rect.size
mouse_filter = Control.MOUSE_FILTER_IGNORE mouse_filter = Control.MOUSE_FILTER_IGNORE
for e in Cards.ELEMENTS: for e in all():
own[e] = 0 own[e] = 0
played[e] = 0 played[e] = 0
static func all() -> Array:
return Cards.ELEMENTS + [Cards.NEUTRAL]
func update_from(_g: Game, me: Game.Player) -> void: func update_from(_g: Game, me: Game.Player) -> void:
for e in Cards.ELEMENTS: for e in all():
own[e] = me.element_count(e) if me != null else 0 own[e] = me.element_count(e) if me != null else 0
played[e] = int(me.played.get(e, 0)) if me != null else 0 played[e] = int(me.played.get(e, 0)) if me != null else 0
queue_redraw() queue_redraw()
func paired(e: String) -> bool: func paired(e: String) -> bool:
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR return Cards.ELEMENT_KEYWORD.has(e) and int(own.get(e, 0)) >= Game.ELEMENT_PAIR
func bonus(e: String) -> int: func bonus(e: String) -> int:
@@ -37,7 +41,7 @@ func bonus(e: String) -> int:
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten). ## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
func shown() -> Array: func shown() -> Array:
return Cards.ELEMENTS.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 or int(played.get(e, 0)) > 0)
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus. ## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
@@ -62,6 +66,7 @@ func _draw() -> void:
var px := 10.0 var px := 10.0
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active) Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
px = 76 px = 76
if Cards.ELEMENT_KEYWORD.has(e):
for k in Game.ELEMENT_PAIR: for k in Game.ELEMENT_PAIR:
var filled := k < int(own.get(e, 0)) var filled := k < int(own.get(e, 0))
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3)) Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))

View File

@@ -91,6 +91,7 @@ func _map_gamepad() -> void:
if not InputMap.action_get_events(action).any(func(e): return e is InputEventJoypadButton): if not InputMap.action_get_events(action).any(func(e): return e is InputEventJoypadButton):
var ev := InputEventJoypadButton.new() var ev := InputEventJoypadButton.new()
ev.button_index = pair[1] ev.button_index = pair[1]
ev.device = -1 # jedes Gamepad
InputMap.action_add_event(action, ev) InputMap.action_add_event(action, ev)

View File

@@ -19,7 +19,7 @@ func _ready() -> void:
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost) sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
for k in 3: for k in 3:
var c: Cards.Card = Cards.by_id(sample[k * 7]) var c: Cards.Card = Cards.by_id(sample[k * 7])
var v := CardView.new(c, Vector2(110, 140), false, "Common") var v := CardView.new(c, Vector2(120, 152), false, "Common")
v.position = Vector2(x + 20 + k * 40, 210 + k * 14) v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
v.rotation_degrees = -6 + k * 6 v.rotation_degrees = -6 + k * 6
v.mouse_filter = Control.MOUSE_FILTER_IGNORE v.mouse_filter = Control.MOUSE_FILTER_IGNORE

View File

@@ -151,7 +151,9 @@ static func header(parent: Node, title: String, on_back: Callable) -> Button:
## Kleine Kachel mit Text, etwa für Münzen und Staub. ## Kleine Kachel mit Text, etwa für Münzen und Staub.
static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label: static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label:
panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1) panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1)
return label(parent, text, 17, color, rect, "center", true) var l := label(parent, text, 17, color, rect, "center", true)
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
return l
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme). ## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
@@ -309,13 +311,14 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
# --- Seltenheit als Aussehen ------------------------------------------------------ # --- Seltenheit als Aussehen ------------------------------------------------------
# Common matt; Rare silberner Name und Rahmen mit stehendem Glanzstreifen; Super Rare Holo-Bild # Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild (wandernder Regenbogen im
# (wandernder Regenbogenschimmer); Ultra Rare goldener Name und Rahmen plus Holo-Bild; # Bildkreis); Ultra Rare goldener Name und Holo-Bild; Ultimate Rare goldener Name, goldenes
# Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln, schillernde Farben. # Bild, glänzende Werte, Schimmer über die ganze Karte, Funkeln und schillernder Rahmen.
# Rahmen und Schein tragen ab Rare die Stufenfarbe.
## Ab Super Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu. ## Ab Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
static func rarity_animated(rar: String) -> bool: static func rarity_animated(rar: String) -> bool:
return animations and Cards.RARITIES.find(rar) >= 2 return animations and Cards.RARITIES.find(rar) >= 1
## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben. ## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben.
@@ -330,56 +333,138 @@ static func rarity_frame(rar: String, seed: int = 0) -> Color:
return Color("3a3e4c") if rar == "Common" else rarity_color(rar, seed) return Color("3a3e4c") if rar == "Common" else rarity_color(rar, seed)
## Farbe des Kartennamens: Rare silbern, Ultra Rare golden, Ultimate Rare schillernd, sonst weiß. ## Farbe des Kartennamens: Rare silbern, Ultra und Ultimate Rare golden, sonst weiß.
static func rarity_name_color(rar: String, seed: int = 0) -> Color: static func rarity_name_color(rar: String) -> Color:
match rar: match rar:
"Rare", "Ultra Rare", "Ultimate Rare": "Rare":
return lighter(rarity_color(rar, seed), 0.25) return Cards.RARITY_COLORS["Rare"]
"Ultra Rare", "Ultimate Rare":
return Cards.RARITY_COLORS["Ultra Rare"]
return TEXT return TEXT
## Der Name glänzt bei Rare (silbern), Ultra und Ultimate Rare (golden).
static func rarity_name_shiny(rar: String) -> bool:
return rar in ["Rare", "Ultra Rare", "Ultimate Rare"]
## Ultimate Rare: auch Kosten, Angriff und Leben glänzen.
static func rarity_stats_shiny(rar: String) -> bool:
return rar == "Ultimate Rare"
## Ultimate Rare zeigt das Bild als Relief (Licht- und Schattenkante). ## Ultimate Rare zeigt das Bild als Relief (Licht- und Schattenkante).
static func rarity_relief(rar: String) -> bool: static func rarity_relief(rar: String) -> bool:
return Cards.RARITIES.find(rar) >= 4 return rar == "Ultimate Rare"
## Glanz wie auf einer Folienkarte. Rare: ein stehender weißer Streifen über die Karte. ## Kartenname links in rect, immer eine Zeile, nie abgeschnitten: die Schrift schrumpft bis
## Ab Super Rare: Holo, ein wandernder Regenbogenschimmer über das Bild (picture); Ultimate Rare ## drei Punkte unter die Vorgabe, danach wird der Text schmaler gezeichnet (wie bei langen
## zusätzlich schwach über die ganze Karte. ## Yu-Gi-Oh-Namen).
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int, picture: Rect2 = Rect2()) -> void: static func card_name(ci: CanvasItem, s: String, size_in: int, rar: String, rect: Rect2, seed: int) -> void:
var lvl := Cards.RARITIES.find(rar) var fsize := fit_size(s, true, size_in, rect.size.x, maxi(8, size_in - 3))
if lvl < 1: var w := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(fsize)).x
return var squeeze := minf(1.0, rect.size.x / maxf(w, 1.0))
if picture.size.x <= 0: var y := rect.position.y + (rect.size.y - FONT_BOLD.get_height(fs(fsize))) / 2
picture = rect ci.draw_set_transform(Vector2(rect.position.x, y), 0.0, Vector2(squeeze, 1.0))
if lvl == 1: if rarity_name_shiny(rar):
_sheen_bands(ci, rect, (seed % 100) / 100.0, [[1.0, Color(1, 1, 1, 0.07)], [0.55, Color(1, 1, 1, 0.09)], [0.22, Color(1, 1, 1, 0.13)]]) shiny_text(ci, s, fsize, rarity_name_color(rar), Vector2.ZERO, "topleft", seed)
else:
text(ci, s, fsize, rarity_name_color(rar), Vector2.ZERO, "topleft", true)
ci.draw_set_transform(Vector2.ZERO)
## Metallisch glänzender fetter Text: dunkler Schatten, Grundfarbe, darüber Buchstabe für
## Buchstabe ein heller Streifen, der mit Animation über den Text läuft.
static func shiny_text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String, seed: int) -> void:
var size := fs(size_in)
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
var x := pos.x
var y := pos.y
match anchor:
"center":
x -= dim.x / 2
y -= dim.y / 2
"midtop":
x -= dim.x / 2
"midleft":
y -= dim.y / 2
var phase := (seed % 100) / 100.0
if animations:
phase = fmod(Time.get_ticks_msec() / 1800.0 + phase, 1.0)
var at := Vector2(x, y + FONT_BOLD.get_ascent(size))
ci.draw_string(FONT_BOLD, at + Vector2(1, 1), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, Color(0, 0, 0, 0.55))
var cx := x
for i in s.length():
var ch := s[i]
var w := FONT_BOLD.get_string_size(ch, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x
var d := absf(fmod((cx + w / 2 - x) / maxf(dim.x, 1.0) - phase * 1.6 + 0.3 + 2.0, 1.0) - 0.5) * 2.0 # 0 am Streifen
var k := clampf(1.0 - d * 2.4, 0.0, 1.0)
ci.draw_string(FONT_BOLD, Vector2(cx, at.y), ch, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color.lerp(Color.WHITE, 0.1 + 0.75 * k))
cx += w
## Glanz wie auf einer Folienkarte, auf clip beschnitten. lvl 2 und 3: Holo, ein wandernder
## Regenbogenschimmer; lvl 4: goldene Bänder.
static func sheen(ci: CanvasItem, clip: PackedVector2Array, bounds: Rect2, seed: int, gold: bool = false) -> void:
var phase := (seed % 100) / 100.0
if animations:
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
var layers := []
for i in 4:
var col := Color(1.0, 0.86, 0.45, 0.2 - i * 0.03) if gold else Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)
layers.append([1.0 - i * 0.22, col])
_sheen_bands(ci, clip, bounds, phase, layers)
## Ultimate Rare: schwacher schillernder Schimmer über die ganze Karte.
static func card_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
if rar != "Ultimate Rare":
return return
var phase := (seed % 100) / 100.0 var phase := (seed % 100) / 100.0
if animations: if animations:
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0) phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
var holo := []
for i in 4:
holo.append([1.0 - i * 0.22, Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)])
_sheen_bands(ci, picture, phase, holo)
if lvl >= 4:
var soft := [] var soft := []
for i in 3: for i in 3:
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)]) soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
_sheen_bands(ci, rect, fmod(phase + 0.5, 1.0), soft) _sheen_bands(ci, rect_poly(rect), rect, fmod(phase + 0.5, 1.0), soft)
## Schräge Streifen über rect, auf rect beschnitten. layers: [Breitenanteil, Farbe]. static func rect_poly(rect: Rect2) -> PackedVector2Array:
static func _sheen_bands(ci: CanvasItem, rect: Rect2, phase: float, layers: Array) -> void: return PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
var w := rect.size.x * 0.34
var k := rect.size.y * 0.75 # Schräge static func circle_poly(c: Vector2, r: float, steps: int = 40) -> PackedVector2Array:
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase var pts := PackedVector2Array()
for i in steps:
var a := TAU * i / steps
pts.append(c + Vector2(cos(a), sin(a)) * r)
return pts
## Rechteck mit runden Ecken als Polygon (für Beschnitt).
static func rounded_poly(rect: Rect2, radius: float) -> PackedVector2Array:
var pts := PackedVector2Array()
var r := minf(radius, minf(rect.size.x, rect.size.y) / 2)
var corners := [[rect.end - Vector2(r, r), 0.0], [Vector2(rect.position.x + r, rect.end.y - r), PI / 2],
[rect.position + Vector2(r, r), PI], [Vector2(rect.end.x - r, rect.position.y + r), PI * 1.5]]
for corner in corners:
for k in 7:
var a: float = corner[1] + PI / 2 * k / 6
pts.append(corner[0] + Vector2(cos(a), sin(a)) * r)
return pts
## Schräge Streifen über bounds, auf clip beschnitten. layers: [Breitenanteil, Farbe].
static func _sheen_bands(ci: CanvasItem, clip: PackedVector2Array, bounds: Rect2, phase: float, layers: Array) -> void:
var w := bounds.size.x * 0.34
var k := bounds.size.y * 0.75 # Schräge
var x0 := bounds.position.x - w + (bounds.size.x + w + k) * phase
for layer in layers: for layer in layers:
var lw: float = w * layer[0] var lw: float = w * layer[0]
var lx := x0 + (w - lw) / 2 var lx := x0 + (w - lw) / 2
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)]) var band := PackedVector2Array([Vector2(lx, bounds.position.y), Vector2(lx + lw, bounds.position.y), Vector2(lx + lw - k, bounds.end.y), Vector2(lx - k, bounds.end.y)])
for poly in Geometry2D.intersect_polygons(band, frame): for poly in Geometry2D.intersect_polygons(band, clip):
if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen
ci.draw_colored_polygon(poly, layer[1]) ci.draw_colored_polygon(poly, layer[1])
@@ -394,16 +479,15 @@ static func _area(poly: PackedVector2Array) -> float:
## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen. ## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
## avoid: Bereich, der frei bleibt (der Kartentext). static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void: if rar != "Ultimate Rare":
if Cards.RARITIES.find(rar) < 4:
return return
var t := Time.get_ticks_msec() / 1000.0 var t := Time.get_ticks_msec() / 1000.0
for i in 9: for i in 9:
var h := absi(hash(Vector2i(seed, i))) var h := absi(hash(Vector2i(seed, i)))
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16)) var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0 var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
if tw < 0.15 or avoid.has_point(p): if tw < 0.15:
continue continue
var s := 2.0 + 4.0 * tw var s := 2.0 + 4.0 * tw
var col := Color(1, 1, 1, 0.3 + 0.65 * tw) var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
@@ -422,65 +506,70 @@ static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spr
ci.draw_style_box(sb, rect) ci.draw_style_box(sb, rect)
## Edelstein in der Farbe der Stufe (ab Rare). ## Hintergrund in den Farben der Elemente: ein Element füllt die Karte, verschmolzene Wesen
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void: ## tragen je Element einen senkrechten Streifen.
if rar == "Common":
return
var col := rarity_color(rar, seed)
var diamond := func(h: float, k: Color) -> void:
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.72, 0), c + Vector2(0, h), c + Vector2(-h * 0.72, 0)]), k)
diamond.call(s + 2, Color("14141c"))
diamond.call(s, col)
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
## Hintergrund in der Farbe des ersten Elements; weitere Elemente zeigt element_medallion.
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void: static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect) ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
if elements.size() < 2:
return
## Medaillon eines verschmolzenen Wesens: ein Kreis hinter dem Bild, als Tortenstücke in den var clip := rounded_poly(rect, radius)
## Farben seiner Elemente, mit dunklem Rand. var w := rect.size.x / elements.size()
static func element_medallion(ci: CanvasItem, c: Vector2, r: float, elements: Array) -> void: for i in range(1, elements.size()):
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75)) var strip := Rect2(rect.position.x + w * i, rect.position.y, w, rect.size.y)
var n := elements.size() for poly in Geometry2D.intersect_polygons(rect_poly(strip), clip):
for i in n: if poly.size() >= 3:
var from := -PI / 2 + TAU * i / n ci.draw_colored_polygon(poly, Cards.ELEMENT_COLORS[elements[i]])
var to := -PI / 2 + TAU * (i + 1) / n ci.draw_line(strip.position + Vector2(0, 2), Vector2(strip.position.x, strip.end.y - 2), Color(0.05, 0.05, 0.08, 0.5), 1.5)
var pts := PackedVector2Array([c])
var steps := 24
for k in steps + 1:
var ang := lerpf(from, to, float(k) / steps)
pts.append(c + Vector2(cos(ang), sin(ang)) * r)
ci.draw_colored_polygon(pts, Cards.ELEMENT_COLORS[elements[i]])
for i in n:
var ang := -PI / 2 + TAU * i / n
ci.draw_line(c, c + Vector2(cos(ang), sin(ang)) * r, Color(0.05, 0.05, 0.08, 0.75), 2.0)
# --- Kartenteile ------------------------------------------------------ # --- Kartenteile ------------------------------------------------------
## Kostenkristall: sechseckig, blau, Zahl in der Mitte. ## Bild der Karte: dunkler Kreis mit Ring in der Elementfarbe, darin das Symbol der Bildkarte
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void: ## (bei Verschmelzungen die des stärkeren Elternteils). Ab Super Rare glänzt der Kreis (Holo),
## bei Ultimate Rare golden und als Relief.
static func picture(ci: CanvasItem, card, c: Vector2, r: float, rar: String, seed: int) -> void:
var src = card.art_card()
var ring := lighter(Cards.ELEMENT_COLORS[src.elements[0]], 0.3)
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.8))
ci.draw_circle(c, r, Color(0.09, 0.09, 0.13, 0.9))
glyph(ci, src, c, r * 1.3, 0.95, rarity_relief(rar))
var lvl := Cards.RARITIES.find(rar)
if lvl >= 2:
sheen(ci, circle_poly(c, r), Rect2(c - Vector2(r, r), Vector2(r * 2, r * 2)), seed, lvl >= 4)
ci.draw_arc(c, r + 1.5, 0, TAU, 48, Color(1.0, 0.86, 0.45) if lvl >= 4 else ring, 3.0, true)
## Kostenkristall: sechseckig, blau, Zahl in der Mitte; shiny: goldene, glänzende Zahl.
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int, shiny: bool = false) -> void:
var pts := PackedVector2Array() var pts := PackedVector2Array()
var outer := PackedVector2Array() var outer := PackedVector2Array()
for i in 6: for i in 6:
var a := -PI / 2 + i * PI / 3 var a := -PI / 2 + i * PI / 3
pts.append(c + Vector2(cos(a), sin(a)) * r) pts.append(c + Vector2(cos(a), sin(a)) * r)
outer.append(c + Vector2(cos(a), sin(a)) * (r + 2)) outer.append(c + Vector2(cos(a), sin(a)) * (r + 2))
ci.draw_colored_polygon(outer, Color("14141c")) ci.draw_colored_polygon(outer, Color(1.0, 0.86, 0.45) if shiny else Color("14141c"))
ci.draw_colored_polygon(pts, MANA) ci.draw_colored_polygon(pts, MANA)
var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)]) var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)])
ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22)) ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22))
text_outlined(ci, str(cost), font, Color.WHITE, c, "center", 3) _stat_text(ci, str(cost), font, Color.WHITE, c, shiny)
## Wertkreis (Angriff gelb, Leben rot) mit Zahl. ## Wertkreis (Angriff gelb, Leben blau) mit Zahl; shiny: goldener Ring, glänzende Zahl.
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE) -> void: static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE, shiny: bool = false) -> void:
ci.draw_circle(c, r + 2, Color("14141c")) ci.draw_circle(c, r + 2, Color(1.0, 0.86, 0.45) if shiny else Color("14141c"))
ci.draw_circle(c, r, color) ci.draw_circle(c, r, color)
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16)) ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
text_outlined(ci, value, font, text_col, c, "center", 3) _stat_text(ci, value, font, text_col, c, shiny)
static func _stat_text(ci: CanvasItem, s: String, font: int, col: Color, c: Vector2, shiny: bool) -> void:
if shiny and col == Color.WHITE:
var size := fs(font)
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
ci.draw_string_outline(FONT_BOLD, c - dim / 2 + Vector2(0, FONT_BOLD.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, 3, Color(0.05, 0.05, 0.08, 0.95))
shiny_text(ci, s, font, Color(1.0, 0.86, 0.45), c, "center", int(c.x + c.y))
else:
text_outlined(ci, s, font, col, c, "center", 3)
## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil, ## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil,
@@ -568,6 +657,11 @@ static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: fl
for i in 2 + variant: for i in 2 + variant:
var r := h * (1.0 - i * 0.28) var r := h * (1.0 - i * 0.28)
ci.draw_arc(c + Vector2(i * size * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, size * 0.09)) ci.draw_arc(c + Vector2(i * size * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, size * 0.09))
_: # Neutral: ein schlichter Schild
var w := h * 0.8
var pts := PackedVector2Array([c + Vector2(-w, -h * 0.8), c + Vector2(w, -h * 0.8), c + Vector2(w, h * 0.1), c + Vector2(0, h), c + Vector2(-w, h * 0.1)])
ci.draw_polyline(pts + PackedVector2Array([pts[0]]), col, maxf(2, size * 0.09))
ci.draw_line(c + Vector2(0, -h * 0.5), c + Vector2(0, h * 0.6), col, maxf(2, size * 0.07))
static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void: static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:

View File

@@ -68,6 +68,7 @@ func _initialize() -> void:
test_elementboni() test_elementboni()
test_spott_und_regeneration() test_spott_und_regeneration()
test_verschmelzen() test_verschmelzen()
test_ki_verschmelzen()
test_limits() test_limits()
test_gleiche_regeln() test_gleiche_regeln()
test_zugreihenfolge() test_zugreihenfolge()
@@ -95,20 +96,21 @@ func _initialize() -> void:
quit(1 if failed > 0 else 0) quit(1 if failed > 0 else 0)
## 120 Karten: je Element alle Werte von 0/1 bis 5/5, die ID verrät die Werte. ## 150 Karten: je Element und für Neutral alle Werte von 0/1 bis 5/5, die ID verrät die Werte.
func test_kartenpool() -> void: func test_kartenpool() -> void:
check(Cards.all().size() == 120, "120 Karten") check(Cards.all().size() == 150, "150 Karten")
check(Cards.all().all(func(c): return c.elements.size() == 1 and c.elements[0] in Cards.ELEMENTS), "jede Karte hat genau ein Element") 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: 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() == 30, "30 Karten je Element: " + e)
var names := {} var names := {}
for c in Cards.all(): for c in Cards.all():
var parts: PackedStringArray = c.id.split("_") var parts: PackedStringArray = c.id.split("_")
check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 5 and c.health >= 1 and c.health <= 5, "ID passt zu den Werten: " + c.id) check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 5 and c.health >= 1 and c.health <= 5, "ID passt zu den Werten: " + c.id)
check(c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id) check(c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.keywords.is_empty() and c.text() == "" and c.name_en != "", "Karten kommen ohne Eigenschaften, mit englischem Namen: " + c.id) check(c.keywords.is_empty() and c.text() == ("Neutral." if c.is_neutral() else "") and c.name_en != "", "Karten kommen ohne Eigenschaften (außer Neutral), mit englischem Namen: " + c.id)
names[c.name] = true names[c.name] = true
check(names.size() == 120, "alle Namen verschieden") check(names.size() == 150, "alle Namen verschieden")
check(C("n_3_2").is_neutral() and C("n_3_2").text() == "Neutral." and not C("f_3_2").is_neutral(), "neutrale Karten tragen die Eigenschaft Neutral")
check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts") check(C("f_0_2").cost == 0 and C("f_2_1").cost == 1 and C("f_3_2").cost == 2 and C("f_5_5").cost == 5 and C("e_0_4").cost == 1, "Kostenbeispiele, 0/2 kostet nichts")
check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10") check(Cards.cost_of(20, 20) == 10, "Kosten enden bei 10")
check(C("f_3_2").name == "Brennender Kobold" and C("w_4_5").name == "Flutender Leviathan", "Namen aus Eigenschaft und Wesen") check(C("f_3_2").name == "Brennender Kobold" and C("w_4_5").name == "Flutender Leviathan", "Namen aus Eigenschaft und Wesen")
@@ -123,6 +125,7 @@ func test_kombos() -> void:
check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz") check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz")
check(not C("f_5_5").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen") check(not C("f_5_5").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen")
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre") check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
check(Cards.combo_name(["Neutral", "Feuer"]) == "Feuerritter" and Cards.combo_name(["Neutral", "Neutral"]) == "Veteran", "Neutral verschmolzen: Elementritter, zweimal neutral: Veteran")
## Mana: 1 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten. ## Mana: 1 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
@@ -144,7 +147,7 @@ func test_mana_und_kosten() -> void:
h.energy = 5 h.energy = 5
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_3_2").cost, "Ausspielen kostet die Kartenkosten") check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_3_2").cost, "Ausspielen kostet die Kartenkosten")
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy) check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 5 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren, mindestens 1") check(Cards.fusion_value(4, 2) == 4 and Cards.fusion_value(8, 1) == 6 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 3 and Cards.fusion_value(8, 8) == 9, "Fusionswert: aufgerundete halbe Summe plus eins")
## Stapel: 40 verschiedene Karten, zwölf davon im ersten Zug bezahlbar; Kampagnengegner bekommen ## Stapel: 40 verschiedene Karten, zwölf davon im ersten Zug bezahlbar; Kampagnengegner bekommen
@@ -172,10 +175,17 @@ func test_stapel() -> void:
var others := sd.filter(func(id): return Cards.by_id(id).elements[0] == o) 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(others.size() == 5 and others.all(func(id): return Cards.by_id(id).cost <= Game.START_MANA), "Startdeck %s: fünf billige %s" % [e, o])
var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3) var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3)
check(filled == ["f_0_1", "w_5_5", "e_0_4"], "Auffüllen: teuerste zuerst, dann nach ID, nichts doppelt, bis zur Grenze") 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")
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")
## Start: fünf im ersten Zug bezahlbare Karten vom eigenen Stapel. Ziehen bringt eine Karte; ## Start: die obersten fünf Karten vom eigenen Stapel, ungefiltert. Ziehen bringt eine Karte;
## die erste im Zug ist frei, dann 1, 2, 3 … Mana. ## die erste im Zug ist frei, dann 1, 2, 3 … Mana.
func test_ziehen() -> void: func test_ziehen() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
@@ -184,13 +194,12 @@ func test_ziehen() -> void:
for p in g.players: for p in g.players:
check(p.deck.size() == Game.DECK_MIN - Game.START_HAND, "jeder hat seinen Stapel, fünf Karten sind auf der Hand") check(p.deck.size() == Game.DECK_MIN - Game.START_HAND, "jeder hat seinen Stapel, fünf Karten sind auf der Hand")
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand") check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare") check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
var first: Game.Player = g.current() var first: Game.Player = g.current()
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit") for i in 8:
first.hand.pop_back() first.hand.append(first.draw_from_deck())
var deck_before := first.deck.size() var deck_before := first.deck.size()
check(g.draw_cost(first) == 0 and g.draw(first) == "" and first.energy == Game.START_MANA and first.deck.size() == deck_before - 1, "erstes Ziehen frei, eine Karte vom eigenen Stapel") check(g.draw_cost(first) == 0 and g.draw(first) == "" and first.hand.size() == Game.START_HAND + 9 and first.energy == Game.START_MANA and first.deck.size() == deck_before - 1, "erstes Ziehen frei, eine Karte vom eigenen Stapel, auch mit dreizehn auf der Hand")
first.hand.pop_back() first.hand.pop_back()
check(g.draw_cost(first) == 1 and g.draw(first) == "" and first.energy == 0, "zweites Ziehen kostet 1") check(g.draw_cost(first) == 1 and g.draw(first) == "" and first.energy == 0, "zweites Ziehen kostet 1")
first.hand.pop_back() first.hand.pop_back()
@@ -210,11 +219,11 @@ func test_ziehen() -> void:
g.end_turn() g.end_turn()
check(first.life == Game.START_LIFE - 3, "und dann 2, 3 …") check(first.life == Game.START_LIFE - 3, "und dann 2, 3 …")
# Der eigene Stapel aus dem Spielstand # Der eigene Stapel aus dem Spielstand
var ids := ["f_0_1", "f_0_2", "w_0_1", "w_0_2", "e_0_1", "e_0_2", "l_0_1", "l_0_2", "f_5_5", "w_5_5"] var ids := ["f_5_5", "w_5_5", "e_5_5", "l_5_5", "f_4_5", "w_4_5", "e_0_1", "e_0_2", "l_0_1", "l_0_2"]
var g2 := Game.new_battle(r, {"deck": ids}) var g2 := Game.new_battle(r, {"deck": ids})
var seen := g2.human.deck + g2.human.hand.map(func(c): return c.id) var seen := g2.human.deck + g2.human.hand.map(func(c): return c.id)
check(seen.size() == ids.size() and seen.all(func(id): return id in ids), "der Mensch spielt mit seinem Stapel") check(seen.size() == ids.size() and seen.all(func(id): return id in ids), "der Mensch spielt mit seinem Stapel")
check(g2.human.hand.all(func(c): return c.cost <= Game.START_MANA), "die teuren bleiben im Stapel") check(g2.human.hand.any(func(c): return c.cost > Game.START_MANA), "die Starthand ist ungefiltert: teure Karten kommen mit (eigenes Deck, eigene Verantwortung)")
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn. ## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
@@ -257,6 +266,10 @@ func test_elementvorteil() -> void:
check(Cards.matchup(["Erde"], dampf) == 0 and Cards.matchup(["Wasser"], dampf) == 0, "auch als Verteidiger zählt das bessere Element") check(Cards.matchup(["Erde"], dampf) == 0 and Cards.matchup(["Wasser"], dampf) == 0, "auch als Verteidiger zählt das bessere Element")
check(Cards.matchup(dampf, ["Wasser", "Erde"]) == -1 and Cards.matchup(["Wasser", "Erde"], dampf) == 1, "beide Elemente geschlagen: Nachteil") check(Cards.matchup(dampf, ["Wasser", "Erde"]) == -1 and Cards.matchup(["Wasser", "Erde"], dampf) == 1, "beide Elemente geschlagen: Nachteil")
check(Cards.matchup(dampf, []) == 0 and Cards.matchup([], dampf) == 0, "unbekannte Elemente: kein Vorteil") check(Cards.matchup(dampf, []) == 0 and Cards.matchup([], dampf) == 0, "unbekannte Elemente: kein Vorteil")
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
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
@@ -265,31 +278,42 @@ func test_elementvorteil() -> void:
var wolf := put(o, "f_3_2") # Feuer 3/2 var wolf := put(o, "f_3_2") # Feuer 3/2
var life: int = o.life var life: int = o.life
g.attack(tiefen, wolf) g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life and tiefen.health == 5 - 2, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter macht 1 weniger, nichts schlägt durch") 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 s3 := setup()
var w3 := put(s3[1], "f_3_2") var w3 := put(s3[1], "w_4_5")
var t3 := put(s3[2], "w_4_5") var f3 := put(s3[2], "f_3_2")
s3[0].attack(w3, t3) s3[2].played["Feuer"] = 6
check(t3.health == 5 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr") s3[0].apply_auras()
# 0 Angriff: nur mit Elementvorteil geht etwas check(f3.aura == 2 and f3.power() == 5 and f3.max_health == 4 and Game.matchup_bonus(w3, f3) == 2 and Game.matchup_bonus(f3, w3) == -2, "Feuer mit +2/+2: Wasser schlägt 2 mehr, Feuer 2 weniger")
s3[0].attack(w3, f3)
check(f3.health <= 0 and w3.health == 5 - 3, "Wasser trifft für 4 + 2 durch den Bonus, der Konter kommt ohne Bonus: 3")
# Im Nachteil fehlt der eigene Angriffsbonus; der Konter geht durch den eigenen Lebensbonus
var s4 := setup() var s4 := setup()
var funke := put(s4[1], "f_0_2") # Feuer 0/2 var f4 := put(s4[1], "f_3_2")
var hauch := put(s4[2], "l_0_2") # Luft 0/2 var w4 := put(s4[2], "w_4_5")
var wall := put(s4[2], "e_0_4") # Erde 0/4 s4[1].played["Feuer"] = 6
check(s4[0].can_attack(funke, wall)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff ohne Vorteil: kein Angriff") s4[0].apply_auras()
check(s4[0].attack(funke, hauch) == "" and hauch.health == 1 and funke.health == 2, "0 Angriff mit Vorteil: 1 Schaden, kein Konter") 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
var s5 := setup() var s5 := setup()
var hauch2 := put(s5[1], "l_0_2") var funke := put(s5[1], "f_0_2") # Feuer 0/2
var funke2 := put(s5[2], "f_0_2") var hauch := put(s5[2], "l_0_2") # Luft 0/2
check(s5[0].attack(hauch2, funke2) == "Leiser Bote würde keinen Schaden machen" and funke2.health == 2, "im Nachteil nichts") check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff, Ziel ohne Bonus: kein Angriff")
var wall2 := put(s5[2], "e_0_4") 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 var geist5 := put(s5[1], "w_2_2") # Wasser 2/2
check(s5[0].attack(geist5, wall2) == "" and wall2.health == 4 - 1 and geist5.health == 2 - 1, "Erdhüter kontert mit Elementvorteil für 1") check(s5[0].attack(geist5, wall) == "" and wall.health == 2 and geist5.health == 2, "Erdhüter ohne Bonus kontert nicht")
var s2 := setup() var s2 := setup()
var g2: Game = s2[0] var g2: Game = s2[0]
var geist := put(s2[1], "w_2_2") # Wasser 2/2 var geist := put(s2[1], "w_2_2")
s2[1].played["Wasser"] = 3
g2.apply_auras()
g2.attack(geist, s2[2]) g2.attack(geist, s2[2])
check(s2[2].life == 13, "gegen Helden gibt es keinen Vorteil") check(s2[2].life == 12, "gegen Helden gibt es keinen Vorteil, nur den eigenen Bonus")
## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt ## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt
@@ -321,7 +345,7 @@ func test_verdeckt() -> void:
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft ## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
## Flink, Wasser Regeneration, Feuer Doppelschlag); je fünf ausgespielte Karten eines Elements geben ## 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. ## den eigenen Wesen des Elements +1/+1. Jeder Spieler zählt für sich.
func test_elementboni() -> void: func test_elementboni() -> void:
var s := setup(["f_4_3", "f_4_3"]) var s := setup(["f_4_3", "f_4_3"])
@@ -336,29 +360,29 @@ func test_elementboni() -> void:
var hidden_fire := put(o, "f_4_3", true, true) var hidden_fire := put(o, "f_4_3", true, true)
g.apply_auras() g.apply_auras()
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar") check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
# Ausgespielte Karten zählen; je fünf gibt es +1/+1 für die eigenen Wesen des Elements # Ausgespielte Karten zählen; je drei gibt es +1/+1 für die eigenen Wesen des Elements
h.played["Feuer"] = 4 h.played["Feuer"] = 2
g.play_card(h, h.hand[0]) # Flammender Wolf: die fünfte Feuerkarte g.play_card(h, h.hand[0]) # Flammender Wolf: die dritte Feuerkarte
check(int(h.played["Feuer"]) == 5 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and h.board[2].aura == 1, "fünf Feuerkarten ausgespielt: +1/+1 für alle eigenen Feuerwesen") 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") check(bird.aura == 0 and bird.health == 1 and hidden_fire.aura == 0, "der Gegner zählt für sich: nichts")
g._flip(hidden_fire) g._flip(hidden_fire)
check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus") check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt
var secret: Game.Creature = h.board.back() var secret: Game.Creature = h.board.back()
check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 6, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen") check(secret.hidden and secret.aura == 0 and int(h.played["Feuer"]) == 4, "verdeckt gelegt zählt als ausgespielt, der Bonus kommt erst offen")
g._flip(secret) g._flip(secret)
check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus") check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus")
# Vorschau # Vorschau
h.played["Luft"] = 4 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 fünfte Luftkarte und bekäme +1/+1") 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 # Merkmal: doppelt
h.perks = ["element_doppelt"] h.perks = ["element_doppelt"]
g.apply_auras() g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "Elementbonus zählt doppelt") check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "Elementbonus zählt doppelt")
h.perks = [] h.perks = []
h.played["Feuer"] = 10 h.played["Feuer"] = 6
g.apply_auras() g.apply_auras()
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "zehn Feuerkarten: +2/+2") check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "sechs Feuerkarten: +2/+2")
fire[0].health = 1 fire[0].health = 1
h.played["Feuer"] = 0 h.played["Feuer"] = 0
g.apply_auras() g.apply_auras()
@@ -383,10 +407,21 @@ func test_elementboni() -> void:
g2.apply_auras() g2.apply_auras()
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte") check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare") check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
h2.played["Feuer"] = 5 h2.played["Feuer"] = 3
h2.played["Wasser"] = 5 h2.played["Wasser"] = 3
g2.apply_auras() g2.apply_auras()
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "fünf Feuer und fünf Wasser: Dampf bekommt +2/+2") check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
# Neutral: kein Paar, aber ein eigener Zähler
var s7 := setup(["n_2_2", "n_2_2"])
var 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")
## Abwerfen: eine Handkarte loswerden, dafür 1 Mana. ## Abwerfen: eine Handkarte loswerden, dafür 1 Mana.
@@ -442,68 +477,77 @@ func test_spott_und_regeneration() -> void:
check(g2.events.any(func(e): return e.type == "heal" and e.uid == reg.uid), "Ereignis fürs Heilen") check(g2.events.any(func(e): return e.type == "heal" and e.uid == reg.uid), "Ereignis fürs Heilen")
## Verschmelzen: besserer Wert plus ein Drittel des schwächeren, Eigenschaften beider, Kosten die ## Verschmelzen: kostenlos, Hand und Brett auch gemischt; das Ergebnis kommt unverletzt auf die
## halbe Manasumme; auch auf der Hand. ## Hand. Je Wert die aufgerundete halbe Grundwertsumme plus eins, Kosten daraus, dann die höhere
## Stufe der Eltern obendrauf.
func test_verschmelzen() -> void: func test_verschmelzen() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var a := put(h, "f_3_2") # 3/2, Kosten 2 var a := put(h, "f_3_2") # 3/2, Kosten 2
var b := put(h, "w_4_5") # 4/5, Kosten 4 var b := put(h, "w_4_5") # 4/5, Kosten 4
b.health = 4 b.health = 1
a.card.keywords.append("Spott") a.card.keywords.append("Spott")
check(Game.fusion_cost(a, b) == 3, "Verschmelzen kostet die halbe Manasumme: (2 + 4) / 2") check(g.fuse(h, a, b) == "" and h.board.is_empty() and h.hand.size() == 1 and h.energy == 10, "beide Wesen verlassen das Brett, das Ergebnis liegt auf der Hand, kostenlos")
check(g.fuse(h, a, b) == "" and h.board.size() == 1 and h.energy == 7, "verschmolzen") var f: Cards.Card = h.hand[0]
var f: Game.Creature = h.board[0] check(f.name == "Dampf" and f.attack == 5 and f.health == 5 and f.cost == 5, "Dampf: ⌈7/2⌉ + 1 = 5 Angriff und Leben, Kosten (10 + 5) / 3 = 5, die Verletzung ist weg")
check(f.name == "Dampf" and f.attack == 5 and f.max_health == 6 and f.health == 5, "Dampf: 4+1 Angriff, 5+1 Leben, aktuell 4+1") check("Spott" in f.keywords and f.elements == ["Feuer", "Wasser"] and f.look == 0, "Eigenschaften und Elemente beider, Stufe Common")
check("Spott" in f.card.keywords and f.card.cost == 5, "Eigenschaften geerbt, Kosten aus den neuen Werten") 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(f.card.elements == ["Feuer", "Wasser"] and f.ready, "beide Elemente, beide waren bereit") check(h.played["Feuer"] == 1 and h.played["Wasser"] == 1, "zählt beim Legen für beide Elemente")
var c := put(h, "e_0_4", false) # Wesen mit Handkarte, auch verdeckt; stark plus schwach ergibt weniger als das starke
check(Game.fusion_cost(f, c) == 3, "Dampf (5) mit Hüter (1): 3 Mana") var dampf: Game.Creature = h.board[0]
g.fuse(h, f, c) var secret := put(h, "e_0_4", false, true)
var f2: Game.Creature = h.board[0] h.hand = [C("l_1_1")]
check(f2.name == "Chimäre" and h.energy == 4 and not f2.ready, "Chimäre aus drei Elementen, nicht bereit, weil der Hüter es nicht war") check(g.fuse(h, dampf, h.hand[0]) == "" and h.hand.size() == 1 and h.board == [secret], "Wesen mit Handkarte verschmolzen")
var d := put(h, "l_1_1") var chi: Cards.Card = h.hand[0]
check(Game.fusion_cost(f2, d) == ceili((f2.card.cost + 1) / 2.0), "Kosten folgen den Karten") check(chi.name == "Chimäre" and chi.attack == 4 and chi.health == 4 and chi.cost == 4, "Chimäre aus drei Elementen: ⌈6/2⌉ + 1 = 4/4, schwächer als Dampf, dafür frisch")
h.energy = 1 check(g.fuse(h, h.hand[0], secret) == "" and h.hand[0].name == "Chimäre" and h.hand[0].health == 5 and h.board.is_empty(), "auch mit einem verdeckten Wesen")
check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer") check(g.can_fuse(h, h.hand[0], h.hand[0])[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst") check(g.can_fuse(h, h.hand[0], put(s[2], "f_2_1"))[1] == "Nur eigene Karten" and g.can_fuse(h, h.hand[0], C("f_2_1"))[1] == "Nur eigene Karten", "fremde Wesen und fremde Karten nicht")
check(Game.fusion_cost(put(h, "f_0_2"), put(h, "w_0_2")) == 0, "zwei Karten für 0 verschmelzen umsonst") h.energy = 0
# Verdeckte verschmelzen offen check(g.can_fuse(h, h.hand[0], put(h, "f_2_1"))[0], "ohne Mana geht es trotzdem")
var s2 := setup() # Stufe: aus den Grundwerten gerechnet, dann die höhere Stufe obendrauf
var x := put(s2[1], "f_3_2", true, true) var s4 := setup(["f_3_2", "l_1_1"])
var y := put(s2[1], "f_2_1", true, true)
s2[0].fuse(s2[1], x, y)
check(not s2[1].board[0].hidden and s2[1].board[0].name == "Inferno", "das Ergebnis liegt offen")
# Elementbonus der Eltern zählt nicht doppelt; Dampf bekommt die Boni beider Elemente
var s3 := setup()
var g3: Game = s3[0]
var h3: Game.Player = s3[1]
var fa := put(h3, "f_3_2")
var fb := put(h3, "w_4_5")
h3.played["Feuer"] = 5
g3.apply_auras()
check(fa.aura == 1 and fa.power() == 4 and fa.max_health == 3, "fünf Feuerkarten ausgespielt vor dem Verschmelzen")
g3.fuse(h3, fa, fb)
var ff: Game.Creature = h3.board[0]
check(ff.aura == 1 and ff.max_health == 7 and ff.health == 7 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal (6 + 1)")
h3.played["Wasser"] = 5
g3.apply_auras()
check(ff.aura == 2 and ff.power() == 7 and ff.max_health == 8, "Feuer und Wasser voll: Dampf bekommt +2/+2")
# Auf der Hand
var s4 := setup(["f_3_2", "w_4_5", "l_1_1"])
var g4: Game = s4[0] var g4: Game = s4[0]
var h4: Game.Player = s4[1] var h4: Game.Player = s4[1]
var hand_a: Cards.Card = h4.hand[0] h4.hand[1] = h4.hand[1].leveled(4) # Hauch in Ultimate Rare: 5/5 für 1 Mana
var hand_b: Cards.Card = h4.hand[1] check(h4.hand[1].attack == 5 and h4.hand[1].base_attack() == 1 and h4.hand[1].cost == 1, "der Stufenbonus liegt auf den Grundwerten")
check(g4.can_fuse(h4, hand_a, put(h4, "e_0_4"))[1] == "Nur Hand mit Hand oder Brett mit Brett", "Hand und Brett mischen sich nicht") check(g4.fuse(h4, h4.hand[0], h4.hand[1]) == "" and h4.hand.size() == 1, "auf der Hand verschmolzen")
check(g4.fuse(h4, hand_a, hand_b) == "" and h4.hand.size() == 2 and h4.energy == 7, "zwei Handkarten verschmolzen, 3 Mana") var blitz: Cards.Card = h4.hand[0]
var hc: Cards.Card = h4.hand[0] check(blitz.name == "Blitz" and blitz.look == 4 and blitz.base_attack() == 3 and blitz.base_health() == 3 and blitz.attack == 7 and blitz.health == 7 and blitz.cost == 3, "Blitz: Grundwerte ⌈4/2⌉ + 1 = 3/3, Kosten 3, Stufe 4 macht daraus 7/7")
check(hc.name == "Dampf" and hc.attack == 5 and hc.health == 6 and hc.cost == 5, "Dampf auf der Hand mit 5/6 und Kosten 5") check(g4.log.any(func(l): return l == "Brennender Kobold und Flüchtiger Hauch verschmelzen zu Blitz (7/7) auf der Hand."), "Verlauf")
var lava := Cards.fused(C("f_4_3"), C("f_2_1"), 4, 8) # Bild
check(Cards.fused(lava, C("f_2_1")).attack == 5 and Cards.fused(lava, C("f_2_1")).health == 9, "ein Funke macht Lava 4/8 zu 5/9, nie schlechter") var lava := Cards.fused(C("f_4_3"), C("f_2_1"))
check(g4.play_card(h4, hc) == "" and h4.energy == 7 - 5 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten") check(lava.name == "Inferno" and lava.attack == 4 and lava.health == 3 and lava.art == "f_4_3" and lava.art_card().id == "f_4_3" and Cards.fused(C("w_2_2"), lava).art == "f_4_3", "das Bild trägt der stärkere Elternteil, auch über Verschmelzungen hinweg")
check(g4.log.any(func(l): return l == "Brennender Kobold und Flutender Leviathan verschmelzen auf der Hand zu Dampf (5/6)."), "Verlauf") check(Cards.fused(C("f_3_2"), C("w_3_2")).art == "f_3_2" and C("f_3_2").art_card().id == "f_3_2" and Cards.Card.from_dict(lava.to_dict()).art == "f_4_3", "bei Gleichstand die erste Karte; Grundkarten zeigen sich selbst; das Bild überlebt das Speichern")
## KI: heilt ein Wesen mit höchstens halbem Leben per Fusion mit der billigsten Handkarte, wenn
## das Ergebnis bezahlbar bleibt; die Vorausschau kennt Fusionen mit Handkarten und bildet sie
## zwischen Kopie und Original ab.
func test_ki_verschmelzen() -> void:
var s := setup(["f_2_1", "w_4_5"])
var g: Game = s[0]
var h: Game.Player = s[1]
var hurt := put(h, "e_3_5")
hurt.health = 2
var act := AI.choose_action_greedy(g, h)
check(act.type == "fuse" and act.creature == hurt and act.other == h.hand[0], "gierig: das halb tote Wesen mit dem Funken verschmelzen")
h.energy = 1
act = AI.choose_action_greedy(g, h)
check(act.type == "play", "ohne Mana fürs Ergebnis keine Heilfusion, sondern legen")
h.energy = 10
var cands := AI.candidate_actions(g, h)
check(cands.any(func(a): return a.type == "fuse" and a.creature == hurt and a.other == h.hand[0]), "Vorausschau: Wesen plus Handkarte als Kandidat")
check(cands.any(func(a): return a.type == "fuse" and a.creature is Cards.Card and a.other is Cards.Card), "Vorausschau: zwei Handkarten als Kandidat")
var fuse_act: Dictionary = cands.filter(func(a): return a.type == "fuse" and a.creature == hurt)[0]
var g2 := g.clone()
AI.apply_on_clone(g2, g, fuse_act)
check(g2.players[0].board.is_empty() and g2.players[0].hand.size() == 2 and h.board == [hurt] and h.hand.size() == 2, "auf der Kopie verschmolzen, das Original bleibt")
var back := AI.map_action(g, g2, fuse_act)
check(back.creature == hurt and back.other == h.hand[0], "zurück aufs Original abgebildet")
hurt.health = 5
check(not AI.candidate_actions(g, h).any(func(a): return a.type == "fuse" and a.creature == hurt), "unverletzt: kein Heilkandidat")
func test_limits() -> void: func test_limits() -> void:
@@ -516,9 +560,9 @@ func test_limits() -> void:
check(g.can_play(h, h.hand[0])[1] == "Dein Feld ist voll", "Feld voll") check(g.can_play(h, h.hand[0])[1] == "Dein Feld ist voll", "Feld voll")
h.hand = [] h.hand = []
h.deck = ["f_2_1"] h.deck = ["f_2_1"]
for i in Game.MAX_HAND: for i in 12:
h.hand.append(C("f_2_1")) h.hand.append(C("f_2_1"))
check(g.draw(h) == "Deine Hand ist voll" and h.hand.size() == Game.MAX_HAND, "volle Hand zieht nicht") check(g.draw(h) == "" and h.hand.size() == 13, "keine Grenze für Handkarten")
check(g.play_card(h, C("f_2_1")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen") check(g.play_card(h, C("f_2_1")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
@@ -643,8 +687,8 @@ func test_booster() -> void:
g.play_card(h, h.hand[0]) g.play_card(h, h.hand[0])
var fused: Game.Creature = h.board[1] var fused: Game.Creature = h.board[1]
h.energy = 10 h.energy = 10
check(g.fuse(h, fused, kobold) == "", "auf dem Brett verschmolzen") check(g.fuse(h, fused, kobold) == "" and h.board.is_empty(), "zwei Wesen vom Brett verschmolzen")
check(h.board[0].card.look == 3 and h.board[0].card.rarity_in(h.looks) == "Ultra Rare", "auch auf dem Brett und über zwei Fusionen") check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "die Stufe bleibt über zwei Fusionen")
## Sammlung und Deck: Start mit einem Startdeck, Herstellen für Staub, Zerlegen entfernt ## Sammlung und Deck: Start mit einem Startdeck, Herstellen für Staub, Zerlegen entfernt
@@ -943,7 +987,7 @@ func test_ki_zeitbudget() -> void:
var g := Game.new_battle(r) var g := Game.new_battle(r)
var h: Game.Player = g.players[0] var h: Game.Player = g.players[0]
h.energy = 30 h.energy = 30
for i in Game.MAX_HAND: for i in 7:
h.hand.append(C("f_4_3")) h.hand.append(C("f_4_3"))
for p in g.players: for p in g.players:
for i in 3: for i in 3:

View File

@@ -3,6 +3,7 @@ extends Node
## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn ## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn
const SAVE := "user://test_ui_spielstand.json" const SAVE := "user://test_ui_spielstand.json"
const DEVICE := 7 # Gerätenummer der Testereignisse; alles andere fängt der InputFilter ab
var main: Control var main: Control
var shots := "" var shots := ""
@@ -20,6 +21,21 @@ func check(cond: bool, msg: String) -> void:
get_tree().quit(1) get_tree().quit(1)
## Lässt nur Ereignisse des Tests durch; echte Maus und Tastatur des Nutzers werden abgefangen,
## bevor Knöpfe oder Bildschirme sie sehen. Sitzt als letzter Knoten im Baum und bekommt _input
## darum als Erster.
class InputFilter extends Node:
func _input(event: InputEvent) -> void:
if event.device != DEVICE:
get_viewport().set_input_as_handled()
## Testereignis an das Fenster, mit der eigenen Gerätenummer.
func push(ev: InputEvent) -> void:
ev.device = DEVICE
get_viewport().push_input(ev)
func frames(n: int = 2) -> void: func frames(n: int = 2) -> void:
for i in n: for i in n:
await get_tree().process_frame await get_tree().process_frame
@@ -39,10 +55,10 @@ func click_at(pos: Vector2) -> void:
down.global_position = pos down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true down.pressed = true
get_viewport().push_input(down) push(down)
var up: InputEventMouseButton = down.duplicate() var up: InputEventMouseButton = down.duplicate()
up.pressed = false up.pressed = false
get_viewport().push_input(up) push(up)
await frames(2) await frames(2)
@@ -63,11 +79,11 @@ func double_click_at(pos: Vector2) -> void:
down.button_index = MOUSE_BUTTON_LEFT down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true down.pressed = true
down.double_click = true down.double_click = true
get_viewport().push_input(down) push(down)
var up: InputEventMouseButton = down.duplicate() var up: InputEventMouseButton = down.duplicate()
up.pressed = false up.pressed = false
up.double_click = false up.double_click = false
get_viewport().push_input(up) push(up)
await frames(2) await frames(2)
@@ -77,10 +93,10 @@ func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
down.global_position = pos down.global_position = pos
down.button_index = button down.button_index = button
down.pressed = true down.pressed = true
get_viewport().push_input(down) push(down)
var up: InputEventMouseButton = down.duplicate() var up: InputEventMouseButton = down.duplicate()
up.pressed = false up.pressed = false
get_viewport().push_input(up) push(up)
await frames(2) await frames(2)
@@ -88,11 +104,11 @@ func key(code: int) -> void:
var down := InputEventKey.new() var down := InputEventKey.new()
down.keycode = code down.keycode = code
down.pressed = true down.pressed = true
get_viewport().push_input(down) push(down)
var up := InputEventKey.new() var up := InputEventKey.new()
up.keycode = code up.keycode = code
up.pressed = false up.pressed = false
get_viewport().push_input(up) push(up)
await frames(2) await frames(2)
@@ -100,7 +116,7 @@ func pad(button: int) -> void:
var ev := InputEventJoypadButton.new() var ev := InputEventJoypadButton.new()
ev.button_index = button ev.button_index = button
ev.pressed = true ev.pressed = true
get_viewport().push_input(ev) push(ev)
await frames(2) await frames(2)
@@ -109,7 +125,7 @@ func hover(node: Control) -> void:
var ev := InputEventMouseMotion.new() var ev := InputEventMouseMotion.new()
ev.position = node.get_global_rect().get_center() ev.position = node.get_global_rect().get_center()
ev.global_position = ev.position ev.global_position = ev.position
get_viewport().push_input(ev) push(ev)
await frames(2) await frames(2)
@@ -168,6 +184,7 @@ func _ready() -> void:
main = load("res://scenes/main.tscn").instantiate() main = load("res://scenes/main.tscn").instantiate()
main.save_path = SAVE main.save_path = SAVE
add_child(main) add_child(main)
add_child(InputFilter.new()) # nach main, damit er im Baum zuletzt steht
main.sfx.muted = true # Tests bleiben still main.sfx.muted = true # Tests bleiben still
await frames(3) await frames(3)
var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen, var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
@@ -244,46 +261,65 @@ func target_view(scr, valid: Array) -> Control:
return null return null
## Zieht eine Karte oder Kachel auf eine andere und hält sie dort, bis die beiden verschmelzen. ## Zieht eine Karte oder Kachel auf eine andere und lässt sie dort los; danach wartet das Ergebnis
## über dem Ziel auf accept_fuse oder „Abbrechen“.
func drag_fuse(scr, from: Control, to: Control) -> void: func drag_fuse(scr, from: Control, to: Control) -> void:
await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken) await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
await settle(scr) await settle(scr)
var a := from.get_global_rect().get_center() var a := from.get_global_rect().get_center()
var b := to.get_global_rect().get_center() var b := to.get_global_rect().get_center()
var fo = from.card if from is CardView else from.creature
var to_o = to.card if to is CardView else to.creature
var down := InputEventMouseButton.new() var down := InputEventMouseButton.new()
down.position = a down.position = a
down.global_position = a down.global_position = a
down.button_index = MOUSE_BUTTON_LEFT down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true down.pressed = true
get_viewport().push_input(down) push(down)
await frames(1) await frames(1)
for i in range(1, 6): for i in range(1, 6):
var m := InputEventMouseMotion.new() var m := InputEventMouseMotion.new()
m.position = a.lerp(b, i / 5.0) m.position = a.lerp(b, i / 5.0)
m.global_position = m.position m.global_position = m.position
m.button_mask = MOUSE_BUTTON_MASK_LEFT m.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(m) push(m)
await frames(1) await frames(1)
if scr.drag_ghost == null or scr.drag_target == null: if scr.drag_ghost == null or scr.drag_target == null:
var fo = from.card if from is CardView else from.creature
var to_o = to.card if to is CardView else to.creature
print("DEBUG drag: ghost=", scr.drag_ghost, " target=", scr.drag_target, " press=", scr.press_target, " dran=", scr.my_turn(), print("DEBUG drag: ghost=", scr.drag_ghost, " target=", scr.drag_target, " press=", scr.press_target, " dran=", scr.my_turn(),
" overlay=", scr.any_overlay(), " mana=", scr.game.human.energy, " kann=", scr.game.can_fuse(scr.game.human, fo, to_o), " from=", from.get_global_rect(), " to=", to.get_global_rect()) " overlay=", scr.any_overlay(), " mana=", scr.game.human.energy, " kann=", scr.game.can_fuse(scr.game.human, fo, to_o), " from=", from.get_global_rect(), " to=", to.get_global_rect())
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel") check(scr.drag_ghost != null and scr.drag_target == to_o and to.drop, "Karte schwebt über einem Verschmelzungsziel, das golden gerahmt ist")
var waited := 0
while scr.drag_ghost != null and waited < 240:
var still := InputEventMouseMotion.new() # die Maus bleibt auf dem Ziel, auch wenn die echte zuckt
still.position = b
still.global_position = b
still.button_mask = MOUSE_BUTTON_MASK_LEFT
get_viewport().push_input(still)
await frames(1)
waited += 1
var up: InputEventMouseButton = down.duplicate() var up: InputEventMouseButton = down.duplicate()
up.pressed = false up.pressed = false
up.position = b up.position = b
up.global_position = b up.global_position = b
get_viewport().push_input(up) push(up)
await frames(2)
var want: Cards.Card = Game.fusion_result(scr.game.human, fo, to_o)
var shown: Array = scr.menu_layer.get_children().filter(func(c): return c is CardView)
check(scr.fuse_pending == [fo, to_o] and scr.menu_layer.visible and shown.size() == 1 and shown[0].card.name == want.name and shown[0].card.attack == want.attack and shown[0].card.health == want.health, "nach dem Loslassen schwebt %s (%d/%d) über dem Ziel und wartet auf Bestätigung" % [want.name, want.attack, want.health])
check(shown[0].position.y + shown[0].size.y < to.get_global_rect().position.y and not to.drop, "das Ergebnis steht über dem Ziel, der goldene Rahmen ist weg")
## Bestätigt die offene Verschmelzungsfrage; die neue Karte gleitet von oben in die Hand.
func accept_fuse(scr) -> void:
var pair: Array = scr.fuse_pending
check(pair != null, "eine Verschmelzung wartet")
var before: Array = scr.game.human.hand.duplicate()
var btn := find_button("Verschmelzen", scr.menu_layer)
check(btn != null, "Knopf Verschmelzen")
var at := btn.get_global_rect().get_center()
var down := InputEventMouseButton.new()
down.position = at
down.global_position = at
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
push(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
push(up)
await frames(1)
var fresh: Array = scr.hand_views.filter(func(v): return not (v.card in before))
check(fresh.size() == 1 and (fresh[0].position.y < BattleLayout.HAND_Y - 40 or not scr.fx.enabled), "die neue Karte gleitet von oben in die Hand")
await frames(2) await frames(2)
@@ -367,7 +403,8 @@ func test_gegnerauswahl() -> void:
var opp: Game.Player = hard.game.opponent var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life) check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand") check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar") 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")
await shot("05_starthand") await shot("05_starthand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben") check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben")
await shot("11_kampf") await shot("11_kampf")
@@ -445,6 +482,14 @@ func test_deck() -> void:
check(scr is DeckScreen, "Deck geöffnet") check(scr is DeckScreen, "Deck geöffnet")
var p: Profile = main.profile var p: Profile = main.profile
check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt") check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt")
check(scr.strength_label.text == "Stärke 100", "Startdeck aus Grundkarten: Stärke 100")
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)
p.collection[p.deck[0]] = 0
scr.refresh()
await frames(2)
var views: Array = scr.grid.views() var views: Array = scr.grid.views()
check(views.all(func(v): return not v.dim) and p.deck.size() == Game.DECK_MIN, "nach dem Start sind alle eigenen Karten im Stapel") check(views.all(func(v): return not v.dim) and p.deck.size() == Game.DECK_MIN, "nach dem Start sind alle eigenen Karten im Stapel")
var first_id: String = views[0].card.id var first_id: String = views[0].card.id
@@ -518,6 +563,7 @@ func test_gamepad_menue() -> void:
var down := InputEventJoypadButton.new() var down := InputEventJoypadButton.new()
down.button_index = JOY_BUTTON_DPAD_DOWN down.button_index = JOY_BUTTON_DPAD_DOWN
down.pressed = true down.pressed = true
down.device = DEVICE
Input.parse_input_event(down) Input.parse_input_event(down)
await frames(2) await frames(2)
var f2 := get_viewport().gui_get_focus_owner() var f2 := get_viewport().gui_get_focus_owner()
@@ -527,12 +573,12 @@ func test_gamepad_menue() -> void:
var accept := InputEventJoypadButton.new() var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A accept.button_index = JOY_BUTTON_A
accept.pressed = true accept.pressed = true
get_viewport().push_input(accept) push(accept)
await frames(1) await frames(1)
var release := InputEventJoypadButton.new() var release := InputEventJoypadButton.new()
release.button_index = JOY_BUTTON_A release.button_index = JOY_BUTTON_A
release.pressed = false release.pressed = false
get_viewport().push_input(release) push(release)
await frames(3) await frames(3)
check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text) check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text)
await hover(main) await hover(main)
@@ -657,17 +703,13 @@ func test_kampf_grenzen() -> void:
g.human.energy = 100 g.human.energy = 100
scr.refresh() scr.refresh()
await frames(1) await frames(1)
# Hand voll ziehen # Keine Grenze für Handkarten: acht weitere ziehen
var guard := 0 var hand_before: int = g.human.hand.size()
while g.human.hand.size() < Game.MAX_HAND and guard < 30: for i in 8:
guard += 1
await click(scr.deck) await click(scr.deck)
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND) check(g.human.hand.size() == hand_before + 8 and scr.deck.enabled and scr.hand_views.size() == hand_before + 8, "acht Karten nachgezogen, Ziehen bleibt möglich")
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
await click(scr.deck)
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
# Feld voll spielen # Feld voll spielen
guard = 0 var guard := 0
while g.human.board.size() < Game.MAX_BOARD and guard < 30: while g.human.board.size() < Game.MAX_BOARD and guard < 30:
guard += 1 guard += 1
if g.human.hand.is_empty(): if g.human.hand.is_empty():
@@ -681,9 +723,10 @@ func test_kampf_grenzen() -> void:
await shot("25_kampf_voll") await shot("25_kampf_voll")
# Verschmelzen per Ziehen macht wieder Platz # Verschmelzen per Ziehen macht wieder Platz
var before: int = g.human.board.size() var before: int = g.human.board.size()
scr.hold_time = 0.3 var hand_n: int = g.human.hand.size()
await drag_fuse(scr, scr.own_views[0], scr.own_views[1]) await drag_fuse(scr, scr.own_views[0], scr.own_views[1])
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei") await accept_fuse(scr)
check(g.human.board.size() == before - 2 and g.human.hand.size() == hand_n + 1, "Verschmelzen macht zwei Plätze frei, das Ergebnis liegt auf der Hand")
# Ohne Mana kein Ziehen # Ohne Mana kein Ziehen
g.human.energy = 0 g.human.energy = 0
g.human.draws = 1 g.human.draws = 1
@@ -699,12 +742,11 @@ func test_kampf_elemente_und_verschmelzen() -> void:
var scr = screen() var scr = screen()
check(scr is BattleScreen, "Kampf gestartet") check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0 scr.ai_delay = 0.0
scr.hold_time = 0.3
await wait_for_human(scr) await wait_for_human(scr)
var g: Game = scr.game var g: Game = scr.game
g.human.board.clear() g.human.board.clear()
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
for e in Cards.ELEMENTS: for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
scr.opponent().played[e] = 0 scr.opponent().played[e] = 0
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature: var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid) var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
@@ -756,18 +798,23 @@ func test_kampf_elemente_und_verschmelzen() -> void:
mine_hidden.ready = true mine_hidden.ready = true
await click(scr.own_views[3]) await click(scr.own_views[3])
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text()) check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/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 fünf Karten kein Bonus, aufgedeckt greift sofort an") 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"] = 5 g.human.played["Feuer"] = 3
g.apply_auras() g.apply_auras()
scr.refresh() scr.refresh()
await frames(1) await frames(1)
check(scr.element_view.text().begins_with("Feuer 3/5 (Doppelschlag) (+1/+1)") and wolf.power() == 4 and falke.power() == 3, "fünf Feuerkarten: +1/+1 für die eigenen Feuerwesen: " + scr.element_view.text()) 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") check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
await shot("23b_vier_feuer") await shot("23b_vier_feuer")
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt # Angriffsziel: Elementvorteil schlägt durch den Stapelbonus des Ziels, verdeckte ohne Anzeige, Held gesperrt
scr.opponent().played["Luft"] = 3
g.apply_auras()
scr.refresh()
await frames(1)
check(falke.aura == 1 and falke.power() == 4 and scr.opp_element_view.text() == "Luft 1/3 (+1/+1)", "der Gegner hat drei Luftkarten gespielt, der Falke trägt +1/+1: " + scr.opp_element_view.text())
await click(scr.own_views[0]) await click(scr.own_views[0])
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte") check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen 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 (1 weniger) und sein echtes Leben") 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")
await hover(scr.tiles[falke.uid]) await hover(scr.tiles[falke.uid])
check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() + 1, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt mit +1 zu") check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() + 1, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt mit +1 zu")
await shot("23c_angreifer_gewaehlt") await shot("23c_angreifer_gewaehlt")
@@ -777,19 +824,27 @@ func test_kampf_elemente_und_verschmelzen() -> void:
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt") check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
await double_click_at(scr.own_views[0].get_global_rect().get_center()) await double_click_at(scr.own_views[0].get_global_rect().get_center())
check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text) check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text)
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten # Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen, erst abbrechen, dann bestätigen
mana = g.human.energy mana = g.human.energy
var before: int = g.human.board.size() var before: int = g.human.board.size()
await drag_fuse(scr, scr.own_views[1], scr.own_views[2]) await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 3, "zu Dampf verschmolzen, 3 Mana bezahlt (halbe Manasumme)") await shot("24a_verschmelzen_frage")
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0] await click(find_button("Abbrechen", scr.menu_layer))
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (der Reiter war Ultra Rare)") check(scr.fuse_pending == null and not scr.menu_layer.visible and g.human.board.size() == before and g.human.energy == mana, "Abbrechen lässt beide Wesen und das Mana, wie sie waren")
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
await accept_fuse(scr)
var dampf_cards: Array = g.human.hand.filter(func(c): return c.name == "Dampf")
check(g.human.board.size() == before - 2 and dampf_cards.size() == 1 and g.human.energy == mana, "beide Wesen verlassen das Brett, Dampf liegt auf der Hand, kostenlos")
var dampf: Cards.Card = dampf_cards[0]
var dampf_views: Array = scr.hand_views.filter(func(v): return v.card == dampf)
check(dampf.look == 3 and dampf.attack == 8 and dampf.health == 7 and dampf.cost == 4 and dampf_views.size() == 1 and dampf_views[0].rarity == "Ultra Rare", "Dampf 5/4 für 4 Mana aus den Grundwerten, Stufe 3 des Reiters macht 8/7")
await shot("24_verschmelzen") await shot("24_verschmelzen")
# Verschmelzen auf der Hand per Ziehen # Verschmelzen auf der Hand per Ziehen
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()] g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
scr.refresh() scr.refresh()
await frames(1) await frames(1)
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1]) await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
await accept_fuse(scr)
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Funken auf der Hand zu Inferno 3/2") check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Funken auf der Hand zu Inferno 3/2")
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit … # Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true) scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
@@ -797,7 +852,12 @@ func test_kampf_elemente_und_verschmelzen() -> void:
await click(find_button("Verschmelzen mit …", scr.menu_layer)) await click(find_button("Verschmelzen mit …", scr.menu_layer))
check(scr.fuse_first == g.human.hand[0], "Partner wählen") check(scr.fuse_first == g.human.hand[0], "Partner wählen")
await click(scr.hand_views[1]) await click(scr.hand_views[1])
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz") check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Hauch zu Blitz")
# Handkarte mit Wesen: Blitz auf den Kobold ziehen
var board_n: int = g.human.board.size()
await drag_fuse(scr, scr.hand_views[0], scr.own_views[0])
await accept_fuse(scr)
check(g.human.board.size() == board_n - 1 and g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz" and g.human.hand[0].base_attack() == 4, "Handkarte und Wesen verschmelzen: der Kobold verlässt das Brett, Blitz 4/4 bleibt auf der Hand")
await give_up() await give_up()

View File

@@ -81,6 +81,7 @@ Starten,Start
Sammlung,Collection Sammlung,Collection
Verschmelzen,Fuse Verschmelzen,Fuse
Nur eigene Wesen,Only your own creatures Nur eigene Wesen,Only your own creatures
Nur eigene Karten,Only your own cards
Sammlung: %d von %d Karten · Kampagne: %d/%d,Collection: %d of %d cards · Campaign: %d/%d Sammlung: %d von %d Karten · Kampagne: %d/%d,Collection: %d of %d cards · Campaign: %d/%d
%s ist jetzt %s,%s is now %s %s ist jetzt %s,%s is now %s
Alle Kapitel abgeschlossen,All chapters completed Alle Kapitel abgeschlossen,All chapters completed
@@ -103,7 +104,6 @@ Profil wirklich zurücksetzen?,Really reset your profile?
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win
Start gegen die KI,Start against the AI Start gegen die KI,Start against the AI
Ziehen,Draw Ziehen,Draw
Deine Hand ist voll,Your hand is full
Verdeckt legen ◆ %d,Set face-down ◆ %d Verdeckt legen ◆ %d,Set face-down ◆ %d
Aufdecken,Reveal Aufdecken,Reveal
verdeckt,face-down verdeckt,face-down
@@ -154,6 +154,7 @@ Abwerfen +%d ◆,Discard +%d ◆
Überspringen,Skip Überspringen,Skip
Zug %d,Turn %d Zug %d,Turn %d
Stapel: %d Karten (%d bis %d),Deck: %d cards (%d to %d) Stapel: %d Karten (%d bis %d),Deck: %d cards (%d to %d)
Stärke %d,Strength %d
Herstellen (%d Staub),Craft (%d dust) Herstellen (%d Staub),Craft (%d dust)
Nicht in der Sammlung,Not in the collection Nicht in der Sammlung,Not in the collection
%s hergestellt,%s crafted %s hergestellt,%s crafted
@@ -161,9 +162,16 @@ Neu!,New!
Auffüllen,Fill up Auffüllen,Fill up
Leeren,Clear Leeren,Clear
Dein Stapel ist leer,Your deck is empty Dein Stapel ist leer,Your deck is empty
Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.,Click adds a card to the deck or removes it. Fill up takes the most expensive cards of the filter. 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 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." "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 %s wählen,Choose %s
Optionen,Options Optionen,Options
Hauptmenü,Main menu Hauptmenü,Main menu
Neutral,Neutral
Nie im Vorteil oder Nachteil,Never at an advantage or disadvantage
Veteran,Veteran
Feuerritter,Fire Knight
Wasserritter,Water Knight
Erdritter,Earth Knight
Luftritter,Air Knight
1 keys en
81 Sammlung Collection
82 Verschmelzen Fuse
83 Nur eigene Wesen Only your own creatures
84 Nur eigene Karten Only your own cards
85 Sammlung: %d von %d Karten · Kampagne: %d/%d Collection: %d of %d cards · Campaign: %d/%d
86 %s ist jetzt %s %s is now %s
87 Alle Kapitel abgeschlossen All chapters completed
104 Gegner mit %d Leben · %d Münzen bei Sieg Opponent with %d life · %d coins for a win
105 Start gegen die KI Start against the AI
106 Ziehen Draw
Deine Hand ist voll Your hand is full
107 Verdeckt legen ◆ %d Set face-down ◆ %d
108 Aufdecken Reveal
109 verdeckt face-down
154 Überspringen Skip
155 Zug %d Turn %d
156 Stapel: %d Karten (%d bis %d) Deck: %d cards (%d to %d)
157 Stärke %d Strength %d
158 Herstellen (%d Staub) Craft (%d dust)
159 Nicht in der Sammlung Not in the collection
160 %s hergestellt %s crafted
162 Auffüllen Fill up
163 Leeren Clear
164 Dein Stapel ist leer Your deck is empty
165 Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters. 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 expensive cards of the filter. Click adds a card to the deck or removes it. Fill up takes the most valuable cards of the filter.
166 Wähle dein Startdeck Choose your starting deck
167 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.
168 %s wählen Choose %s
169 Optionen Options
170 Hauptmenü Main menu
171 Neutral Neutral
172 Nie im Vorteil oder Nachteil Never at an advantage or disadvantage
173 Veteran Veteran
174 Feuerritter Fire Knight
175 Wasserritter Water Knight
176 Erdritter Earth Knight
177 Luftritter Air Knight

Binary file not shown.