update
This commit is contained in:
14
README.md
14
README.md
@@ -20,19 +20,19 @@ Der Spielstand liegt unter `user://spielstand.json`
|
||||
|
||||
## 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)
|
||||
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen und Verschmelzen
|
||||
- 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)
|
||||
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen und Legen
|
||||
- Jeder spielt mit seinem eigenen Stapel aus 40 bis 60 Karten (Menü „Deck“: eigene Karten rein oder raus, „Auffüllen“ nimmt die wertvollsten des gewählten Filters, bei gleichem Wert die teuersten; „Stärke“ ist der mittlere Kartenwert: ⌊(2·Angriff + Leben)/3⌋ − Kosten + 1, mal 100, also 100 für Grundkarten bis 500 für Ultimate Rare). Zu Beginn die obersten fünf Karten des gemischten Stapels. Ziehen bringt eine Karte, die erste im Zug ist frei, dann 1, 2, 3 … Mana; ist der Stapel leer, kostet jeder Zug 1, 2, 3 … Leben (Erschöpfung); eine Handkarte abwerfen bringt 1 Mana (Rechtsklick)
|
||||
- Eigenschaften gibt es nur über Elementpaare (siehe Elementboni): Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
||||
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an, zählt nicht fürs Elementpaar und bekommt den Elementbonus erst offen; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
||||
- Beim ersten Start wählt man eines von vier Startdecks (40 Karten: 25 eines Elements bis Angriff 4, je fünf billige der anderen), der Rest kommt aus Boostern oder der Werkstatt. Jede eigene Karte hat eine Seltenheitsstufe (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare) und ist je Stufe +1/+1 stärker, bei gleichen Kosten; die Stufe zeigt sich auch in der Optik (Glanz, Holo, Relief, Funkeln). Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
||||
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (45/25/15/10/5 %, mindestens eine Rare); neue kommen in Sammlung und Stapel, eine höhere Stufe ersetzt die alte Version (die wird zu Staub), der Rest wird zu Staub (50/100/200/500/1000). Werkstatt in der Sammlung: Aufwerten kostet 100/200/500/1000 Staub, Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
|
||||
- Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je 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
|
||||
- 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“
|
||||
- Elementboni, jeder Spieler für sich: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); je drei ausgespielte Karten eines Elements geben den eigenen Wesen dieses Elements +1/+1 (verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni. Die Zähler stehen links neben dem jeweiligen Brett und zeigen nur Elemente, bei denen etwas passiert ist: zwei Symbole, gefüllt je offenem eigenen Wesen, und die Zahl der ausgespielten Karten
|
||||
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, schlägt durch den Stapelbonus des Ziels (so viel Schaden mehr, wie das Ziel an +x/+x trägt); wer im Nachteil ist, schlägt ohne seinen eigenen Stapelbonus zu; beim Angriff wie beim Konter, gegen Helden nichts; ohne Stapelbonus bewirkt das Element nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
|
||||
- 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
|
||||
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
||||
- Verschmelzen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen und loslassen, auch gemischt; das Ergebnis erscheint über dem Ziel, darunter „Verschmelzen“ oder „Abbrechen“ (Gamepad: Y, „Verschmelzen mit …“). Es kostet nichts; Wesen verlassen das Brett, die neue Karte kommt unverletzt auf die Hand und wird wie jede andere ausgespielt. Je Wert die aufgerundete Hälfte der Grundwertsumme plus eins, Kosten daraus wie bei jeder Karte, dann die höhere Stufe der Eltern mit ihrem +1/+1; Elemente und Eigenschaften beider; drei oder mehr Elemente ergeben eine Chimäre
|
||||
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 7 Karten, das Feld 5 Wesen
|
||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
- Booster öffnen (Chancen 45/25/15/10/5 %, mindestens eine Rare): neue Karten kommen in Sammlung und Stapel, von jeder Karte zählt die seltenste Version, die alte wird zu Staub, der Rest wird zu Staub (50/100/200/500/1000)
|
||||
- Werkstatt: Staub wertet gezielt auf (100/200/500/1000), Zerlegen bringt den Wert zurück (Common verlässt die Sammlung), Herstellen einer fehlenden Karte kostet 200 Staub
|
||||
- Jeder hat sein Deck: 40 bis 60 eigene Karten, im Menü „Deck“ zusammengestellt (Auffüllen folgt dem Filter); 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
|
||||
- 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
|
||||
- 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 und Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, höchstens 10; 0/2 kostet 0), Abwerfen bringt 1; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; keine Grenze für Handkarten
|
||||
- Karten haben nur Angriff und Leben; Eigenschaften kommen allein über Elementpaare: Spott (muss zuerst angegriffen werden), Flink (greift sofort an), Regeneration (heilt am Zugende ganz), Doppelschlag (greift zweimal je Zug)
|
||||
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
||||
- 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
|
||||
|
||||
## 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)
|
||||
- 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
|
||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet, mindestens 1), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
||||
- Je drei ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements, jeder Spieler zählt für sich; verdeckt gelegte zählen mit, bekommen den Bonus aber erst offen; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, schlägt durch den Stapelbonus des Ziels: so viel Schaden mehr, wie das Ziel an +x/+x trägt; wer im Nachteil ist, schlägt ohne seinen eigenen Stapelbonus zu; beim Angriff wie beim Konter; ohne Stapelbonus bewirkt das Element nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
|
||||
- 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
|
||||
|
||||
300
data/karten.json
300
data/karten.json
@@ -1199,6 +1199,306 @@
|
||||
],
|
||||
"attack": 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
design.md
40
design.md
@@ -21,17 +21,22 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
||||
|
||||
## Karte
|
||||
|
||||
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
|
||||
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
|
||||
Stufe. Karten haben keinen Text: nur Element, Angriff, Leben.
|
||||
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol.
|
||||
- Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau.
|
||||
- Seltenheit nach Yu-Gi-Oh: Common matt; Rare silberner Name und Rahmen, stehender Glanzstreifen,
|
||||
Edelstein; Super Rare Holo-Bild (wandernder Regenbogenschimmer über dem Symbol); Ultra Rare
|
||||
goldener Name und Rahmen plus Holo-Bild; Ultimate Rare Relief am Symbol, Holo auf Bild und
|
||||
Rahmen, Funkeln, schillernde Farben. Fusionen tragen die höhere Stufe ihrer Eltern.
|
||||
- Mehrere Elemente: der Hintergrund trägt das erste Element, ein rundes Medaillon hinter dem
|
||||
Symbol zeigt alle Elemente als Tortenstücke.
|
||||
- Oben links der Kostenkristall, daneben der Name (immer eine Zeile, nie abgeschnitten: die
|
||||
Schrift schrumpft etwas, dann wird der Text schmaler gezeichnet; das Feld ist so hoch wie der
|
||||
Kristall), in der Mitte das Bild als Elementsymbol im dunklen Kreis, so groß wie
|
||||
der Platz erlaubt, unten Angriff (gelb, links) und Leben (blau, rechts) als Kreise. Karten
|
||||
haben keinen Text: nur Element, Angriff, Leben. Auf dem Feld stehen unter dem Namen links die
|
||||
Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration,
|
||||
gekreuzte Klingen = Doppelschlag, in der Farbe des Elements, das sie gibt), rechts der
|
||||
Elementbonus.
|
||||
- Die Hand sind alle Karten in Feldgröße, ab sechs überlappend; Details kommen aus der schwebenden Vorschau.
|
||||
- Seltenheit nach Yu-Gi-Oh: Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild
|
||||
(wandernder Regenbogenschimmer im Bildkreis); Ultra Rare golden glänzender Name plus Holo-Bild;
|
||||
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
|
||||
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:
|
||||
|
||||
- **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
|
||||
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.
|
||||
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
||||
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
||||
- **Verschmelzen** per Ziehen: eine Handkarte oder ein eigenes Wesen auf eine andere ziehen
|
||||
(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).
|
||||
- **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.
|
||||
@@ -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
|
||||
öffnet darüber. Zug beenden per Leertaste oder Rechtsklick ins Leere, oben rechts nur Rückgängig.
|
||||
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
||||
steht der Elementkonter (der ignorierte Stapelbonus) am Ziel, bei Verdeckten nicht.
|
||||
|
||||
## Bewegung und Ton
|
||||
|
||||
@@ -85,7 +92,8 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||
stärker, nicht über Begleiter.
|
||||
- Sammlung: sechs Spalten, eigene Karten zuerst (höchste Stufen vorn), fehlende abgedunkelt, rechts
|
||||
die große Vorschau mit Aufwerten, Zerlegen oder Herstellen. Deck: gleiches Raster, nur eigene Karten,
|
||||
Stapelkarten hell, die anderen abgedunkelt; Klick wechselt.
|
||||
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
|
||||
|
||||
|
||||
@@ -101,14 +101,7 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
out.append({"type": "reveal", "creature": cr})
|
||||
var pairs := []
|
||||
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:
|
||||
for pr in fusion_pairs(game, p, prune):
|
||||
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
|
||||
for cr in p.board:
|
||||
if not cr.can_attack():
|
||||
@@ -126,6 +119,39 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
||||
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.
|
||||
static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
||||
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":
|
||||
g2.reveal(p2, g2.find_creature(action.creature.uid))
|
||||
"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))
|
||||
|
||||
@@ -150,9 +176,9 @@ static func map_target(g2: Game, game: Game, t):
|
||||
static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary:
|
||||
var out := action.duplicate()
|
||||
if action.has("creature"):
|
||||
out["creature"] = game.find_creature(action.creature.uid)
|
||||
out["creature"] = _part(game, action.creature)
|
||||
if action.has("other"):
|
||||
out["other"] = game.find_creature(action.other.uid)
|
||||
out["other"] = _part(game, action.other)
|
||||
if action.get("target") != null:
|
||||
out["target"] = map_target(game, copy, action.target)
|
||||
return out
|
||||
@@ -211,7 +237,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
static func _kills(attacker, defender) -> bool:
|
||||
if defender is Game.Player:
|
||||
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:
|
||||
@@ -234,8 +260,14 @@ static func _best(list: Array, key: Callable):
|
||||
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||
return {"type": "end"}
|
||||
# Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
|
||||
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen.
|
||||
# Gierig: ein Wesen mit höchstens halbem Leben heilen (mit der billigsten Handkarte
|
||||
# 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])
|
||||
if not playable.is_empty():
|
||||
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:
|
||||
card = c
|
||||
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()
|
||||
weakest.sort_custom(func(a, b): return _value(a) < _value(b))
|
||||
if game.can_fuse(p, weakest[0], weakest[1])[0]:
|
||||
|
||||
@@ -2,14 +2,24 @@ class_name Cards
|
||||
## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool.
|
||||
|
||||
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 := {
|
||||
"Feuer": Color("be3c23"),
|
||||
"Wasser": Color("2d64be"),
|
||||
"Erde": Color("73552d"),
|
||||
"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 := {
|
||||
"Neutral+Neutral": "Veteran",
|
||||
"Feuer+Neutral": "Feuerritter",
|
||||
"Wasser+Neutral": "Wasserritter",
|
||||
"Erde+Neutral": "Erdritter",
|
||||
"Luft+Neutral": "Luftritter",
|
||||
"Feuer+Feuer": "Inferno",
|
||||
"Wasser+Wasser": "Tiefsee",
|
||||
"Erde+Erde": "Berg",
|
||||
@@ -47,9 +57,15 @@ const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regenerat
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
@@ -98,11 +114,10 @@ static func cost_of(attack: int, health: int) -> int:
|
||||
return mini(10, (2 * attack + health) / 3)
|
||||
|
||||
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
||||
## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
|
||||
## Wert eines verschmolzenen Wesens aus den Grundwerten der Eltern: die aufgerundete Hälfte der
|
||||
## Summe plus eins. Ein starker und ein schwacher Partner ergeben also weniger als der starke.
|
||||
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).
|
||||
@@ -110,18 +125,18 @@ static func fusion_name(elements: Array) -> String:
|
||||
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
|
||||
|
||||
|
||||
## Karte des verschmolzenen Wesens: Elemente und Eigenschaften beider Karten, Werte nach
|
||||
## fusion_value (oder die übergebenen), eine Verschmelzung mehr als der erfahrenere Elternteil.
|
||||
static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
||||
if attack < 0:
|
||||
attack = fusion_value(a.attack, b.attack)
|
||||
if health < 0:
|
||||
health = fusion_value(a.health, b.health)
|
||||
## Verschmelzung zweier Karten: Elemente (sortiert) und Eigenschaften beider, Angriff und Leben
|
||||
## nach fusion_value aus den Grundwerten, Kosten daraus wie bei jeder Karte; dann die Stufe level
|
||||
## mit ihrem +1/+1 (ohne Angabe die höhere der Eltern). Das Bild trägt der stärkere Elternteil
|
||||
## (Angriff plus Leben).
|
||||
static func fused(a: Card, b: Card, level: int = -1) -> Card:
|
||||
var attack := fusion_value(a.base_attack(), b.base_attack())
|
||||
var health := fusion_value(a.base_health(), b.base_health())
|
||||
var els := []
|
||||
for e in a.elements + b.elements:
|
||||
if not (e in els):
|
||||
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()
|
||||
c.id = "fus_%s_%s" % [a.id, b.id]
|
||||
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:
|
||||
if k in a.keywords or k in b.keywords:
|
||||
c.keywords.append(k)
|
||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt
|
||||
return c
|
||||
var strong := a if a.attack + a.health >= b.attack + b.health else b
|
||||
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.
|
||||
@@ -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 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 art := "" # verschmolzen: Karte, deren Bild das Wesen trägt (der stärkere Elternteil)
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
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
|
||||
if look >= 0:
|
||||
d["look"] = look
|
||||
if art != "":
|
||||
d["art"] = art
|
||||
if cost != Cards.cost_of(attack, health):
|
||||
d["cost"] = cost
|
||||
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).
|
||||
func rarity_in(looks: Dictionary) -> String:
|
||||
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.keywords = Array(d.get("keywords", []))
|
||||
c.look = int(d.get("look", -1))
|
||||
c.art = str(d.get("art", ""))
|
||||
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.
|
||||
func copy() -> Card:
|
||||
return Card.from_dict(to_dict())
|
||||
@@ -212,14 +248,19 @@ class Card extends RefCounted:
|
||||
func combo() -> String:
|
||||
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:
|
||||
if keywords.is_empty():
|
||||
return ""
|
||||
var kws: Array[String] = []
|
||||
if is_neutral():
|
||||
kws.append(tr(Cards.NEUTRAL))
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in keywords:
|
||||
kws.append(tr(k))
|
||||
if kws.is_empty():
|
||||
return ""
|
||||
return ", ".join(kws) + "."
|
||||
|
||||
|
||||
|
||||
@@ -40,11 +40,15 @@ static func start_deck(element: String) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die teuersten zuerst; nichts doppelt.
|
||||
static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die wertvollsten zuerst (Card.value in der
|
||||
## Stufe aus levels: ID -> Stufe), bei gleichem Wert die teuersten; nichts doppelt.
|
||||
static func fill(deck: Array, pool: Array, limit: int, levels: Dictionary = {}) -> Array:
|
||||
var out := deck.duplicate()
|
||||
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id))
|
||||
cards.sort_custom(func(a, b): return a.cost > b.cost if a.cost != b.cost else a.id < b.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):
|
||||
if a.value() != b.value():
|
||||
return a.value() > b.value()
|
||||
return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||
for c in cards:
|
||||
if out.size() >= limit:
|
||||
break
|
||||
@@ -52,5 +56,16 @@ static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||
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:
|
||||
return Game.Player.new("Gegner", false, life)
|
||||
|
||||
123
scripts/game.gd
123
scripts/game.gd
@@ -4,14 +4,13 @@ extends RefCounted
|
||||
## Elementboni und Verschmelzen.
|
||||
|
||||
const MAX_BOARD := 5
|
||||
const MAX_HAND := 5
|
||||
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
||||
const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels
|
||||
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele)
|
||||
const DECK_MAX := 60 # und höchstens so viele
|
||||
const START_LIFE := 30
|
||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||
const ELEMENT_COUNT := 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
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
@@ -154,23 +153,11 @@ class Player extends RefCounted:
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
|
||||
## Eine Karte vom eigenen Stapel 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(max_cost: int = 99) -> Cards.Card:
|
||||
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 == "":
|
||||
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe; null, wenn er leer ist.
|
||||
func draw_from_deck() -> Cards.Card:
|
||||
if deck.is_empty():
|
||||
return null
|
||||
var id: String = deck.pop_back()
|
||||
return Cards.by_id(id).leveled(int(looks.get(id, 0)))
|
||||
|
||||
## Offene eigene Wesen mit diesem Element.
|
||||
@@ -295,8 +282,8 @@ func masked_clone(viewer: Player) -> Game:
|
||||
|
||||
# --- Zugablauf ---------------------------------------------------------------
|
||||
|
||||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind fünf im
|
||||
## ersten Zug bezahlbare Karten daraus.
|
||||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind die
|
||||
## obersten fünf Karten daraus, wie gemischt.
|
||||
func start(fixed_order: bool = false) -> void:
|
||||
if not fixed_order:
|
||||
shuffle(players, rng) # zufällige Zugreihenfolge
|
||||
@@ -307,7 +294,7 @@ func start(fixed_order: bool = false) -> void:
|
||||
p.deck = DeckGen.random_deck(rng)
|
||||
shuffle(p.deck, rng)
|
||||
for k in START_HAND:
|
||||
var c := p.draw_from_deck(START_MANA)
|
||||
var c := p.draw_from_deck()
|
||||
if c != null:
|
||||
p.hand.append(c)
|
||||
turn = 1
|
||||
@@ -324,8 +311,6 @@ func start(fixed_order: bool = false) -> void:
|
||||
func can_draw(p: Player) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if p.hand.size() >= MAX_HAND:
|
||||
return [false, "Deine Hand ist voll"]
|
||||
if p.deck.is_empty():
|
||||
return [false, "Dein Stapel ist leer"]
|
||||
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))
|
||||
|
||||
|
||||
## Kosten: die Hälfte der Manasumme beider Karten, aufgerundet.
|
||||
static func fusion_cost(a, b) -> int:
|
||||
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.
|
||||
## a und b: zwei verschiedene eigene Karten, auf der Hand oder als Wesen auf dem Brett, auch
|
||||
## gemischt. Verschmelzen kostet kein Mana.
|
||||
func can_fuse(p: Player, a, b) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if a == null or b == null or a == b:
|
||||
return [false, "Zwei verschiedene Karten wählen"]
|
||||
if a is Creature and b is Creature:
|
||||
if not (a in p.board) or not (b in p.board):
|
||||
return [false, "Nur eigene Wesen"]
|
||||
elif a is Creature or b is Creature:
|
||||
return [false, "Nur Hand mit Hand oder Brett mit Brett"]
|
||||
elif not (a in p.hand) or not (b in p.hand):
|
||||
return [false, "Nicht auf der Hand"]
|
||||
if p.energy < fusion_cost(a, b):
|
||||
return [false, "Nicht genug Mana"]
|
||||
for x in [a, b]:
|
||||
if not ((x in p.board) if x is Creature else (x in p.hand)):
|
||||
return [false, "Nur eigene Karten"]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
## Verschmilzt zwei Handkarten zu einer oder zwei eigene Wesen zu einem offenen: Elemente und
|
||||
## Eigenschaften beider, Werte nach Cards.fusion_value, Kosten folgen den Werten.
|
||||
## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
|
||||
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:
|
||||
var ok := can_fuse(p, a, b)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
p.energy -= fusion_cost(a, b)
|
||||
if a is Creature:
|
||||
return _fuse_board(p, a, b)
|
||||
var card := Cards.fused(a, b)
|
||||
card.look = maxi(look_of(p, a), look_of(p, b))
|
||||
var at := p.hand.find(a)
|
||||
p.hand.erase(a)
|
||||
p.hand.erase(b)
|
||||
var card := fusion_result(p, a, b)
|
||||
var at := p.hand.size()
|
||||
var gone := []
|
||||
for x in [a, b]:
|
||||
if x is Creature:
|
||||
p.board.erase(x)
|
||||
gone.append(x.uid)
|
||||
else:
|
||||
at = mini(at, p.hand.find(x))
|
||||
p.hand.erase(x)
|
||||
p.hand.insert(mini(at, p.hand.size()), card)
|
||||
say("%s und %s verschmelzen auf der Hand zu %s (%d/%d)." % [a.name, b.name, card.name, card.attack, card.health])
|
||||
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": []})
|
||||
return ""
|
||||
|
||||
|
||||
## Auf dem Brett zählen die Werte der Wesen (ohne Elementbonus, den bekommt das neue selbst);
|
||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||
var cr := Creature.new(card, p, next_uid)
|
||||
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()
|
||||
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": gone})
|
||||
return ""
|
||||
|
||||
|
||||
# --- 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:
|
||||
var a := 0
|
||||
for e in elements:
|
||||
@@ -648,12 +609,18 @@ func attack(cr: Creature, target) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Elementvorteil beim Zuschlagen und beim Konter: +1 im Vorteil, -1 im Nachteil,
|
||||
## gegen Helden gibt es keinen.
|
||||
## Elementvorteil beim Zuschlagen und beim Konter: im Vorteil zählt der Stapelbonus des Ziels
|
||||
## nicht (so viel Schaden mehr), im Nachteil der eigene nicht (so viel weniger). Gegen Helden
|
||||
## gibt es keinen.
|
||||
static func matchup_bonus(source: Creature, target) -> int:
|
||||
if not (target is Creature):
|
||||
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:
|
||||
|
||||
@@ -16,9 +16,9 @@ const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||
const DIFFICULTIES := {
|
||||
"leicht": {"name": "Leicht", "life": 25, "reward": 0.75, "smart": false},
|
||||
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true},
|
||||
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
|
||||
"leicht": {"name": "Leicht", "life": 25, "reward": 0.75, "smart": false},
|
||||
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true},
|
||||
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
|
||||
}
|
||||
# Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub.
|
||||
const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
|
||||
@@ -36,215 +36,215 @@ var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||
|
||||
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
|
||||
static func starter() -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
return p
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
return p
|
||||
|
||||
|
||||
## Hat der Spieler schon ein Startdeck gewählt?
|
||||
func started() -> bool:
|
||||
return not collection.is_empty()
|
||||
return not collection.is_empty()
|
||||
|
||||
|
||||
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||
func choose_start(element: String) -> void:
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
collection[id] = 0
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
collection[id] = 0
|
||||
|
||||
|
||||
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
||||
static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
||||
for candidate in [path, path + ".bak"]:
|
||||
if FileAccess.file_exists(candidate):
|
||||
var json := JSON.new()
|
||||
if json.parse(FileAccess.get_file_as_string(candidate)) == OK and json.data is Dictionary:
|
||||
return from_dict(json.data)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.rename_absolute(ProjectSettings.globalize_path(path), ProjectSettings.globalize_path(path) + ".corrupt")
|
||||
var p := starter()
|
||||
p.save(path)
|
||||
return p
|
||||
for candidate in [path, path + ".bak"]:
|
||||
if FileAccess.file_exists(candidate):
|
||||
var json := JSON.new()
|
||||
if json.parse(FileAccess.get_file_as_string(candidate)) == OK and json.data is Dictionary:
|
||||
return from_dict(json.data)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.rename_absolute(ProjectSettings.globalize_path(path), ProjectSettings.globalize_path(path) + ".corrupt")
|
||||
var p := starter()
|
||||
p.save(path)
|
||||
return p
|
||||
|
||||
|
||||
## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und
|
||||
## Einstellungen und bekommen die Startsammlung.
|
||||
static func from_dict(d: Dictionary) -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = int(d.get("coins", 0))
|
||||
p.dust = int(d.get("dust", 0))
|
||||
p.campaign_stage = int(d.get("campaign_stage", 0))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
var coll = d.get("collection", {})
|
||||
for cid in coll:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var ids := []
|
||||
for cid in d.get("deck", []):
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||
"big_font": bool(se.get("big_font", false))}
|
||||
if not DIFFICULTIES.has(p.settings.difficulty):
|
||||
p.settings.difficulty = "normal"
|
||||
p.perk = str(d.get("perk", ""))
|
||||
if not Game.PERKS.has(p.perk):
|
||||
p.perk = ""
|
||||
return p
|
||||
var p := Profile.new()
|
||||
p.coins = int(d.get("coins", 0))
|
||||
p.dust = int(d.get("dust", 0))
|
||||
p.campaign_stage = int(d.get("campaign_stage", 0))
|
||||
if int(d.get("version", 1)) >= SAVE_VERSION:
|
||||
var coll = d.get("collection", {})
|
||||
for cid in coll:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var ids := []
|
||||
for cid in d.get("deck", []):
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||
"big_font": bool(se.get("big_font", false))}
|
||||
if not DIFFICULTIES.has(p.settings.difficulty):
|
||||
p.settings.difficulty = "normal"
|
||||
p.perk = str(d.get("perk", ""))
|
||||
if not Game.PERKS.has(p.perk):
|
||||
p.perk = ""
|
||||
return p
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
func save(path: String = SAVE_PATH) -> void:
|
||||
var abs := ProjectSettings.globalize_path(path)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.copy_absolute(abs, abs + ".bak")
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(to_dict(), "\t"))
|
||||
f.close()
|
||||
var abs := ProjectSettings.globalize_path(path)
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.copy_absolute(abs, abs + ".bak")
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(to_dict(), "\t"))
|
||||
f.close()
|
||||
|
||||
|
||||
func take_notices() -> Array[String]:
|
||||
var out := notices.duplicate()
|
||||
notices.clear()
|
||||
return out
|
||||
var out := notices.duplicate()
|
||||
notices.clear()
|
||||
return out
|
||||
|
||||
|
||||
# --- Sammlung und Stapel -----------------------------------------------------------
|
||||
|
||||
func owns(cid: String) -> bool:
|
||||
return collection.has(cid)
|
||||
return collection.has(cid)
|
||||
|
||||
|
||||
func level(cid: String) -> int:
|
||||
return int(collection.get(cid, 0))
|
||||
return int(collection.get(cid, 0))
|
||||
|
||||
|
||||
func rarity(cid: String) -> String:
|
||||
return Cards.RARITIES[level(cid)]
|
||||
return Cards.RARITIES[level(cid)]
|
||||
|
||||
|
||||
## Karten-ID -> Stufe für den Kampf (nur aufgewertete Karten, siehe Game.Player.looks).
|
||||
func looks() -> Dictionary:
|
||||
var out := {}
|
||||
for cid in collection:
|
||||
if int(collection[cid]) > 0:
|
||||
out[cid] = int(collection[cid])
|
||||
return out
|
||||
var out := {}
|
||||
for cid in collection:
|
||||
if int(collection[cid]) > 0:
|
||||
out[cid] = int(collection[cid])
|
||||
return out
|
||||
|
||||
|
||||
## 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).
|
||||
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.
|
||||
func toggle_deck(cid: String) -> String:
|
||||
if cid in deck:
|
||||
deck.erase(cid)
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
if deck.size() >= Game.DECK_MAX:
|
||||
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||
deck.append(cid)
|
||||
return ""
|
||||
if cid in deck:
|
||||
deck.erase(cid)
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
if deck.size() >= Game.DECK_MAX:
|
||||
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||
deck.append(cid)
|
||||
return ""
|
||||
|
||||
|
||||
## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func upgrade(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
return "Schon auf der höchsten Stufe"
|
||||
var cost: int = DUST_UPGRADE[Cards.RARITIES[lvl + 1]]
|
||||
if dust < cost:
|
||||
return "Nicht genug Staub (%d nötig)" % cost
|
||||
dust -= cost
|
||||
collection[cid] = lvl + 1
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
return "Schon auf der höchsten Stufe"
|
||||
var cost: int = DUST_UPGRADE[Cards.RARITIES[lvl + 1]]
|
||||
if dust < cost:
|
||||
return "Nicht genug Staub (%d nötig)" % cost
|
||||
dust -= cost
|
||||
collection[cid] = lvl + 1
|
||||
return ""
|
||||
|
||||
|
||||
## Entzaubern: eine Stufe abgeben bringt Staub; eine Common-Karte verlässt die Sammlung.
|
||||
## Gibt "" zurück oder eine Fehlermeldung.
|
||||
func dismantle(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
else:
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
else:
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
|
||||
|
||||
## Herstellen: eine fehlende Karte für Staub als Common. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func craft(cid: String) -> String:
|
||||
if owns(cid):
|
||||
return "Karte schon in der Sammlung"
|
||||
if dust < CRAFT_COST:
|
||||
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||
dust -= CRAFT_COST
|
||||
collection[cid] = 0
|
||||
return ""
|
||||
if owns(cid):
|
||||
return "Karte schon in der Sammlung"
|
||||
if dust < CRAFT_COST:
|
||||
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||
dust -= CRAFT_COST
|
||||
collection[cid] = 0
|
||||
return ""
|
||||
|
||||
|
||||
# --- Booster ----------------------------------------------------------------------
|
||||
|
||||
## Fünf Karten mit gewürfelter Seltenheit.
|
||||
static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
var rarities := RARITY_WEIGHTS.keys()
|
||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
|
||||
rar = "Rare" # mindestens eine Rare pro Booster
|
||||
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
|
||||
return out
|
||||
var rarities := RARITY_WEIGHTS.keys()
|
||||
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
||||
var pool := Cards.all()
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
|
||||
rar = "Rare" # mindestens eine Rare pro Booster
|
||||
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
|
||||
return out
|
||||
|
||||
|
||||
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder
|
||||
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche
|
||||
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
if coins < BOOSTER_COST:
|
||||
return []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
res["upgraded"] = false
|
||||
res["dust"] = 0
|
||||
if res.new:
|
||||
collection[res.card.id] = lvl
|
||||
if deck.size() < Game.DECK_MAX:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
dust += res.dust
|
||||
return out
|
||||
if coins < BOOSTER_COST:
|
||||
return []
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
coins -= BOOSTER_COST
|
||||
var out := roll_booster(r)
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
res["new"] = not owns(res.card.id)
|
||||
res["upgraded"] = false
|
||||
res["dust"] = 0
|
||||
if res.new:
|
||||
collection[res.card.id] = lvl
|
||||
if deck.size() < Game.DECK_MAX:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
dust += res.dust
|
||||
return out
|
||||
|
||||
|
||||
# --- Kampf und Kampagne -----------------------------------------------------------
|
||||
@@ -252,20 +252,20 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
|
||||
var reward := REWARD_LOSS
|
||||
if won:
|
||||
reward = int(round(REWARD_WIN * reward_factor))
|
||||
if stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
|
||||
coins += reward
|
||||
return reward
|
||||
var reward := REWARD_LOSS
|
||||
if won:
|
||||
reward = int(round(REWARD_WIN * reward_factor))
|
||||
if stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
|
||||
coins += reward
|
||||
return reward
|
||||
|
||||
|
||||
func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
func perk_unlocked() -> bool:
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
|
||||
@@ -139,14 +139,31 @@ func pop_in(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:
|
||||
tile.queue_free()
|
||||
view.queue_free()
|
||||
return
|
||||
var tw := tile.create_tween().set_parallel(true)
|
||||
tw.tween_property(tile, "modulate:a", 0.0, 0.3)
|
||||
tw.tween_property(tile, "scale", Vector2(0.6, 0.6), 0.3)
|
||||
tw.chain().tween_callback(tile.queue_free)
|
||||
var tw := view.create_tween().set_parallel(true)
|
||||
tw.tween_property(view, "modulate:a", 0.0, 0.3)
|
||||
tw.tween_property(view, "scale", Vector2(0.6, 0.6), 0.3)
|
||||
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:
|
||||
|
||||
@@ -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 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_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 OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
|
||||
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 DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
|
||||
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 140
|
||||
const HAND_GAP := 10
|
||||
const HAND_Y := 610
|
||||
const OWN_X := 295
|
||||
const HAND_W := 130 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 165
|
||||
const HAND_GAP := 6
|
||||
const HAND_Y := 605
|
||||
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 PREVIEW_SIZE := Vector2(170, 238)
|
||||
@@ -29,19 +29,23 @@ const CENTER_W := 800
|
||||
|
||||
|
||||
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.
|
||||
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:
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -5,8 +5,9 @@ extends Control
|
||||
## die Leertaste oder der Rechtsklick ins Leere.
|
||||
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
||||
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
||||
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||
## Verschmelzen: eine Handkarte oder ein eigenes Wesen auf ein anderes ziehen und loslassen; das
|
||||
## 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.
|
||||
|
||||
var app
|
||||
@@ -17,15 +18,13 @@ var fx: BattleFx
|
||||
var selected = null # Game.Creature
|
||||
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 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_view: Control = null
|
||||
var press_pos := Vector2.ZERO
|
||||
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
|
||||
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
|
||||
var drag_target_view: Control = null
|
||||
var hold_t := 0.0
|
||||
var hold_pos := Vector2.ZERO
|
||||
var msg_t := 0.0
|
||||
var ai_delay := 0.75
|
||||
var ai_timer := 0.0
|
||||
@@ -77,6 +76,7 @@ var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||||
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
||||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||
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 overlay: Control
|
||||
var result_label: Label
|
||||
@@ -408,15 +408,6 @@ func _process(delta: float) -> void:
|
||||
undo_t -= delta
|
||||
if undo_t <= 0.0:
|
||||
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 not ended:
|
||||
ended = true
|
||||
@@ -487,7 +478,7 @@ func cancel() -> void:
|
||||
_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:
|
||||
_end_drag()
|
||||
@@ -505,23 +496,28 @@ func _drag_motion(pos: Vector2) -> void:
|
||||
_start_drag(pos)
|
||||
drag_ghost.position = pos - drag_ghost.size / 2
|
||||
var t = _fusion_target_at(pos)
|
||||
if t != drag_target or pos.distance_to(hold_pos) > 6.0:
|
||||
_set_hold(0.0)
|
||||
if t != drag_target:
|
||||
_mark_target(false)
|
||||
drag_target = 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:
|
||||
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
|
||||
press_target = null
|
||||
_end_drag()
|
||||
if obj == null:
|
||||
return
|
||||
if dragged:
|
||||
refresh()
|
||||
if target != null:
|
||||
open_fuse_confirm(obj, target, target_rect)
|
||||
else:
|
||||
refresh()
|
||||
elif obj is Cards.Card:
|
||||
on_click("hand", obj)
|
||||
else:
|
||||
@@ -549,24 +545,27 @@ func _end_drag() -> void:
|
||||
drag_ghost = null
|
||||
if is_instance_valid(press_view):
|
||||
press_view.modulate.a = 1.0
|
||||
_set_hold(0.0)
|
||||
_mark_target(false)
|
||||
drag_target = null
|
||||
drag_target_view = null
|
||||
|
||||
|
||||
func _set_hold(v: float) -> void:
|
||||
hold_t = v
|
||||
func _mark_target(on: bool) -> void:
|
||||
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()
|
||||
|
||||
|
||||
## 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):
|
||||
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 r: Rect2 = Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect() # Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten
|
||||
if obj != press_target and r.has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||||
if obj != press_target and _rect_of(v).has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||||
return obj
|
||||
return null
|
||||
|
||||
@@ -581,9 +580,9 @@ func _view_of(obj) -> Control:
|
||||
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:
|
||||
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])
|
||||
|
||||
|
||||
@@ -675,6 +674,37 @@ func close_menu() -> void:
|
||||
menu_layer.visible = false
|
||||
Style.clear(menu_layer)
|
||||
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.
|
||||
@@ -696,13 +726,26 @@ func do_draw() -> void:
|
||||
fx.sfx("buy")
|
||||
|
||||
|
||||
## Verschmilzt zwei Handkarten oder zwei eigene Wesen (siehe Game.fuse).
|
||||
func do_fuse(a, b) -> void:
|
||||
## Verschmilzt zwei eigene Karten zur neuen Handkarte (siehe Game.fuse). from: von dort gleitet
|
||||
## 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()
|
||||
clear_undo()
|
||||
var before: Array = perspective.hand.duplicate()
|
||||
var err := game.fuse(perspective, a, b)
|
||||
if 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:
|
||||
@@ -747,7 +790,8 @@ func on_click(kind: String, obj) -> void:
|
||||
var first = fuse_first
|
||||
fuse_first = null
|
||||
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:
|
||||
say(tr("Abgebrochen"))
|
||||
after_action()
|
||||
@@ -846,6 +890,8 @@ func creature_info(cr: Game.Creature) -> String:
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
if cr.card.is_neutral():
|
||||
parts.append(tr(Cards.NEUTRAL_TEXT))
|
||||
for k in cr.keywords():
|
||||
if not (k in cr.card.keywords):
|
||||
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)
|
||||
v.rotation_degrees = arc[0]
|
||||
v.position.y += arc[1]
|
||||
if hand_arrivals.has(card):
|
||||
fx.glide(v, hand_arrivals[card])
|
||||
hand_arrivals.erase(card)
|
||||
v.lift = mine
|
||||
v.bonus = g.forecast(h, card)
|
||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||
|
||||
@@ -5,9 +5,9 @@ extends ScrollContainer
|
||||
signal card_clicked(card: Cards.Card)
|
||||
|
||||
const COLS := 6
|
||||
const CW := 112
|
||||
const CH := 157
|
||||
const GAP := 14
|
||||
const CW := 118
|
||||
const CH := 165
|
||||
const GAP := 10
|
||||
|
||||
var grid: GridContainer
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
class_name CardView
|
||||
extends Control
|
||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
||||
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Eigenschaften als
|
||||
## Symbole, Angriff und Leben als Kreise unten. Das erste Element färbt den Hintergrund, weitere
|
||||
## Elemente zeigt ein Medaillon als Tortenstücke hinter dem Bild; die Seltenheit steckt in Name,
|
||||
## Rahmen, Glanz und Edelstein (siehe Style). Sehr flache Karten zeigen nur das Nötigste.
|
||||
## Oben Kosten (links) und Name, in der Mitte das Bild im Kreis, unten Angriff (links) und
|
||||
## Leben (rechts). Die Elemente färben den Hintergrund (verschmolzen: ein Streifen je Element),
|
||||
## das Bild trägt bei Verschmelzungen der stärkere Elternteil; die Seltenheit steckt in Name,
|
||||
## Bild, Werten und Rahmen (siehe Style).
|
||||
|
||||
signal clicked(view: CardView)
|
||||
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 lift := false # hebt sich beim Zeigen an (Hand)
|
||||
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
|
||||
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
|
||||
var drop := false # Ziel der gerade gezogenen Karte
|
||||
|
||||
|
||||
## 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":
|
||||
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)
|
||||
if size.y < 90:
|
||||
_draw_compact(rect, frame, seed)
|
||||
else:
|
||||
_draw_full(rect, frame, seed)
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 140
|
||||
var f_name := 17 if big else (14 if mid else 11)
|
||||
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:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||
if hold > 0.0:
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
|
||||
|
||||
## Volle Karte: 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)
|
||||
if drop:
|
||||
Style.border(self, rect, Style.GOLD, 5, 8)
|
||||
|
||||
@@ -21,13 +21,14 @@ var b_craft: Button
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
for i in filters.size():
|
||||
var e: String = filters[i]
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(444, 86)
|
||||
search_box.size = Vector2(220, 34)
|
||||
search_box.position = Vector2(526, 86)
|
||||
search_box.size = Vector2(190, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
|
||||
@@ -12,11 +12,11 @@ var width := 2
|
||||
var flash := false # kurz rot: wurde gerade angegriffen
|
||||
var slot: Vector2 # Zielposition im Raster
|
||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (+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 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 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:
|
||||
@@ -54,7 +54,7 @@ func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
if secret:
|
||||
_draw_back(rect)
|
||||
_draw_hold()
|
||||
_draw_drop()
|
||||
return
|
||||
var small := size.x < 100
|
||||
var rar := rarity()
|
||||
@@ -62,44 +62,41 @@ func _draw() -> void:
|
||||
if rar != "Common":
|
||||
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 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)
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
var fs := 11 if small else 13
|
||||
var name_w: float = size.x - 6
|
||||
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
||||
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
|
||||
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)
|
||||
# Elementbonus als ein Abzeichen rechts unter dem Namen; die Symbole rücken dafür nach links
|
||||
var badge := ""
|
||||
var bw := 0.0
|
||||
if creature.aura != 0:
|
||||
badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||
var by := Style.fs(fs) + 13
|
||||
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
||||
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, by + 8), "center", true)
|
||||
# Eigenschaften als Symbole in der Farbe des Elements, das sie gibt
|
||||
var icon := 7.0 if small else 9.0
|
||||
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 shiny := Style.rarity_stats_shiny(rar)
|
||||
# Oben: Kosten links, daneben der Name; das Namensfeld ist genau so hoch wie der Kristall
|
||||
var fs := 12 if small else 13
|
||||
var r_cost := 10.0 if small else 11.0
|
||||
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, creature.card.title(), fs, rar, Rect2(r_cost * 2 + 10, 2, size.x - r_cost * 2 - 16, top_h), seed)
|
||||
# Mitte: das Bild im Kreis; unten Angriff links, Leben rechts
|
||||
var r_stat := 12.0 if small else 14.0
|
||||
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, creature.card, Vector2(size.x / 2, (pic_top + pic_bottom) / 2), r_pic, rar, seed)
|
||||
# Unter dem Namen links die Eigenschaften (in der Farbe des Elements, das sie gibt), rechts der Elementbonus
|
||||
var icon := 6.0 if small else 7.0
|
||||
var y := top_h + 6 + icon
|
||||
for k in creature.keywords():
|
||||
var col := Color("f2f2f6")
|
||||
for e in creature.card.elements:
|
||||
if Cards.ELEMENT_KEYWORD.get(e, "") == k:
|
||||
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col)
|
||||
x += icon * 2 + 6
|
||||
_draw_stats(15 if small else 17, 12.0 if small else 14.0)
|
||||
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||
Style.keyword_icon(self, k, Vector2(6 + icon, y), icon, col)
|
||||
y += icon * 2 + 6
|
||||
if creature.aura != 0:
|
||||
var badge := "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||
var bw := Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||
draw_rect(Rect2(size.x - bw - 4, top_h + 5, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
||||
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, top_h + 13), "center", true)
|
||||
_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:
|
||||
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)
|
||||
@@ -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))
|
||||
Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3)
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
_draw_hold()
|
||||
_draw_drop()
|
||||
|
||||
|
||||
## Ring, der sich füllt, solange eine Karte zum Verschmelzen darüber gehalten wird.
|
||||
func _draw_hold() -> void:
|
||||
if hold <= 0.0:
|
||||
return
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
## Goldener Rahmen, solange eine Karte zum Verschmelzen darüber schwebt.
|
||||
func _draw_drop() -> void:
|
||||
if drop:
|
||||
Style.border(self, Rect2(Vector2.ZERO, size), Style.GOLD, 5, 6)
|
||||
|
||||
|
||||
## 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.
|
||||
func _draw_stats(f: int, r: float) -> void:
|
||||
func _draw_stats(f: int, r: float, shiny: bool) -> void:
|
||||
var shown := shown_stats()
|
||||
var atk: int = shown[0]
|
||||
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 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(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_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, shiny)
|
||||
|
||||
@@ -2,20 +2,23 @@ class_name DeckScreen
|
||||
extends Control
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
|
||||
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
|
||||
## 40 bis 60 Karten; „Auffüllen“ nimmt die 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 filter := ""
|
||||
var grid: CardGrid
|
||||
var count_label: Label
|
||||
var strength_label: Label
|
||||
var msg_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var filters: Array = Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
for i in filters.size():
|
||||
var e: String = filters[i]
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||
@@ -23,11 +26,13 @@ func _ready() -> void:
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
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")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center")
|
||||
strength_label = Style.chip(self, "", Rect2(860, 144, 340, 36), Style.GOLD)
|
||||
var hint := Style.label(self, "", 15, Style.MUTED, Rect2(840, 188, 380, 66), "center")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD # vor dem Text, sonst wächst die Breite auf eine Zeile
|
||||
hint.text = "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die wertvollsten Karten des Filters."
|
||||
Style.button(self, "Auffüllen", Rect2(840, 262, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 318, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 382, 380, 48), "center")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -50,13 +55,13 @@ func toggle(card: Cards.Card) -> void:
|
||||
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:
|
||||
var p: Profile = app.profile
|
||||
var pool: Array = p.collection.keys()
|
||||
if filter != "":
|
||||
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()
|
||||
refresh()
|
||||
|
||||
@@ -67,7 +72,7 @@ func clear_deck() -> void:
|
||||
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:
|
||||
var p: Profile = app.profile
|
||||
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
|
||||
if ia != ib:
|
||||
return ia
|
||||
if p.level(a.id) != p.level(b.id):
|
||||
return p.level(a.id) > p.level(b.id)
|
||||
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||
return out
|
||||
|
||||
@@ -86,6 +93,7 @@ func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
|
||||
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
|
||||
strength_label.text = tr("Stärke %d") % DeckGen.strength(p.deck, p.collection)
|
||||
var shown := cards()
|
||||
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||
var views := grid.views()
|
||||
|
||||
@@ -2,8 +2,8 @@ class_name ElementView
|
||||
extends Control
|
||||
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
|
||||
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
|
||||
## offenem Wesen, beide: Effekt aktiv), die Zahl der ausgespielten Karten und ab
|
||||
## Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
## offenem Wesen, beide: Effekt aktiv; Neutral hat keinen), die Zahl der ausgespielten Karten
|
||||
## und ab Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
|
||||
const ROW := 32.0
|
||||
|
||||
@@ -15,20 +15,24 @@ func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in Cards.ELEMENTS:
|
||||
for e in all():
|
||||
own[e] = 0
|
||||
played[e] = 0
|
||||
|
||||
|
||||
static func all() -> Array:
|
||||
return Cards.ELEMENTS + [Cards.NEUTRAL]
|
||||
|
||||
|
||||
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
|
||||
played[e] = int(me.played.get(e, 0)) if me != null else 0
|
||||
queue_redraw()
|
||||
|
||||
|
||||
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:
|
||||
@@ -37,7 +41,7 @@ func bonus(e: String) -> int:
|
||||
|
||||
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||
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.
|
||||
@@ -62,9 +66,10 @@ func _draw() -> void:
|
||||
var px := 10.0
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
|
||||
px = 76
|
||||
for k in Game.ELEMENT_PAIR:
|
||||
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))
|
||||
if Cards.ELEMENT_KEYWORD.has(e):
|
||||
for k in Game.ELEMENT_PAIR:
|
||||
var filled := k < int(own.get(e, 0))
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||
px += Game.ELEMENT_PAIR * 24 + 8
|
||||
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||
if bonus(e) > 0:
|
||||
|
||||
@@ -91,6 +91,7 @@ func _map_gamepad() -> void:
|
||||
if not InputMap.action_get_events(action).any(func(e): return e is InputEventJoypadButton):
|
||||
var ev := InputEventJoypadButton.new()
|
||||
ev.button_index = pair[1]
|
||||
ev.device = -1 # jedes Gamepad
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func _ready() -> void:
|
||||
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
|
||||
for k in 3:
|
||||
var c: Cards.Card = Cards.by_id(sample[k * 7])
|
||||
var v := CardView.new(c, Vector2(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.rotation_degrees = -6 + k * 6
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
@@ -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.
|
||||
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)
|
||||
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).
|
||||
@@ -309,13 +311,14 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
|
||||
|
||||
# --- Seltenheit als Aussehen ------------------------------------------------------
|
||||
# Common matt; Rare silberner Name und Rahmen mit stehendem Glanzstreifen; Super Rare Holo-Bild
|
||||
# (wandernder Regenbogenschimmer); Ultra Rare goldener Name und Rahmen plus Holo-Bild;
|
||||
# Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln, schillernde Farben.
|
||||
# Common matt; Rare silbern glänzender Name; Super Rare Holo-Bild (wandernder Regenbogen im
|
||||
# Bildkreis); Ultra Rare goldener Name und Holo-Bild; Ultimate Rare goldener Name, goldenes
|
||||
# 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:
|
||||
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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
## Farbe des Kartennamens: Rare silbern, Ultra Rare golden, Ultimate Rare schillernd, sonst weiß.
|
||||
static func rarity_name_color(rar: String, seed: int = 0) -> Color:
|
||||
## Farbe des Kartennamens: Rare silbern, Ultra und Ultimate Rare golden, sonst weiß.
|
||||
static func rarity_name_color(rar: String) -> Color:
|
||||
match rar:
|
||||
"Rare", "Ultra Rare", "Ultimate Rare":
|
||||
return lighter(rarity_color(rar, seed), 0.25)
|
||||
"Rare":
|
||||
return Cards.RARITY_COLORS["Rare"]
|
||||
"Ultra Rare", "Ultimate Rare":
|
||||
return Cards.RARITY_COLORS["Ultra Rare"]
|
||||
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).
|
||||
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.
|
||||
## Ab Super Rare: Holo, ein wandernder Regenbogenschimmer über das Bild (picture); Ultimate Rare
|
||||
## zusätzlich schwach über die ganze Karte.
|
||||
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int, picture: Rect2 = Rect2()) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 1:
|
||||
return
|
||||
if picture.size.x <= 0:
|
||||
picture = rect
|
||||
if lvl == 1:
|
||||
_sheen_bands(ci, rect, (seed % 100) / 100.0, [[1.0, Color(1, 1, 1, 0.07)], [0.55, Color(1, 1, 1, 0.09)], [0.22, Color(1, 1, 1, 0.13)]])
|
||||
## Kartenname links in rect, immer eine Zeile, nie abgeschnitten: die Schrift schrumpft bis
|
||||
## drei Punkte unter die Vorgabe, danach wird der Text schmaler gezeichnet (wie bei langen
|
||||
## Yu-Gi-Oh-Namen).
|
||||
static func card_name(ci: CanvasItem, s: String, size_in: int, rar: String, rect: Rect2, seed: int) -> void:
|
||||
var fsize := fit_size(s, true, size_in, rect.size.x, maxi(8, size_in - 3))
|
||||
var w := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(fsize)).x
|
||||
var squeeze := minf(1.0, rect.size.x / maxf(w, 1.0))
|
||||
var y := rect.position.y + (rect.size.y - FONT_BOLD.get_height(fs(fsize))) / 2
|
||||
ci.draw_set_transform(Vector2(rect.position.x, y), 0.0, Vector2(squeeze, 1.0))
|
||||
if rarity_name_shiny(rar):
|
||||
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
|
||||
var phase := (seed % 100) / 100.0
|
||||
if animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||
var holo := []
|
||||
for i in 4:
|
||||
holo.append([1.0 - i * 0.22, Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)])
|
||||
_sheen_bands(ci, picture, phase, holo)
|
||||
if lvl >= 4:
|
||||
var soft := []
|
||||
for i in 3:
|
||||
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
|
||||
_sheen_bands(ci, rect, fmod(phase + 0.5, 1.0), soft)
|
||||
var soft := []
|
||||
for i in 3:
|
||||
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
|
||||
_sheen_bands(ci, rect_poly(rect), rect, fmod(phase + 0.5, 1.0), soft)
|
||||
|
||||
|
||||
## Schräge Streifen über rect, auf rect beschnitten. layers: [Breitenanteil, Farbe].
|
||||
static func _sheen_bands(ci: CanvasItem, rect: Rect2, phase: float, layers: Array) -> void:
|
||||
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
var w := rect.size.x * 0.34
|
||||
var k := rect.size.y * 0.75 # Schräge
|
||||
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
|
||||
static func rect_poly(rect: Rect2) -> PackedVector2Array:
|
||||
return PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
|
||||
|
||||
static func circle_poly(c: Vector2, r: float, steps: int = 40) -> PackedVector2Array:
|
||||
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:
|
||||
var lw: float = w * layer[0]
|
||||
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)])
|
||||
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||
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, clip):
|
||||
if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen
|
||||
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.
|
||||
## avoid: Bereich, der frei bleibt (der Kartentext).
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
|
||||
if Cards.RARITIES.find(rar) < 4:
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||
if rar != "Ultimate Rare":
|
||||
return
|
||||
var t := Time.get_ticks_msec() / 1000.0
|
||||
for i in 9:
|
||||
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 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
|
||||
var s := 2.0 + 4.0 * 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)
|
||||
|
||||
|
||||
## Edelstein in der Farbe der Stufe (ab Rare).
|
||||
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
|
||||
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.
|
||||
## Hintergrund in den Farben der Elemente: ein Element füllt die Karte, verschmolzene Wesen
|
||||
## tragen je Element einen senkrechten Streifen.
|
||||
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)
|
||||
|
||||
|
||||
## Medaillon eines verschmolzenen Wesens: ein Kreis hinter dem Bild, als Tortenstücke in den
|
||||
## Farben seiner Elemente, mit dunklem Rand.
|
||||
static func element_medallion(ci: CanvasItem, c: Vector2, r: float, elements: Array) -> void:
|
||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75))
|
||||
var n := elements.size()
|
||||
for i in n:
|
||||
var from := -PI / 2 + TAU * i / n
|
||||
var to := -PI / 2 + TAU * (i + 1) / n
|
||||
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)
|
||||
if elements.size() < 2:
|
||||
return
|
||||
var clip := rounded_poly(rect, radius)
|
||||
var w := rect.size.x / elements.size()
|
||||
for i in range(1, elements.size()):
|
||||
var strip := Rect2(rect.position.x + w * i, rect.position.y, w, rect.size.y)
|
||||
for poly in Geometry2D.intersect_polygons(rect_poly(strip), clip):
|
||||
if poly.size() >= 3:
|
||||
ci.draw_colored_polygon(poly, Cards.ELEMENT_COLORS[elements[i]])
|
||||
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)
|
||||
|
||||
|
||||
# --- Kartenteile ------------------------------------------------------
|
||||
|
||||
## Kostenkristall: sechseckig, blau, Zahl in der Mitte.
|
||||
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void:
|
||||
## Bild der Karte: dunkler Kreis mit Ring in der Elementfarbe, darin das Symbol der Bildkarte
|
||||
## (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 outer := PackedVector2Array()
|
||||
for i in 6:
|
||||
var a := -PI / 2 + i * PI / 3
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * r)
|
||||
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)
|
||||
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))
|
||||
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.
|
||||
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE) -> void:
|
||||
ci.draw_circle(c, r + 2, Color("14141c"))
|
||||
## 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, shiny: bool = false) -> void:
|
||||
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 + 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,
|
||||
@@ -568,6 +657,11 @@ static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: fl
|
||||
for i in 2 + variant:
|
||||
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))
|
||||
_: # 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:
|
||||
|
||||
@@ -68,6 +68,7 @@ func _initialize() -> void:
|
||||
test_elementboni()
|
||||
test_spott_und_regeneration()
|
||||
test_verschmelzen()
|
||||
test_ki_verschmelzen()
|
||||
test_limits()
|
||||
test_gleiche_regeln()
|
||||
test_zugreihenfolge()
|
||||
@@ -95,20 +96,21 @@ func _initialize() -> void:
|
||||
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:
|
||||
check(Cards.all().size() == 120, "120 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")
|
||||
for e in Cards.ELEMENTS:
|
||||
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 or c.elements[0] == Cards.NEUTRAL)), "jede Karte hat genau ein Element oder ist neutral")
|
||||
for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
|
||||
check(Cards.all().filter(func(c): return c.elements == [e]).size() == 30, "30 Karten je Element: " + e)
|
||||
var names := {}
|
||||
for c in Cards.all():
|
||||
var parts: PackedStringArray = c.id.split("_")
|
||||
check(c.attack == int(parts[1]) and c.health == int(parts[2]) and c.attack <= 5 and c.health >= 1 and c.health <= 5, "ID passt zu den Werten: " + c.id)
|
||||
check(c.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
|
||||
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(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")
|
||||
@@ -123,6 +125,7 @@ func test_kombos() -> void:
|
||||
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(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.
|
||||
@@ -144,7 +147,7 @@ func test_mana_und_kosten() -> void:
|
||||
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.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
|
||||
@@ -172,10 +175,17 @@ func test_stapel() -> void:
|
||||
var others := sd.filter(func(id): return Cards.by_id(id).elements[0] == o)
|
||||
check(others.size() == 5 and others.all(func(id): return Cards.by_id(id).cost <= Game.START_MANA), "Startdeck %s: fünf billige %s" % [e, o])
|
||||
var filled := DeckGen.fill(["f_0_1"], ["f_0_1", "f_2_1", "e_0_4", "l_1_1", "w_5_5"], 3)
|
||||
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.
|
||||
func test_ziehen() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
@@ -184,13 +194,12 @@ func test_ziehen() -> void:
|
||||
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.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")
|
||||
var first: Game.Player = g.current()
|
||||
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
||||
first.hand.pop_back()
|
||||
for i in 8:
|
||||
first.hand.append(first.draw_from_deck())
|
||||
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()
|
||||
check(g.draw_cost(first) == 1 and g.draw(first) == "" and first.energy == 0, "zweites Ziehen kostet 1")
|
||||
first.hand.pop_back()
|
||||
@@ -210,11 +219,11 @@ func test_ziehen() -> void:
|
||||
g.end_turn()
|
||||
check(first.life == Game.START_LIFE - 3, "und dann 2, 3 …")
|
||||
# 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 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(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.
|
||||
@@ -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(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(["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 g: Game = s[0]
|
||||
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 life: int = o.life
|
||||
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 w3 := put(s3[1], "f_3_2")
|
||||
var t3 := put(s3[2], "w_4_5")
|
||||
s3[0].attack(w3, t3)
|
||||
check(t3.health == 5 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
|
||||
# 0 Angriff: nur mit Elementvorteil geht etwas
|
||||
var w3 := put(s3[1], "w_4_5")
|
||||
var f3 := put(s3[2], "f_3_2")
|
||||
s3[2].played["Feuer"] = 6
|
||||
s3[0].apply_auras()
|
||||
check(f3.aura == 2 and f3.power() == 5 and f3.max_health == 4 and Game.matchup_bonus(w3, f3) == 2 and Game.matchup_bonus(f3, w3) == -2, "Feuer mit +2/+2: Wasser schlägt 2 mehr, Feuer 2 weniger")
|
||||
s3[0].attack(w3, f3)
|
||||
check(f3.health <= 0 and w3.health == 5 - 3, "Wasser trifft für 4 + 2 durch den Bonus, der Konter kommt ohne Bonus: 3")
|
||||
# Im Nachteil fehlt der eigene Angriffsbonus; der Konter geht durch den eigenen Lebensbonus
|
||||
var s4 := setup()
|
||||
var funke := put(s4[1], "f_0_2") # Feuer 0/2
|
||||
var hauch := put(s4[2], "l_0_2") # Luft 0/2
|
||||
var wall := put(s4[2], "e_0_4") # Erde 0/4
|
||||
check(s4[0].can_attack(funke, wall)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff ohne Vorteil: kein Angriff")
|
||||
check(s4[0].attack(funke, hauch) == "" and hauch.health == 1 and funke.health == 2, "0 Angriff mit Vorteil: 1 Schaden, kein Konter")
|
||||
var f4 := put(s4[1], "f_3_2")
|
||||
var w4 := put(s4[2], "w_4_5")
|
||||
s4[1].played["Feuer"] = 6
|
||||
s4[0].apply_auras()
|
||||
s4[0].attack(f4, w4)
|
||||
check(w4.health == 5 - 3 and f4.health <= 0, "Feuer mit +2/+2 greift Wasser an: schlägt ohne Bonus für 3, der Konter 4 + 2 geht durch den Bonus")
|
||||
# 0 Angriff: nur der Stapelbonus des Ziels gibt Schaden
|
||||
var s5 := setup()
|
||||
var hauch2 := put(s5[1], "l_0_2")
|
||||
var funke2 := put(s5[2], "f_0_2")
|
||||
check(s5[0].attack(hauch2, funke2) == "Leiser Bote würde keinen Schaden machen" and funke2.health == 2, "im Nachteil nichts")
|
||||
var wall2 := put(s5[2], "e_0_4")
|
||||
var funke := put(s5[1], "f_0_2") # Feuer 0/2
|
||||
var hauch := put(s5[2], "l_0_2") # Luft 0/2
|
||||
check(s5[0].can_attack(funke, hauch)[1] == "Schwelender Kobold würde keinen Schaden machen", "0 Angriff, Ziel ohne Bonus: kein Angriff")
|
||||
s5[2].played["Luft"] = 3
|
||||
s5[0].apply_auras()
|
||||
check(hauch.power() == 1 and hauch.health == 3 and s5[0].attack(funke, hauch) == "" and hauch.health == 2 and funke.health == 2, "Ziel mit +1/+1: 0 Angriff trifft durch den Bonus für 1, der Konter 1 − 1 bleibt aus")
|
||||
var wall := put(s5[2], "e_0_4")
|
||||
var geist5 := put(s5[1], "w_2_2") # Wasser 2/2
|
||||
check(s5[0].attack(geist5, 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 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])
|
||||
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
|
||||
@@ -321,7 +345,7 @@ func test_verdeckt() -> void:
|
||||
|
||||
|
||||
## 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.
|
||||
func test_elementboni() -> void:
|
||||
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)
|
||||
g.apply_auras()
|
||||
check(not hidden_fire.has("Doppelschlag") and o.element_count("Feuer") == 0, "verdeckte zählen nicht fürs Paar")
|
||||
# Ausgespielte Karten zählen; je fünf gibt es +1/+1 für die eigenen Wesen des Elements
|
||||
h.played["Feuer"] = 4
|
||||
g.play_card(h, h.hand[0]) # Flammender Wolf: die fünfte 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")
|
||||
# Ausgespielte Karten zählen; je drei gibt es +1/+1 für die eigenen Wesen des Elements
|
||||
h.played["Feuer"] = 2
|
||||
g.play_card(h, h.hand[0]) # Flammender Wolf: die dritte Feuerkarte
|
||||
check(int(h.played["Feuer"]) == 3 and fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and h.board[2].aura == 1, "drei Feuerkarten ausgespielt: +1/+1 für alle eigenen Feuerwesen")
|
||||
check(bird.aura == 0 and bird.health == 1 and hidden_fire.aura == 0, "der Gegner zählt für sich: nichts")
|
||||
g._flip(hidden_fire)
|
||||
check(hidden_fire.aura == 0 and int(o.played["Feuer"]) == 0, "aufgedeckt, aber nie ausgespielt gezählt: kein Bonus")
|
||||
g.play_card(h, h.hand[0], true) # zweiter Wolf verdeckt
|
||||
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)
|
||||
check(secret.aura == 1 and secret.power() == 5 and secret.health == 4, "aufgedeckt kommt der Bonus")
|
||||
# Vorschau
|
||||
h.played["Luft"] = 4
|
||||
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")
|
||||
h.played["Luft"] = 2
|
||||
check(g.forecast(h, C("l_1_1")) == 1 and g.forecast(h, C("w_2_2")) == 0, "Vorschau: der Hauch wäre die dritte Luftkarte und bekäme +1/+1")
|
||||
# Merkmal: doppelt
|
||||
h.perks = ["element_doppelt"]
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 2 and fire[0].power() == 5 and fire[0].max_health == 4, "Elementbonus zählt doppelt")
|
||||
h.perks = []
|
||||
h.played["Feuer"] = 10
|
||||
h.played["Feuer"] = 6
|
||||
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
|
||||
h.played["Feuer"] = 0
|
||||
g.apply_auras()
|
||||
@@ -383,10 +407,21 @@ func test_elementboni() -> void:
|
||||
g2.apply_auras()
|
||||
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
|
||||
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
|
||||
h2.played["Feuer"] = 5
|
||||
h2.played["Wasser"] = 5
|
||||
h2.played["Feuer"] = 3
|
||||
h2.played["Wasser"] = 3
|
||||
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.
|
||||
@@ -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")
|
||||
|
||||
|
||||
## Verschmelzen: besserer Wert plus ein Drittel des schwächeren, Eigenschaften beider, Kosten die
|
||||
## halbe Manasumme; auch auf der Hand.
|
||||
## Verschmelzen: kostenlos, Hand und Brett auch gemischt; das Ergebnis kommt unverletzt auf die
|
||||
## Hand. Je Wert die aufgerundete halbe Grundwertsumme plus eins, Kosten daraus, dann die höhere
|
||||
## Stufe der Eltern obendrauf.
|
||||
func test_verschmelzen() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var a := put(h, "f_3_2") # 3/2, Kosten 2
|
||||
var b := put(h, "w_4_5") # 4/5, Kosten 4
|
||||
b.health = 4
|
||||
b.health = 1
|
||||
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.size() == 1 and h.energy == 7, "verschmolzen")
|
||||
var f: Game.Creature = h.board[0]
|
||||
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.card.keywords and f.card.cost == 5, "Eigenschaften geerbt, Kosten aus den neuen Werten")
|
||||
check(f.card.elements == ["Feuer", "Wasser"] and f.ready, "beide Elemente, beide waren bereit")
|
||||
var c := put(h, "e_0_4", false)
|
||||
check(Game.fusion_cost(f, c) == 3, "Dampf (5) mit Hüter (1): 3 Mana")
|
||||
g.fuse(h, f, c)
|
||||
var f2: Game.Creature = h.board[0]
|
||||
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")
|
||||
var d := put(h, "l_1_1")
|
||||
check(Game.fusion_cost(f2, d) == ceili((f2.card.cost + 1) / 2.0), "Kosten folgen den Karten")
|
||||
h.energy = 1
|
||||
check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer")
|
||||
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
|
||||
check(Game.fusion_cost(put(h, "f_0_2"), put(h, "w_0_2")) == 0, "zwei Karten für 0 verschmelzen umsonst")
|
||||
# Verdeckte verschmelzen offen
|
||||
var s2 := setup()
|
||||
var x := put(s2[1], "f_3_2", true, true)
|
||||
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"])
|
||||
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")
|
||||
var f: Cards.Card = h.hand[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("Spott" in f.keywords and f.elements == ["Feuer", "Wasser"] and f.look == 0, "Eigenschaften und Elemente beider, Stufe Common")
|
||||
check(g.play_card(h, f) == "" and h.energy == 5 and h.board[0].health == 5 and not h.board[0].ready, "ausgespielt für die neuen Kosten, unverletzt, erst nächsten Zug bereit")
|
||||
check(h.played["Feuer"] == 1 and h.played["Wasser"] == 1, "zählt beim Legen für beide Elemente")
|
||||
# Wesen mit Handkarte, auch verdeckt; stark plus schwach ergibt weniger als das starke
|
||||
var dampf: Game.Creature = h.board[0]
|
||||
var secret := put(h, "e_0_4", false, true)
|
||||
h.hand = [C("l_1_1")]
|
||||
check(g.fuse(h, dampf, h.hand[0]) == "" and h.hand.size() == 1 and h.board == [secret], "Wesen mit Handkarte verschmolzen")
|
||||
var chi: Cards.Card = h.hand[0]
|
||||
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")
|
||||
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.can_fuse(h, h.hand[0], h.hand[0])[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")
|
||||
h.energy = 0
|
||||
check(g.can_fuse(h, h.hand[0], put(h, "f_2_1"))[0], "ohne Mana geht es trotzdem")
|
||||
# Stufe: aus den Grundwerten gerechnet, dann die höhere Stufe obendrauf
|
||||
var s4 := setup(["f_3_2", "l_1_1"])
|
||||
var g4: Game = s4[0]
|
||||
var h4: Game.Player = s4[1]
|
||||
var hand_a: Cards.Card = h4.hand[0]
|
||||
var hand_b: Cards.Card = h4.hand[1]
|
||||
check(g4.can_fuse(h4, hand_a, put(h4, "e_0_4"))[1] == "Nur Hand mit Hand oder Brett mit Brett", "Hand und Brett mischen sich nicht")
|
||||
check(g4.fuse(h4, hand_a, hand_b) == "" and h4.hand.size() == 2 and h4.energy == 7, "zwei Handkarten verschmolzen, 3 Mana")
|
||||
var hc: Cards.Card = h4.hand[0]
|
||||
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")
|
||||
var lava := Cards.fused(C("f_4_3"), C("f_2_1"), 4, 8)
|
||||
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")
|
||||
check(g4.play_card(h4, hc) == "" and h4.energy == 7 - 5 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
|
||||
check(g4.log.any(func(l): return l == "Brennender Kobold und Flutender Leviathan verschmelzen auf der Hand zu Dampf (5/6)."), "Verlauf")
|
||||
h4.hand[1] = h4.hand[1].leveled(4) # Hauch in Ultimate Rare: 5/5 für 1 Mana
|
||||
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.fuse(h4, h4.hand[0], h4.hand[1]) == "" and h4.hand.size() == 1, "auf der Hand verschmolzen")
|
||||
var blitz: 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(g4.log.any(func(l): return l == "Brennender Kobold und Flüchtiger Hauch verschmelzen zu Blitz (7/7) auf der Hand."), "Verlauf")
|
||||
# Bild
|
||||
var lava := Cards.fused(C("f_4_3"), C("f_2_1"))
|
||||
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(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:
|
||||
@@ -516,9 +560,9 @@ func test_limits() -> void:
|
||||
check(g.can_play(h, h.hand[0])[1] == "Dein Feld ist voll", "Feld voll")
|
||||
h.hand = []
|
||||
h.deck = ["f_2_1"]
|
||||
for i in Game.MAX_HAND:
|
||||
for i in 12:
|
||||
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")
|
||||
|
||||
|
||||
@@ -643,8 +687,8 @@ func test_booster() -> void:
|
||||
g.play_card(h, h.hand[0])
|
||||
var fused: Game.Creature = h.board[1]
|
||||
h.energy = 10
|
||||
check(g.fuse(h, fused, kobold) == "", "auf dem 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(g.fuse(h, fused, kobold) == "" and h.board.is_empty(), "zwei Wesen vom Brett verschmolzen")
|
||||
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
|
||||
@@ -943,7 +987,7 @@ func test_ki_zeitbudget() -> void:
|
||||
var g := Game.new_battle(r)
|
||||
var h: Game.Player = g.players[0]
|
||||
h.energy = 30
|
||||
for i in Game.MAX_HAND:
|
||||
for i in 7:
|
||||
h.hand.append(C("f_4_3"))
|
||||
for p in g.players:
|
||||
for i in 3:
|
||||
|
||||
166
tests/ui_test.gd
166
tests/ui_test.gd
@@ -3,6 +3,7 @@ extends Node
|
||||
## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn
|
||||
|
||||
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 shots := ""
|
||||
@@ -20,6 +21,21 @@ func check(cond: bool, msg: String) -> void:
|
||||
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:
|
||||
for i in n:
|
||||
await get_tree().process_frame
|
||||
@@ -39,10 +55,10 @@ func click_at(pos: Vector2) -> void:
|
||||
down.global_position = pos
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
push(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
push(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -63,11 +79,11 @@ func double_click_at(pos: Vector2) -> void:
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
down.double_click = true
|
||||
get_viewport().push_input(down)
|
||||
push(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
up.double_click = false
|
||||
get_viewport().push_input(up)
|
||||
push(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -77,10 +93,10 @@ func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
|
||||
down.global_position = pos
|
||||
down.button_index = button
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
push(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
push(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -88,11 +104,11 @@ func key(code: int) -> void:
|
||||
var down := InputEventKey.new()
|
||||
down.keycode = code
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
push(down)
|
||||
var up := InputEventKey.new()
|
||||
up.keycode = code
|
||||
up.pressed = false
|
||||
get_viewport().push_input(up)
|
||||
push(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -100,7 +116,7 @@ func pad(button: int) -> void:
|
||||
var ev := InputEventJoypadButton.new()
|
||||
ev.button_index = button
|
||||
ev.pressed = true
|
||||
get_viewport().push_input(ev)
|
||||
push(ev)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -109,7 +125,7 @@ func hover(node: Control) -> void:
|
||||
var ev := InputEventMouseMotion.new()
|
||||
ev.position = node.get_global_rect().get_center()
|
||||
ev.global_position = ev.position
|
||||
get_viewport().push_input(ev)
|
||||
push(ev)
|
||||
await frames(2)
|
||||
|
||||
|
||||
@@ -168,6 +184,7 @@ func _ready() -> void:
|
||||
main = load("res://scenes/main.tscn").instantiate()
|
||||
main.save_path = SAVE
|
||||
add_child(main)
|
||||
add_child(InputFilter.new()) # nach main, damit er im Baum zuletzt steht
|
||||
main.sfx.muted = true # Tests bleiben still
|
||||
await frames(3)
|
||||
var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
||||
@@ -244,46 +261,65 @@ func target_view(scr, valid: Array) -> Control:
|
||||
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:
|
||||
await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
|
||||
await settle(scr)
|
||||
var a := from.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()
|
||||
down.position = a
|
||||
down.global_position = a
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
push(down)
|
||||
await frames(1)
|
||||
for i in range(1, 6):
|
||||
var m := InputEventMouseMotion.new()
|
||||
m.position = a.lerp(b, i / 5.0)
|
||||
m.global_position = m.position
|
||||
m.button_mask = MOUSE_BUTTON_MASK_LEFT
|
||||
get_viewport().push_input(m)
|
||||
push(m)
|
||||
await frames(1)
|
||||
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(),
|
||||
" 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")
|
||||
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
|
||||
check(scr.drag_ghost != null and scr.drag_target == to_o and to.drop, "Karte schwebt über einem Verschmelzungsziel, das golden gerahmt ist")
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
up.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)
|
||||
|
||||
|
||||
@@ -367,7 +403,8 @@ func test_gegnerauswahl() -> void:
|
||||
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(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")
|
||||
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben")
|
||||
await shot("11_kampf")
|
||||
@@ -445,6 +482,14 @@ func test_deck() -> void:
|
||||
check(scr is DeckScreen, "Deck geöffnet")
|
||||
var p: Profile = main.profile
|
||||
check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt")
|
||||
check(scr.strength_label.text == "Stärke 100", "Startdeck aus Grundkarten: Stärke 100")
|
||||
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()
|
||||
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
|
||||
@@ -518,6 +563,7 @@ func test_gamepad_menue() -> void:
|
||||
var down := InputEventJoypadButton.new()
|
||||
down.button_index = JOY_BUTTON_DPAD_DOWN
|
||||
down.pressed = true
|
||||
down.device = DEVICE
|
||||
Input.parse_input_event(down)
|
||||
await frames(2)
|
||||
var f2 := get_viewport().gui_get_focus_owner()
|
||||
@@ -527,12 +573,12 @@ func test_gamepad_menue() -> void:
|
||||
var accept := InputEventJoypadButton.new()
|
||||
accept.button_index = JOY_BUTTON_A
|
||||
accept.pressed = true
|
||||
get_viewport().push_input(accept)
|
||||
push(accept)
|
||||
await frames(1)
|
||||
var release := InputEventJoypadButton.new()
|
||||
release.button_index = JOY_BUTTON_A
|
||||
release.pressed = false
|
||||
get_viewport().push_input(release)
|
||||
push(release)
|
||||
await frames(3)
|
||||
check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text)
|
||||
await hover(main)
|
||||
@@ -657,17 +703,13 @@ func test_kampf_grenzen() -> void:
|
||||
g.human.energy = 100
|
||||
scr.refresh()
|
||||
await frames(1)
|
||||
# Hand voll ziehen
|
||||
var guard := 0
|
||||
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
||||
guard += 1
|
||||
# Keine Grenze für Handkarten: acht weitere ziehen
|
||||
var hand_before: int = g.human.hand.size()
|
||||
for i in 8:
|
||||
await click(scr.deck)
|
||||
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
||||
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
|
||||
await click(scr.deck)
|
||||
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
||||
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")
|
||||
# Feld voll spielen
|
||||
guard = 0
|
||||
var guard := 0
|
||||
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
||||
guard += 1
|
||||
if g.human.hand.is_empty():
|
||||
@@ -681,9 +723,10 @@ func test_kampf_grenzen() -> void:
|
||||
await shot("25_kampf_voll")
|
||||
# Verschmelzen per Ziehen macht wieder Platz
|
||||
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])
|
||||
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
|
||||
g.human.energy = 0
|
||||
g.human.draws = 1
|
||||
@@ -699,12 +742,11 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampf gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
scr.hold_time = 0.3
|
||||
await wait_for_human(scr)
|
||||
var g: Game = scr.game
|
||||
g.human.board.clear()
|
||||
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
|
||||
for e in Cards.ELEMENTS:
|
||||
for e in Cards.ELEMENTS + [Cards.NEUTRAL]:
|
||||
scr.opponent().played[e] = 0
|
||||
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
|
||||
var 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
|
||||
await click(scr.own_views[3])
|
||||
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3/1 (Doppelschlag)"), "aufgedeckt: drei offene Feuer, eine ausgespielt: " + scr.element_view.text())
|
||||
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter fünf Karten kein Bonus, aufgedeckt greift sofort an")
|
||||
g.human.played["Feuer"] = 5
|
||||
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "unter drei Karten kein Bonus, aufgedeckt greift sofort an")
|
||||
g.human.played["Feuer"] = 3
|
||||
g.apply_auras()
|
||||
scr.refresh()
|
||||
await frames(1)
|
||||
check(scr.element_view.text().begins_with("Feuer 3/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")
|
||||
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])
|
||||
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.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.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen den Falken (sein Bonus zählt nicht), nichts gegen Verdeckte")
|
||||
check(scr.tiles[falke.uid].shown_stats() == [falke.power() - 1, falke.health], "der Falke zeigt seinen Konter gegen den Wolf (ohne seinen Bonus) und sein echtes Leben")
|
||||
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")
|
||||
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")
|
||||
await double_click_at(scr.own_views[0].get_global_rect().get_center())
|
||||
check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text)
|
||||
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
|
||||
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen, erst abbrechen, dann bestätigen
|
||||
mana = g.human.energy
|
||||
var before: int = g.human.board.size()
|
||||
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)")
|
||||
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
|
||||
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (der Reiter war Ultra Rare)")
|
||||
await shot("24a_verschmelzen_frage")
|
||||
await click(find_button("Abbrechen", scr.menu_layer))
|
||||
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")
|
||||
# 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()]
|
||||
scr.refresh()
|
||||
await frames(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")
|
||||
# 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)
|
||||
@@ -797,7 +852,12 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
||||
await click(find_button("Verschmelzen mit …", scr.menu_layer))
|
||||
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
|
||||
await click(scr.hand_views[1])
|
||||
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ Starten,Start
|
||||
Sammlung,Collection
|
||||
Verschmelzen,Fuse
|
||||
Nur eigene Wesen,Only your own creatures
|
||||
Nur eigene Karten,Only your own cards
|
||||
Sammlung: %d von %d Karten · Kampagne: %d/%d,Collection: %d of %d cards · Campaign: %d/%d
|
||||
%s ist jetzt %s,%s is now %s
|
||||
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
|
||||
Start gegen die KI,Start against the AI
|
||||
Ziehen,Draw
|
||||
Deine Hand ist voll,Your hand is full
|
||||
Verdeckt legen ◆ %d,Set face-down ◆ %d
|
||||
Aufdecken,Reveal
|
||||
verdeckt,face-down
|
||||
@@ -154,6 +154,7 @@ Abwerfen +%d ◆,Discard +%d ◆
|
||||
Überspringen,Skip
|
||||
Zug %d,Turn %d
|
||||
Stapel: %d Karten (%d bis %d),Deck: %d cards (%d to %d)
|
||||
Stärke %d,Strength %d
|
||||
Herstellen (%d Staub),Craft (%d dust)
|
||||
Nicht in der Sammlung,Not in the collection
|
||||
%s hergestellt,%s crafted
|
||||
@@ -161,9 +162,16 @@ Neu!,New!
|
||||
Auffüllen,Fill up
|
||||
Leeren,Clear
|
||||
Dein Stapel ist leer,Your deck is empty
|
||||
Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die 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
|
||||
"25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest.","25 cards of the element plus five of each other one. Boosters and the workshop bring the rest later."
|
||||
%s wählen,Choose %s
|
||||
Optionen,Options
|
||||
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
|
||||
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user