update
This commit is contained in:
31
README.md
31
README.md
@@ -20,20 +20,19 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
|
|
||||||
## Spielsysteme
|
## Spielsysteme
|
||||||
|
|
||||||
- 77 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos entstehen erst im Spiel (Verschmelzen, Feldkombos)
|
- 77 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
|
||||||
- Kein Deck und keine Starthand: jeder hat einen eigenen Laden mit fünf Angeboten. Kaufen kostet Mana, Ausspielen nichts, neu würfeln kostet 1 Mana
|
- Karten haben nur Element, Angriff und Leben; die Kosten sind Angriff plus Leben, halbiert und aufgerundet
|
||||||
- Mana wächst um 1 je Zug (bis 10) und bleibt liegen, wenn du es nicht ausgibst — sparen ist eine echte Entscheidung
|
- Mana: 5 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen
|
||||||
- Der Laden zeigt nur Karten, die höchstens dein Einkommen plus eins kosten: teure Wesen tauchen erst später auf
|
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf sehen, drei behalten; Ziehen kostet 1 Mana und zeigt drei, eine bleibt; angeboten wird nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
|
||||||
|
- Verbessern: Schild (3) und Doppelschlag (4) lassen sich für Mana auf jede Hand- oder Brettkarte kaufen (Rechtsklick oder Y). Frisch gelegte Wesen greifen sofort an, zu Zugbeginn heilen die eigenen Wesen ganz
|
||||||
|
- Verdeckt legen: gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementregel; aufdecken ist kostenlos, ein Angriff deckt es auf
|
||||||
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Holo-Karten: selten farbiger Rahmen, Schein und Edelstein, episch ein wandernder Lichtstreifen, legendär Funkeln, mythisch schillernde Farben; auch auf dem Feld für alle sichtbar, Werte und Text bleiben gleich
|
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Holo-Karten: selten farbiger Rahmen, Schein und Edelstein, episch ein wandernder Lichtstreifen, legendär Funkeln, mythisch schillernde Farben; auch auf dem Feld für alle sichtbar, Werte und Text bleiben gleich
|
||||||
- Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
- Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
||||||
- Resonanz: gleiche Elemente auf dem Feld verstärken sich (+1, +2, +3, +3); der Bonus steht als „+n“ am Angriff, verbundene Wesen sind unter der Reihe verlinkt
|
- Vier offene Wesen eines Elements auf dem Tisch: dieses Element bekommt +1/+1, das Element, das es schlägt, −1/−1 (Wasser > Feuer > Luft > Erde > Wasser)
|
||||||
- Feldkombos: ein neues Wesen neben einem Wesen mit anderem Element löst einen Effekt aus
|
- Elementvorteil beim Kampf: der Angreifer macht 1 Schaden mehr im Vorteil und 1 weniger im Nachteil, der Konter bleibt gleich, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht, auch bei der Vier-Elemente-Regel zählt sein bester Wert
|
||||||
- Karteneffekte lösen beim Ausspielen aus (Schaden, Einfrieren, Heilen, Zurückwerfen, Verstärken, Schild, Mana); Kartenvorteil ist hier Manavorteil
|
- Angriffe treffen erst Wesen; das Leben ist nur ohne Wesen angreifbar. Schaden über das Leben eines Wesens hinaus schlägt auf den Besitzer durch
|
||||||
- Elementvorteil im Kreis (Wasser > Feuer > Luft > Erde > Wasser): 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, auch für Karteneffekte gegen Wesen
|
- Verschmelzen: zwei eigene Wesen werden eins mit dem besseren Angriff und Leben plus 1, Elementen und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
|
||||||
- Gemeinsames Feld: vier gespielte Karten eines Elements kippen es (Vulkan, Flut, Fels, Wirbel), Wesen dieses Elements bekommen +1 Angriff; die Feldzeile zeigt je Element die Ladung als vier Punkte
|
- Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
||||||
- Vorschau: Hand- und Ladenkarten zeigen, welchen Bonus und welche Feldkombo das Ausspielen jetzt brächte; beim Zeigen ist der Kombo-Partner markiert, beim Angreifen steht der Elementvorteil (+1/−1) am Ziel
|
|
||||||
- Verschmelzen: zwei eigene Wesen werden eins (Elemente und Schlüsselwörter beider, Werte Durchschnitt plus 1, Kosten der Durchschnitt der Karten); drei oder mehr Elemente ergeben eine Chimäre
|
|
||||||
- Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 8 Karten, das Feld 5 Wesen
|
|
||||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||||
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
||||||
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
|
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
|
||||||
@@ -55,7 +54,7 @@ godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen K
|
|||||||
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
||||||
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
||||||
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
|
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
|
||||||
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Ladenangebot, Musik
|
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Stapel, Musik
|
||||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,14 +70,14 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
|||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
||||||
- `scripts/game.gd` – Spielregeln, Züge, Kampf, Feldkombos, Resonanz, Feld, Verschmelzen
|
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementregel, Kampf, Verbessern, Verschmelzen
|
||||||
- `scripts/deckgen.gd` – Ladenangebot und Gegner
|
- `scripts/deckgen.gd` – gemeinsamer Stapel und Gegner
|
||||||
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
||||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||||
- `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Booster, Einstellungen
|
- `scripts/profile.gd` – Spielstand (versioniert), Sammlung, Booster, Einstellungen
|
||||||
- `scripts/campaign.gd` – Kampagnenstufen
|
- `scripts/campaign.gd` – Kampagnenstufen
|
||||||
- `scenes/main.tscn` – Startszene
|
- `scenes/main.tscn` – Startszene
|
||||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||||
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `field_view.gd` die Feldzeile, `battle_layout.gd` die Aufteilung des Kampfes
|
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `element_view.gd` den Elementzähler, `battle_layout.gd` die Aufteilung des Kampfes
|
||||||
- `translations/ui.csv` – englische Oberflächentexte
|
- `translations/ui.csv` – englische Oberflächentexte
|
||||||
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
||||||
|
|||||||
@@ -15,15 +15,17 @@
|
|||||||
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik wie bei Holo-Karten: je höher die Stufe, desto mehr Glanz (Rahmen, Schein, Lichtstreifen, Funkeln, Schillern), auch im Netzwerkspiel sichtbar
|
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik wie bei Holo-Karten: je höher die Stufe, desto mehr Glanz (Rahmen, Schein, Lichtstreifen, Funkeln, Schillern), auch im Netzwerkspiel sichtbar
|
||||||
- Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub
|
- Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub
|
||||||
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
||||||
- Kein Deckbau und kein Nachziehstapel: fünf Angebote im Laden, Kaufen kostet Mana, Ausspielen nichts
|
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf sehen, drei behalten; Ziehen kostet 1 Mana und zeigt drei, eine bleibt; angeboten wird nur, was man sich in diesem Zug leisten kann
|
||||||
- Nicht ausgegebenes Mana bleibt liegen, neu würfeln kostet 1 Mana (wie in TFT)
|
- Mana für Ziehen, Legen (Kosten = Angriff + Leben, halbiert), Verbessern und Verschmelzen; 5 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
|
||||||
|
- Karten haben nur Angriff und Leben; Eigenschaften (Schild, Doppelschlag) kauft man per Rechtsklick für Mana dazu; gelegte Wesen greifen sofort an, eigene Wesen heilen zu Zugbeginn
|
||||||
|
- Wesen lassen sich verdeckt legen; aufdecken ist frei, ein Angriff deckt auf
|
||||||
|
- Leben erst ohne Wesen angreifbar; Überschuss schlägt durch
|
||||||
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
||||||
|
|
||||||
## Elemente
|
## Elemente
|
||||||
- Jede Karte gehört zu genau einem Element
|
- Jede Karte gehört zu genau einem Element
|
||||||
- Kombos entstehen erst im Spiel: beim Verschmelzen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) und als Feldkombo
|
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
||||||
- Elemente wirken auf dem Feld zusammen: gleiche verstärken sich (Resonanz), verschiedene lösen Feldkombos aus; beides ist vor dem Ausspielen auf der Karte zu sehen
|
- Vier offene Wesen eines Elements: +1/+1 für das Element, −1/−1 für das, das es schlägt; die Karte zeigt vorher, was sie bekäme
|
||||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (1 Schaden mehr im Vorteil, 1 weniger im Nachteil; Mehrfachelemente nehmen den für sie besseren Wert)
|
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (der Angreifer macht 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, der Konter bleibt gleich; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht, und nimmt auch bei der Vier-Elemente-Regel seinen besten Wert)
|
||||||
- Das Feld gehört beiden: vier gespielte Karten eines Elements kippen es, Wesen dieses Elements bekommen +1 Angriff
|
- Zwei eigene Wesen lassen sich für Mana verschmelzen: das neue erbt Elemente und Eigenschaften beider, seine Werte sind jeweils der bessere Wert plus 1
|
||||||
- Zwei eigene Wesen lassen sich für Energie verschmelzen: das neue erbt Elemente und Fähigkeiten beider, seine Werte sind der Durchschnitt plus 1
|
|
||||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||||
|
|||||||
719
data/karten.json
719
data/karten.json
File diff suppressed because it is too large
Load Diff
44
design.md
44
design.md
@@ -8,9 +8,10 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
|||||||
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
und Leben in Kreisen unten), der Name liegt auf einem Band über die volle Breite, Text bleibt
|
||||||
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, Verlauf links, Zug beenden rechts, Wesen
|
kurz. Auf dem Brett: eigene Seite unten, Gegner oben, Verlauf links, Zug beenden rechts, Wesen
|
||||||
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
als kompakte Kacheln statt voller Karten. Zugbeginn und Kartenzüge werden angesagt.
|
||||||
- **Teamfight Tactics** – der Laden ist eine Reihe von fünf großen Karten unten, Neuwürfeln steht
|
- **Hearthstones Entdecken** – drei Karten sehen, eine nehmen: Zufall mit Entscheidung. Jede
|
||||||
mit seinem Preis direkt daneben, die Bank (bei uns die Hand) liegt als schmale Reihe darüber.
|
Wahl zeigt an, was zum eigenen Brett passt (Elementbonus als „+1/+1“ oder „−1/−1“).
|
||||||
Der Laden zeigt an, was zum eigenen Brett passt (bei uns: „+n“ und die Feldkombo).
|
- **Yu-Gi-Oh** – verdeckt gelegte Monster, Angriffe erst auf Monster, Kampfschaden schlägt
|
||||||
|
durch, Fusion.
|
||||||
- **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole
|
- **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole
|
||||||
statt Sätze.
|
statt Sätze.
|
||||||
- **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen,
|
- **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen,
|
||||||
@@ -21,45 +22,46 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
|||||||
## Karte
|
## Karte
|
||||||
|
|
||||||
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
|
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
|
||||||
Schlüsselwörter als Symbole (Mauer = Wächter, Schild, Doppelpfeil = Flink, gekreuzte Klingen =
|
gekaufte Eigenschaften als Symbole (Schild, gekreuzte Klingen = Doppelschlag), unten Angriff (gelb) und Leben (rot) als Kreise, dazwischen der Edelstein der
|
||||||
Doppelschlag), dann der Effekttext ohne Einleitung, unten Angriff (gelb) und Leben (rot) als
|
Stufe. Karten haben keinen Text: nur Element, Angriff, Leben.
|
||||||
Kreise, dazwischen der Edelstein der Stufe.
|
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol.
|
||||||
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol und die
|
- Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau.
|
||||||
Überschrift „Beim Ausspielen“ über den Effekt. Kleine Karten zeigen nur Symbole.
|
|
||||||
- Handkacheln (96×62) sind flach: Kosten, Name, Werte, Symbole oder das Kombo-Schild. Details
|
|
||||||
kommen aus der schwebenden Vorschau.
|
|
||||||
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen;
|
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen;
|
||||||
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben.
|
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben.
|
||||||
- Der Kartentext in Daten und Sammlungssuche bleibt vollständig (`Card.text()`), die Karte
|
- Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit
|
||||||
zeichnet nur `effect_text()`.
|
dem Vermerk „verdeckt“.
|
||||||
|
|
||||||
## Kampfbildschirm (1280×800)
|
## Kampfbildschirm (1280×800)
|
||||||
|
|
||||||
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||||
|
|
||||||
- **Laden** als fünf große Karten, links daneben „Neu würfeln“ mit Preis, rechts „Zug beenden“.
|
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 1“ (Klick zieht), rechts
|
||||||
- **Hand** als flache Kacheln (die Bank), für andere verdeckt.
|
„Zug beenden“.
|
||||||
|
- **Wahl** als Überlagerung: zu Beginn „Wähle 3 von 5“, nach dem Ziehen „Wähle 1 von 3“; solange
|
||||||
|
sie offen ist, geht nichts anderes.
|
||||||
- **Eigenes Brett** mit fünf sichtbaren Plätzen, färbt sich mit dem aktiven Feld; links daneben
|
- **Eigenes Brett** mit fünf sichtbaren Plätzen, färbt sich mit dem aktiven Feld; links daneben
|
||||||
die eigene Leiste (Leben, Mana, Einkommen, Zug), rechts „Verschmelzen“ und „Rückgängig“, nur
|
die eigene Leiste (Leben, Mana, Einkommen, Zug), rechts „Verschmelzen“ und „Rückgängig“, nur
|
||||||
wenn sie gerade möglich sind.
|
wenn sie gerade möglich sind.
|
||||||
- **Feldband** mit vier Ladepunkten je Element.
|
- **Elementzähler** mit vier Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1,
|
||||||
|
das geschlagene Element bekommt −1/−1.
|
||||||
|
- **Kartenmenü** per Rechtsklick oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken,
|
||||||
|
Schild und Doppelschlag kaufen.
|
||||||
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie
|
- **Gegner** oben in der Mitte mit seinem Brett in voller Größe. Das Spiel ist ein Duell wie
|
||||||
Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
|
Hearthstone: mehr als zwei Bretter passen nicht lesbar auf einen Bildschirm, und ein Gegner
|
||||||
macht Ziele, Texte und Balance eindeutig.
|
macht Ziele, Texte und Balance eindeutig.
|
||||||
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
|
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
|
||||||
und Feldkombo darunter.
|
und Feldkombo darunter.
|
||||||
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
|
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
|
||||||
- Hand- und Ladenkarten tragen „+n“ (Bonus auf dem Brett) und das Kombo-Schild; beim Zeigen ist
|
- Hand- und Wahlkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||||
der Partner auf dem Brett markiert; beim Angreifen steht der Elementkonter (+1/−1) am Ziel.
|
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
||||||
- Wesen mit gleichem Element sind unter der Reihe verbunden (Resonanz).
|
|
||||||
|
|
||||||
## Bewegung und Ton
|
## Bewegung und Ton
|
||||||
|
|
||||||
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Laden wird ausgeteilt, Ton.
|
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Ton.
|
||||||
- Kaufen: die Karte fliegt aus dem Laden auf die Hand, Münzton.
|
- Ziehen: die Wahl wird ausgeteilt; die genommene Karte fliegt auf die Hand.
|
||||||
- Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein.
|
- Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein.
|
||||||
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
|
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
|
||||||
- Feldwechsel: Band in Elementfarbe, das Brett färbt sich.
|
- Aufdecken: „Aufgedeckt!“ am Wesen; Verbessern: die Eigenschaft steigt grün auf.
|
||||||
- Karten heben sich beim Zeigen an, Knöpfe federn beim Drücken, Bildschirme blenden ein.
|
- Karten heben sich beim Zeigen an, Knöpfe federn beim Drücken, Bildschirme blenden ein.
|
||||||
- Booster: Karten drehen sich auf, seltene Stufen blitzen in ihrer Farbe auf.
|
- Booster: Karten drehen sich auf, seltene Stufen blitzen in ihrer Farbe auf.
|
||||||
- Alles hängt an der Einstellung „Animationen“.
|
- Alles hängt an der Einstellung „Animationen“.
|
||||||
|
|||||||
353
scripts/ai.gd
353
scripts/ai.gd
@@ -1,25 +1,19 @@
|
|||||||
class_name AI
|
class_name AI
|
||||||
## KI mit Vorausschau: jede mögliche Aktion wird auf einer Kopie des Spiels
|
## KI mit Vorausschau: jede mögliche Aktion wird auf einer Kopie des Spiels ausgeführt und die
|
||||||
## ausgeführt und die Stellung danach bewertet; die besten Erstschritte werden um
|
## Stellung danach bewertet; die besten Erstschritte werden um einen Folgeschritt vertieft.
|
||||||
## einen Folgeschritt vertieft. Die gierige Variante (choose_action_greedy) dient
|
## Verdeckte gegnerische Wesen kennt die KI nicht (Game.masked_clone). Die gierige Variante
|
||||||
## als Vergleich und für die Schwierigkeit Leicht. Alles hier ist zustandslos und
|
## (choose_action_greedy) dient als Vergleich und für die Schwierigkeit Leicht. Alles hier ist
|
||||||
## darf in einem Hintergrund-Thread auf einer Spielkopie laufen.
|
## zustandslos und darf in einem Hintergrund-Thread auf einer Spielkopie laufen.
|
||||||
|
|
||||||
|
|
||||||
# --- Bewertung -------------------------------------------------------------------
|
# --- Bewertung -------------------------------------------------------------------
|
||||||
|
|
||||||
static func creature_value(c: Game.Creature) -> float:
|
static func creature_value(c: Game.Creature) -> float:
|
||||||
var v := float(c.power() + c.health)
|
var v := float(c.power() + c.health)
|
||||||
if c.taunt():
|
|
||||||
v += 2.0
|
|
||||||
if c.shield:
|
if c.shield:
|
||||||
v += 1.5
|
v += 1.0
|
||||||
if c.attacks_per_turn() == 2:
|
if c.attacks_per_turn() == 2:
|
||||||
v += c.power()
|
v += c.power() * 0.5
|
||||||
if c.frozen:
|
|
||||||
v -= c.power() # setzt den nächsten Angriff aus: der Angriffswert zählt nicht
|
|
||||||
if not c.ready:
|
|
||||||
v -= c.power() * 0.25
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
@@ -38,16 +32,14 @@ static func hand_value(p: Game.Player) -> float:
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv,
|
## Stellung aus Sicht von p: eigenes Leben, Brett und Hand zählen positiv, die des Gegners
|
||||||
## die des Gegners negativ. Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung,
|
## negativ. Ohne eigene Wesen zielt gegnerische Angriffskraft aufs Leben, tödliche besonders.
|
||||||
## tödliche besonders.
|
|
||||||
static func evaluate(g: Game, p: Game.Player) -> float:
|
static func evaluate(g: Game, p: Game.Player) -> float:
|
||||||
if not p.alive():
|
if not p.alive():
|
||||||
return -10000.0
|
return -10000.0
|
||||||
var score := p.life * 1.0 + board_value(p) * 1.0 + hand_value(p) + p.energy * 0.02
|
var score := p.life * 1.0 + board_value(p) * 1.0 + hand_value(p) + p.energy * 0.01
|
||||||
var alive_enemies := 0
|
var alive_enemies := 0
|
||||||
var threat := 0.0
|
var threat := 0.0
|
||||||
var leader := 0.0
|
|
||||||
for e in g.players:
|
for e in g.players:
|
||||||
if e == p:
|
if e == p:
|
||||||
continue
|
continue
|
||||||
@@ -55,59 +47,40 @@ static func evaluate(g: Game, p: Game.Player) -> float:
|
|||||||
score += 40.0
|
score += 40.0
|
||||||
continue
|
continue
|
||||||
alive_enemies += 1
|
alive_enemies += 1
|
||||||
var ev: float = e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2
|
score -= e.life * 0.6 + board_value(e) * 0.9 + e.hand.size() * 0.2
|
||||||
score -= ev
|
|
||||||
leader = maxf(leader, ev)
|
|
||||||
if e.life <= 6:
|
if e.life <= 6:
|
||||||
score += 3.0 # fast besiegt: dranbleiben
|
score += 3.0 # fast besiegt: dranbleiben
|
||||||
for c in e.board:
|
for c in e.board:
|
||||||
if not c.frozen:
|
if not c.hidden:
|
||||||
threat += c.power() * c.attacks_per_turn()
|
threat += c.power() * c.attacks_per_turn()
|
||||||
if alive_enemies == 0:
|
if alive_enemies == 0:
|
||||||
return 10000.0
|
return 10000.0
|
||||||
score -= leader * 0.25
|
if p.board.is_empty():
|
||||||
var guarded := false
|
if threat >= p.life:
|
||||||
for c in p.board:
|
score -= 25.0
|
||||||
if c.taunt():
|
else:
|
||||||
guarded = true
|
score -= threat * 0.3
|
||||||
break
|
|
||||||
if guarded:
|
|
||||||
score -= threat * 0.1
|
|
||||||
elif threat >= p.life:
|
|
||||||
score -= 25.0
|
|
||||||
else:
|
else:
|
||||||
score -= threat * 0.3
|
score -= threat * 0.12 # Durchschlag: auch hinter Wesen tut Angriffskraft weh
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
# --- Vorausschau ---------------------------------------------------------------
|
# --- Vorausschau ---------------------------------------------------------------
|
||||||
|
|
||||||
const MAX_TARGETS := 4 # je Karte bzw. Angreifer nur die aussichtsreichsten Ziele prüfen
|
const MAX_TARGETS := 4 # je Angreifer nur die aussichtsreichsten Ziele prüfen
|
||||||
|
static var allow_hidden := true # zum Messen: verdeckt legen erwägen
|
||||||
|
static var allow_upgrade := true # zum Messen: Verbessern erwägen
|
||||||
const DEEPEN := 8 # so viele beste Erstschritte bekommen Folgeschritte
|
const DEEPEN := 8 # so viele beste Erstschritte bekommen Folgeschritte
|
||||||
const FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat
|
const FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat
|
||||||
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
|
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
|
||||||
|
|
||||||
|
|
||||||
## Grobe Rangfolge eines Ziels für einen Karteneffekt (nur zum Vorsortieren).
|
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest; Durchschlag zählt mit.
|
||||||
static func _target_rank(kind: String, amount: int, t) -> float:
|
|
||||||
if t is Game.Player:
|
|
||||||
return 50.0 - t.life if kind == "damage" else 0.0
|
|
||||||
match kind:
|
|
||||||
"damage":
|
|
||||||
return 100.0 + _value(t) if not t.shield and t.health <= amount else float(_value(t))
|
|
||||||
"freeze", "bounce":
|
|
||||||
return t.card.cost * 10.0 + t.power()
|
|
||||||
"buff", "shield":
|
|
||||||
return (100.0 if t.can_attack() else 0.0) + t.power()
|
|
||||||
return float(_value(t))
|
|
||||||
|
|
||||||
|
|
||||||
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann Helden, dann der Rest.
|
|
||||||
static func _attack_rank(cr: Game.Creature, t) -> float:
|
static func _attack_rank(cr: Game.Creature, t) -> float:
|
||||||
if t is Game.Player:
|
if t is Game.Player:
|
||||||
return 50.0 - t.life
|
return 50.0 - t.life
|
||||||
if _kills(cr, t):
|
if _kills(cr, t):
|
||||||
return (100.0 if not _kills(t, cr) else 60.0) + _value(t)
|
return (100.0 if not _kills(t, cr) else 60.0) + _value(t) + _overflow(cr, t)
|
||||||
return -float(t.health)
|
return -float(t.health)
|
||||||
|
|
||||||
|
|
||||||
@@ -121,65 +94,44 @@ static func _top(list: Array, rank: Callable, n: int) -> Array:
|
|||||||
return ranked.slice(0, n).map(func(pair): return pair[1])
|
return ranked.slice(0, n).map(func(pair): return pair[1])
|
||||||
|
|
||||||
|
|
||||||
## Mögliche Aktionen; mit prune nur die MAX_TARGETS besten Ziele je Karte und Angreifer.
|
## Mögliche Aktionen; mit prune nur die MAX_TARGETS besten Ziele je Angreifer.
|
||||||
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
|
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
|
||||||
var out := []
|
var out := []
|
||||||
var valid_cache := {}
|
|
||||||
var afford := false
|
|
||||||
var seen := {}
|
|
||||||
for i in p.shop.size():
|
|
||||||
var offer = p.shop[i]
|
|
||||||
if offer != null and p.energy >= offer.cost and p.hand.size() < Game.MAX_HAND:
|
|
||||||
afford = true
|
|
||||||
if not seen.has(offer.id): # dasselbe Angebot zweimal zu prüfen bringt nichts
|
|
||||||
seen[offer.id] = true
|
|
||||||
out.append({"type": "buy", "slot": i, "card": offer})
|
|
||||||
# Neu würfeln lohnt nur, wenn ein Laden da ist und nichts Bezahlbares darin liegt.
|
|
||||||
if not afford and not p.shop.is_empty() and p.energy >= Game.REROLL_COST and p.hand.size() < Game.MAX_HAND:
|
|
||||||
out.append({"type": "reroll"})
|
|
||||||
for card in p.hand:
|
for card in p.hand:
|
||||||
if not game.can_play(p, card)[0]:
|
if not game.can_play(p, card)[0]:
|
||||||
continue
|
continue
|
||||||
var tt: String = card.target_type()
|
out.append({"type": "play", "card": card, "face_down": false})
|
||||||
if tt == "":
|
if allow_hidden and game.forecast(p, card) < 0:
|
||||||
out.append({"type": "play", "card": card, "target": null})
|
out.append({"type": "play", "card": card, "face_down": true})
|
||||||
continue
|
for cr in p.board:
|
||||||
if not valid_cache.has(tt):
|
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||||
valid_cache[tt] = game.valid_targets(p, tt)
|
out.append({"type": "reveal", "creature": cr})
|
||||||
var valid: Array = valid_cache[tt]
|
elif not cr.hidden and allow_upgrade:
|
||||||
if valid.is_empty():
|
# Nur Verbesserungen, die sich noch in diesem Zug auszahlen können: Doppelschlag für
|
||||||
out.append({"type": "play", "card": card, "target": null})
|
# ein Wesen, das gleich angreift; Schild nur, wenn kein Wesen mehr spielbar ist.
|
||||||
continue
|
var has_target := not game.enemies_of(p).is_empty()
|
||||||
if prune:
|
if cr.attacks_left > 0 and has_target and game.can_upgrade(p, cr, "Doppelschlag")[0]:
|
||||||
var fx: Cards.Effect = null
|
out.append({"type": "upgrade", "creature": cr, "keyword": "Doppelschlag"})
|
||||||
for f in card.effects:
|
if not p.hand.any(func(c): return game.can_play(p, c)[0]) and game.can_upgrade(p, cr, "Schild")[0]:
|
||||||
if f.target == tt:
|
out.append({"type": "upgrade", "creature": cr, "keyword": "Schild"})
|
||||||
fx = f
|
|
||||||
break
|
|
||||||
var kind: String = fx.kind if fx != null else ""
|
|
||||||
var amount: int = fx.amount if fx != null else 0
|
|
||||||
var key := "%s/%s/%d" % [tt, kind, amount]
|
|
||||||
if not valid_cache.has(key):
|
|
||||||
valid_cache[key] = _top(valid, func(t): return _target_rank(kind, amount, t), MAX_TARGETS)
|
|
||||||
valid = valid_cache[key]
|
|
||||||
for t in valid:
|
|
||||||
out.append({"type": "play", "card": card, "target": t})
|
|
||||||
var pairs := []
|
var pairs := []
|
||||||
for i in p.board.size():
|
for i in p.board.size():
|
||||||
for j in range(i + 1, p.board.size()):
|
for j in range(i + 1, p.board.size()):
|
||||||
if game.can_fuse(p, p.board[i], p.board[j])[0]:
|
if game.can_fuse(p, p.board[i], p.board[j])[0]:
|
||||||
pairs.append({"type": "fuse", "creature": p.board[i], "other": p.board[j]})
|
pairs.append([p.board[i], p.board[j]])
|
||||||
if prune: # Verschmelzen lohnt vor allem bei schwachen Wesen
|
if prune and pairs.size() > MAX_TARGETS:
|
||||||
pairs = _top(pairs, func(a): return -float(_value(a.creature) + _value(a.other)), MAX_TARGETS)
|
pairs = _top(pairs, func(pr): return _value(pr[0]) + _value(pr[1]), MAX_TARGETS)
|
||||||
out.append_array(pairs)
|
for pr in pairs:
|
||||||
|
out.append({"type": "fuse", "creature": pr[0], "other": pr[1]})
|
||||||
for cr in p.board:
|
for cr in p.board:
|
||||||
if not cr.can_attack():
|
if not cr.can_attack():
|
||||||
continue
|
continue
|
||||||
var targets := []
|
var targets := []
|
||||||
for e in game.enemies_of(p):
|
for e in game.enemies_of(p):
|
||||||
for t in e.board + [e]:
|
targets.append_array(e.board)
|
||||||
if game.can_attack(cr, t)[0]:
|
if e.board.is_empty():
|
||||||
targets.append(t)
|
targets.append(e)
|
||||||
|
targets = targets.filter(func(t): return game.can_attack(cr, t)[0])
|
||||||
if prune:
|
if prune:
|
||||||
targets = _top(targets, func(t): return _attack_rank(cr, t), MAX_TARGETS)
|
targets = _top(targets, func(t): return _attack_rank(cr, t), MAX_TARGETS)
|
||||||
for t in targets:
|
for t in targets:
|
||||||
@@ -190,20 +142,17 @@ static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -
|
|||||||
## Führt eine Aktion des Originals auf der Kopie aus.
|
## Führt eine Aktion des Originals auf der Kopie aus.
|
||||||
static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
static func apply_on_clone(g2: Game, game: Game, action: Dictionary) -> void:
|
||||||
var p2: Game.Player = g2.players[game.current_idx]
|
var p2: Game.Player = g2.players[game.current_idx]
|
||||||
var target = null
|
|
||||||
if action.get("target") != null:
|
|
||||||
target = map_target(g2, game, action.target)
|
|
||||||
match action.type:
|
match action.type:
|
||||||
"play":
|
"play":
|
||||||
g2.play_card(p2, action.card, target)
|
g2.play_card(p2, action.card, bool(action.get("face_down", false)))
|
||||||
"buy":
|
"reveal":
|
||||||
g2.buy(p2, int(action.slot))
|
g2.reveal(p2, g2.find_creature(action.creature.uid))
|
||||||
"reroll":
|
"upgrade":
|
||||||
g2.reroll(p2)
|
g2.upgrade(p2, g2.find_creature(action.creature.uid), action.keyword)
|
||||||
"fuse":
|
"fuse":
|
||||||
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid))
|
g2.fuse(p2, g2.find_creature(action.creature.uid), g2.find_creature(action.other.uid))
|
||||||
_:
|
_:
|
||||||
g2.attack(g2.find_creature(action.creature.uid), target)
|
g2.attack(g2.find_creature(action.creature.uid), map_target(g2, game, action.target))
|
||||||
|
|
||||||
|
|
||||||
static func map_target(g2: Game, game: Game, t):
|
static func map_target(g2: Game, game: Game, t):
|
||||||
@@ -224,21 +173,24 @@ static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Beste Aktion nach einer Vorausschau von zwei Schritten: erst wird jede Aktion auf einer
|
## Beste Aktion nach einer Vorausschau von zwei Schritten auf einer Kopie, die verdeckte
|
||||||
## Kopie ausgeführt und bewertet, dann bekommen die DEEPEN besten Folgeschritte
|
## gegnerische Wesen nicht kennt. Bei Zeitmangel gilt der beste bisherige Zug.
|
||||||
## (z. B. verstärken, dann angreifen). Bei Zeitmangel gilt der beste bisherige Zug.
|
|
||||||
static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||||
return {"type": "end"}
|
return {"type": "end"}
|
||||||
|
if not p.offer.is_empty():
|
||||||
|
return {"type": "pick", "index": pick_index(game, p)}
|
||||||
|
var view := game.masked_clone(p)
|
||||||
|
var me: Game.Player = view.players[game.players.find(p)]
|
||||||
var t0 := Time.get_ticks_msec()
|
var t0 := Time.get_ticks_msec()
|
||||||
var base := evaluate(game, p) + 0.01
|
var base := evaluate(view, me) + 0.01
|
||||||
var firsts := [] # {action, game, score}
|
var firsts := [] # {action, game, score}
|
||||||
for action in candidate_actions(game, p, true):
|
for action in candidate_actions(view, me, true):
|
||||||
if not firsts.is_empty() and Time.get_ticks_msec() - t0 > time_budget_ms:
|
if not firsts.is_empty() and Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||||
break
|
break
|
||||||
var g2 := game.clone()
|
var g2 := view.clone()
|
||||||
apply_on_clone(g2, game, action)
|
apply_on_clone(g2, view, action)
|
||||||
firsts.append({"action": action, "game": g2, "score": evaluate(g2, g2.players[game.current_idx])})
|
firsts.append({"action": action, "game": g2, "score": evaluate(g2, g2.players[view.current_idx])})
|
||||||
firsts.sort_custom(func(a, b): return a.score > b.score)
|
firsts.sort_custom(func(a, b): return a.score > b.score)
|
||||||
var best := {"type": "end"}
|
var best := {"type": "end"}
|
||||||
var best_score := base
|
var best_score := base
|
||||||
@@ -247,7 +199,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
var g2: Game = f.game
|
var g2: Game = f.game
|
||||||
var score: float = f.score
|
var score: float = f.score
|
||||||
if g2.winner == "":
|
if g2.winner == "":
|
||||||
var p2: Game.Player = g2.players[game.current_idx]
|
var p2: Game.Player = g2.players[view.current_idx]
|
||||||
var count := 0
|
var count := 0
|
||||||
for nxt in candidate_actions(g2, p2, true):
|
for nxt in candidate_actions(g2, p2, true):
|
||||||
if count >= FOLLOW_UPS or Time.get_ticks_msec() - t0 > time_budget_ms:
|
if count >= FOLLOW_UPS or Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||||
@@ -255,7 +207,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
count += 1
|
count += 1
|
||||||
var g3 := g2.clone()
|
var g3 := g2.clone()
|
||||||
apply_on_clone(g3, g2, nxt)
|
apply_on_clone(g3, g2, nxt)
|
||||||
score = maxf(score, evaluate(g3, g3.players[game.current_idx]) - 0.001)
|
score = maxf(score, evaluate(g3, g3.players[view.current_idx]) - 0.001)
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best = f.action
|
best = f.action
|
||||||
@@ -264,57 +216,81 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
if firsts[i].score > best_score:
|
if firsts[i].score > best_score:
|
||||||
best_score = firsts[i].score
|
best_score = firsts[i].score
|
||||||
best = firsts[i].action
|
best = firsts[i].action
|
||||||
return best
|
if best.type == "end":
|
||||||
|
if game.can_draw(p)[0]:
|
||||||
|
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||||
|
return best
|
||||||
|
return map_action(game, view, best)
|
||||||
|
|
||||||
|
|
||||||
# --- Gierige Vergleichs-KI -------------------------------------------------------
|
# --- Einfache Heuristiken ----------------------------------------------------------
|
||||||
|
|
||||||
static func _kills(attacker, defender) -> bool:
|
static func _kills(attacker, defender) -> bool:
|
||||||
return not defender.shield and defender.health <= attacker.power() + Game.matchup_bonus(attacker, defender)
|
if defender is Game.Player:
|
||||||
|
return attacker.power() >= defender.life
|
||||||
|
if defender.shield:
|
||||||
|
return false
|
||||||
|
return attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) >= defender.health
|
||||||
|
|
||||||
|
|
||||||
|
## Schaden, der beim Angriff auf das Wesen auf dessen Besitzer durchschlägt.
|
||||||
|
static func _overflow(attacker, defender) -> int:
|
||||||
|
if defender is Game.Player or defender.shield:
|
||||||
|
return 0
|
||||||
|
return maxi(0, attacker.power() + Cards.matchup(attacker.card.elements, defender.card.elements) - defender.health)
|
||||||
|
|
||||||
|
|
||||||
static func _value(c) -> int:
|
static func _value(c) -> int:
|
||||||
return c.power() + c.health + (2 if c.taunt() else 0)
|
if c is Game.Player:
|
||||||
|
return 1000
|
||||||
|
return c.power() + c.health
|
||||||
|
|
||||||
|
|
||||||
static func _best(arr: Array, key: Callable):
|
static func _best(list: Array, key: Callable):
|
||||||
var best = null
|
var best = null
|
||||||
var best_v := 0
|
var best_v := -INF
|
||||||
for x in arr:
|
for t in list:
|
||||||
var v: int = key.call(x)
|
var v := float(key.call(t))
|
||||||
if best == null or v > best_v:
|
if v > best_v:
|
||||||
best = x
|
|
||||||
best_v = v
|
best_v = v
|
||||||
|
best = t
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
static func _threat_score(h) -> int:
|
## Wert einer angebotenen Karte für p: Werte je Mana, jetzt spielbar zählt extra, der
|
||||||
var board_attack := 0
|
## Elementbonus und ein Vorteil gegen das gegnerische Brett auch; viel zu teuer zählt ab.
|
||||||
for c in h.board:
|
static func offer_score(game: Game, p: Game.Player, c: Cards.Card) -> float:
|
||||||
board_attack += c.power()
|
var score := float(c.attack + c.health) / maxf(1.0, c.cost) * 2.0 + c.attack + c.health
|
||||||
return h.life - board_attack
|
if c.cost <= p.energy:
|
||||||
|
score += 3.0
|
||||||
|
if c.cost > p.max_energy + 2:
|
||||||
|
score -= 2.0 * (c.cost - p.max_energy - 2)
|
||||||
|
score += 2.0 * game.forecast(p, c)
|
||||||
|
for e in game.enemies_of(p):
|
||||||
|
for o in e.board:
|
||||||
|
if not o.hidden:
|
||||||
|
score += 1.0 * Cards.matchup(c.elements, o.card.elements)
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
static func _weakest(heroes: Array):
|
## Welche der angebotenen Karten p nimmt.
|
||||||
var best = null
|
static func pick_index(game: Game, p: Game.Player) -> int:
|
||||||
for h in heroes:
|
var best := 0
|
||||||
if best == null or _threat_score(h) < _threat_score(best):
|
var best_score := -INF
|
||||||
best = h
|
for i in p.offer.size():
|
||||||
|
var s := offer_score(game, p, p.offer[i])
|
||||||
|
if s > best_score:
|
||||||
|
best_score = s
|
||||||
|
best = i
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||||
return {"type": "end"}
|
return {"type": "end"}
|
||||||
# Gierig: erst das teuerste Bezahlbare kaufen, dann ausspielen, dann angreifen.
|
if not p.offer.is_empty():
|
||||||
var best := -1
|
return {"type": "pick", "index": pick_index(game, p)}
|
||||||
for k in p.shop.size():
|
# Gierig: erst das teuerste Spielbare legen, dann aufdecken und angreifen, dann ziehen.
|
||||||
var offer = p.shop[k]
|
|
||||||
if offer != null and p.energy >= offer.cost and p.hand.size() < Game.MAX_HAND:
|
|
||||||
if best < 0 or offer.cost > p.shop[best].cost:
|
|
||||||
best = k
|
|
||||||
if best >= 0:
|
|
||||||
return {"type": "buy", "slot": best}
|
|
||||||
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
||||||
if not playable.is_empty():
|
if not playable.is_empty():
|
||||||
Game.shuffle(playable, game.rng)
|
Game.shuffle(playable, game.rng)
|
||||||
@@ -322,71 +298,35 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
|||||||
for c in playable:
|
for c in playable:
|
||||||
if c.cost > card.cost:
|
if c.cost > card.cost:
|
||||||
card = c
|
card = c
|
||||||
return {"type": "play", "card": card, "target": pick_target(game, p, card)}
|
return {"type": "play", "card": card, "face_down": false}
|
||||||
|
for cr in p.board:
|
||||||
|
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||||
|
return {"type": "reveal", "creature": cr}
|
||||||
for cr in p.board:
|
for cr in p.board:
|
||||||
if cr.can_attack():
|
if cr.can_attack():
|
||||||
var t = pick_attack_target(game, p, cr)
|
var t = pick_attack_target(game, p, cr)
|
||||||
if t != null:
|
if t != null:
|
||||||
return {"type": "attack", "creature": cr, "target": t}
|
return {"type": "attack", "creature": cr, "target": t}
|
||||||
|
if game.can_draw(p)[0]:
|
||||||
|
return {"type": "draw"}
|
||||||
return {"type": "end"}
|
return {"type": "end"}
|
||||||
|
|
||||||
|
|
||||||
static func pick_target(game: Game, p: Game.Player, card: Cards.Card):
|
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
||||||
var tt := card.target_type()
|
## der Held nur, wenn kein Wesen mehr steht.
|
||||||
if tt == "":
|
|
||||||
return null
|
|
||||||
var valid := game.valid_targets(p, tt)
|
|
||||||
if valid.is_empty():
|
|
||||||
return null
|
|
||||||
var fx: Cards.Effect = null
|
|
||||||
for f in card.effects:
|
|
||||||
if f.target == tt:
|
|
||||||
fx = f
|
|
||||||
break
|
|
||||||
var creatures := valid.filter(func(t): return t is Game.Creature)
|
|
||||||
var heroes := valid.filter(func(t): return t is Game.Player)
|
|
||||||
match fx.kind:
|
|
||||||
"damage":
|
|
||||||
var killable := creatures.filter(func(c): return not c.shield and c.health <= fx.amount)
|
|
||||||
if not killable.is_empty():
|
|
||||||
return _best(killable, Callable(AI, "_value"))
|
|
||||||
if not heroes.is_empty():
|
|
||||||
return _weakest(heroes)
|
|
||||||
return _best(creatures, Callable(AI, "_value"))
|
|
||||||
"freeze", "bounce":
|
|
||||||
return _best(creatures, func(c): return c.card.cost * 100 + c.power())
|
|
||||||
"buff", "shield":
|
|
||||||
return _best(creatures, func(c): return (100 if c.can_attack() else 0) + c.power())
|
|
||||||
return valid[0]
|
|
||||||
|
|
||||||
|
|
||||||
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
||||||
var enemies := game.enemies_of(p)
|
for e in game.enemies_of(p):
|
||||||
var total := 0
|
if e.board.is_empty():
|
||||||
for c in p.board:
|
return e if game.can_attack(cr, e)[0] else null
|
||||||
if c.can_attack():
|
var creatures: Array = e.board.filter(func(c): return game.can_attack(cr, c)[0])
|
||||||
total += c.power() * c.attacks_left
|
if creatures.is_empty():
|
||||||
var options := []
|
continue
|
||||||
for e in enemies:
|
var good: Array = creatures.filter(func(c): return not c.hidden and _kills(cr, c) and not _kills(c, cr))
|
||||||
var taunts: Array = e.board.filter(func(c): return c.taunt())
|
if not good.is_empty():
|
||||||
if not taunts.is_empty():
|
return _best(good, Callable(AI, "_value"))
|
||||||
options.append_array(taunts)
|
var trade: Array = creatures.filter(func(c): return not c.hidden and _kills(cr, c) and _value(c) > _value(cr))
|
||||||
else:
|
if not trade.is_empty():
|
||||||
if total >= e.life:
|
return _best(trade, Callable(AI, "_value"))
|
||||||
return e
|
|
||||||
options.append_array(e.board)
|
|
||||||
options.append(e)
|
|
||||||
var creatures := options.filter(func(t): return t is Game.Creature)
|
|
||||||
var heroes := options.filter(func(t): return t is Game.Player)
|
|
||||||
var good := creatures.filter(func(c): return _kills(cr, c) and not _kills(c, cr))
|
|
||||||
if not good.is_empty():
|
|
||||||
return _best(good, Callable(AI, "_value"))
|
|
||||||
var trade := creatures.filter(func(c): return _kills(cr, c) and _value(c) > _value(cr))
|
|
||||||
if not trade.is_empty():
|
|
||||||
return _best(trade, Callable(AI, "_value"))
|
|
||||||
if not heroes.is_empty():
|
|
||||||
return _weakest(heroes)
|
|
||||||
if not creatures.is_empty():
|
|
||||||
return _best(creatures, func(c): return 1000 - c.health)
|
return _best(creatures, func(c): return 1000 - c.health)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -396,11 +336,15 @@ static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
|||||||
static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void:
|
static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void:
|
||||||
match action.type:
|
match action.type:
|
||||||
"play":
|
"play":
|
||||||
game.play_card(p, action.card, action.target)
|
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||||
"buy":
|
"pick":
|
||||||
game.buy(p, int(action.slot))
|
game.pick(p, int(action.index))
|
||||||
"reroll":
|
"draw":
|
||||||
game.reroll(p)
|
game.draw(p)
|
||||||
|
"reveal":
|
||||||
|
game.reveal(p, action.creature)
|
||||||
|
"upgrade":
|
||||||
|
game.upgrade(p, action.creature, action.keyword)
|
||||||
"attack":
|
"attack":
|
||||||
game.attack(action.creature, action.target)
|
game.attack(action.creature, action.target)
|
||||||
"fuse":
|
"fuse":
|
||||||
@@ -416,3 +360,4 @@ static func take_turn(game: Game, p: Game.Player, greedy: bool = false, limit: i
|
|||||||
apply_action(game, p, action)
|
apply_action(game, p, action)
|
||||||
if action.type == "end" or game.winner != "":
|
if action.type == "end" or game.winner != "":
|
||||||
return
|
return
|
||||||
|
game.end_turn()
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
class_name Campaign
|
class_name Campaign
|
||||||
## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Leben und Merkmalen.
|
## Kampagne: zehn benannte Magier in drei Kapiteln. Die Bosse haben weniger Leben als du,
|
||||||
|
## dafür Merkmale; je Kapitel kommt eins dazu.
|
||||||
|
|
||||||
const STAGES := [
|
const STAGES := [
|
||||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "life": 30, "reward": 100, "perk": "feuer_angriff"},
|
{"name": "Fenn", "title": "Kobold-Zähmer", "life": 22, "reward": 100, "perk": "feuer_angriff"},
|
||||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "life": 30, "reward": 100, "perk": "leben_35"},
|
{"name": "Mara", "title": "Gezeitenhexe", "life": 22, "reward": 100, "perk": "leben_35"},
|
||||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "life": 31, "reward": 150, "perk": "waechter_leben"},
|
{"name": "Borin", "title": "Steinfaust", "life": 24, "reward": 150, "perk": "wesen_angriff"},
|
||||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "life": 31, "reward": 150, "perk": "flink_angriff"},
|
{"name": "Liska", "title": "Windläuferin", "life": 24, "reward": 150, "perk": "ziehen_gratis"},
|
||||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "life": 32, "reward": 200, "perk": "wesen_leben"},
|
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"},
|
||||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 200, "perk": "extra_mana"},
|
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"},
|
||||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "life": 33, "reward": 250, "perk": "resonanz_doppelt"},
|
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"},
|
||||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "life": 33, "reward": 250, "perk": "schild_wesen"},
|
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "schild_wesen"},
|
||||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 300, "perk": "alle_flink"},
|
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "verbessern_billig"},
|
||||||
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 500, "perk": "start_energie"},
|
{"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
|
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
|
||||||
const STAGES2 := [
|
const STAGES2 := [
|
||||||
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "life": 30, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
{"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||||
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "life": 30, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
{"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
||||||
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "life": 31, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "schild_wesen"]},
|
||||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "life": 31, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
{"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "verbessern_billig"]},
|
||||||
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "life": 32, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
{"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
||||||
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
|
{"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]},
|
||||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "life": 33, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]},
|
{"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]},
|
||||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "life": 33, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["verbessern_billig", "start_energie"]},
|
||||||
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]},
|
{"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
## Kapitel 3: drei Merkmale und volle Tische.
|
## Kapitel 3: drei Merkmale und volle Tische.
|
||||||
const STAGES3 := [
|
const STAGES3 := [
|
||||||
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "life": 32, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
{"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
||||||
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "life": 32, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
||||||
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "life": 33, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
|
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "schild_wesen", "leben_35"]},
|
||||||
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "life": 33, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]},
|
{"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "verbessern_billig", "extra_mana"]},
|
||||||
{"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "life": 34, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "waechter_leben"]},
|
{"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]},
|
||||||
{"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "life": 34, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]},
|
{"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]},
|
||||||
{"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "life": 35, "reward": 900, "perks": ["resonanz_doppelt", "wesen_leben", "start_energie"]},
|
{"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]},
|
||||||
{"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "life": 35, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
|
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["schild_wesen", "leben_35", "element_doppelt"]},
|
||||||
{"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "life": 36, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
|
{"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["verbessern_billig", "start_energie", "wesen_leben"]},
|
||||||
{"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "life": 36, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]},
|
{"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
||||||
@@ -63,9 +64,9 @@ static func stage(i: int) -> Dictionary:
|
|||||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||||
|
|
||||||
|
|
||||||
## Boss einer Stufe: handelt nur mit seinen Elementen, mit dem Leben und den Merkmalen der Stufe.
|
## Boss einer Stufe mit dem Leben und den Merkmalen der Stufe.
|
||||||
static func opponent(stage_index: int) -> Game.Player:
|
static func opponent(stage_index: int) -> Game.Player:
|
||||||
var st := stage(stage_index)
|
var st := stage(stage_index)
|
||||||
var boss := Game.Player.new(st.name, false, int(st.life), st.title, st.elements)
|
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
|
||||||
boss.perks = Array(st.perks)
|
boss.perks = Array(st.perks)
|
||||||
return boss
|
return boss
|
||||||
|
|||||||
240
scripts/cards.gd
240
scripts/cards.gd
@@ -1,5 +1,5 @@
|
|||||||
class_name Cards
|
class_name Cards
|
||||||
## Elemente, Kombos und der Kartenpool.
|
## Elemente, Kombonamen, Schlüsselwörter und der Kartenpool.
|
||||||
|
|
||||||
const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"]
|
const ELEMENTS: Array[String] = ["Feuer", "Wasser", "Erde", "Luft"]
|
||||||
const ELEMENT_COLORS := {
|
const ELEMENT_COLORS := {
|
||||||
@@ -8,8 +8,7 @@ const ELEMENT_COLORS := {
|
|||||||
"Erde": Color("73552d"),
|
"Erde": Color("73552d"),
|
||||||
"Luft": Color("78aac8"),
|
"Luft": Color("78aac8"),
|
||||||
}
|
}
|
||||||
# Jede Karte hat genau ein Element. Kombos entstehen erst im Spiel: beim Verschmelzen
|
# Jede Karte hat genau ein Element. Kombos sind die Namen verschmolzener Wesen.
|
||||||
# (der Name des neuen Wesens) und als Feldkombo zwischen benachbarten Elementen.
|
|
||||||
const COMBOS := {
|
const COMBOS := {
|
||||||
"Feuer+Feuer": "Inferno",
|
"Feuer+Feuer": "Inferno",
|
||||||
"Wasser+Wasser": "Tiefsee",
|
"Wasser+Wasser": "Tiefsee",
|
||||||
@@ -22,26 +21,8 @@ const COMBOS := {
|
|||||||
"Wasser+Luft": "Sturm",
|
"Wasser+Luft": "Sturm",
|
||||||
"Erde+Luft": "Sand",
|
"Erde+Luft": "Sand",
|
||||||
}
|
}
|
||||||
# Feldkombos: entstehen, wenn ein Wesen neben einem Wesen mit anderem Element ausgespielt wird.
|
|
||||||
const FIELD_COMBO_TEXT := {
|
|
||||||
"Dampf": "Friere das stärkste gegnerische Wesen ein",
|
|
||||||
"Lava": "1 Schaden an das stärkste gegnerische Wesen",
|
|
||||||
"Blitz": "2 Schaden an den Gegner",
|
|
||||||
"Schlamm": "Heile dich um 3",
|
|
||||||
"Sturm": "Erhalte 1 Mana",
|
|
||||||
"Sand": "Das neue Wesen erhält +1/+1",
|
|
||||||
}
|
|
||||||
# Elementkreis: Wasser löscht Feuer, Feuer verbrennt Luft, Luft trägt Erde ab, Erde saugt Wasser auf.
|
# Elementkreis: Wasser löscht Feuer, Feuer verbrennt Luft, Luft trägt Erde ab, Erde saugt Wasser auf.
|
||||||
const BEATS := {"Wasser": "Feuer", "Feuer": "Luft", "Luft": "Erde", "Erde": "Wasser"}
|
const BEATS := {"Wasser": "Feuer", "Feuer": "Luft", "Luft": "Erde", "Erde": "Wasser"}
|
||||||
# Das Feld gehört allen am Tisch: jede gespielte Karte lädt ihre Elemente, bei FIELD_CHARGE kippt es.
|
|
||||||
const FIELD_CHARGE := 4
|
|
||||||
const FIELD_NAMES := {"Feuer": "Vulkan", "Wasser": "Flut", "Erde": "Fels", "Luft": "Wirbel"}
|
|
||||||
const FIELD_TEXT := {
|
|
||||||
"Feuer": "Feuerwesen haben +1 Angriff",
|
|
||||||
"Wasser": "Wasserwesen haben +1 Angriff",
|
|
||||||
"Erde": "Erdwesen haben +1 Angriff",
|
|
||||||
"Luft": "Luftwesen haben +1 Angriff",
|
|
||||||
}
|
|
||||||
# Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich.
|
# Seltenheit ist reine Optik: jede Karte gibt es in jeder Stufe, die Werte bleiben gleich.
|
||||||
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
|
const RARITIES: Array[String] = ["gewöhnlich", "selten", "episch", "legendär", "mythisch"]
|
||||||
const RARITY_COLORS := {
|
const RARITY_COLORS := {
|
||||||
@@ -51,8 +32,10 @@ const RARITY_COLORS := {
|
|||||||
"legendär": Color("f0aa28"),
|
"legendär": Color("f0aa28"),
|
||||||
"mythisch": Color("ff5a8c"),
|
"mythisch": Color("ff5a8c"),
|
||||||
}
|
}
|
||||||
const KEYWORDS: Array[String] = ["Wächter", "Flink", "Schild", "Doppelschlag"]
|
# Schlüsselwörter haben Karten nicht von Haus aus; sie werden für Mana dazugekauft (Game.upgrade).
|
||||||
const CHOSEN_TARGETS: Array[String] = ["enemy_creature", "enemy_any", "friendly_creature"]
|
const KEYWORDS: Array[String] = ["Schild", "Doppelschlag"]
|
||||||
|
const KEYWORD_COST := {"Schild": 3, "Doppelschlag": 4}
|
||||||
|
const KEYWORD_TEXT := {"Schild": "Blockt den ersten Schaden", "Doppelschlag": "Greift zweimal je Zug"}
|
||||||
const DATA_PATH := "res://data/karten.json"
|
const DATA_PATH := "res://data/karten.json"
|
||||||
|
|
||||||
|
|
||||||
@@ -70,20 +53,49 @@ static func combo_name(elements: Array) -> String:
|
|||||||
|
|
||||||
|
|
||||||
## Elementvorteil aus Sicht von own: +1 im Vorteil, -1 im Nachteil, sonst 0.
|
## Elementvorteil aus Sicht von own: +1 im Vorteil, -1 im Nachteil, sonst 0.
|
||||||
## Mehrfachelemente nehmen den für sie besseren Wert.
|
## Hat ein Wesen mehrere Elemente, kämpft es mit dem, das gegen die Gegenseite am besten steht.
|
||||||
static func matchup(own: Array, other: Array) -> int:
|
static func matchup(own: Array, other: Array) -> int:
|
||||||
for e in own:
|
var mine := best_element(own, other)
|
||||||
if BEATS.get(e, "") in other:
|
var theirs := best_element(other, own)
|
||||||
return 1
|
if mine == "" or theirs == "":
|
||||||
for e in other:
|
return 0
|
||||||
if BEATS.get(e, "") in own:
|
return duel(mine, theirs)
|
||||||
return -1
|
|
||||||
|
|
||||||
|
## +1, wenn a b schlägt, -1, wenn b a schlägt, sonst 0.
|
||||||
|
static func duel(a: String, b: String) -> int:
|
||||||
|
if BEATS.get(a, "") == b:
|
||||||
|
return 1
|
||||||
|
if BEATS.get(b, "") == a:
|
||||||
|
return -1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
## Mana für eine Verschmelzung: Durchschnitt der Kartenkosten, aufgerundet.
|
## Das Element aus own, das gegen die Elemente in other am besten steht.
|
||||||
static func fusion_cost(a: Card, b: Card) -> int:
|
static func best_element(own: Array, other: Array) -> String:
|
||||||
return ceili((a.cost + b.cost) / 2.0)
|
var best := ""
|
||||||
|
var best_score := -9
|
||||||
|
for e in own:
|
||||||
|
var score := 0
|
||||||
|
for o in other:
|
||||||
|
score += duel(e, o)
|
||||||
|
if score > best_score:
|
||||||
|
best = e
|
||||||
|
best_score = score
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
## Welches Element ein Element schlägt (und von welchem es geschlagen wird).
|
||||||
|
static func beaten_by(element: String) -> String:
|
||||||
|
for e in BEATS:
|
||||||
|
if BEATS[e] == element:
|
||||||
|
return e
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Kosten einer Karte: Angriff plus Leben, halbiert und aufgerundet.
|
||||||
|
static func cost_of(attack: int, health: int) -> int:
|
||||||
|
return ceili((attack + health) / 2.0)
|
||||||
|
|
||||||
|
|
||||||
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
||||||
@@ -91,8 +103,8 @@ static func fusion_name(elements: Array) -> String:
|
|||||||
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
|
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
|
||||||
|
|
||||||
|
|
||||||
## Karte des verschmolzenen Wesens: Elemente und Schlüsselwörter beider Karten,
|
## Karte des verschmolzenen Wesens: Elemente und Schlüsselwörter beider Karten, die Werte
|
||||||
## Werte kommen vom Spiel (Durchschnitt plus 1, siehe Game.fuse).
|
## bestimmt Game.fuse, eine Verschmelzung mehr als der erfahrenere Elternteil.
|
||||||
static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
|
static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
|
||||||
var els := []
|
var els := []
|
||||||
for e in a.elements + b.elements:
|
for e in a.elements + b.elements:
|
||||||
@@ -103,99 +115,27 @@ static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
|
|||||||
c.id = "fus_%s_%s" % [a.id, b.id]
|
c.id = "fus_%s_%s" % [a.id, b.id]
|
||||||
c.name = fusion_name(els)
|
c.name = fusion_name(els)
|
||||||
c.elements = els
|
c.elements = els
|
||||||
c.cost = fusion_cost(a, b)
|
|
||||||
c.attack = attack
|
c.attack = attack
|
||||||
c.health = health
|
c.health = health
|
||||||
|
c.cost = cost_of(attack, health)
|
||||||
for k in KEYWORDS:
|
for k in KEYWORDS:
|
||||||
if k in a.keywords or k in b.keywords:
|
if k in a.keywords or k in b.keywords:
|
||||||
c.keywords.append(k)
|
c.keywords.append(k)
|
||||||
|
c.fusions = maxi(a.fusions, b.fusions) + 1
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
static func field_combo_effect(name: String) -> Effect:
|
## Platzhalter für ein verdecktes gegnerisches Wesen: unbekanntes Element, geschätzte Werte.
|
||||||
match name:
|
static func unknown() -> Card:
|
||||||
"Dampf":
|
var c := Card.new()
|
||||||
return Effect.new("freeze", "strongest_enemy_creature")
|
c.id = "?"
|
||||||
"Lava":
|
c.name = "Verdeckt"
|
||||||
return Effect.new("damage", "strongest_enemy_creature", 1)
|
c.name_en = "Face-down"
|
||||||
"Blitz":
|
c.elements = []
|
||||||
return Effect.new("damage", "weakest_enemy_hero", 2)
|
c.attack = 2
|
||||||
"Schlamm":
|
c.health = 3
|
||||||
return Effect.new("heal", "self_hero", 3)
|
c.cost = cost_of(2, 3)
|
||||||
"Sturm":
|
return c
|
||||||
return Effect.new("mana", "none", 1)
|
|
||||||
"Sand":
|
|
||||||
return Effect.new("buff", "self_creature", 1, 1)
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
class Effect extends RefCounted:
|
|
||||||
var kind: String # damage, heal, mana, freeze, bounce, buff, shield
|
|
||||||
var target: String # siehe CHOSEN_TARGETS und Game.resolve()
|
|
||||||
var amount: int
|
|
||||||
var amount2: int
|
|
||||||
|
|
||||||
func _init(k: String, t: String, a: int = 0, a2: int = 0) -> void:
|
|
||||||
kind = k
|
|
||||||
target = t
|
|
||||||
amount = a
|
|
||||||
amount2 = a2
|
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
|
||||||
var d := {"kind": kind, "target": target}
|
|
||||||
if amount != 0:
|
|
||||||
d["amount"] = amount
|
|
||||||
if amount2 != 0:
|
|
||||||
d["amount2"] = amount2
|
|
||||||
return d
|
|
||||||
|
|
||||||
static func from_dict(d: Dictionary) -> Effect:
|
|
||||||
return Effect.new(d.kind, d.get("target", "none"), int(d.get("amount", 0)), int(d.get("amount2", 0)))
|
|
||||||
|
|
||||||
## Kartentext in der aktuellen Sprache.
|
|
||||||
func text() -> String:
|
|
||||||
var n := str(amount)
|
|
||||||
match kind:
|
|
||||||
"damage":
|
|
||||||
match target:
|
|
||||||
"enemy_creature":
|
|
||||||
return tr("%s Schaden an ein gegnerisches Wesen") % n
|
|
||||||
"enemy_any":
|
|
||||||
return tr("%s Schaden an ein Ziel") % n
|
|
||||||
"all_enemy_creatures":
|
|
||||||
return tr("%s Schaden an alle gegnerischen Wesen") % n
|
|
||||||
"all_enemy_heroes":
|
|
||||||
return tr("%s Schaden an den Gegner") % n
|
|
||||||
"all_enemies":
|
|
||||||
return tr("%s Schaden an den Gegner und seine Wesen") % n
|
|
||||||
"heal":
|
|
||||||
return tr("Heile dich um %s") % n
|
|
||||||
"mana":
|
|
||||||
if amount == 1:
|
|
||||||
return tr("Erhalte 1 Mana")
|
|
||||||
return tr("Erhalte %s Mana") % n
|
|
||||||
"freeze":
|
|
||||||
if target == "enemy_creature":
|
|
||||||
return tr("Friere ein gegnerisches Wesen ein")
|
|
||||||
if target == "strongest_enemy_creature":
|
|
||||||
return tr("Friere das stärkste gegnerische Wesen ein")
|
|
||||||
return tr("Friere alle gegnerischen Wesen ein")
|
|
||||||
"bounce":
|
|
||||||
if target == "enemy_creature":
|
|
||||||
return tr("Ein gegnerisches Wesen zurück auf die Hand")
|
|
||||||
return tr("Alle gegnerischen Wesen zurück auf die Hand")
|
|
||||||
"buff":
|
|
||||||
var b := "+%d/+%d" % [amount, amount2]
|
|
||||||
if target == "friendly_creature":
|
|
||||||
return tr("Ein befreundetes Wesen erhält %s") % b
|
|
||||||
return tr("Alle befreundeten Wesen erhalten %s") % b
|
|
||||||
"shield":
|
|
||||||
if target == "friendly_creature":
|
|
||||||
return tr("Ein befreundetes Wesen erhält Schild")
|
|
||||||
if target == "self_creature":
|
|
||||||
return tr("Erhält Schild")
|
|
||||||
return tr("Alle befreundeten Wesen erhalten Schild")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Card extends RefCounted:
|
class Card extends RefCounted:
|
||||||
@@ -203,19 +143,18 @@ class Card extends RefCounted:
|
|||||||
var name: String # deutscher Name (auch im Verlauf und in Tests)
|
var name: String # deutscher Name (auch im Verlauf und in Tests)
|
||||||
var name_en := "" # englischer Name
|
var name_en := "" # englischer Name
|
||||||
var elements: Array
|
var elements: Array
|
||||||
var cost: int
|
|
||||||
var attack: int = 0
|
var attack: int = 0
|
||||||
var health: int = 0
|
var health: int = 0
|
||||||
var keywords: Array = []
|
var cost: int = 0 # abgeleitet: (Angriff + Leben) / 2, aufgerundet
|
||||||
var effects: Array = []
|
var keywords: Array = [] # dazugekauft, je Exemplar
|
||||||
|
var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …)
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
func to_dict() -> Dictionary:
|
||||||
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "cost": cost,
|
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
||||||
"attack": attack, "health": health}
|
|
||||||
if not keywords.is_empty():
|
if not keywords.is_empty():
|
||||||
d["keywords"] = keywords
|
d["keywords"] = keywords
|
||||||
if not effects.is_empty():
|
if fusions > 0:
|
||||||
d["effects"] = effects.map(func(e): return e.to_dict())
|
d["fusions"] = fusions
|
||||||
return d
|
return d
|
||||||
|
|
||||||
static func from_dict(d: Dictionary) -> Card:
|
static func from_dict(d: Dictionary) -> Card:
|
||||||
@@ -224,14 +163,17 @@ class Card extends RefCounted:
|
|||||||
c.name = d.name
|
c.name = d.name
|
||||||
c.name_en = str(d.get("name_en", ""))
|
c.name_en = str(d.get("name_en", ""))
|
||||||
c.elements = Array(d.elements)
|
c.elements = Array(d.elements)
|
||||||
c.cost = int(d.cost)
|
|
||||||
c.attack = int(d.get("attack", 0))
|
c.attack = int(d.get("attack", 0))
|
||||||
c.health = int(d.get("health", 0))
|
c.health = int(d.get("health", 0))
|
||||||
|
c.cost = Cards.cost_of(c.attack, c.health)
|
||||||
c.keywords = Array(d.get("keywords", []))
|
c.keywords = Array(d.get("keywords", []))
|
||||||
for e in d.get("effects", []):
|
c.fusions = int(d.get("fusions", 0))
|
||||||
c.effects.append(Effect.from_dict(e))
|
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool.
|
||||||
|
func copy() -> Card:
|
||||||
|
return Card.from_dict(to_dict())
|
||||||
|
|
||||||
## Anzeigename in der aktuellen Sprache.
|
## Anzeigename in der aktuellen Sprache.
|
||||||
func title() -> String:
|
func title() -> String:
|
||||||
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
||||||
@@ -244,35 +186,19 @@ class Card extends RefCounted:
|
|||||||
func combo() -> String:
|
func combo() -> String:
|
||||||
return Cards.combo_name(elements)
|
return Cards.combo_name(elements)
|
||||||
|
|
||||||
func target_type() -> String:
|
## Mana für die nächste Verschmelzung mit dieser Karte.
|
||||||
for fx in effects:
|
func fusion_cost() -> int:
|
||||||
if fx.target in Cards.CHOSEN_TARGETS:
|
return 1 << fusions
|
||||||
return fx.target
|
|
||||||
return ""
|
|
||||||
|
|
||||||
## Nur die Effekte (die Karte zeigt Schlüsselwörter als Symbole).
|
|
||||||
func effect_text() -> String:
|
|
||||||
if effects.is_empty():
|
|
||||||
return ""
|
|
||||||
var fx: Array[String] = []
|
|
||||||
for f in effects:
|
|
||||||
fx.append(f.text())
|
|
||||||
return ", ".join(fx) + "."
|
|
||||||
|
|
||||||
|
## Kartentext: die gekauften Schlüsselwörter.
|
||||||
func text() -> String:
|
func text() -> String:
|
||||||
var parts: Array[String] = []
|
if keywords.is_empty():
|
||||||
if not keywords.is_empty():
|
return ""
|
||||||
var kws: Array[String] = []
|
var kws: Array[String] = []
|
||||||
for k in Cards.KEYWORDS:
|
for k in Cards.KEYWORDS:
|
||||||
if k in keywords:
|
if k in keywords:
|
||||||
kws.append(tr(k))
|
kws.append(tr(k))
|
||||||
parts.append(", ".join(kws) + ".")
|
return ", ".join(kws) + "."
|
||||||
if not effects.is_empty():
|
|
||||||
var fx: Array[String] = []
|
|
||||||
for f in effects:
|
|
||||||
fx.append(f.text())
|
|
||||||
parts.append(tr("Beim Ausspielen: %s") % (", ".join(fx) + "."))
|
|
||||||
return " ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
static var _all: Array = []
|
static var _all: Array = []
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
class_name DeckGen
|
class_name DeckGen
|
||||||
## Ladenangebot und Gegner (ausgelagert aus den Spielregeln).
|
## Der gemeinsame Stapel (jede Karte dreimal) und Gegner.
|
||||||
## Es gibt keine Decks: jeder kauft jede Runde aus seinem eigenen Laden.
|
|
||||||
|
|
||||||
# Abnehmende Resonanz je Anzahl gleichelementiger Partner (Index = Partner).
|
const COPIES := 3
|
||||||
const RESONANCE_STEPS := [0, 1, 2, 3, 3]
|
|
||||||
|
|
||||||
|
|
||||||
## Karten, die im Laden auftauchen dürfen: höchstens Einkommen plus eins teuer,
|
## Frisch gemischter Stapel; das Ende des Arrays ist oben.
|
||||||
## bei gesetzten Elementen nur aus diesen.
|
static func deck(r: RandomNumberGenerator) -> Array:
|
||||||
static func pool(income: int, elements: Array = []) -> Array:
|
|
||||||
var limit := income + 1
|
|
||||||
return Cards.all().filter(func(c):
|
|
||||||
return c.cost <= limit and (elements.is_empty() or c.elements.all(func(e): return e in elements)))
|
|
||||||
|
|
||||||
|
|
||||||
## Angebot des Ladens: n zufällige Karten (Wiederholungen erlaubt, wie in TFT).
|
|
||||||
## Mindestens eine Karte ist vom Einkommen allein bezahlbar, sonst wäre der Zug tot.
|
|
||||||
static func shop_cards(r: RandomNumberGenerator, income: int, elements: Array = [], n: int = Game.SHOP_SIZE) -> Array:
|
|
||||||
var p := pool(income, elements)
|
|
||||||
if p.is_empty():
|
|
||||||
p = pool(Game.MAX_ENERGY, elements)
|
|
||||||
assert(not p.is_empty(), "kein Angebot für " + str(elements))
|
|
||||||
var out := []
|
var out := []
|
||||||
for i in n:
|
for c in Cards.all():
|
||||||
out.append(p[r.randi_range(0, p.size() - 1)])
|
for i in COPIES:
|
||||||
var cheap := p.filter(func(c): return c.cost <= income)
|
out.append(c)
|
||||||
if not cheap.is_empty() and not out.any(func(c): return c.cost <= income):
|
Game.shuffle(out, r)
|
||||||
out[r.randi_range(0, out.size() - 1)] = cheap[r.randi_range(0, cheap.size() - 1)]
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Gegner mit eigenem Laden über alle Elemente.
|
|
||||||
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
|
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
|
||||||
return Game.Player.new("Gegner", false, life)
|
return Game.Player.new("Gegner", false, life)
|
||||||
|
|||||||
679
scripts/game.gd
679
scripts/game.gd
@@ -1,27 +1,30 @@
|
|||||||
class_name Game
|
class_name Game
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Spielregeln: Spieler, Wesen, Züge, Angriffe, Effekte, Feldkombos, Resonanz.
|
## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen,
|
||||||
|
## Vier-Elemente-Regel, Verschmelzen und Verbessern.
|
||||||
|
|
||||||
const MAX_BOARD := 5
|
const MAX_BOARD := 5
|
||||||
const MAX_HAND := 8
|
const MAX_HAND := 5
|
||||||
const MAX_ENERGY := 10 # Einkommen je Zug, höher steigt es nicht
|
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||||
const SHOP_SIZE := 5 # so viele Karten liegen im Laden
|
const DRAW_COST := 1 # Ziehen: drei Karten sehen, eine behalten
|
||||||
const REROLL_COST := 1
|
const OFFER_SIZE := 3
|
||||||
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
|
const START_OFFER := 5 # zu Beginn fünf sehen, drei behalten
|
||||||
|
const START_PICKS := 3
|
||||||
const START_LIFE := 30
|
const START_LIFE := 30
|
||||||
|
const ELEMENT_STACK := 4 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1, das geschlagene Element -1/-1
|
||||||
|
|
||||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||||
const PERKS := {
|
const PERKS := {
|
||||||
"feuer_angriff": "Feuerwesen haben +1 Angriff",
|
"feuer_angriff": "Feuerwesen haben +1 Angriff",
|
||||||
"flink_angriff": "Flinke Wesen haben +1 Angriff",
|
"ziehen_gratis": "Ziehen kostet kein Mana",
|
||||||
"waechter_leben": "Wächter kommen mit +2 Leben",
|
"wesen_angriff": "Wesen kommen mit +1 Angriff",
|
||||||
"wesen_leben": "Wesen kommen mit +1 Leben",
|
"wesen_leben": "Wesen kommen mit +1 Leben",
|
||||||
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
||||||
"leben_35": "Beginnt mit 5 Leben mehr",
|
"leben_35": "Beginnt mit 5 Leben mehr",
|
||||||
"schild_wesen": "Wesen kommen mit Schild",
|
"schild_wesen": "Wesen kommen mit Schild",
|
||||||
"alle_flink": "Alle Wesen sind flink",
|
"verbessern_billig": "Eigenschaften kosten 1 weniger",
|
||||||
"start_energie": "Beginnt mit 3 Mana",
|
"start_energie": "Hat im ersten Zug 2 Mana mehr",
|
||||||
"resonanz_doppelt": "Resonanz zählt doppelt",
|
"element_doppelt": "Elementbonus zählt doppelt",
|
||||||
}
|
}
|
||||||
|
|
||||||
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
|
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
|
||||||
@@ -38,21 +41,18 @@ static func make_rng() -> RandomNumberGenerator:
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Creature extends RefCounted:
|
class Creature extends RefCounted:
|
||||||
var card: Cards.Card
|
var card: Cards.Card
|
||||||
var owner_ref: WeakRef # schwache Referenz: kein Zyklus Spieler <-> Wesen
|
var owner_ref: WeakRef # schwache Referenz: kein Zyklus Spieler <-> Wesen
|
||||||
var name: String
|
var name: String
|
||||||
var uid: int
|
var uid: int
|
||||||
var attack: int
|
var attack: int
|
||||||
var resonance := 0 # +1, wenn ein anderes eigenes Wesen ein Element teilt
|
|
||||||
var field_bonus := 0 # +1, wenn das Feld auf dem eigenen Element steht
|
|
||||||
var health: int
|
var health: int
|
||||||
var max_health: int
|
var max_health: int
|
||||||
var shield: bool
|
var shield: bool
|
||||||
var frozen := false
|
|
||||||
var ready: bool
|
|
||||||
var attacks_left: int
|
var attacks_left: int
|
||||||
|
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementregel
|
||||||
|
var aura := 0 # Vier-Elemente-Regel: +1 oder -1 auf den Angriff (Leben wird direkt angepasst)
|
||||||
|
|
||||||
func _init(c: Cards.Card, o, id: int) -> void:
|
func _init(c: Cards.Card, o, id: int) -> void:
|
||||||
card = c
|
card = c
|
||||||
@@ -63,43 +63,35 @@ class Creature extends RefCounted:
|
|||||||
health = c.health
|
health = c.health
|
||||||
max_health = c.health
|
max_health = c.health
|
||||||
shield = "Schild" in c.keywords
|
shield = "Schild" in c.keywords
|
||||||
ready = "Flink" in c.keywords
|
attacks_left = attacks_per_turn() # frisch gelegte Wesen greifen sofort an
|
||||||
attacks_left = attacks_per_turn() if ready else 0
|
|
||||||
|
|
||||||
func owner():
|
func owner():
|
||||||
return owner_ref.get_ref()
|
return owner_ref.get_ref()
|
||||||
|
|
||||||
## Angriffswert inklusive Resonanz, Feld und Boss-Merkmalen.
|
## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
|
||||||
func power() -> int:
|
func power() -> int:
|
||||||
var v := attack + resonance + field_bonus
|
var v := attack + aura
|
||||||
var o = owner()
|
var o = owner()
|
||||||
if o != null:
|
if o != null:
|
||||||
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
||||||
v += 1
|
v += 1
|
||||||
if "flink_angriff" in o.perks and "Flink" in card.keywords:
|
|
||||||
v += 1
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
func attacks_per_turn() -> int:
|
func attacks_per_turn() -> int:
|
||||||
return 2 if "Doppelschlag" in card.keywords else 1
|
return 2 if "Doppelschlag" in card.keywords else 1
|
||||||
|
|
||||||
func taunt() -> bool:
|
|
||||||
return "Wächter" in card.keywords
|
|
||||||
|
|
||||||
func can_attack() -> bool:
|
func can_attack() -> bool:
|
||||||
return ready and attacks_left > 0 and not frozen and power() > 0
|
return attacks_left > 0 and not hidden and power() > 0
|
||||||
|
|
||||||
func clone(new_owner) -> Creature:
|
func clone(new_owner) -> Creature:
|
||||||
var c := Creature.new(card, new_owner, uid)
|
var c := Creature.new(card, new_owner, uid)
|
||||||
c.attack = attack
|
c.attack = attack
|
||||||
c.resonance = resonance
|
|
||||||
c.field_bonus = field_bonus
|
|
||||||
c.health = health
|
c.health = health
|
||||||
c.max_health = max_health
|
c.max_health = max_health
|
||||||
c.shield = shield
|
c.shield = shield
|
||||||
c.frozen = frozen
|
|
||||||
c.ready = ready
|
|
||||||
c.attacks_left = attacks_left
|
c.attacks_left = attacks_left
|
||||||
|
c.hidden = hidden
|
||||||
|
c.aura = aura
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
@@ -109,33 +101,33 @@ class Player extends RefCounted:
|
|||||||
var is_human: bool
|
var is_human: bool
|
||||||
var life: int
|
var life: int
|
||||||
var max_life: int
|
var max_life: int
|
||||||
var shop: Array = [] # SHOP_SIZE Plätze, gekaufte sind null
|
var offer: Array = [] # offene Wahl: angebotene Karten (Start: 5, Ziehen: 3)
|
||||||
var hand: Array = []
|
var picks_left := 0 # wie viele davon noch zu nehmen sind
|
||||||
|
var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
|
||||||
var board: Array = []
|
var board: Array = []
|
||||||
var energy := 0 # Mana; nicht ausgegebenes bleibt liegen
|
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
||||||
var max_energy := 0 # Einkommen je Zug
|
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
|
||||||
var start_bonus := 0 # einmaliges Extra-Mana für den späteren Start
|
var start_bonus := 0 # einmaliges Extra-Mana für den späteren Start
|
||||||
var order := 0 # Platz in der Zugreihenfolge
|
var order := 0 # Platz in der Zugreihenfolge
|
||||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||||
var elements: Array = [] # Laden nur aus diesen Elementen (leer = alle)
|
|
||||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||||
var turns := 0 # begonnene eigene Züge
|
var turns := 0 # begonnene eigene Züge
|
||||||
|
|
||||||
func _init(n: String, h: bool, l: int, t: String = "", els: Array = []) -> void:
|
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||||||
name = n
|
name = n
|
||||||
title = t
|
title = t
|
||||||
is_human = h
|
is_human = h
|
||||||
life = l
|
life = l
|
||||||
max_life = l
|
max_life = l
|
||||||
elements = els.duplicate()
|
|
||||||
|
|
||||||
func alive() -> bool:
|
func alive() -> bool:
|
||||||
return life > 0
|
return life > 0
|
||||||
|
|
||||||
func clone() -> Player:
|
func clone() -> Player:
|
||||||
var p := Player.new(name, is_human, life, title, elements)
|
var p := Player.new(name, is_human, life, title)
|
||||||
p.max_life = max_life
|
p.max_life = max_life
|
||||||
p.shop = shop.duplicate()
|
p.offer = offer.duplicate()
|
||||||
|
p.picks_left = picks_left
|
||||||
p.hand = hand.duplicate()
|
p.hand = hand.duplicate()
|
||||||
p.energy = energy
|
p.energy = energy
|
||||||
p.max_energy = max_energy
|
p.max_energy = max_energy
|
||||||
@@ -155,6 +147,7 @@ var opponent: Player # der andere am Tisch
|
|||||||
var players: Array = [] # in Zugreihenfolge
|
var players: Array = [] # in Zugreihenfolge
|
||||||
var winner_player: Player = null
|
var winner_player: Player = null
|
||||||
var rng: RandomNumberGenerator
|
var rng: RandomNumberGenerator
|
||||||
|
var deck: Array = [] # gemeinsamer Stapel, das Ende ist oben (nur Host und Einzelspieler)
|
||||||
var turn := 0
|
var turn := 0
|
||||||
var current_idx := 0
|
var current_idx := 0
|
||||||
var log: Array[String] = []
|
var log: Array[String] = []
|
||||||
@@ -162,8 +155,6 @@ var events: Array = [] # strukturierte Ereignisse für Animationen
|
|||||||
var winner := "" # "", "human" oder "opponents"
|
var winner := "" # "", "human" oder "opponents"
|
||||||
var quiet := false # in KI-Kopien: kein Verlauf, keine Ereignisse
|
var quiet := false # in KI-Kopien: kein Verlauf, keine Ereignisse
|
||||||
var next_uid := 1
|
var next_uid := 1
|
||||||
var field_element := "" # gemeinsames Feld aller am Tisch, "" = neutral
|
|
||||||
var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel
|
|
||||||
|
|
||||||
|
|
||||||
func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
||||||
@@ -174,8 +165,6 @@ func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
|||||||
if r == null:
|
if r == null:
|
||||||
r = make_rng()
|
r = make_rng()
|
||||||
rng = r
|
rng = r
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
field_charge[e] = 0
|
|
||||||
|
|
||||||
|
|
||||||
func current() -> Player:
|
func current() -> Player:
|
||||||
@@ -198,7 +187,7 @@ func ref_of(target) -> Dictionary:
|
|||||||
return {"kind": "player", "index": players.find(target)}
|
return {"kind": "player", "index": players.find(target)}
|
||||||
|
|
||||||
|
|
||||||
## Der Gegner von p, solange er lebt (als Liste, damit Effekte einheitlich bleiben).
|
## Der Gegner von p, solange er lebt (als Liste, für Schleifen über Ziele).
|
||||||
func enemies_of(p: Player) -> Array:
|
func enemies_of(p: Player) -> Array:
|
||||||
return players.filter(func(o): return o != p and o.alive())
|
return players.filter(func(o): return o != p and o.alive())
|
||||||
|
|
||||||
@@ -215,7 +204,7 @@ func find_creature(uid: int) -> Creature:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
## Unabhängige Kopie des Spielzustands (für die KI-Vorausschau).
|
## Unabhängige Kopie des Spielzustands (für Rückgängig und die KI-Vorausschau).
|
||||||
func clone() -> Game:
|
func clone() -> Game:
|
||||||
var copies := []
|
var copies := []
|
||||||
for p in players:
|
for p in players:
|
||||||
@@ -223,27 +212,46 @@ func clone() -> Game:
|
|||||||
var idx := players.find(human)
|
var idx := players.find(human)
|
||||||
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
||||||
g.players = copies # gleiche Zugreihenfolge wie das Original
|
g.players = copies # gleiche Zugreihenfolge wie das Original
|
||||||
|
g.deck = deck.duplicate()
|
||||||
g.turn = turn
|
g.turn = turn
|
||||||
g.current_idx = current_idx
|
g.current_idx = current_idx
|
||||||
g.winner = winner
|
g.winner = winner
|
||||||
g.next_uid = next_uid
|
g.next_uid = next_uid
|
||||||
g.field_element = field_element
|
|
||||||
g.field_charge = field_charge.duplicate()
|
|
||||||
g.quiet = true
|
g.quiet = true
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
## Kopie aus Sicht von viewer: verdeckte gegnerische Wesen sind unbekannt (für die KI).
|
||||||
|
func masked_clone(viewer: Player) -> Game:
|
||||||
|
var g := clone()
|
||||||
|
var me: Player = g.players[players.find(viewer)]
|
||||||
|
for p in g.players:
|
||||||
|
if p == me:
|
||||||
|
continue
|
||||||
|
for c in p.board:
|
||||||
|
if c.hidden:
|
||||||
|
c.card = Cards.unknown()
|
||||||
|
c.name = c.card.name
|
||||||
|
c.attack = c.card.attack
|
||||||
|
c.health = c.card.health
|
||||||
|
c.max_health = c.card.health
|
||||||
|
c.shield = false
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
# --- Zugablauf ---------------------------------------------------------------
|
# --- Zugablauf ---------------------------------------------------------------
|
||||||
|
|
||||||
func start(fixed_order: bool = false) -> void:
|
func start(fixed_order: bool = false) -> void:
|
||||||
if not fixed_order:
|
if not fixed_order:
|
||||||
shuffle(players, rng) # zufällige Zugreihenfolge
|
shuffle(players, rng) # zufällige Zugreihenfolge
|
||||||
|
deck = DeckGen.deck(rng)
|
||||||
for i in players.size():
|
for i in players.size():
|
||||||
var p: Player = players[i]
|
var p: Player = players[i]
|
||||||
p.order = i
|
p.order = i
|
||||||
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
|
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
|
||||||
p.start_bonus = 0 if i == 0 else 1
|
p.start_bonus = 0 if i == 0 else 1
|
||||||
roll_shop(p)
|
p.offer = draw_cards(START_OFFER, START_MANA + p.start_bonus)
|
||||||
|
p.picks_left = START_PICKS
|
||||||
turn = 1
|
turn = 1
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||||
@@ -253,62 +261,102 @@ func start(fixed_order: bool = false) -> void:
|
|||||||
begin_turn(current())
|
begin_turn(current())
|
||||||
|
|
||||||
|
|
||||||
## Füllt den Laden neu: SHOP_SIZE Karten, höchstens so teuer wie das Einkommen plus eins.
|
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten
|
||||||
func roll_shop(p: Player) -> void:
|
## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer.
|
||||||
p.shop = DeckGen.shop_cards(rng, p.max_energy, p.elements)
|
func draw_cards(n: int, max_cost: int) -> Array:
|
||||||
|
var out := []
|
||||||
|
var skipped := []
|
||||||
|
var looked := 0
|
||||||
|
while out.size() < n:
|
||||||
|
if deck.is_empty():
|
||||||
|
deck = DeckGen.deck(rng)
|
||||||
|
say("Der Stapel wird neu gemischt.")
|
||||||
|
var c: Cards.Card = deck.pop_back()
|
||||||
|
looked += 1
|
||||||
|
if c.cost <= max_cost or looked > deck.size() + skipped.size() + n:
|
||||||
|
out.append(c)
|
||||||
|
else:
|
||||||
|
skipped.append(c)
|
||||||
|
for c in skipped:
|
||||||
|
deck.insert(0, c)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Kauft eine Karte aus dem Laden auf die Hand. Gibt "" zurück oder eine Fehlermeldung.
|
## Solange eine Wahl offen ist, geht nichts anderes.
|
||||||
func buy(p: Player, slot: int) -> String:
|
func blocked(p: Player) -> String:
|
||||||
|
return "Erst wählen" if not p.offer.is_empty() else ""
|
||||||
|
|
||||||
|
|
||||||
|
## Ziehen: kostet DRAW_COST, zeigt OFFER_SIZE Karten, eine davon kommt auf die Hand (siehe pick).
|
||||||
|
func can_draw(p: Player) -> Array:
|
||||||
if winner != "" or p != current():
|
if winner != "" or p != current():
|
||||||
return "Nicht dein Zug"
|
return [false, "Nicht dein Zug"]
|
||||||
if slot < 0 or slot >= p.shop.size() or p.shop[slot] == null:
|
if blocked(p) != "":
|
||||||
return "Kein Angebot"
|
return [false, blocked(p)]
|
||||||
var card: Cards.Card = p.shop[slot]
|
|
||||||
if p.energy < card.cost:
|
|
||||||
return "Nicht genug Mana"
|
|
||||||
if p.hand.size() >= MAX_HAND:
|
if p.hand.size() >= MAX_HAND:
|
||||||
return "Hand voll"
|
return [false, "Deine Hand ist voll"]
|
||||||
p.energy -= card.cost
|
if p.energy < draw_cost(p):
|
||||||
p.shop[slot] = null
|
return [false, "Nicht genug Mana"]
|
||||||
p.hand.append(card)
|
return [true, ""]
|
||||||
say("%s kauft %s." % [p.name, card.name])
|
|
||||||
emit({"type": "buy", "player": players.find(p), "card": card})
|
|
||||||
|
func draw_cost(p: Player) -> int:
|
||||||
|
return 0 if "ziehen_gratis" in p.perks else DRAW_COST
|
||||||
|
|
||||||
|
|
||||||
|
## Preis einer Eigenschaft für p.
|
||||||
|
func upgrade_cost(p: Player, keyword: String) -> int:
|
||||||
|
var c := int(Cards.KEYWORD_COST[keyword])
|
||||||
|
return maxi(1, c - 1) if "verbessern_billig" in p.perks else c
|
||||||
|
|
||||||
|
|
||||||
|
func draw(p: Player) -> String:
|
||||||
|
var ok := can_draw(p)
|
||||||
|
if not ok[0]:
|
||||||
|
return ok[1]
|
||||||
|
p.energy -= draw_cost(p)
|
||||||
|
p.offer = draw_cards(OFFER_SIZE, p.max_energy)
|
||||||
|
p.picks_left = 1
|
||||||
|
say("%s zieht." % p.name)
|
||||||
|
emit({"type": "draw", "player": players.find(p)})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Würfelt den Laden neu. Gibt "" zurück oder eine Fehlermeldung.
|
## Nimmt eine der angebotenen Karten auf die Hand; jederzeit erlaubt, auch außerhalb des Zugs.
|
||||||
func reroll(p: Player) -> String:
|
## Was liegen bleibt, wandert unter den Stapel.
|
||||||
if winner != "" or p != current():
|
func pick(p: Player, index: int) -> String:
|
||||||
return "Nicht dein Zug"
|
if winner != "":
|
||||||
if p.energy < REROLL_COST:
|
return "Vorbei"
|
||||||
return "Nicht genug Mana"
|
if p.offer.is_empty():
|
||||||
p.energy -= REROLL_COST
|
return "Keine Wahl offen"
|
||||||
roll_shop(p)
|
if index < 0 or index >= p.offer.size():
|
||||||
say("%s würfelt den Laden neu." % p.name)
|
return "Keine solche Karte"
|
||||||
emit({"type": "reroll", "player": players.find(p)})
|
var card: Cards.Card = p.offer[index]
|
||||||
|
p.hand.append(card.copy())
|
||||||
|
p.offer.remove_at(index)
|
||||||
|
p.picks_left -= 1
|
||||||
|
if p.picks_left <= 0 or p.hand.size() >= MAX_HAND:
|
||||||
|
for rest in p.offer:
|
||||||
|
deck.insert(0, rest)
|
||||||
|
p.offer = []
|
||||||
|
p.picks_left = 0
|
||||||
|
say("%s nimmt eine Karte." % p.name)
|
||||||
|
emit({"type": "pick", "player": players.find(p)})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func begin_turn(p: Player) -> void:
|
func begin_turn(p: Player) -> void:
|
||||||
p.max_energy = mini(MAX_ENERGY, p.max_energy + 1)
|
p.max_energy = START_MANA + p.turns
|
||||||
# Nicht ausgegebenes Mana bleibt liegen.
|
p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen
|
||||||
p.energy += p.max_energy + (1 if "extra_mana" in p.perks else 0)
|
|
||||||
if p.turns == 0 and "start_energie" in p.perks:
|
if p.turns == 0 and "start_energie" in p.perks:
|
||||||
p.energy += 2 # einmalige Starthilfe, kein dauerhafter Vorsprung
|
p.energy += 2
|
||||||
if p.start_bonus > 0:
|
if p.start_bonus > 0:
|
||||||
p.energy += p.start_bonus
|
p.energy += p.start_bonus
|
||||||
say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus])
|
say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus])
|
||||||
p.start_bonus = 0
|
p.start_bonus = 0
|
||||||
roll_shop(p)
|
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
c.ready = true
|
c.health = c.max_health # Wesen erholen sich zu Zugbeginn ganz
|
||||||
if c.frozen:
|
c.attacks_left = c.attacks_per_turn()
|
||||||
c.frozen = false
|
|
||||||
c.attacks_left = 0
|
|
||||||
say("%s taut auf und setzt aus." % c.name)
|
|
||||||
else:
|
|
||||||
c.attacks_left = c.attacks_per_turn()
|
|
||||||
p.turns += 1
|
p.turns += 1
|
||||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||||
emit({"type": "turn", "player": players.find(p)})
|
emit({"type": "turn", "player": players.find(p)})
|
||||||
@@ -317,6 +365,8 @@ func begin_turn(p: Player) -> void:
|
|||||||
func end_turn() -> void:
|
func end_turn() -> void:
|
||||||
if winner != "":
|
if winner != "":
|
||||||
return
|
return
|
||||||
|
while not current().offer.is_empty(): # offene Wahl: die erste Karte, damit nichts verfällt
|
||||||
|
pick(current(), 0)
|
||||||
var n := players.size()
|
var n := players.size()
|
||||||
for i in range(1, n + 1):
|
for i in range(1, n + 1):
|
||||||
var idx := (current_idx + i) % n
|
var idx := (current_idx + i) % n
|
||||||
@@ -330,302 +380,296 @@ func end_turn() -> void:
|
|||||||
|
|
||||||
# --- Karten spielen ----------------------------------------------------------
|
# --- Karten spielen ----------------------------------------------------------
|
||||||
|
|
||||||
## Ausspielen kostet nichts mehr: bezahlt wird beim Kauf im Laden.
|
## Ausspielen kostet die Kartenkosten; verdeckt gelegt ist es gleich teuer.
|
||||||
func can_play(p: Player, card: Cards.Card) -> Array:
|
func can_play(p: Player, card: Cards.Card) -> Array:
|
||||||
if winner != "" or p != current():
|
if winner != "" or p != current():
|
||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
|
if blocked(p) != "":
|
||||||
|
return [false, blocked(p)]
|
||||||
if not (card in p.hand):
|
if not (card in p.hand):
|
||||||
return [false, "Nicht auf der Hand"]
|
return [false, "Nicht auf der Hand"]
|
||||||
if p.board.size() >= MAX_BOARD:
|
if p.board.size() >= MAX_BOARD:
|
||||||
return [false, "Dein Feld ist voll"]
|
return [false, "Dein Feld ist voll"]
|
||||||
|
if p.energy < card.cost:
|
||||||
|
return [false, "Nicht genug Mana"]
|
||||||
return [true, ""]
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
func valid_targets(p: Player, target_type: String) -> Array:
|
## Spielt eine Karte, offen oder verdeckt. Gibt "" zurück oder eine Fehlermeldung.
|
||||||
var enemies := enemies_of(p)
|
func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||||
var creatures := []
|
|
||||||
for e in enemies:
|
|
||||||
creatures.append_array(e.board)
|
|
||||||
match target_type:
|
|
||||||
"enemy_creature":
|
|
||||||
return creatures
|
|
||||||
"enemy_any":
|
|
||||||
return creatures + enemies
|
|
||||||
"friendly_creature":
|
|
||||||
return p.board.duplicate()
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
## Spielt eine Karte. Gibt "" zurück oder eine Fehlermeldung.
|
|
||||||
func play_card(p: Player, card: Cards.Card, target = null) -> String:
|
|
||||||
var ok := can_play(p, card)
|
var ok := can_play(p, card)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
return ok[1]
|
return ok[1]
|
||||||
var tt := card.target_type()
|
|
||||||
if tt != "":
|
|
||||||
var valid := valid_targets(p, tt)
|
|
||||||
if not valid.is_empty() and not (target in valid):
|
|
||||||
return "Ungültiges Ziel"
|
|
||||||
if valid.is_empty():
|
|
||||||
target = null
|
|
||||||
p.hand.erase(card)
|
p.hand.erase(card)
|
||||||
say("%s spielt %s." % [p.name, card.name])
|
p.energy -= card.cost
|
||||||
var cr := _enter(p, card)
|
|
||||||
for fx in card.effects:
|
|
||||||
resolve(p, fx, target, cr, card.elements)
|
|
||||||
if cr in p.board:
|
|
||||||
trigger_field_combos(p, cr)
|
|
||||||
charge_field(card)
|
|
||||||
update_resonance()
|
|
||||||
cleanup()
|
|
||||||
check_end()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
## Jede gespielte Karte lädt ihre Elemente. Wer FIELD_CHARGE erreicht, kippt das
|
|
||||||
## gemeinsame Feld auf sein Element; danach beginnen alle Zähler von vorn.
|
|
||||||
func charge_field(card: Cards.Card) -> void:
|
|
||||||
for e in card.elements:
|
|
||||||
field_charge[e] = int(field_charge.get(e, 0)) + 1
|
|
||||||
var best := ""
|
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
if e == field_element or int(field_charge.get(e, 0)) < Cards.FIELD_CHARGE:
|
|
||||||
continue
|
|
||||||
if best == "" or int(field_charge[e]) > int(field_charge[best]):
|
|
||||||
best = e
|
|
||||||
if best == "":
|
|
||||||
return
|
|
||||||
field_element = best
|
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
field_charge[e] = 0
|
|
||||||
say("Das Feld wird zu %s: %s." % [Cards.FIELD_NAMES[best], Cards.FIELD_TEXT[best]])
|
|
||||||
emit({"type": "field", "element": best})
|
|
||||||
update_resonance() # der Feldbonus gilt sofort
|
|
||||||
|
|
||||||
|
|
||||||
## Feldkombo: das neue Wesen bildet mit dem ersten eigenen Wesen aus anderem
|
|
||||||
## Element eine Kombo; pro Ausspielen löst höchstens eine Kombo aus.
|
|
||||||
func trigger_field_combos(p: Player, cr: Creature) -> void:
|
|
||||||
for other in p.board:
|
|
||||||
if other == cr:
|
|
||||||
continue
|
|
||||||
for e1 in cr.card.elements:
|
|
||||||
for e2 in other.card.elements:
|
|
||||||
if e1 == e2:
|
|
||||||
continue
|
|
||||||
var name := Cards.combo_name([e1, e2])
|
|
||||||
say("Feldkombo %s: %s." % [name, Cards.FIELD_COMBO_TEXT[name]])
|
|
||||||
emit({"type": "combo", "name": name, "player": players.find(p), "uid": cr.uid})
|
|
||||||
resolve(p, Cards.field_combo_effect(name), null, cr)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
## Wesen betritt das Feld; Boss-Merkmale wirken beim Eintritt.
|
|
||||||
func _enter(p: Player, card: Cards.Card) -> Creature:
|
|
||||||
var cr := Creature.new(card, p, next_uid)
|
var cr := Creature.new(card, p, next_uid)
|
||||||
next_uid += 1
|
next_uid += 1
|
||||||
if "waechter_leben" in p.perks and cr.taunt():
|
cr.hidden = face_down
|
||||||
cr.health += 2
|
if "wesen_angriff" in p.perks:
|
||||||
cr.max_health += 2
|
cr.attack += 1
|
||||||
if "wesen_leben" in p.perks:
|
if "wesen_leben" in p.perks:
|
||||||
cr.health += 1
|
cr.health += 1
|
||||||
cr.max_health += 1
|
cr.max_health += 1
|
||||||
if "schild_wesen" in p.perks:
|
if "schild_wesen" in p.perks:
|
||||||
cr.shield = true
|
cr.shield = true
|
||||||
if "alle_flink" in p.perks:
|
|
||||||
cr.ready = true
|
|
||||||
cr.attacks_left = cr.attacks_per_turn()
|
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card})
|
if face_down:
|
||||||
return cr
|
say("%s legt ein Wesen verdeckt." % p.name)
|
||||||
|
else:
|
||||||
|
say("%s spielt %s." % [p.name, card.name])
|
||||||
|
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down})
|
||||||
|
apply_auras()
|
||||||
|
cleanup()
|
||||||
|
check_end()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Prüft eine Verschmelzung: zwei eigene Wesen, genug Mana.
|
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug.
|
||||||
func can_fuse(p: Player, a: Creature, b: Creature) -> Array:
|
func can_reveal(p: Player, cr: Creature) -> Array:
|
||||||
if winner != "" or p != current():
|
if winner != "" or p != current():
|
||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
if a == null or b == null or a == b:
|
if blocked(p) != "":
|
||||||
return [false, "Zwei verschiedene Wesen wählen"]
|
return [false, blocked(p)]
|
||||||
if not (a in p.board) or not (b in p.board):
|
if cr == null or not (cr in p.board):
|
||||||
return [false, "Nur eigene Wesen"]
|
return [false, "Nur eigene Wesen"]
|
||||||
if p.energy < Cards.fusion_cost(a.card, b.card):
|
if not cr.hidden:
|
||||||
|
return [false, "%s liegt schon offen" % cr.name]
|
||||||
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
|
func reveal(p: Player, cr: Creature) -> String:
|
||||||
|
var ok := can_reveal(p, cr)
|
||||||
|
if not ok[0]:
|
||||||
|
return ok[1]
|
||||||
|
_flip(cr)
|
||||||
|
cleanup()
|
||||||
|
check_end()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Deckt ein Wesen auf; die Elementregel gilt sofort.
|
||||||
|
func _flip(cr: Creature) -> void:
|
||||||
|
cr.hidden = false
|
||||||
|
say("%s wird aufgedeckt." % cr.name)
|
||||||
|
emit({"type": "reveal", "uid": cr.uid, "card": cr.card})
|
||||||
|
apply_auras()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Verbessern: Schlüsselwörter für Mana -----------------------------------
|
||||||
|
|
||||||
|
## target: eigenes Wesen auf dem Brett oder eigene Handkarte.
|
||||||
|
func can_upgrade(p: Player, target, keyword: String) -> Array:
|
||||||
|
if winner != "" or p != current():
|
||||||
|
return [false, "Nicht dein Zug"]
|
||||||
|
if blocked(p) != "":
|
||||||
|
return [false, blocked(p)]
|
||||||
|
if not Cards.KEYWORD_COST.has(keyword):
|
||||||
|
return [false, "Unbekannte Eigenschaft"]
|
||||||
|
var card: Cards.Card = target.card if target is Creature else target
|
||||||
|
if target is Creature:
|
||||||
|
if not (target in p.board):
|
||||||
|
return [false, "Nur eigene Wesen"]
|
||||||
|
elif not (target in p.hand):
|
||||||
|
return [false, "Nicht auf der Hand"]
|
||||||
|
if keyword in card.keywords:
|
||||||
|
return [false, "%s hat schon %s" % [card.name, keyword]]
|
||||||
|
if p.energy < upgrade_cost(p, keyword):
|
||||||
return [false, "Nicht genug Mana"]
|
return [false, "Nicht genug Mana"]
|
||||||
return [true, ""]
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
## Verschmilzt zwei eigene Wesen zu einem. Es erbt Elemente und Schlüsselwörter beider
|
func upgrade(p: Player, target, keyword: String) -> String:
|
||||||
## Karten, seine Werte sind der abgerundete Durchschnitt plus 1. Kosten: Durchschnitt der
|
var ok := can_upgrade(p, target, keyword)
|
||||||
## Kartenkosten, aufgerundet. Eine Verschmelzung ist kein Ausspielen: sie löst keine
|
if not ok[0]:
|
||||||
## Feldkombo aus und lädt das Feld nicht.
|
return ok[1]
|
||||||
|
p.energy -= upgrade_cost(p, keyword)
|
||||||
|
var card: Cards.Card = (target.card if target is Creature else target).copy() # Kopien (KI) teilen sich Karten
|
||||||
|
card.keywords.append(keyword)
|
||||||
|
if target is Creature:
|
||||||
|
target.card = card
|
||||||
|
else:
|
||||||
|
p.hand[p.hand.find(target)] = card
|
||||||
|
if target is Creature:
|
||||||
|
match keyword:
|
||||||
|
"Schild":
|
||||||
|
target.shield = true
|
||||||
|
"Doppelschlag":
|
||||||
|
target.attacks_left = mini(target.attacks_per_turn(), target.attacks_left + 1)
|
||||||
|
say("%s bekommt %s." % [card.name, keyword])
|
||||||
|
emit({"type": "upgrade", "player": players.find(p), "keyword": keyword, "uid": target.uid if target is Creature else -1})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# --- Verschmelzen ------------------------------------------------------------
|
||||||
|
|
||||||
|
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einem der beiden steckt.
|
||||||
|
static func fusion_cost(a: Creature, b: Creature) -> int:
|
||||||
|
return 1 << maxi(a.card.fusions, b.card.fusions)
|
||||||
|
|
||||||
|
|
||||||
|
func can_fuse(p: Player, a: Creature, b: Creature) -> Array:
|
||||||
|
if winner != "" or p != current():
|
||||||
|
return [false, "Nicht dein Zug"]
|
||||||
|
if blocked(p) != "":
|
||||||
|
return [false, blocked(p)]
|
||||||
|
if a == null or b == null or a == b:
|
||||||
|
return [false, "Zwei verschiedene Wesen wählen"]
|
||||||
|
if not (a in p.board) or not (b in p.board):
|
||||||
|
return [false, "Nur eigene Wesen"]
|
||||||
|
if p.energy < fusion_cost(a, b):
|
||||||
|
return [false, "Nicht genug Mana"]
|
||||||
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
|
## Verschmilzt zwei eigene Wesen zu einem offenen Wesen: Elemente und Schlüsselwörter beider,
|
||||||
|
## jeweils der bessere Wert (Angriff, Leben, Lebenshöchstwert) plus 1. Der Elementbonus der
|
||||||
|
## Eltern wird herausgerechnet, das neue Wesen bekommt seinen eigenen.
|
||||||
func fuse(p: Player, a: Creature, b: Creature) -> String:
|
func fuse(p: Player, a: Creature, b: Creature) -> String:
|
||||||
var ok := can_fuse(p, a, b)
|
var ok := can_fuse(p, a, b)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
return ok[1]
|
return ok[1]
|
||||||
p.energy -= Cards.fusion_cost(a.card, b.card)
|
p.energy -= fusion_cost(a, b)
|
||||||
var card := Cards.fused(a.card, b.card, (a.attack + b.attack) / 2 + 1, (a.health + b.health) / 2 + 1)
|
var card := Cards.fused(a.card, b.card, maxi(a.attack, b.attack) + 1,
|
||||||
|
maxi(a.max_health - a.aura, b.max_health - b.aura) + 1)
|
||||||
var cr := Creature.new(card, p, next_uid)
|
var cr := Creature.new(card, p, next_uid)
|
||||||
next_uid += 1
|
next_uid += 1
|
||||||
|
cr.health = maxi(a.health - a.aura, b.health - b.aura) + 1
|
||||||
cr.shield = cr.shield or a.shield or b.shield
|
cr.shield = cr.shield or a.shield or b.shield
|
||||||
cr.frozen = a.frozen or b.frozen
|
cr.attacks_left = mini(a.attacks_left, b.attacks_left)
|
||||||
cr.ready = a.ready and b.ready
|
|
||||||
cr.attacks_left = 0 if not cr.ready else mini(a.attacks_left, b.attacks_left)
|
|
||||||
var at := p.board.find(a)
|
var at := p.board.find(a)
|
||||||
p.board.erase(a)
|
p.board.erase(a)
|
||||||
p.board.erase(b)
|
p.board.erase(b)
|
||||||
p.board.insert(mini(at, p.board.size()), cr)
|
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])
|
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]})
|
emit({"type": "fuse", "player": players.find(p), "uid": cr.uid, "gone": [a.uid, b.uid]})
|
||||||
update_resonance()
|
apply_auras()
|
||||||
cleanup()
|
cleanup()
|
||||||
check_end()
|
check_end()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Resonanz je Anzahl Partner mit gleichem Element: +1, +2, +3, +3.
|
# --- Vier-Elemente-Regel -----------------------------------------------------
|
||||||
static func resonance_for(partners: int) -> int:
|
|
||||||
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
|
|
||||||
|
|
||||||
|
## Offene Wesen je Element auf dem ganzen Tisch.
|
||||||
## Vorschau fürs Ausspielen: Angriffsbonus aus Resonanz und Feld sowie die Feldkombo,
|
func element_counts() -> Dictionary:
|
||||||
## die die Karte mit dem ersten eigenen Wesen aus anderem Element auslösen würde.
|
var counts := {}
|
||||||
func forecast(p: Player, card: Cards.Card) -> Dictionary:
|
for e in Cards.ELEMENTS:
|
||||||
var partners := 0
|
counts[e] = 0
|
||||||
var combo := ""
|
|
||||||
var partner: Creature = null
|
|
||||||
for o in p.board:
|
|
||||||
if o.card.elements.any(func(e): return e in card.elements):
|
|
||||||
partners += 1
|
|
||||||
if combo == "":
|
|
||||||
for e1 in card.elements:
|
|
||||||
for e2 in o.card.elements:
|
|
||||||
if e1 != e2 and combo == "":
|
|
||||||
combo = Cards.combo_name([e1, e2])
|
|
||||||
partner = o
|
|
||||||
var resonance := resonance_for(partners) * (2 if "resonanz_doppelt" in p.perks else 1)
|
|
||||||
var field := 1 if field_element != "" and field_element in card.elements else 0
|
|
||||||
return {"resonance": resonance, "field": field, "combo": combo, "partner": partner}
|
|
||||||
|
|
||||||
|
|
||||||
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
|
|
||||||
func update_resonance() -> void:
|
|
||||||
for p in players:
|
for p in players:
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
var bonus := 0
|
if not c.hidden:
|
||||||
for o in p.board:
|
for e in c.card.elements:
|
||||||
if o != c and o.card.elements.any(func(e): return e in c.card.elements):
|
counts[e] += 1
|
||||||
bonus += 1
|
return counts
|
||||||
c.resonance = resonance_for(bonus) * (2 if "resonanz_doppelt" in p.perks else 1)
|
|
||||||
c.field_bonus = 1 if field_element != "" and field_element in c.card.elements else 0
|
|
||||||
|
|
||||||
|
|
||||||
func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, elements: Array = []) -> void:
|
## Bonus aus der Elementregel für diese Elemente: +1, wenn ELEMENT_STACK offene Wesen des
|
||||||
var enemies := enemies_of(p)
|
## Elements stehen, -1, wenn das Element, das es schlägt, den Stapel voll hat.
|
||||||
var enemy_creatures := []
|
## Mehrere Elemente nehmen den besten Wert.
|
||||||
for e in enemies:
|
static func aura_of(elements: Array, counts: Dictionary) -> int:
|
||||||
enemy_creatures.append_array(e.board)
|
var best := 0
|
||||||
var targets := []
|
for i in elements.size():
|
||||||
if fx.target in Cards.CHOSEN_TARGETS:
|
var e: String = elements[i]
|
||||||
targets = [target] if target != null else []
|
var a := 0
|
||||||
elif fx.target == "all_enemy_creatures":
|
if int(counts[e]) >= ELEMENT_STACK:
|
||||||
targets = enemy_creatures
|
a += 1
|
||||||
elif fx.target == "all_enemy_heroes":
|
var predator := Cards.beaten_by(e)
|
||||||
targets = enemies.duplicate()
|
if predator != "" and int(counts[predator]) >= ELEMENT_STACK:
|
||||||
elif fx.target == "all_enemies":
|
a -= 1
|
||||||
targets = enemy_creatures + enemies
|
best = a if i == 0 else maxi(best, a)
|
||||||
elif fx.target == "all_friendly_creatures":
|
return best
|
||||||
targets = p.board.duplicate()
|
|
||||||
elif fx.target == "self_hero":
|
|
||||||
targets = [p]
|
## Bonus eines offenen Wesens aus der Elementregel; verdeckte zählen nicht.
|
||||||
elif fx.target == "self_creature":
|
func aura_for(c: Creature, counts: Dictionary) -> int:
|
||||||
targets = [source] if source != null else []
|
if c.hidden:
|
||||||
elif fx.target == "strongest_enemy_creature":
|
return 0
|
||||||
var best: Creature = null
|
var a := aura_of(c.card.elements, counts)
|
||||||
for c in enemy_creatures:
|
var o = c.owner()
|
||||||
if best == null or c.power() > best.power():
|
if o != null and "element_doppelt" in o.perks:
|
||||||
best = c
|
a *= 2
|
||||||
targets = [best] if best != null else []
|
return a
|
||||||
elif fx.target == "weakest_enemy_hero":
|
|
||||||
var weakest: Player = null
|
|
||||||
for e in enemies:
|
## Rechnet die Elementregel neu; Leben folgt dem Bonus direkt (+1/+1 bzw. -1/-1).
|
||||||
if weakest == null or e.life < weakest.life:
|
func apply_auras() -> void:
|
||||||
weakest = e
|
var counts := element_counts()
|
||||||
targets = [weakest] if weakest != null else []
|
for p in players:
|
||||||
else:
|
for c in p.board:
|
||||||
targets = [null]
|
var a := aura_for(c, counts)
|
||||||
for t in targets:
|
if a != c.aura:
|
||||||
match fx.kind:
|
var delta: int = a - c.aura
|
||||||
"damage":
|
c.health += delta
|
||||||
damage(t, fx.amount + (Cards.matchup(elements, t.card.elements) if t is Creature else 0))
|
c.max_health += delta
|
||||||
"heal":
|
c.aura = a
|
||||||
t.life = mini(t.max_life, t.life + fx.amount)
|
|
||||||
say("%s heilt sich um %d." % [t.name, fx.amount])
|
|
||||||
emit({"type": "heal", "target": ref_of(t), "amount": fx.amount})
|
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme.
|
||||||
"mana":
|
func forecast(p: Player, card: Cards.Card) -> int:
|
||||||
p.energy += fx.amount
|
var counts := element_counts()
|
||||||
say("%s erhält %d Mana." % [p.name, fx.amount])
|
for e in card.elements:
|
||||||
emit({"type": "mana", "player": players.find(p), "amount": fx.amount})
|
counts[e] += 1
|
||||||
"freeze":
|
return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1)
|
||||||
t.frozen = true
|
|
||||||
t.attacks_left = 0
|
|
||||||
say("%s ist eingefroren." % t.name)
|
|
||||||
emit({"type": "freeze", "target": ref_of(t)})
|
|
||||||
"bounce":
|
|
||||||
var o = t.owner()
|
|
||||||
o.board.erase(t)
|
|
||||||
emit({"type": "bounce", "target": ref_of(t)})
|
|
||||||
if o.hand.size() < MAX_HAND:
|
|
||||||
o.hand.append(t.card)
|
|
||||||
say("%s kehrt auf die Hand zurück." % t.name)
|
|
||||||
else:
|
|
||||||
say("%s wird weggeweht (Hand voll)." % t.name)
|
|
||||||
"buff":
|
|
||||||
t.attack += fx.amount
|
|
||||||
t.health += fx.amount2
|
|
||||||
t.max_health += fx.amount2
|
|
||||||
say("%s erhält +%d/+%d." % [t.name, fx.amount, fx.amount2])
|
|
||||||
emit({"type": "buff", "target": ref_of(t), "text": "+%d/+%d" % [fx.amount, fx.amount2]})
|
|
||||||
"shield":
|
|
||||||
t.shield = true
|
|
||||||
say("%s erhält Schild." % t.name)
|
|
||||||
emit({"type": "buff", "target": ref_of(t), "text": "Schild"})
|
|
||||||
|
|
||||||
|
|
||||||
# --- Kampf -------------------------------------------------------------------
|
# --- Kampf -------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat.
|
||||||
func can_attack(cr: Creature, target) -> Array:
|
func can_attack(cr: Creature, target) -> Array:
|
||||||
if winner != "" or cr.owner() != current():
|
if winner != "" or cr.owner() != current():
|
||||||
return [false, "Nicht dein Zug"]
|
return [false, "Nicht dein Zug"]
|
||||||
|
if blocked(cr.owner()) != "":
|
||||||
|
return [false, blocked(cr.owner())]
|
||||||
|
if cr.hidden:
|
||||||
|
return [false, "%s liegt verdeckt" % cr.name]
|
||||||
if not cr.can_attack():
|
if not cr.can_attack():
|
||||||
return [false, "%s kann nicht angreifen" % cr.name]
|
return [false, "%s kann nicht angreifen" % cr.name]
|
||||||
var tgt_owner = target.owner() if target is Creature else target
|
var tgt_owner = target.owner() if target is Creature else target
|
||||||
if not (tgt_owner in enemies_of(cr.owner())):
|
if not (tgt_owner in enemies_of(cr.owner())):
|
||||||
return [false, "Kein gültiges Ziel"]
|
return [false, "Kein gültiges Ziel"]
|
||||||
var has_taunt := false
|
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||||
for c in tgt_owner.board:
|
return [false, "Erst die Wesen"]
|
||||||
if c.taunt():
|
|
||||||
has_taunt = true
|
|
||||||
if has_taunt and not (target is Creature and target.taunt()):
|
|
||||||
return [false, "Ein Wächter muss zuerst angegriffen werden"]
|
|
||||||
return [true, ""]
|
return [true, ""]
|
||||||
|
|
||||||
|
|
||||||
## Greift an. Gibt "" zurück oder eine Fehlermeldung.
|
## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Schaden über das Leben eines Wesens
|
||||||
|
## hinaus schlägt auf seinen Besitzer durch (sonst wäre eine Mauer unverwundbar).
|
||||||
|
## Gibt "" zurück oder eine Fehlermeldung.
|
||||||
func attack(cr: Creature, target) -> String:
|
func attack(cr: Creature, target) -> String:
|
||||||
var ok := can_attack(cr, target)
|
var ok := can_attack(cr, target)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
return ok[1]
|
return ok[1]
|
||||||
cr.attacks_left -= 1
|
cr.attacks_left -= 1
|
||||||
|
if target is Creature and target.hidden:
|
||||||
|
_flip(target)
|
||||||
|
if target.health <= 0: # der Elementbonus hat es schon erledigt
|
||||||
|
cleanup()
|
||||||
|
check_end()
|
||||||
|
return ""
|
||||||
say("%s greift %s an." % [cr.name, target.name])
|
say("%s greift %s an." % [cr.name, target.name])
|
||||||
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
||||||
damage(target, cr.power() + matchup_bonus(cr, target))
|
var dealt := cr.power() + matchup_bonus(cr, target)
|
||||||
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht
|
if target is Creature:
|
||||||
damage(cr, target.power() + matchup_bonus(target, cr))
|
var had_shield: bool = target.shield
|
||||||
|
var hp_before: int = target.health
|
||||||
|
damage(target, dealt)
|
||||||
|
if not had_shield and dealt > hp_before:
|
||||||
|
var o = target.owner()
|
||||||
|
var excess := dealt - hp_before
|
||||||
|
o.life -= excess
|
||||||
|
say("%d Schaden schlagen durch auf %s." % [excess, o.name])
|
||||||
|
emit({"type": "damage", "target": ref_of(o), "amount": excess})
|
||||||
|
if target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil
|
||||||
|
damage(cr, target.power())
|
||||||
|
else:
|
||||||
|
damage(target, dealt)
|
||||||
cleanup()
|
cleanup()
|
||||||
check_end()
|
check_end()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## Elementvorteil beim Zuschlagen: gegen Helden gibt es keinen.
|
## Elementvorteil beim Zuschlagen: +1 im Vorteil, -1 im Nachteil (die Verteidigung des
|
||||||
|
## Ziels), gegen Helden gibt es keinen.
|
||||||
static func matchup_bonus(source: Creature, target) -> int:
|
static func matchup_bonus(source: Creature, target) -> int:
|
||||||
if not (target is Creature):
|
if not (target is Creature):
|
||||||
return 0
|
return 0
|
||||||
@@ -648,21 +692,24 @@ func damage(target, amount: int) -> void:
|
|||||||
emit({"type": "damage", "target": ref_of(target), "amount": amount})
|
emit({"type": "damage", "target": ref_of(target), "amount": amount})
|
||||||
|
|
||||||
|
|
||||||
|
## Räumt Tote ab; danach gilt die Elementregel neu, was weitere Tote nach sich ziehen kann.
|
||||||
func cleanup() -> void:
|
func cleanup() -> void:
|
||||||
var died := false
|
for round in 8:
|
||||||
for p in players:
|
var died := false
|
||||||
for c in p.board.duplicate():
|
for p in players:
|
||||||
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
for c in p.board.duplicate():
|
||||||
if c.health <= 0 or not p.alive():
|
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||||||
p.board.erase(c)
|
if c.health <= 0 or not p.alive():
|
||||||
if c.health > 0:
|
p.board.erase(c)
|
||||||
say("%s verschwindet mit %s." % [c.name, p.name])
|
if c.health > 0:
|
||||||
else:
|
say("%s verschwindet mit %s." % [c.name, p.name])
|
||||||
say("%s stirbt." % c.name)
|
else:
|
||||||
emit({"type": "death", "target": ref_of(c)})
|
say("%s stirbt." % c.name)
|
||||||
died = true
|
emit({"type": "death", "target": ref_of(c)})
|
||||||
if died:
|
died = true
|
||||||
update_resonance()
|
if not died:
|
||||||
|
return
|
||||||
|
apply_auras()
|
||||||
|
|
||||||
|
|
||||||
## Ende: einer von beiden hat kein Leben mehr.
|
## Ende: einer von beiden hat kein Leben mehr.
|
||||||
|
|||||||
@@ -384,23 +384,37 @@ func _apply(id: int, action: Dictionary) -> void:
|
|||||||
if game == null or not seats.has(id):
|
if game == null or not seats.has(id):
|
||||||
return
|
return
|
||||||
var p: Game.Player = game.players[seats[id]]
|
var p: Game.Player = game.players[seats[id]]
|
||||||
if game.current() != p or game.winner != "":
|
if game.winner != "":
|
||||||
|
return
|
||||||
|
if str(action.get("type", "")) == "pick": # wählen geht jederzeit, auch außerhalb des Zugs
|
||||||
|
game.pick(p, int(action.get("index", -1)))
|
||||||
|
action_applied.emit()
|
||||||
|
return
|
||||||
|
if game.current() != p:
|
||||||
return
|
return
|
||||||
match str(action.get("type", "")):
|
match str(action.get("type", "")):
|
||||||
"play":
|
"play":
|
||||||
var idx := int(action.get("hand", -1))
|
var idx := int(action.get("hand", -1))
|
||||||
if idx < 0 or idx >= p.hand.size():
|
if idx < 0 or idx >= p.hand.size():
|
||||||
return
|
return
|
||||||
game.play_card(p, p.hand[idx], resolve_ref(action.get("target")))
|
game.play_card(p, p.hand[idx], bool(action.get("face_down", false)))
|
||||||
|
"reveal":
|
||||||
|
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||||
|
if cr != null:
|
||||||
|
game.reveal(p, cr)
|
||||||
|
"upgrade":
|
||||||
|
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||||
|
var hi := int(action.get("hand", -1))
|
||||||
|
var target = cr if cr != null else (p.hand[hi] if hi >= 0 and hi < p.hand.size() else null)
|
||||||
|
if target != null:
|
||||||
|
game.upgrade(p, target, str(action.get("keyword", "")))
|
||||||
"attack":
|
"attack":
|
||||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||||
var t = resolve_ref(action.get("target"))
|
var t = resolve_ref(action.get("target"))
|
||||||
if cr != null and t != null:
|
if cr != null and t != null:
|
||||||
game.attack(cr, t)
|
game.attack(cr, t)
|
||||||
"buy":
|
"draw":
|
||||||
game.buy(p, int(action.get("slot", -1)))
|
game.draw(p)
|
||||||
"reroll":
|
|
||||||
game.reroll(p)
|
|
||||||
"fuse":
|
"fuse":
|
||||||
var a := game.find_creature(int(action.get("uid", -1)))
|
var a := game.find_creature(int(action.get("uid", -1)))
|
||||||
var b := game.find_creature(int(action.get("other", -1)))
|
var b := game.find_creature(int(action.get("other", -1)))
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ class_name NetState
|
|||||||
## Serialisierter Spielzustand für das Netzwerk (ausgelagert aus den Spielregeln).
|
## Serialisierter Spielzustand für das Netzwerk (ausgelagert aus den Spielregeln).
|
||||||
|
|
||||||
|
|
||||||
## Serialisierter Zustand aus Sicht eines Spielers: fremde Hände nur als Anzahl, fremde Läden gar nicht.
|
## Karten reisen als ID; eigene Exemplare mit Schlüsselwörtern und verschmolzene als Ganzes.
|
||||||
## viewer -1: alles sichtbar (Spielstand, Host-Wechsel); -2: nichts Verborgenes (Zuschauer).
|
|
||||||
## Karten reisen als ID; verschmolzene stehen nicht im Pool und gehen als Ganzes mit.
|
|
||||||
static func card_ref(c: Cards.Card):
|
static func card_ref(c: Cards.Card):
|
||||||
return c.id if Cards.has_id(c.id) else c.to_dict()
|
return c.id if Cards.has_id(c.id) and c.keywords.is_empty() and c.fusions == 0 else c.to_dict()
|
||||||
|
|
||||||
|
|
||||||
static func card_of(ref) -> Cards.Card:
|
static func card_of(ref) -> Cards.Card:
|
||||||
return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref)
|
return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref).copy()
|
||||||
|
|
||||||
|
|
||||||
|
## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde Wahl gar nicht, fremde
|
||||||
|
## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer.
|
||||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||||
var ps := []
|
var ps := []
|
||||||
for i in g.players.size():
|
for i in g.players.size():
|
||||||
@@ -20,15 +20,16 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
|||||||
var mine := i == viewer or viewer == -1
|
var mine := i == viewer or viewer == -1
|
||||||
var board := []
|
var board := []
|
||||||
for c in p.board:
|
for c in p.board:
|
||||||
board.append({"uid": c.uid, "card": card_ref(c.card),
|
var masked: bool = c.hidden and not mine
|
||||||
"attack": c.attack, "resonance": c.resonance, "field_bonus": c.field_bonus,
|
board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card),
|
||||||
"health": c.health, "max_health": c.max_health, "shield": c.shield, "frozen": c.frozen,
|
"attack": 0 if masked else c.attack, "health": 0 if masked else c.health,
|
||||||
"ready": c.ready, "attacks_left": c.attacks_left})
|
"max_health": 0 if masked else c.max_health, "shield": c.shield and not masked,
|
||||||
|
"attacks_left": c.attacks_left, "hidden": c.hidden, "aura": c.aura})
|
||||||
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
||||||
"energy": p.energy, "max_energy": p.max_energy, "start_bonus": p.start_bonus,
|
"energy": p.energy, "max_energy": p.max_energy, "start_bonus": p.start_bonus,
|
||||||
"order": p.order, "perks": p.perks, "elements": p.elements, "looks": p.looks, "turns": p.turns,
|
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns,
|
||||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(),
|
"hand": p.hand.map(card_ref) if mine else p.hand.size(),
|
||||||
"shop": p.shop.map(func(c): return c.id if c != null else "") if mine else [],
|
"offer": p.offer.map(func(c): return c.id) if mine else [], "picks_left": p.picks_left,
|
||||||
"board": board})
|
"board": board})
|
||||||
var evs := []
|
var evs := []
|
||||||
for ev in g.events:
|
for ev in g.events:
|
||||||
@@ -38,7 +39,7 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
|||||||
evs.append(e)
|
evs.append(e)
|
||||||
return {"players": ps, "human": g.players.find(g.human), "turn": g.turn, "current_idx": g.current_idx,
|
return {"players": ps, "human": g.players.find(g.human), "turn": g.turn, "current_idx": g.current_idx,
|
||||||
"winner": g.winner, "winner_player": g.players.find(g.winner_player) if g.winner_player != null else -1,
|
"winner": g.winner, "winner_player": g.players.find(g.winner_player) if g.winner_player != null else -1,
|
||||||
"next_uid": g.next_uid, "field_element": g.field_element, "field_charge": g.field_charge,
|
"next_uid": g.next_uid, "deck_size": g.deck.size(),
|
||||||
"log": g.log.slice(maxi(0, g.log.size() - 80)), "events": evs}
|
"log": g.log.slice(maxi(0, g.log.size() - 80)), "events": evs}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
var placeholder: Cards.Card = Cards.all()[0]
|
var placeholder: Cards.Card = Cards.all()[0]
|
||||||
var list := []
|
var list := []
|
||||||
for pd in d.players:
|
for pd in d.players:
|
||||||
var p := Game.Player.new(pd.name, pd.is_human, int(pd.life), pd.title, Array(pd.get("elements", [])))
|
var p := Game.Player.new(pd.name, pd.is_human, int(pd.life), pd.title)
|
||||||
p.max_life = int(pd.max_life)
|
p.max_life = int(pd.max_life)
|
||||||
p.energy = int(pd.energy)
|
p.energy = int(pd.energy)
|
||||||
p.max_energy = int(pd.max_energy)
|
p.max_energy = int(pd.max_energy)
|
||||||
@@ -63,20 +64,19 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
else:
|
else:
|
||||||
for i in int(pd.hand):
|
for i in int(pd.hand):
|
||||||
p.hand.append(placeholder)
|
p.hand.append(placeholder)
|
||||||
for cid in pd.get("shop", []):
|
for cid in pd.get("offer", []):
|
||||||
p.shop.append(Cards.by_id(cid) if str(cid) != "" else null)
|
p.offer.append(Cards.by_id(cid))
|
||||||
|
p.picks_left = int(pd.get("picks_left", 0))
|
||||||
for cd in pd.board:
|
for cd in pd.board:
|
||||||
var card := card_of(cd.card)
|
var card := card_of(cd.card)
|
||||||
var c := Game.Creature.new(card, p, int(cd.uid))
|
var c := Game.Creature.new(card, p, int(cd.uid))
|
||||||
c.attack = int(cd.attack)
|
c.attack = int(cd.attack)
|
||||||
c.resonance = int(cd.resonance)
|
|
||||||
c.field_bonus = int(cd.get("field_bonus", 0))
|
|
||||||
c.health = int(cd.health)
|
c.health = int(cd.health)
|
||||||
c.max_health = int(cd.max_health)
|
c.max_health = int(cd.max_health)
|
||||||
c.shield = bool(cd.shield)
|
c.shield = bool(cd.shield)
|
||||||
c.frozen = bool(cd.frozen)
|
|
||||||
c.ready = bool(cd.ready)
|
|
||||||
c.attacks_left = int(cd.attacks_left)
|
c.attacks_left = int(cd.attacks_left)
|
||||||
|
c.hidden = bool(cd.get("hidden", false))
|
||||||
|
c.aura = int(cd.get("aura", 0))
|
||||||
p.board.append(c)
|
p.board.append(c)
|
||||||
list.append(p)
|
list.append(p)
|
||||||
var hi := int(d.human)
|
var hi := int(d.human)
|
||||||
@@ -87,9 +87,8 @@ static func from_snapshot(d: Dictionary) -> Game:
|
|||||||
g.winner = d.winner
|
g.winner = d.winner
|
||||||
g.winner_player = list[int(d.winner_player)] if int(d.winner_player) >= 0 else null
|
g.winner_player = list[int(d.winner_player)] if int(d.winner_player) >= 0 else null
|
||||||
g.next_uid = int(d.next_uid)
|
g.next_uid = int(d.next_uid)
|
||||||
g.field_element = str(d.get("field_element", ""))
|
for i in int(d.get("deck_size", 0)):
|
||||||
for e in Cards.ELEMENTS:
|
g.deck.append(placeholder) # nur die Größe zählt; gezogen wird beim Host
|
||||||
g.field_charge[e] = int(d.get("field_charge", {}).get(e, 0))
|
|
||||||
for line in d.log:
|
for line in d.log:
|
||||||
g.log.append(str(line))
|
g.log.append(str(line))
|
||||||
for ev in d.events:
|
for ev in d.events:
|
||||||
|
|||||||
@@ -32,32 +32,21 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
|||||||
"damage":
|
"damage":
|
||||||
sfx("damage")
|
sfx("damage")
|
||||||
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
|
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
|
||||||
"heal":
|
|
||||||
sfx("heal")
|
|
||||||
popup("+%d" % ev.amount, Style.GREEN, center_of(ev.target, centers), delay)
|
|
||||||
"buff":
|
|
||||||
popup(ev.text, Style.GREEN, center_of(ev.target, centers), delay)
|
|
||||||
"blocked":
|
"blocked":
|
||||||
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay)
|
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay)
|
||||||
"freeze":
|
|
||||||
popup("Eis", Color("aaddff"), center_of(ev.target, centers), delay)
|
|
||||||
"combo":
|
|
||||||
sfx("combo")
|
|
||||||
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
|
|
||||||
if where.x < 0:
|
|
||||||
where = screen.target_center({"kind": "player", "index": ev.player})
|
|
||||||
popup("%s!" % tr(ev.name), Style.GOLD, where + Vector2(0, -30), 0.1)
|
|
||||||
"play":
|
"play":
|
||||||
sfx("play")
|
sfx("click" if ev.get("hidden", false) else "play")
|
||||||
|
"reveal":
|
||||||
|
sfx("combo")
|
||||||
|
popup(tr("Aufgedeckt!"), Style.GOLD, screen.target_center({"kind": "creature", "uid": ev.uid}), 0.0)
|
||||||
|
"upgrade":
|
||||||
|
sfx("heal")
|
||||||
|
if ev.uid >= 0:
|
||||||
|
popup("+" + tr(ev.keyword), Style.GREEN, screen.target_center({"kind": "creature", "uid": ev.uid}), 0.0)
|
||||||
"fuse":
|
"fuse":
|
||||||
sfx("combo")
|
sfx("combo")
|
||||||
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||||
"field":
|
"draw":
|
||||||
sfx("combo")
|
|
||||||
banner("%s: %s" % [tr(Cards.FIELD_NAMES[ev.element]), tr(Cards.FIELD_TEXT[ev.element])], Cards.ELEMENT_COLORS[ev.element])
|
|
||||||
"buy":
|
|
||||||
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
|
||||||
"reroll":
|
|
||||||
sfx("click")
|
sfx("click")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
class_name BattleGamepad
|
class_name BattleGamepad
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Laden, Hand, eigene Wesen
|
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Stapel, Hand, eigene Wesen
|
||||||
## und Ziele, A bestätigt, B bricht ab, Start beendet den Zug, Y überspringt Gegnerzüge,
|
## und Ziele (bei offener Wahl über deren Karten), A bestätigt, B bricht ab, Start beendet den
|
||||||
## X macht rückgängig.
|
## Zug, Y öffnet das Kartenmenü (im Gegnerzug: überspringen), X macht rückgängig.
|
||||||
|
|
||||||
var screen # BattleScreen
|
var screen # BattleScreen
|
||||||
var active := false
|
var active := false
|
||||||
var zone := "shop"
|
var zone := "deck"
|
||||||
var index := 0
|
var index := 0
|
||||||
var targets: Array = []
|
var targets: Array = []
|
||||||
|
|
||||||
@@ -19,6 +19,12 @@ func button(button: int) -> void:
|
|||||||
active = true
|
active = true
|
||||||
if screen.game == null:
|
if screen.game == null:
|
||||||
return
|
return
|
||||||
|
if not screen.pick_views.is_empty() and zone != "pick":
|
||||||
|
zone = "pick"
|
||||||
|
index = 0
|
||||||
|
elif screen.pick_views.is_empty() and zone == "pick":
|
||||||
|
zone = "hand"
|
||||||
|
index = 0
|
||||||
match button:
|
match button:
|
||||||
JOY_BUTTON_START:
|
JOY_BUTTON_START:
|
||||||
if screen.my_turn() and not screen.any_overlay():
|
if screen.my_turn() and not screen.any_overlay():
|
||||||
@@ -27,10 +33,20 @@ func button(button: int) -> void:
|
|||||||
if screen.confirm.visible:
|
if screen.confirm.visible:
|
||||||
screen.confirm.visible = false
|
screen.confirm.visible = false
|
||||||
screen.cancel_all()
|
screen.cancel_all()
|
||||||
zone = "shop"
|
zone = "deck"
|
||||||
JOY_BUTTON_Y:
|
JOY_BUTTON_Y:
|
||||||
if screen.b_skip.visible:
|
if screen.b_skip.visible:
|
||||||
screen.skip_ai = true
|
screen.skip_ai = true
|
||||||
|
elif screen.menu_layer.visible:
|
||||||
|
screen.close_menu()
|
||||||
|
else:
|
||||||
|
var items := items()
|
||||||
|
if not items.is_empty() and zone in ["hand", "own"]:
|
||||||
|
var item = items[clampi(index, 0, items.size() - 1)]
|
||||||
|
if zone == "hand":
|
||||||
|
screen.open_menu(item.card, Rect2(item.position, item.size))
|
||||||
|
else:
|
||||||
|
screen.open_menu(item.creature, Rect2(item.slot, item.size))
|
||||||
JOY_BUTTON_X:
|
JOY_BUTTON_X:
|
||||||
if screen.b_undo.visible:
|
if screen.b_undo.visible:
|
||||||
screen.undo()
|
screen.undo()
|
||||||
@@ -49,7 +65,7 @@ func button(button: int) -> void:
|
|||||||
## Wechselt den Bereich und überspringt leere (eine leere Hand ist keine Station).
|
## Wechselt den Bereich und überspringt leere (eine leere Hand ist keine Station).
|
||||||
## step 0: beim aktuellen Bereich bleiben, falls er etwas enthält.
|
## step 0: beim aktuellen Bereich bleiben, falls er etwas enthält.
|
||||||
func _next_zone(step: int) -> void:
|
func _next_zone(step: int) -> void:
|
||||||
var order := ["shop", "hand", "own"]
|
var order := ["deck", "hand", "own"]
|
||||||
var at := order.find(zone)
|
var at := order.find(zone)
|
||||||
index = 0
|
index = 0
|
||||||
if step == 0 and not items().is_empty():
|
if step == 0 and not items().is_empty():
|
||||||
@@ -64,8 +80,10 @@ func _next_zone(step: int) -> void:
|
|||||||
|
|
||||||
func items() -> Array:
|
func items() -> Array:
|
||||||
match zone:
|
match zone:
|
||||||
"shop":
|
"deck":
|
||||||
return screen.shop_views
|
return [screen.deck] if screen.deck.visible else []
|
||||||
|
"pick":
|
||||||
|
return screen.pick_views
|
||||||
"hand":
|
"hand":
|
||||||
return screen.hand_views
|
return screen.hand_views
|
||||||
"own":
|
"own":
|
||||||
@@ -95,24 +113,13 @@ func confirm() -> void:
|
|||||||
index = clampi(index, 0, items.size() - 1)
|
index = clampi(index, 0, items.size() - 1)
|
||||||
var item = items[index]
|
var item = items[index]
|
||||||
match zone:
|
match zone:
|
||||||
"shop":
|
"deck":
|
||||||
screen.on_click("shop", int(item.get_meta("slot", 0)))
|
screen.on_click("deck", null)
|
||||||
|
"pick":
|
||||||
|
screen.do_pick(index)
|
||||||
|
index = 0
|
||||||
"hand":
|
"hand":
|
||||||
screen.on_click("hand", item.card)
|
screen.on_click("hand", item.card)
|
||||||
if screen.pending != null:
|
|
||||||
targets = []
|
|
||||||
var valid: Array = screen.game.valid_targets(screen.perspective, screen.pending.target_type())
|
|
||||||
for v in screen.enemy_views:
|
|
||||||
if v.creature in valid:
|
|
||||||
targets.append(v)
|
|
||||||
for v in screen.own_views:
|
|
||||||
if v.creature in valid:
|
|
||||||
targets.append(v)
|
|
||||||
for o in screen.others():
|
|
||||||
if o in valid:
|
|
||||||
targets.append(screen.hero_panels[o])
|
|
||||||
zone = "target"
|
|
||||||
index = 0
|
|
||||||
"own":
|
"own":
|
||||||
screen.on_click("own", item.creature)
|
screen.on_click("own", item.creature)
|
||||||
if screen.selected != null:
|
if screen.selected != null:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class_name BattleLayout
|
class_name BattleLayout
|
||||||
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Laden mit Neuwürfeln
|
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem
|
||||||
## und Zug beenden, Hand, eigenes Brett (mit eigener Leiste links), Feldband, oben der Gegner
|
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links),
|
||||||
## mit seinem Brett.
|
## Feldband, oben der Gegner mit seinem Brett. Eine offene Wahl liegt als Überlagerung darüber.
|
||||||
|
|
||||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||||
const B_LOG := Rect2(112, 10, 96, 32)
|
const B_LOG := Rect2(112, 10, 96, 32)
|
||||||
@@ -13,15 +13,14 @@ const STATUS := Rect2(20, 288, 210, 160)
|
|||||||
const B_FUSE := Rect2(1050, 298, 210, 40)
|
const B_FUSE := Rect2(1050, 298, 210, 40)
|
||||||
const B_UNDO := Rect2(1050, 346, 210, 40)
|
const B_UNDO := Rect2(1050, 346, 210, 40)
|
||||||
const OWN_X := 345
|
const OWN_X := 345
|
||||||
const HAND_W := 96
|
const HAND_W := 150
|
||||||
const HAND_H := 62
|
const HAND_H := 205
|
||||||
const HAND_GAP := 4
|
const HAND_GAP := 10
|
||||||
const HAND_Y := 458
|
const HAND_Y := 532
|
||||||
const SHOP_Y := 532
|
const DECK := Rect2(50, 540, 130, 182)
|
||||||
const SHOP_W := 150
|
|
||||||
const SHOP_H := 205
|
|
||||||
const B_REROLL := Rect2(20, 606, 200, 52)
|
|
||||||
const B_END := Rect2(1060, 600, 200, 60)
|
const B_END := Rect2(1060, 600, 200, 60)
|
||||||
|
const PICK_PANEL := Rect2(120, 150, 1040, 440)
|
||||||
|
const PICK_CARD := Vector2(180, 252)
|
||||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||||
const PREVIEW_SIZE := Vector2(180, 252)
|
const PREVIEW_SIZE := Vector2(180, 252)
|
||||||
const CENTER_X := 240
|
const CENTER_X := 240
|
||||||
@@ -37,12 +36,15 @@ static func opp_slot(j: int) -> Rect2:
|
|||||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||||
|
|
||||||
|
|
||||||
## Laden: fünf große Angebote unten, wie in TFT.
|
## Angebotene Karten einer Wahl, mittig im Wahlfenster.
|
||||||
static func shop_rect(i: int) -> Rect2:
|
static func pick_rect(count: int, i: int) -> Rect2:
|
||||||
return Rect2(245 + i * (SHOP_W + 10), SHOP_Y, SHOP_W, SHOP_H)
|
var gap := 20
|
||||||
|
var total := count * PICK_CARD.x + maxi(0, count - 1) * gap
|
||||||
|
var x := PICK_PANEL.position.x + (PICK_PANEL.size.x - total) / 2
|
||||||
|
return Rect2(Vector2(x + i * (PICK_CARD.x + gap), PICK_PANEL.position.y + 96), PICK_CARD)
|
||||||
|
|
||||||
|
|
||||||
## Hand: flache Kacheln zwischen Brett und Laden (die Bank).
|
## Hand: bis zu fünf große Karten unten, mittig.
|
||||||
static func hand_rect(count: int, i: int) -> Rect2:
|
static func hand_rect(count: int, i: int) -> Rect2:
|
||||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||||
var x := CENTER_X + (CENTER_W - total) / 2
|
var x := CENTER_X + (CENTER_W - total) / 2
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class_name BattleScreen
|
class_name BattleScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||||
|
## Rechtsklick (oder Y) auf eine Handkarte oder ein eigenes Wesen öffnet das Kartenmenü:
|
||||||
|
## verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
@@ -12,7 +14,6 @@ var game: Game
|
|||||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||||
var fx: BattleFx
|
var fx: BattleFx
|
||||||
var selected = null # Game.Creature
|
var selected = null # Game.Creature
|
||||||
var pending = null # Cards.Card, wartet auf Ziel
|
|
||||||
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken
|
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken
|
||||||
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung
|
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung
|
||||||
var msg_t := 0.0
|
var msg_t := 0.0
|
||||||
@@ -39,7 +40,7 @@ var undo_t := 0.0
|
|||||||
# Ansichten (auch für Tests)
|
# Ansichten (auch für Tests)
|
||||||
var tiles := {}
|
var tiles := {}
|
||||||
var hand_views: Array = []
|
var hand_views: Array = []
|
||||||
var shop_views: Array = []
|
var pick_views: Array = [] # Karten der offenen Wahl
|
||||||
var own_views: Array = []
|
var own_views: Array = []
|
||||||
var enemy_views: Array = []
|
var enemy_views: Array = []
|
||||||
var hero_panels := {} # Player -> Panel
|
var hero_panels := {} # Player -> Panel
|
||||||
@@ -49,7 +50,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
|||||||
var hero_layer: Control
|
var hero_layer: Control
|
||||||
var tile_layer: Control
|
var tile_layer: Control
|
||||||
var hand_layer: Control
|
var hand_layer: Control
|
||||||
var shop_layer: Control
|
var pick_layer: Control # Überlagerung der offenen Wahl
|
||||||
|
var pick_box: Control # ihr Inhalt (wird je Wahl neu aufgebaut)
|
||||||
|
var pick_key := "" # welche Wahl gerade aufgebaut ist
|
||||||
|
var deck: Deck
|
||||||
var log_overlay: Control
|
var log_overlay: Control
|
||||||
var b_log: Button
|
var b_log: Button
|
||||||
var status_panel: Panel
|
var status_panel: Panel
|
||||||
@@ -64,15 +68,14 @@ var status_label: Label
|
|||||||
var msg_label: Label
|
var msg_label: Label
|
||||||
var b_end: Button
|
var b_end: Button
|
||||||
var b_fuse: Button
|
var b_fuse: Button
|
||||||
var b_reroll: Button
|
|
||||||
var b_giveup: Button
|
var b_giveup: Button
|
||||||
var field_view: FieldView
|
var element_view: ElementView
|
||||||
var link_view: Links # Resonanzlinien unter den Feldreihen
|
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||||
var info_label: Label # Vorschau: Resonanz, Feld, Feldkombo
|
var board_panel: Panel # eigenes Brett
|
||||||
var board_panel: Panel # eigenes Brett, färbt sich mit dem Feld
|
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 arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||||
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||||
var deal_shop := false # nächster Aufbau teilt den Laden animiert aus
|
|
||||||
var overlay: Control
|
var overlay: Control
|
||||||
var result_label: Label
|
var result_label: Label
|
||||||
var reward_label: Label
|
var reward_label: Label
|
||||||
@@ -172,10 +175,10 @@ func _build_static() -> void:
|
|||||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||||
b_log.add_theme_font_size_override("font_size", 13)
|
b_log.add_theme_font_size_override("font_size", 13)
|
||||||
# Mitte: Feldband, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||||
field_view = FieldView.new(L.FIELD)
|
element_view = ElementView.new(L.FIELD)
|
||||||
add_child(field_view)
|
add_child(element_view)
|
||||||
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
||||||
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
||||||
var slots := Control.new()
|
var slots := Control.new()
|
||||||
@@ -200,9 +203,10 @@ func _build_static() -> void:
|
|||||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||||
b_undo.add_theme_font_size_override("font_size", 15)
|
b_undo.add_theme_font_size_override("font_size", 15)
|
||||||
b_undo.visible = false
|
b_undo.visible = false
|
||||||
# unten: Laden mit Neuwürfeln links und Zug beenden rechts
|
# unten: Zieh-Stapel links, Hand in der Mitte, Zug beenden rechts
|
||||||
b_reroll = Style.button(self, "", L.B_REROLL, do_reroll, Color("285a5a"))
|
deck = Deck.new(L.DECK)
|
||||||
b_reroll.add_theme_font_size_override("font_size", 15)
|
deck.clicked.connect(func(): on_click("deck", null))
|
||||||
|
add_child(deck)
|
||||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||||
b_end.add_theme_font_size_override("font_size", 20)
|
b_end.add_theme_font_size_override("font_size", 20)
|
||||||
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
||||||
@@ -211,16 +215,27 @@ func _build_static() -> void:
|
|||||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||||
wait_label.visible = false
|
wait_label.visible = false
|
||||||
hero_layer = _layer()
|
hero_layer = _layer()
|
||||||
link_view = Links.new()
|
|
||||||
add_child(link_view)
|
|
||||||
tile_layer = _layer()
|
tile_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
shop_layer = _layer()
|
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
|
info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
|
||||||
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
info_label.visible = false
|
info_label.visible = false
|
||||||
fx_layer = _layer()
|
fx_layer = _layer()
|
||||||
|
# offene Wahl: drei sehen, eine nehmen (zu Beginn fünf sehen, drei nehmen)
|
||||||
|
pick_layer = Style.overlay(self)
|
||||||
|
pick_box = Control.new()
|
||||||
|
pick_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
pick_layer.add_child(pick_box)
|
||||||
|
# Kartenmenü: durchsichtiger Fänger, der das Menü beim Klick daneben schließt
|
||||||
|
menu_layer = Control.new()
|
||||||
|
menu_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
menu_layer.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
menu_layer.visible = false
|
||||||
|
menu_layer.gui_input.connect(func(ev):
|
||||||
|
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
close_menu())
|
||||||
|
add_child(menu_layer)
|
||||||
cursor_layer = _layer()
|
cursor_layer = _layer()
|
||||||
# Verlauf als Überlagerung
|
# Verlauf als Überlagerung
|
||||||
log_overlay = Style.overlay(self)
|
log_overlay = Style.overlay(self)
|
||||||
@@ -339,14 +354,23 @@ func my_turn() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func any_overlay() -> bool:
|
func any_overlay() -> bool:
|
||||||
return confirm.visible or overlay.visible or log_overlay.visible
|
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if game == null:
|
if game == null:
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
cancel_all()
|
var under := get_viewport().gui_get_hovered_control()
|
||||||
|
if menu_layer.visible:
|
||||||
|
close_menu()
|
||||||
|
elif under is CardView and under in hand_views and my_turn():
|
||||||
|
open_menu(under.card, Rect2(under.position, under.size))
|
||||||
|
elif under is CreatureView and under in own_views and my_turn():
|
||||||
|
open_menu(under.creature, Rect2(under.slot, under.size))
|
||||||
|
else:
|
||||||
|
cancel_all()
|
||||||
|
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
||||||
return
|
return
|
||||||
if event is InputEventJoypadButton and event.pressed:
|
if event is InputEventJoypadButton and event.pressed:
|
||||||
pad.button(event.button_index)
|
pad.button(event.button_index)
|
||||||
@@ -357,9 +381,11 @@ func _input(event: InputEvent) -> void:
|
|||||||
KEY_ESCAPE:
|
KEY_ESCAPE:
|
||||||
if confirm.visible:
|
if confirm.visible:
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
|
elif menu_layer.visible:
|
||||||
|
close_menu()
|
||||||
elif log_overlay.visible:
|
elif log_overlay.visible:
|
||||||
log_overlay.visible = false
|
log_overlay.visible = false
|
||||||
elif selected != null or pending != null or fusing:
|
elif selected != null or fusing:
|
||||||
cancel_all()
|
cancel_all()
|
||||||
elif not any_overlay():
|
elif not any_overlay():
|
||||||
confirm.visible = true
|
confirm.visible = true
|
||||||
@@ -375,15 +401,20 @@ func _input(event: InputEvent) -> void:
|
|||||||
if overlay.visible:
|
if overlay.visible:
|
||||||
leave()
|
leave()
|
||||||
_:
|
_:
|
||||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and not spectating() and not any_overlay():
|
||||||
var idx: int = event.keycode - KEY_1
|
var idx: int = event.keycode - KEY_1
|
||||||
if idx < perspective.hand.size():
|
if not perspective.offer.is_empty():
|
||||||
|
do_pick(idx)
|
||||||
|
elif my_turn() and idx < perspective.hand.size():
|
||||||
on_click("hand", perspective.hand[idx])
|
on_click("hand", perspective.hand[idx])
|
||||||
|
elif event.keycode == KEY_Z and my_turn() and not any_overlay():
|
||||||
|
on_click("deck", null)
|
||||||
|
|
||||||
|
|
||||||
func cancel_all() -> void:
|
func cancel_all() -> void:
|
||||||
if confirm.visible:
|
if confirm.visible:
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
|
close_menu()
|
||||||
cancel()
|
cancel()
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
@@ -438,7 +469,7 @@ func _process(delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _start_ai(cur: Game.Player) -> void:
|
func _start_ai(cur: Game.Player) -> void:
|
||||||
ai_copy = game.clone()
|
ai_copy = game.masked_clone(cur) # verdeckte Wesen des Menschen kennt die KI nicht
|
||||||
var copy := ai_copy
|
var copy := ai_copy
|
||||||
var idx := game.current_idx
|
var idx := game.current_idx
|
||||||
var box := ai_box
|
var box := ai_box
|
||||||
@@ -472,25 +503,24 @@ func say(m: String) -> void:
|
|||||||
|
|
||||||
func cancel() -> void:
|
func cancel() -> void:
|
||||||
selected = null
|
selected = null
|
||||||
pending = null
|
|
||||||
fusing = false
|
fusing = false
|
||||||
fuse_first = null
|
fuse_first = null
|
||||||
|
|
||||||
|
|
||||||
func play(card: Cards.Card, target) -> void:
|
func play(card: Cards.Card, face_down: bool = false) -> void:
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
|
net.send_action({"type": "play", "hand": perspective.hand.find(card), "face_down": face_down})
|
||||||
return
|
return
|
||||||
var before: Game = game.clone() if not is_online() else null
|
var before: Game = game.clone() if not is_online() else null
|
||||||
var log_len := game.log.size()
|
var log_len := game.log.size()
|
||||||
var from := _hand_rect_of(card)
|
var from := _hand_rect_of(card)
|
||||||
var err := game.play_card(perspective, card, target)
|
var err := game.play_card(perspective, card, face_down)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
return
|
return
|
||||||
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
|
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
|
||||||
arrivals[perspective.board[-1].uid] = from
|
arrivals[perspective.board[-1].uid] = from
|
||||||
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
|
# Rückgängig nur, wenn nichts weiter passiert ist (kein Wesen gestorben)
|
||||||
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||||
undo_state = before
|
undo_state = before
|
||||||
undo_log_len = log_len
|
undo_log_len = log_len
|
||||||
@@ -499,6 +529,84 @@ func play(card: Cards.Card, target) -> void:
|
|||||||
clear_undo()
|
clear_undo()
|
||||||
|
|
||||||
|
|
||||||
|
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
|
||||||
|
func do_reveal(cr: Game.Creature) -> void:
|
||||||
|
clear_undo()
|
||||||
|
if is_client():
|
||||||
|
net.send_action({"type": "reveal", "uid": cr.uid})
|
||||||
|
return
|
||||||
|
var err := game.reveal(perspective, cr)
|
||||||
|
if err != "":
|
||||||
|
say(tr(err))
|
||||||
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
|
## Kauft eine Eigenschaft für eine Handkarte oder ein eigenes Wesen.
|
||||||
|
func do_upgrade(target, keyword: String) -> void:
|
||||||
|
clear_undo()
|
||||||
|
if is_client():
|
||||||
|
if target is Game.Creature:
|
||||||
|
net.send_action({"type": "upgrade", "uid": target.uid, "keyword": keyword})
|
||||||
|
else:
|
||||||
|
net.send_action({"type": "upgrade", "hand": perspective.hand.find(target), "keyword": keyword})
|
||||||
|
return
|
||||||
|
var err := game.upgrade(perspective, target, keyword)
|
||||||
|
if err != "":
|
||||||
|
say(tr(err))
|
||||||
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
|
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen.
|
||||||
|
func open_menu(target, anchor: Rect2) -> void:
|
||||||
|
if spectating() or not my_turn():
|
||||||
|
return
|
||||||
|
Style.clear(menu_layer)
|
||||||
|
menu_target = target
|
||||||
|
var entries := [] # [Text, Callable, aktiv]
|
||||||
|
var is_card: bool = target is Cards.Card
|
||||||
|
var card: Cards.Card = target if is_card else target.card
|
||||||
|
if is_card:
|
||||||
|
var ok := game.can_play(perspective, card)
|
||||||
|
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
||||||
|
play(card, true)
|
||||||
|
after_action(), ok[0]])
|
||||||
|
elif target.hidden:
|
||||||
|
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||||
|
for kw in Cards.KEYWORDS:
|
||||||
|
var k: String = kw
|
||||||
|
var ok := game.can_upgrade(perspective, target, k)
|
||||||
|
var text := "%s ◆ %d" % [tr(k), game.upgrade_cost(perspective, k)]
|
||||||
|
if k in card.keywords:
|
||||||
|
text = "%s ✓" % tr(k)
|
||||||
|
entries.append([text, func(): do_upgrade(target, k), ok[0]])
|
||||||
|
var w := 230.0
|
||||||
|
var h := 18.0 + entries.size() * 44 + 34
|
||||||
|
var x := clampf(anchor.end.x + 8, 10, 1270 - w)
|
||||||
|
if x + w > anchor.position.x and x < anchor.end.x:
|
||||||
|
x = clampf(anchor.position.x - 8 - w, 10, 1270 - w)
|
||||||
|
var y := clampf(anchor.position.y, 10, 790 - h)
|
||||||
|
var box := Rect2(x, y, w, h)
|
||||||
|
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||||||
|
Style.label(menu_layer, card.title(), 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
||||||
|
var by := box.position.y + 36
|
||||||
|
for e in entries:
|
||||||
|
var act: Callable = e[1]
|
||||||
|
var b := Style.button(menu_layer, e[0], Rect2(box.position.x + 12, by, w - 24, 38), func():
|
||||||
|
close_menu()
|
||||||
|
act.call())
|
||||||
|
b.add_theme_font_size_override("font_size", 15)
|
||||||
|
b.disabled = not e[2]
|
||||||
|
by += 44
|
||||||
|
menu_layer.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func close_menu() -> void:
|
||||||
|
if menu_layer.visible:
|
||||||
|
menu_layer.visible = false
|
||||||
|
Style.clear(menu_layer)
|
||||||
|
menu_target = null
|
||||||
|
|
||||||
|
|
||||||
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse).
|
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse).
|
||||||
func toggle_fuse() -> void:
|
func toggle_fuse() -> void:
|
||||||
var on := not fusing
|
var on := not fusing
|
||||||
@@ -508,37 +616,36 @@ func toggle_fuse() -> void:
|
|||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
## Kauft ein Angebot aus dem Laden auf die Hand.
|
## Ziehen: kostet Mana, öffnet die Wahl aus drei Karten.
|
||||||
func do_buy(slot: int) -> void:
|
func do_draw() -> void:
|
||||||
clear_undo()
|
clear_undo()
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "buy", "slot": slot})
|
net.send_action({"type": "draw"})
|
||||||
return
|
return
|
||||||
var from := BattleLayout.shop_rect(slot)
|
var err := game.draw(perspective)
|
||||||
var offer = perspective.shop[slot] if slot < perspective.shop.size() else null
|
|
||||||
var err := game.buy(perspective, slot)
|
|
||||||
if err != "":
|
|
||||||
say(tr(err))
|
|
||||||
elif offer != null:
|
|
||||||
fx.sfx("buy")
|
|
||||||
fx.fly_card(offer, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
|
|
||||||
Cards.RARITIES[int(perspective.looks.get(offer.id, 0))])
|
|
||||||
|
|
||||||
|
|
||||||
## Würfelt den Laden neu (kostet Mana).
|
|
||||||
func do_reroll() -> void:
|
|
||||||
if not my_turn() or any_overlay():
|
|
||||||
return
|
|
||||||
clear_undo()
|
|
||||||
if is_client():
|
|
||||||
net.send_action({"type": "reroll"})
|
|
||||||
after_action()
|
|
||||||
return
|
|
||||||
var err := game.reroll(perspective)
|
|
||||||
if err != "":
|
if err != "":
|
||||||
say(tr(err))
|
say(tr(err))
|
||||||
else:
|
else:
|
||||||
deal_shop = true
|
fx.sfx("buy")
|
||||||
|
|
||||||
|
|
||||||
|
## Nimmt eine Karte der offenen Wahl auf die Hand (jederzeit, auch außerhalb des Zugs).
|
||||||
|
func do_pick(index: int) -> void:
|
||||||
|
if spectating() or index < 0 or index >= pick_views.size():
|
||||||
|
return
|
||||||
|
clear_undo()
|
||||||
|
var from := Rect2(pick_views[index].position, pick_views[index].size)
|
||||||
|
var card: Cards.Card = pick_views[index].card
|
||||||
|
if is_client():
|
||||||
|
net.send_action({"type": "pick", "index": index})
|
||||||
|
return
|
||||||
|
var err := game.pick(perspective, index)
|
||||||
|
if err != "":
|
||||||
|
say(tr(err))
|
||||||
|
return
|
||||||
|
fx.sfx("play")
|
||||||
|
fx.fly_card(card, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
|
||||||
|
Cards.RARITIES[int(perspective.looks.get(card.id, 0))])
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
@@ -609,15 +716,6 @@ func on_click(kind: String, obj) -> void:
|
|||||||
do_fuse(fuse_first, obj)
|
do_fuse(fuse_first, obj)
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
if pending != null:
|
|
||||||
var card = pending
|
|
||||||
pending = null
|
|
||||||
if obj in g.valid_targets(perspective, card.target_type()):
|
|
||||||
play(card, obj)
|
|
||||||
else:
|
|
||||||
say(tr("Abgebrochen"))
|
|
||||||
after_action()
|
|
||||||
return
|
|
||||||
if selected != null:
|
if selected != null:
|
||||||
var cr = selected
|
var cr = selected
|
||||||
selected = null
|
selected = null
|
||||||
@@ -632,18 +730,18 @@ func on_click(kind: String, obj) -> void:
|
|||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
match kind:
|
match kind:
|
||||||
"shop":
|
"deck":
|
||||||
do_buy(int(obj))
|
do_draw()
|
||||||
"hand":
|
"hand":
|
||||||
var ok := g.can_play(perspective, obj)
|
var ok := g.can_play(perspective, obj)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
say(tr(ok[1]))
|
say(tr(ok[1]))
|
||||||
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
|
|
||||||
pending = obj
|
|
||||||
say(tr("Wähle ein Ziel"))
|
|
||||||
else:
|
else:
|
||||||
play(obj, null)
|
play(obj)
|
||||||
"own":
|
"own":
|
||||||
|
if obj.hidden:
|
||||||
|
do_reveal(obj)
|
||||||
|
return
|
||||||
if obj.can_attack():
|
if obj.can_attack():
|
||||||
selected = obj
|
selected = obj
|
||||||
say(tr("Wähle ein Angriffsziel"))
|
say(tr("Wähle ein Angriffsziel"))
|
||||||
@@ -669,8 +767,6 @@ func announce_turn() -> void:
|
|||||||
banner_turn = game.turn
|
banner_turn = game.turn
|
||||||
fx.banner(tr("Dein Zug"), Style.GOLD)
|
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||||||
fx.sfx("turn")
|
fx.sfx("turn")
|
||||||
deal_shop = true
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _hand_rect_of(card: Cards.Card) -> Rect2:
|
func _hand_rect_of(card: Cards.Card) -> Rect2:
|
||||||
@@ -696,87 +792,109 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
|
|||||||
info_label.size = ir.size
|
info_label.size = ir.size
|
||||||
|
|
||||||
|
|
||||||
## Eigene Hand- und Ladenkarte: zeigt, was das Ausspielen jetzt brächte, und markiert den Kombo-Partner.
|
## Eigene Hand- und Wahlkarte: zeigt den Elementbonus, den sie offen gelegt bekäme.
|
||||||
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||||||
var f := game.forecast(perspective, card)
|
var a := game.forecast(perspective, card)
|
||||||
var lines: Array[String] = []
|
var info := ""
|
||||||
var parts: Array[String] = []
|
if a > 0:
|
||||||
if f.resonance > 0:
|
info = tr("Elementbonus +%d/+%d") % [a, a]
|
||||||
parts.append(tr("Resonanz +%d") % f.resonance)
|
elif a < 0:
|
||||||
if f.field > 0:
|
info = tr("Elementbonus %d/%d") % [a, a]
|
||||||
parts.append(tr("Feld +%d") % f.field)
|
show_preview(card, entered, perspective.looks, info, anchor)
|
||||||
if not parts.is_empty():
|
|
||||||
lines.append(" · ".join(parts))
|
|
||||||
if f.combo != "":
|
|
||||||
lines.append(tr("Feldkombo %s mit %s: %s") % [tr(f.combo), f.partner.card.title(), tr(Cards.FIELD_COMBO_TEXT[f.combo])])
|
|
||||||
for t in tiles.values():
|
|
||||||
t.mark = ""
|
|
||||||
t.queue_redraw()
|
|
||||||
if entered and f.partner != null and tiles.has(f.partner.uid):
|
|
||||||
tiles[f.partner.uid].mark = f.combo
|
|
||||||
tiles[f.partner.uid].queue_redraw()
|
|
||||||
show_preview(card, entered, perspective.looks, "\n".join(lines), anchor)
|
|
||||||
|
|
||||||
|
|
||||||
## Hand- und Ladenkarte tragen ihren Bonus und ihre Feldkombo als Vorschau.
|
## Offene Wahl als Überlagerung: nur neu aufbauen, wenn sich das Angebot geändert hat.
|
||||||
|
func build_pick() -> void:
|
||||||
|
var h := perspective
|
||||||
|
var key := ""
|
||||||
|
if not spectating() and not h.offer.is_empty():
|
||||||
|
key = "%d:%s" % [h.picks_left, ",".join(h.offer.map(func(c): return c.id))]
|
||||||
|
if key == pick_key:
|
||||||
|
return
|
||||||
|
pick_key = key
|
||||||
|
Style.clear(pick_box)
|
||||||
|
pick_views = []
|
||||||
|
if key == "":
|
||||||
|
pick_layer.visible = false
|
||||||
|
return
|
||||||
|
var box := BattleLayout.PICK_PANEL
|
||||||
|
Style.panel(pick_box, box, Style.PANEL, 12, Style.GOLD, 2)
|
||||||
|
Style.label(pick_box, tr("Wähle %d von %d") % [h.picks_left, h.offer.size()], 26, Style.GOLD,
|
||||||
|
Rect2(box.position.x, box.position.y + 24, box.size.x, 40), "center", true)
|
||||||
|
for i in h.offer.size():
|
||||||
|
var card: Cards.Card = h.offer[i]
|
||||||
|
var r := BattleLayout.pick_rect(h.offer.size(), i)
|
||||||
|
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
||||||
|
v.position = r.position
|
||||||
|
v.lift = true
|
||||||
|
apply_forecast(v)
|
||||||
|
var idx := i
|
||||||
|
v.clicked.connect(func(_view): do_pick(idx))
|
||||||
|
pick_box.add_child(v)
|
||||||
|
pick_views.append(v)
|
||||||
|
pick_layer.visible = true
|
||||||
|
if Style.animations:
|
||||||
|
for i in pick_views.size():
|
||||||
|
fx.deal(pick_views[i], i)
|
||||||
|
|
||||||
|
|
||||||
|
## Hand- und Wahlkarten tragen den Elementbonus als Vorschau.
|
||||||
func apply_forecast(v: CardView) -> void:
|
func apply_forecast(v: CardView) -> void:
|
||||||
var f := game.forecast(perspective, v.card)
|
v.bonus = game.forecast(perspective, v.card)
|
||||||
v.bonus = f.resonance + f.field
|
|
||||||
v.tag = f.combo
|
|
||||||
|
|
||||||
|
|
||||||
## Was den Angriff eines Wesens gerade hebt.
|
## Was den Angriff eines Wesens gerade hebt oder senkt.
|
||||||
func creature_info(cr: Game.Creature) -> String:
|
func creature_info(cr: Game.Creature) -> String:
|
||||||
var parts: Array[String] = []
|
var parts: Array[String] = []
|
||||||
if cr.resonance > 0:
|
if cr.aura > 0:
|
||||||
parts.append(tr("Resonanz +%d") % cr.resonance)
|
parts.append(tr("Elementbonus +%d/+%d") % [cr.aura, cr.aura])
|
||||||
if cr.field_bonus > 0:
|
elif cr.aura < 0:
|
||||||
parts.append(tr("Feld +%d") % cr.field_bonus)
|
parts.append(tr("Elementbonus %d/%d") % [cr.aura, cr.aura])
|
||||||
var perk := cr.power() - cr.attack - cr.resonance - cr.field_bonus
|
var perk := cr.power() - cr.attack - cr.aura
|
||||||
if perk > 0:
|
if perk > 0:
|
||||||
parts.append(tr("Merkmal +%d") % perk)
|
parts.append(tr("Merkmal +%d") % perk)
|
||||||
|
if cr.hidden:
|
||||||
|
parts.append(tr("verdeckt"))
|
||||||
return " · ".join(parts)
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
## Elementvorteil gegen ein mögliches Ziel: des gewählten Angreifers oder eines Schadenszaubers auf der Hand.
|
## Elementvorteil des gewählten Angreifers gegen ein mögliches Ziel; gegen Verdeckte unbekannt.
|
||||||
func matchup_for(cr: Game.Creature, valid: Array) -> int:
|
func matchup_for(cr: Game.Creature) -> int:
|
||||||
if selected != null and game.can_attack(selected, cr)[0]:
|
if selected != null and not cr.hidden and game.can_attack(selected, cr)[0]:
|
||||||
return Game.matchup_bonus(selected, cr)
|
return Game.matchup_bonus(selected, cr)
|
||||||
if pending != null and cr in valid and pending.effects.any(func(fx): return fx.kind == "damage" and fx.target in Cards.CHOSEN_TARGETS):
|
|
||||||
return Cards.matchup(pending.elements, cr.card.elements)
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
## Resonanzgruppen je Reihe: Wesen mit gleichem Element werden unter der Reihe verbunden.
|
class Deck extends Control:
|
||||||
func resonance_groups() -> Array:
|
signal clicked
|
||||||
var out := []
|
var enabled := true
|
||||||
for p in game.players:
|
var count := 0
|
||||||
var k := 0
|
var cost := Game.DRAW_COST
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
var xs: Array[float] = []
|
|
||||||
for j in p.board.size():
|
|
||||||
if e in p.board[j].card.elements:
|
|
||||||
xs.append(slot_rect(p, j).get_center().x)
|
|
||||||
if xs.size() < 2:
|
|
||||||
continue
|
|
||||||
out.append({"color": Style.lighter(Cards.ELEMENT_COLORS[e], 0.25), "y": slot_rect(p, 0).end.y + 4 + k * 6, "xs": xs})
|
|
||||||
k += 1
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
func _init(rect: Rect2) -> void:
|
||||||
|
position = rect.position
|
||||||
|
size = rect.size
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
class Links extends Control:
|
func _gui_input(event: InputEvent) -> void:
|
||||||
var groups: Array = []
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
accept_event()
|
||||||
func _init() -> void:
|
clicked.emit()
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
for gr in groups:
|
for k in range(2, -1, -1):
|
||||||
var y: float = gr.y
|
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12)).grow(0)
|
||||||
draw_line(Vector2(gr.xs[0], y), Vector2(gr.xs[-1], y), gr.color, 3.0)
|
r.position.y += 6
|
||||||
for x in gr.xs:
|
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled else Style.MUTED, 2), r)
|
||||||
draw_circle(Vector2(x, y), 4.0, gr.color)
|
if k == 0:
|
||||||
|
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||||
|
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
|
||||||
|
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
|
||||||
|
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
|
||||||
|
if not enabled:
|
||||||
|
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Netzwerk -------------------------------------------------------------------------------
|
# --- Netzwerk -------------------------------------------------------------------------------
|
||||||
@@ -845,9 +963,7 @@ func refresh() -> void:
|
|||||||
var h := perspective
|
var h := perspective
|
||||||
var now := Time.get_ticks_msec()
|
var now := Time.get_ticks_msec()
|
||||||
var mine := my_turn()
|
var mine := my_turn()
|
||||||
var valid: Array = g.valid_targets(h, pending.target_type()) if pending != null else []
|
|
||||||
hand_views = []
|
hand_views = []
|
||||||
shop_views = []
|
|
||||||
own_views = []
|
own_views = []
|
||||||
enemy_views = []
|
enemy_views = []
|
||||||
|
|
||||||
@@ -858,7 +974,7 @@ func refresh() -> void:
|
|||||||
if g.current() == o:
|
if g.current() == o:
|
||||||
bcol = Style.BLUE
|
bcol = Style.BLUE
|
||||||
bw = 3
|
bw = 3
|
||||||
if o in valid or (selected != null and o.alive() and g.can_attack(selected, o)[0]):
|
if selected != null and o.alive() and g.can_attack(selected, o)[0]:
|
||||||
bcol = Style.ORANGE
|
bcol = Style.ORANGE
|
||||||
bw = 3
|
bw = 3
|
||||||
if int(flash_until.get(g.players.find(o), 0)) > now:
|
if int(flash_until.get(g.players.find(o), 0)) > now:
|
||||||
@@ -870,7 +986,7 @@ func refresh() -> void:
|
|||||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||||
if o.alive():
|
if o.alive():
|
||||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||||
labels.stats.text = "Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy]
|
labels.stats.text = "Hand %d ◆ %d" % [o.hand.size(), o.energy]
|
||||||
else:
|
else:
|
||||||
labels.life.text = "besiegt"
|
labels.life.text = "besiegt"
|
||||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||||
@@ -898,7 +1014,11 @@ func refresh() -> void:
|
|||||||
tiles[cr.uid] = tile
|
tiles[cr.uid] = tile
|
||||||
tile_layer.add_child(tile)
|
tile_layer.add_child(tile)
|
||||||
var looks: Dictionary = p.looks
|
var looks: Dictionary = p.looks
|
||||||
tile.hover.connect(func(card, entered): show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
tile.hover.connect(func(card, entered):
|
||||||
|
if tile.secret:
|
||||||
|
show_preview(null, false)
|
||||||
|
else:
|
||||||
|
show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
||||||
var uid := cr.uid
|
var uid := cr.uid
|
||||||
tile.clicked.connect(func(_v):
|
tile.clicked.connect(func(_v):
|
||||||
var c := game.find_creature(uid)
|
var c := game.find_creature(uid)
|
||||||
@@ -920,7 +1040,7 @@ func refresh() -> void:
|
|||||||
elif fusing and p == h and mine:
|
elif fusing and p == h and mine:
|
||||||
b = Style.PURPLE
|
b = Style.PURPLE
|
||||||
w = 3
|
w = 3
|
||||||
elif cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
elif selected != null and g.can_attack(selected, cr)[0]:
|
||||||
b = Style.ORANGE
|
b = Style.ORANGE
|
||||||
w = 3
|
w = 3
|
||||||
elif p == h and mine and cr.can_attack():
|
elif p == h and mine and cr.can_attack():
|
||||||
@@ -928,8 +1048,8 @@ func refresh() -> void:
|
|||||||
w = 3
|
w = 3
|
||||||
tile.border = b
|
tile.border = b
|
||||||
tile.width = w
|
tile.width = w
|
||||||
tile.mark = ""
|
tile.secret = cr.hidden and p != h
|
||||||
tile.matchup = matchup_for(cr, valid)
|
tile.matchup = matchup_for(cr)
|
||||||
tile.queue_redraw()
|
tile.queue_redraw()
|
||||||
if p == h:
|
if p == h:
|
||||||
own_views.append(tile)
|
own_views.append(tile)
|
||||||
@@ -940,31 +1060,12 @@ func refresh() -> void:
|
|||||||
var t: CreatureView = tiles[uid]
|
var t: CreatureView = tiles[uid]
|
||||||
tiles.erase(uid)
|
tiles.erase(uid)
|
||||||
fx.fade_out(t)
|
fx.fade_out(t)
|
||||||
link_view.groups = resonance_groups()
|
build_pick()
|
||||||
link_view.queue_redraw()
|
deck.visible = not spectating()
|
||||||
|
deck.enabled = mine and g.can_draw(h)[0]
|
||||||
Style.clear(shop_layer)
|
deck.count = g.deck.size()
|
||||||
if not spectating():
|
deck.cost = g.draw_cost(h)
|
||||||
for i in h.shop.size():
|
deck.queue_redraw()
|
||||||
var offer = h.shop[i]
|
|
||||||
var sr := BattleLayout.shop_rect(i)
|
|
||||||
if offer == null:
|
|
||||||
Style.panel(shop_layer, sr, Style.darker(Style.PANEL, 0.8), 8, Style.PANEL_LIGHT, 2)
|
|
||||||
continue
|
|
||||||
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
|
||||||
sv.position = sr.position
|
|
||||||
sv.lift = mine
|
|
||||||
apply_forecast(sv)
|
|
||||||
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
|
||||||
sv.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(sv.position, sv.size)))
|
|
||||||
var slot := i
|
|
||||||
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
|
||||||
sv.clicked.connect(func(_view): on_click("shop", slot))
|
|
||||||
shop_layer.add_child(sv)
|
|
||||||
shop_views.append(sv)
|
|
||||||
if deal_shop:
|
|
||||||
fx.deal(sv, i)
|
|
||||||
deal_shop = false
|
|
||||||
|
|
||||||
Style.clear(hand_layer)
|
Style.clear(hand_layer)
|
||||||
if spectating():
|
if spectating():
|
||||||
@@ -978,9 +1079,7 @@ func refresh() -> void:
|
|||||||
v.position = r.position
|
v.position = r.position
|
||||||
v.lift = mine
|
v.lift = mine
|
||||||
apply_forecast(v)
|
apply_forecast(v)
|
||||||
if card == pending:
|
if playable:
|
||||||
v.border = Style.YELLOW
|
|
||||||
elif playable:
|
|
||||||
v.border = Style.GREEN
|
v.border = Style.GREEN
|
||||||
v.dim = not playable
|
v.dim = not playable
|
||||||
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
||||||
@@ -992,14 +1091,10 @@ func refresh() -> void:
|
|||||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||||
bar_border = Style.RED
|
bar_border = Style.RED
|
||||||
status_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
status_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
||||||
var board_col := Style.darker(Style.PANEL, 0.8)
|
|
||||||
if g.field_element != "":
|
|
||||||
board_col = board_col.lerp(Cards.ELEMENT_COLORS[g.field_element], 0.22) # das Feld färbt das Brett
|
|
||||||
board_panel.add_theme_stylebox_override("panel", Style.box(board_col, 10))
|
|
||||||
name_label.text = h.name
|
name_label.text = h.name
|
||||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||||
energy_label.text = "◆ %d" % h.energy
|
energy_label.text = "◆ %d" % h.energy
|
||||||
income_label.text = tr("+%d je Zug") % h.max_energy
|
income_label.text = tr("nächster Zug ◆ %d") % (h.max_energy + 1)
|
||||||
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
||||||
if spectating():
|
if spectating():
|
||||||
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
||||||
@@ -1009,21 +1104,18 @@ func refresh() -> void:
|
|||||||
b_end.visible = not ai_running and not spectating()
|
b_end.visible = not ai_running and not spectating()
|
||||||
b_undo.visible = undo_state != null and mine
|
b_undo.visible = undo_state != null and mine
|
||||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||||
b_reroll.visible = mine and not spectating()
|
|
||||||
b_reroll.text = tr("Neu würfeln ◆ %d") % Game.REROLL_COST
|
|
||||||
b_reroll.disabled = h.energy < Game.REROLL_COST
|
|
||||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||||
field_view.update_from(g)
|
element_view.update_from(g)
|
||||||
var lines: Array[String] = []
|
var lines: Array[String] = []
|
||||||
var start := maxi(0, g.log.size() - 60)
|
var start := maxi(0, g.log.size() - 60)
|
||||||
for i in range(start, g.log.size()):
|
for i in range(start, g.log.size()):
|
||||||
var line := g.log[i]
|
var line := g.log[i]
|
||||||
if line.begins_with("---"):
|
if line.begins_with("---"):
|
||||||
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
||||||
elif line.begins_with("Das Feld wird"):
|
elif " verschmelzen zu " in line or " wird aufgedeckt." in line:
|
||||||
lines.append("[color=#8c78dc]%s[/color]" % line)
|
lines.append("[color=#8c78dc]%s[/color]" % line)
|
||||||
elif line.begins_with("Feldkombo"):
|
elif " bekommt " in line:
|
||||||
lines.append("[color=#f5c850]%s[/color]" % line)
|
lines.append("[color=#5ad282]%s[/color]" % line)
|
||||||
else:
|
else:
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
log_box.text = "\n".join(lines)
|
log_box.text = "\n".join(lines)
|
||||||
|
|||||||
@@ -88,17 +88,9 @@ func build() -> void:
|
|||||||
var next := selected == p.campaign_stage
|
var next := selected == p.campaign_stage
|
||||||
Style.panel(self, Rect2(240, 270, 800, 300))
|
Style.panel(self, Rect2(240, 270, 800, 300))
|
||||||
Style.label(self, "%s · %s" % [st.name, st.title], 28, Style.TEXT, Rect2(270, 290, 740, 40), "center", true)
|
Style.label(self, "%s · %s" % [st.name, st.title], 28, Style.TEXT, Rect2(270, 290, 740, 40), "center", true)
|
||||||
var dots := Control.new()
|
Style.label(self, "%s · %s" % [tr("%d Leben") % int(st.life), tr("%d Münzen") % st.reward], 16, Style.MUTED, Rect2(270, 350, 740, 24), "center")
|
||||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
var elems: Array = st.elements
|
|
||||||
dots.draw.connect(func():
|
|
||||||
var w := elems.size() * 26 - 6
|
|
||||||
for m in elems.size():
|
|
||||||
dots.draw_circle(Vector2(640 - w / 2.0 + 10 + m * 26, 348), 10, Cards.ELEMENT_COLORS[elems[m]]))
|
|
||||||
add_child(dots)
|
|
||||||
Style.label(self, "%s · %s" % [tr("%d Leben") % int(st.life), tr("%d Münzen") % st.reward], 16, Style.MUTED, Rect2(270, 370, 740, 24), "center")
|
|
||||||
var perks: Array = st.perks
|
var perks: Array = st.perks
|
||||||
var py := 402
|
var py := 390
|
||||||
for k in perks:
|
for k in perks:
|
||||||
Style.label(self, "★ " + tr(Game.PERKS[k]), 15, Style.GOLD, Rect2(270, py, 740, 22), "center")
|
Style.label(self, "★ " + tr(Game.PERKS[k]), 15, Style.GOLD, Rect2(270, py, 740, 22), "center")
|
||||||
py += 24
|
py += 24
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ var card: Cards.Card
|
|||||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||||
var rarity := "gewöhnlich" # nur Optik
|
var rarity := "gewöhnlich" # nur Optik
|
||||||
var dim := false
|
var dim := false
|
||||||
var bonus := 0 # Vorschau im Kampf: Angriff, den die Karte auf dem Feld dazubekäme
|
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme (+1 oder -1)
|
||||||
var tag := "" # Vorschau im Kampf: Feldkombo, die das Ausspielen auslöst
|
|
||||||
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
|
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
|
||||||
|
|
||||||
|
|
||||||
@@ -132,32 +131,27 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
|||||||
y += icon * 2 + 8
|
y += icon * 2 + 8
|
||||||
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
|
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
|
||||||
var box := Rect2()
|
var box := Rect2()
|
||||||
var fx := card.effect_text()
|
var fx := card.text()
|
||||||
var box_bottom: float = size.y - r_stat * 2 - 4
|
var box_bottom: float = size.y - r_stat * 2 - 4
|
||||||
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
|
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
|
||||||
var tsize := Style.fs(f_text)
|
var tsize := Style.fs(f_text)
|
||||||
var heading := big
|
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8
|
||||||
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8 + (tsize + 2 if heading else 0)
|
|
||||||
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
|
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
|
||||||
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
|
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
|
||||||
var ty := box.position.y + 4
|
var ty := box.position.y + 4
|
||||||
if heading:
|
|
||||||
Style.text(self, tr("Beim Ausspielen"), f_text - 2, Style.MUTED, Vector2(box.position.x + 4, ty), "topleft")
|
|
||||||
ty += tsize + 2
|
|
||||||
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
|
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)),
|
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)
|
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||||
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
|
# 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(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.CRIMSON, f_stat)
|
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
|
||||||
if bonus > 0:
|
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.text_outlined(self, ("+%d/+%d" % [bonus, bonus]) if bonus > 0 else ("%d/%d" % [bonus, bonus]), f_text,
|
||||||
|
Style.GREEN if bonus > 0 else Style.RED, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||||
if tag != "":
|
|
||||||
_draw_tag(Vector2(size.x - 6, 6), f_text - 1)
|
|
||||||
Style.rarity_sparkle(self, rect, rarity, seed, box)
|
Style.rarity_sparkle(self, rect, rarity, seed, box)
|
||||||
|
|
||||||
|
|
||||||
@@ -170,29 +164,16 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
|||||||
var nf := Style.fit_size(card.title(), true, 12, name_w)
|
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.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)
|
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
|
||||||
if tag != "":
|
var kws: Array[String] = []
|
||||||
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(9)).x + 12
|
for k in Cards.KEYWORDS:
|
||||||
_draw_tag(Vector2(size.x / 2 + w / 2, size.y - 21), 9)
|
if k in card.keywords:
|
||||||
else:
|
kws.append(k)
|
||||||
var kws: Array[String] = []
|
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
|
||||||
for k in Cards.KEYWORDS:
|
for k in kws:
|
||||||
if k in card.keywords:
|
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
|
||||||
kws.append(k)
|
x += 18
|
||||||
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(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.CRIMSON, 12)
|
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
|
||||||
if bonus > 0:
|
if bonus != 0:
|
||||||
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
|
Style.text_outlined(self, ("+%d" % bonus) if bonus > 0 else str(bonus), 10, Style.GREEN if bonus > 0 else Style.RED, Vector2(4, size.y - 29), "midleft", 3)
|
||||||
Style.rarity_sparkle(self, rect, rarity, seed)
|
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||||
|
|
||||||
|
|
||||||
## Goldenes Schild mit der Feldkombo, rechts oben.
|
|
||||||
func _draw_tag(top_right: Vector2, font: int) -> void:
|
|
||||||
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(font)).x + 12
|
|
||||||
var h := Style.fs(font) + 6
|
|
||||||
var r := Rect2(top_right - Vector2(w, 0), Vector2(w, h))
|
|
||||||
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), r)
|
|
||||||
Style.text(self, tr(tag), font, Style.GOLD, r.get_center(), "center", true)
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ var width := 2
|
|||||||
var flash := false # kurz rot: wurde gerade angegriffen
|
var flash := false # kurz rot: wurde gerade angegriffen
|
||||||
var slot: Vector2 # Zielposition im Raster
|
var slot: Vector2 # Zielposition im Raster
|
||||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||||
var mark := "" # Feldkombo, die dieses Wesen als Partner auslösen würde
|
|
||||||
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
|
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
|
||||||
|
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
|
||||||
|
|
||||||
|
|
||||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||||
@@ -45,36 +45,31 @@ func rarity() -> String:
|
|||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var rect := Rect2(Vector2.ZERO, size)
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
|
if secret:
|
||||||
|
_draw_back(rect)
|
||||||
|
return
|
||||||
var small := size.x < 100
|
var small := size.x < 100
|
||||||
var mini := size.x < 60
|
|
||||||
var rar := rarity()
|
var rar := rarity()
|
||||||
var seed := hash(creature.card.id) + creature.uid
|
var seed := hash(creature.card.id) + creature.uid
|
||||||
if rar != "gewöhnlich":
|
if rar != "gewöhnlich":
|
||||||
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 4 if mini else 6)
|
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6)
|
||||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||||
if not mini:
|
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
|
||||||
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
|
|
||||||
Style.rarity_sheen(self, rect, rar, seed)
|
Style.rarity_sheen(self, rect, rar, seed)
|
||||||
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||||
if flash:
|
if flash:
|
||||||
bcol = Style.RED
|
bcol = Style.RED
|
||||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||||
if mark != "":
|
|
||||||
Style.border(self, rect.grow(3), Style.GOLD, 3, 8)
|
|
||||||
if mini:
|
|
||||||
_draw_mini()
|
|
||||||
Style.rarity_sparkle(self, rect, rar, seed)
|
|
||||||
return
|
|
||||||
var fs := 11 if small else 13
|
var fs := 11 if small else 13
|
||||||
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
|
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
|
||||||
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
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))
|
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.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true)
|
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||||
# Schlüsselwörter als Symbole
|
# Schlüsselwörter als Symbole (Schild nur, solange er steht)
|
||||||
var icon := 7.0 if small else 9.0
|
var icon := 7.0 if small else 9.0
|
||||||
var kws: Array[String] = []
|
var kws: Array[String] = []
|
||||||
for k in Cards.KEYWORDS:
|
for k in Cards.KEYWORDS:
|
||||||
if k in creature.card.keywords:
|
if k in creature.card.keywords and (k != "Schild" or creature.shield):
|
||||||
kws.append(k)
|
kws.append(k)
|
||||||
if creature.shield and not ("Schild" in kws):
|
if creature.shield and not ("Schild" in kws):
|
||||||
kws.append("Schild")
|
kws.append("Schild")
|
||||||
@@ -85,23 +80,31 @@ func _draw() -> void:
|
|||||||
x += icon * 2 + 6
|
x += icon * 2 + 6
|
||||||
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
|
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
|
||||||
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||||
if mark != "":
|
if creature.hidden:
|
||||||
var w := Style.FONT_BOLD.get_string_size(tr(mark), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(11)).x + 14
|
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
|
||||||
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), Rect2(size.x / 2 - w / 2, -9, w, 18))
|
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
|
||||||
Style.text(self, tr(mark), 11, Style.GOLD, Vector2(size.x / 2, 0), "center", true)
|
|
||||||
if creature.frozen:
|
|
||||||
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
|
||||||
Style.text_outlined(self, "❄", 22 if small else 28, Color("dff2ff"), size / 2, "center", 3)
|
|
||||||
Style.rarity_sparkle(self, rect, rar, seed)
|
Style.rarity_sparkle(self, rect, rar, seed)
|
||||||
|
|
||||||
|
|
||||||
## Angriff (grün mit „+n“, wenn Resonanz, Feld oder Merkmale ihn heben), Leben, Elementvorteil.
|
## Verdecktes fremdes Wesen: ein Kartenrücken.
|
||||||
|
func _draw_back(rect: Rect2) -> void:
|
||||||
|
var bcol: Color = border if border != null else Style.GOLD
|
||||||
|
if flash:
|
||||||
|
bcol = Style.RED
|
||||||
|
draw_style_box(Style.box(Color("3c285a"), 6, bcol, 4 if flash else maxi(width, 2)), rect)
|
||||||
|
Style.border(self, rect.grow(-10), Color("5a3c82"), 2, 6)
|
||||||
|
Style.text(self, "?", 36, Style.GOLD, size / 2, "center", true)
|
||||||
|
if matchup != 0:
|
||||||
|
pass # unbekanntes Element: kein Vorteil anzeigen
|
||||||
|
|
||||||
|
|
||||||
|
## Angriff (grün oder rot, wenn die Elementregel ihn hebt oder senkt), Leben, Elementvorteil.
|
||||||
func _draw_stats(f: int, f_small: int, r: float) -> void:
|
func _draw_stats(f: int, f_small: int, r: float) -> void:
|
||||||
var extra := creature.power() - creature.attack
|
var extra := creature.power() - creature.attack
|
||||||
var atk_col := Style.GREEN if extra > 0 else Style.AMBER
|
var atk_col := Style.GREEN if extra > 0 else (Style.RED if extra < 0 else Style.AMBER)
|
||||||
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
|
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
|
||||||
if extra > 0:
|
if extra != 0:
|
||||||
Style.text_outlined(self, "+%d" % extra, f_small, Style.GREEN, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
|
Style.text_outlined(self, ("+%d" % extra) if extra > 0 else ("−%d" % -extra), f_small, Style.GREEN if extra > 0 else Style.RED, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
|
||||||
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
|
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
|
||||||
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
|
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
|
||||||
if matchup != 0:
|
if matchup != 0:
|
||||||
@@ -109,26 +112,3 @@ func _draw_stats(f: int, f_small: int, r: float) -> void:
|
|||||||
draw_circle(c, 11, Color("14141c"))
|
draw_circle(c, 11, Color("14141c"))
|
||||||
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
|
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
|
||||||
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
|
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
|
||||||
|
|
||||||
|
|
||||||
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
|
|
||||||
func _draw_mini() -> void:
|
|
||||||
var letters: Array[String] = []
|
|
||||||
if creature.taunt():
|
|
||||||
letters.append("W")
|
|
||||||
if creature.shield:
|
|
||||||
letters.append("S")
|
|
||||||
if creature.attacks_per_turn() == 2:
|
|
||||||
letters.append("2")
|
|
||||||
if creature.frozen:
|
|
||||||
letters.append("❄")
|
|
||||||
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
|
||||||
var extra := creature.power() - creature.attack
|
|
||||||
Style.text_outlined(self, str(creature.power()), 13, Style.GREEN if extra > 0 else Style.YELLOW, Vector2(9, size.y - 9), "center", 3)
|
|
||||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
|
||||||
Style.text_outlined(self, str(creature.health), 13, hp_col, Vector2(size.x - 9, size.y - 9), "center", 3)
|
|
||||||
if matchup != 0:
|
|
||||||
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), 10,
|
|
||||||
Style.GREEN if matchup > 0 else Style.RED, Vector2(size.x - 3, 2), "topright", true)
|
|
||||||
if creature.frozen:
|
|
||||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
|
||||||
|
|||||||
73
scripts/ui/element_view.gd
Normal file
73
scripts/ui/element_view.gd
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
class_name ElementView
|
||||||
|
extends Control
|
||||||
|
## Die Vier-Elemente-Regel auf einen Blick: offene Wesen je Element auf dem Tisch. Ab vier
|
||||||
|
## bekommt das Element +1/+1 und das Element, das es schlägt, -1/-1.
|
||||||
|
|
||||||
|
var counts := {}
|
||||||
|
|
||||||
|
|
||||||
|
func _init(rect: Rect2) -> void:
|
||||||
|
position = rect.position
|
||||||
|
size = rect.size
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
counts[e] = 0
|
||||||
|
|
||||||
|
|
||||||
|
func update_from(g: Game) -> void:
|
||||||
|
counts = g.element_counts()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func strong(e: String) -> bool:
|
||||||
|
return int(counts.get(e, 0)) >= Game.ELEMENT_STACK
|
||||||
|
|
||||||
|
|
||||||
|
func weak(e: String) -> bool:
|
||||||
|
var predator := Cards.beaten_by(e)
|
||||||
|
return predator != "" and strong(predator)
|
||||||
|
|
||||||
|
|
||||||
|
## Der Inhalt als Text (Tests, Protokoll).
|
||||||
|
func text() -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
for e in Cards.ELEMENTS:
|
||||||
|
var s := "%s %d" % [tr(e), int(counts.get(e, 0))]
|
||||||
|
if strong(e):
|
||||||
|
s += " (+1/+1)"
|
||||||
|
if weak(e):
|
||||||
|
s += " (-1/-1)"
|
||||||
|
parts.append(s)
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var w := size.x / Cards.ELEMENTS.size()
|
||||||
|
for i in Cards.ELEMENTS.size():
|
||||||
|
var e: String = Cards.ELEMENTS[i]
|
||||||
|
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||||
|
var x0 := i * w
|
||||||
|
var cy := size.y / 2
|
||||||
|
var n := int(counts.get(e, 0))
|
||||||
|
if strong(e):
|
||||||
|
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||||
|
elif weak(e):
|
||||||
|
draw_style_box(Style.box(Color(Style.RED, 0.12), 6, Style.RED, 1), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||||
|
Style.text(self, tr(e), 13, Style.TEXT if strong(e) else Style.MUTED, Vector2(x0 + 12, cy), "midleft", strong(e))
|
||||||
|
var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12
|
||||||
|
for k in Game.ELEMENT_STACK:
|
||||||
|
var c := Vector2(px + k * 13, cy)
|
||||||
|
if k < n:
|
||||||
|
draw_circle(c, 4.5, col)
|
||||||
|
else:
|
||||||
|
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
||||||
|
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
||||||
|
if n > Game.ELEMENT_STACK:
|
||||||
|
Style.text(self, "+%d" % (n - Game.ELEMENT_STACK), 11, Style.MUTED, Vector2(px + Game.ELEMENT_STACK * 13 + 2, cy), "midleft")
|
||||||
|
var tag := ""
|
||||||
|
if strong(e):
|
||||||
|
tag = "+1/+1"
|
||||||
|
elif weak(e):
|
||||||
|
tag = "-1/-1"
|
||||||
|
if tag != "":
|
||||||
|
Style.text(self, tag, 13, Style.GREEN if strong(e) else Style.RED, Vector2(x0 + w - 12, cy), "midright", true)
|
||||||
1
scripts/ui/element_view.gd.uid
Normal file
1
scripts/ui/element_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c2tr1bg65rhk6
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
class_name FieldView
|
|
||||||
extends Control
|
|
||||||
## Das gemeinsame Feld: je Element vier Ladepunkte, bei vollen Punkten kippt das Feld.
|
|
||||||
## Das aktive Feld steht farbig mit seinem Bonus da.
|
|
||||||
|
|
||||||
var element := ""
|
|
||||||
var charge := {}
|
|
||||||
|
|
||||||
|
|
||||||
func _init(rect: Rect2) -> void:
|
|
||||||
position = rect.position
|
|
||||||
size = rect.size
|
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
|
|
||||||
|
|
||||||
func update_from(g: Game) -> void:
|
|
||||||
element = g.field_element
|
|
||||||
charge = g.field_charge.duplicate()
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
## Der Inhalt als Text (Tests, Protokoll).
|
|
||||||
func text() -> String:
|
|
||||||
var parts: Array[String] = []
|
|
||||||
for e in Cards.ELEMENTS:
|
|
||||||
if e == element:
|
|
||||||
parts.append("%s: +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")])
|
|
||||||
else:
|
|
||||||
parts.append("%s %d/%d" % [tr(e), int(charge.get(e, 0)), Cards.FIELD_CHARGE])
|
|
||||||
return " · ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var w := size.x / Cards.ELEMENTS.size()
|
|
||||||
for i in Cards.ELEMENTS.size():
|
|
||||||
var e: String = Cards.ELEMENTS[i]
|
|
||||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
|
||||||
var x0 := i * w
|
|
||||||
var cy := size.y / 2
|
|
||||||
if e == element:
|
|
||||||
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
|
||||||
Style.text(self, "%s +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")], 13, Style.lighter(col, 0.4), Vector2(x0 + w / 2, cy), "center", true)
|
|
||||||
continue
|
|
||||||
Style.text(self, tr(e), 13, Style.MUTED, Vector2(x0 + 12, cy), "midleft")
|
|
||||||
var n := int(charge.get(e, 0))
|
|
||||||
var px := x0 + 12 + Style.FONT.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 14
|
|
||||||
for k in Cards.FIELD_CHARGE:
|
|
||||||
var c := Vector2(px + k * 13, cy)
|
|
||||||
if k < n:
|
|
||||||
draw_circle(c, 4.5, col)
|
|
||||||
else:
|
|
||||||
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
|
||||||
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://i4rdq5rmcyuk
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
class_name OpponentSelectScreen
|
class_name OpponentSelectScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||||
## aus dem Laden (siehe DeckGen.shop_cards).
|
## per Ziehen (siehe DeckGen.offer_cards).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var diff_buttons := {}
|
var diff_buttons := {}
|
||||||
|
|||||||
@@ -344,7 +344,17 @@ static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) ->
|
|||||||
var lx := x0 + (w - lw) / 2
|
var lx := x0 + (w - lw) / 2
|
||||||
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
|
var band := PackedVector2Array([Vector2(lx, 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):
|
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||||
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1]))
|
if poly.size() >= 3 and _area(poly) > 2.0: # Splitter am Rand lassen sich nicht zeichnen
|
||||||
|
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1]))
|
||||||
|
|
||||||
|
|
||||||
|
static func _area(poly: PackedVector2Array) -> float:
|
||||||
|
var a := 0.0
|
||||||
|
for i in poly.size():
|
||||||
|
var p := poly[i]
|
||||||
|
var q := poly[(i + 1) % poly.size()]
|
||||||
|
a += p.x * q.y - q.x * p.y
|
||||||
|
return absf(a) / 2.0
|
||||||
|
|
||||||
|
|
||||||
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||||
@@ -434,8 +444,7 @@ static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, col
|
|||||||
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
|
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
|
||||||
|
|
||||||
|
|
||||||
## Schlüsselwort als Symbol auf dunklem Kreis: Wächter Mauer, Schild Schild, Flink Doppelpfeil,
|
## Schlüsselwort als Symbol auf dunklem Kreis: Schild Schild, Doppelschlag gekreuzte Klingen.
|
||||||
## Doppelschlag gekreuzte Klingen.
|
|
||||||
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
|
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
|
||||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
|
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
|
||||||
var w := maxf(1.5, r * 0.22)
|
var w := maxf(1.5, r * 0.22)
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ func _ready() -> void:
|
|||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
var h: Game.Player = scr.game.human
|
var h: Game.Player = scr.game.human
|
||||||
scr.game.buy(h, 0)
|
for i in 3:
|
||||||
|
scr.game.pick(h, 0)
|
||||||
|
h.energy = 10
|
||||||
|
scr.game.draw(h)
|
||||||
|
scr.game.pick(h, 0)
|
||||||
var t0 := Time.get_ticks_usec()
|
var t0 := Time.get_ticks_usec()
|
||||||
for i in 30:
|
for i in 30:
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
print("refresh() mit 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
print("refresh() mit 4 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
||||||
scr.ai_delay = 0.2
|
scr.ai_delay = 0.2
|
||||||
measuring = true
|
measuring = true
|
||||||
var start := Time.get_ticks_msec()
|
var start := Time.get_ticks_msec()
|
||||||
|
|||||||
@@ -73,22 +73,20 @@ func _act_host() -> void:
|
|||||||
_decide(game.players[seat])
|
_decide(game.players[seat])
|
||||||
|
|
||||||
|
|
||||||
## Einfache Züge mit kurzer Bedenkzeit: erst das billigste bezahlbare Angebot kaufen, dann das
|
## Einfache Züge mit kurzer Bedenkzeit: erst eine offene Wahl treffen, dann das erste
|
||||||
## erste spielbare Wesen ohne Ziel, dann angreifen, sonst Zug beenden.
|
## spielbare Wesen ohne Ziel legen, dann angreifen, dann ziehen, sonst Zug beenden.
|
||||||
func _decide(me: Game.Player) -> void:
|
func _decide(me: Game.Player) -> void:
|
||||||
var g := game
|
var g := game
|
||||||
await get_tree().create_timer(0.25).timeout
|
await get_tree().create_timer(0.25).timeout
|
||||||
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
||||||
return # inzwischen ein neuer Zustand
|
return # inzwischen ein neuer Zustand
|
||||||
for i in me.shop.size():
|
if not me.offer.is_empty():
|
||||||
var offer = me.shop[i]
|
net.send_action({"type": "pick", "index": 0})
|
||||||
if offer != null and me.energy >= offer.cost and me.hand.size() < Game.MAX_HAND:
|
return
|
||||||
net.send_action({"type": "buy", "slot": i})
|
|
||||||
return
|
|
||||||
for i in me.hand.size():
|
for i in me.hand.size():
|
||||||
var card: Cards.Card = me.hand[i]
|
var card: Cards.Card = me.hand[i]
|
||||||
if game.can_play(me, card)[0] and card.target_type() == "":
|
if game.can_play(me, card)[0]:
|
||||||
net.send_action({"type": "play", "hand": i, "target": null})
|
net.send_action({"type": "play", "hand": i})
|
||||||
return
|
return
|
||||||
for cr in me.board:
|
for cr in me.board:
|
||||||
if cr.can_attack():
|
if cr.can_attack():
|
||||||
@@ -100,4 +98,7 @@ func _decide(me: Game.Player) -> void:
|
|||||||
if game.can_attack(cr, c)[0]:
|
if game.can_attack(cr, c)[0]:
|
||||||
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "creature", "uid": c.uid}})
|
net.send_action({"type": "attack", "uid": cr.uid, "target": {"kind": "creature", "uid": c.uid}})
|
||||||
return
|
return
|
||||||
|
if game.can_draw(me)[0]:
|
||||||
|
net.send_action({"type": "draw"})
|
||||||
|
return
|
||||||
net.send_action({"type": "end"})
|
net.send_action({"type": "end"})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func _initialize() -> void:
|
|||||||
print("Kartenpool laden: %.1f ms" % t)
|
print("Kartenpool laden: %.1f ms" % t)
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 3
|
r.seed = 3
|
||||||
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
|
print("Stapel mischen: %.2f ms" % ms(func(): DeckGen.deck(r), 20))
|
||||||
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
||||||
for n in [1]:
|
for n in [1]:
|
||||||
var g := Game.new_battle(r)
|
var g := Game.new_battle(r)
|
||||||
|
|||||||
1013
tests/test_runner.gd
1013
tests/test_runner.gd
File diff suppressed because it is too large
Load Diff
248
tests/ui_test.gd
248
tests/ui_test.gd
@@ -51,9 +51,13 @@ func click(node: Control) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func right_click() -> void:
|
func right_click() -> void:
|
||||||
|
await right_click_at(Vector2(5, 5))
|
||||||
|
|
||||||
|
|
||||||
|
func right_click_at(pos: Vector2) -> void:
|
||||||
var down := InputEventMouseButton.new()
|
var down := InputEventMouseButton.new()
|
||||||
down.position = Vector2(5, 5)
|
down.position = pos
|
||||||
down.global_position = down.position
|
down.global_position = pos
|
||||||
down.button_index = MOUSE_BUTTON_RIGHT
|
down.button_index = MOUSE_BUTTON_RIGHT
|
||||||
down.pressed = true
|
down.pressed = true
|
||||||
get_viewport().push_input(down)
|
get_viewport().push_input(down)
|
||||||
@@ -149,7 +153,7 @@ func _ready() -> void:
|
|||||||
await frames(3)
|
await frames(3)
|
||||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||||
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
|
||||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen]
|
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
|
||||||
if not fast:
|
if not fast:
|
||||||
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
suite += [test_kampf.bind(3), test_kampf_abbrechen]
|
||||||
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
|
||||||
@@ -215,18 +219,25 @@ func target_view(scr, valid: Array) -> Control:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
func human_turn(scr) -> void:
|
## Nimmt alle offenen Wahlen des Spielers (immer die erste Karte), auch über das Netz.
|
||||||
|
func pick_all(scr) -> void:
|
||||||
|
var guard := 0
|
||||||
|
while scr.game != null and not scr.perspective.offer.is_empty() and guard < 60:
|
||||||
|
guard += 1
|
||||||
|
if scr.pick_views.is_empty():
|
||||||
|
await frames(1)
|
||||||
|
continue
|
||||||
|
var n: int = scr.perspective.offer.size()
|
||||||
|
await click(scr.pick_views[0])
|
||||||
|
var w := 0
|
||||||
|
while scr.game != null and scr.perspective.offer.size() == n and w < 120:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
|
||||||
|
|
||||||
|
## Legt, was bezahlbar ist.
|
||||||
|
func play_hand(scr) -> void:
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
# erst kaufen, solange Mana und Handplatz reichen
|
|
||||||
for i in 12:
|
|
||||||
var offers: Array = scr.shop_views.filter(func(v): return g.human.energy >= v.card.cost and g.human.hand.size() < Game.MAX_HAND)
|
|
||||||
if offers.is_empty():
|
|
||||||
break
|
|
||||||
var offer: Cards.Card = offers[0].card
|
|
||||||
var mana: int = g.human.energy
|
|
||||||
await click(offers[0])
|
|
||||||
check(g.human.energy == mana - offer.cost, "gekauft: " + offer.name)
|
|
||||||
# dann ausspielen, das kostet nichts
|
|
||||||
for i in 12:
|
for i in 12:
|
||||||
var playable: Array = scr.hand_views.filter(func(v): return g.can_play(g.human, v.card)[0])
|
var playable: Array = scr.hand_views.filter(func(v): return g.can_play(g.human, v.card)[0])
|
||||||
if playable.is_empty():
|
if playable.is_empty():
|
||||||
@@ -234,9 +245,22 @@ func human_turn(scr) -> void:
|
|||||||
var card: Cards.Card = playable[0].card
|
var card: Cards.Card = playable[0].card
|
||||||
var board_before: int = g.human.board.size()
|
var board_before: int = g.human.board.size()
|
||||||
await click(playable[0])
|
await click(playable[0])
|
||||||
if scr.pending != null:
|
|
||||||
await click(target_view(scr, g.valid_targets(g.human, card.target_type())))
|
|
||||||
check(g.human.board.size() > board_before, "Karte gespielt: " + card.name)
|
check(g.human.board.size() > board_before, "Karte gespielt: " + card.name)
|
||||||
|
|
||||||
|
|
||||||
|
func human_turn(scr) -> void:
|
||||||
|
var g: Game = scr.game
|
||||||
|
# erst legen, was bezahlbar ist, dann mit dem Rest ziehen (und wieder legen)
|
||||||
|
for round in 2:
|
||||||
|
await play_hand(scr)
|
||||||
|
for i in 6:
|
||||||
|
if g.winner != "" or not g.can_draw(g.human)[0]:
|
||||||
|
break
|
||||||
|
var mana: int = g.human.energy
|
||||||
|
await click(scr.deck)
|
||||||
|
check(g.human.energy == mana - Game.DRAW_COST and not scr.pick_views.is_empty(), "gezogen: drei zur Wahl")
|
||||||
|
await pick_all(scr)
|
||||||
|
check(scr.pick_views.is_empty(), "gewählt")
|
||||||
for i in 12:
|
for i in 12:
|
||||||
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
|
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
|
||||||
if ready.is_empty() or g.winner != "":
|
if ready.is_empty() or g.winner != "":
|
||||||
@@ -249,7 +273,7 @@ func human_turn(scr) -> void:
|
|||||||
if scr.selected != cr:
|
if scr.selected != cr:
|
||||||
print("DEBUG Auswahl fehlgeschlagen: rect=", ready[0].get_global_rect(), " scale=", ready[0].scale,
|
print("DEBUG Auswahl fehlgeschlagen: rect=", ready[0].get_global_rect(), " scale=", ready[0].scale,
|
||||||
" busy=", ready[0].busy, " unter Maus=", under, " Meldung='", scr.msg_label.text,
|
" busy=", ready[0].busy, " unter Maus=", under, " Meldung='", scr.msg_label.text,
|
||||||
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected)
|
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " selected=", scr.selected)
|
||||||
check(scr.selected == cr, "Wesen gewählt")
|
check(scr.selected == cr, "Wesen gewählt")
|
||||||
var targets := []
|
var targets := []
|
||||||
for o in [g.opponent]:
|
for o in [g.opponent]:
|
||||||
@@ -269,6 +293,7 @@ func start_battle() -> void:
|
|||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||||
await click(find_button("Starten"))
|
await click(find_button("Starten"))
|
||||||
|
await pick_all(screen())
|
||||||
|
|
||||||
|
|
||||||
func test_gegnerauswahl() -> void:
|
func test_gegnerauswahl() -> void:
|
||||||
@@ -285,8 +310,12 @@ func test_gegnerauswahl() -> void:
|
|||||||
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
||||||
var opp: Game.Player = hard.game.opponent
|
var opp: Game.Player = hard.game.opponent
|
||||||
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
||||||
check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
|
check(hard.game.human.offer.size() == Game.START_OFFER and hard.game.human.hand.is_empty() and hard.pick_layer.visible,
|
||||||
"Start mit vollem Laden und leerer Hand")
|
"Start: fünf Karten zur Wahl, Hand leer")
|
||||||
|
check(hard.game.human.offer.all(func(c): return c.cost <= Game.START_MANA + 1), "alle fünf sind im ersten Zug spielbar")
|
||||||
|
await shot("05_startwahl")
|
||||||
|
await pick_all(hard)
|
||||||
|
check(hard.game.human.hand.size() == Game.START_PICKS and not hard.pick_layer.visible, "drei behalten, Wahl geschlossen")
|
||||||
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
|
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
|
||||||
await shot("11_kampf")
|
await shot("11_kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
@@ -294,9 +323,10 @@ func test_gegnerauswahl() -> void:
|
|||||||
await click(find_button("Leicht"))
|
await click(find_button("Leicht"))
|
||||||
await click(find_button("Starten"))
|
await click(find_button("Starten"))
|
||||||
var easy = screen()
|
var easy = screen()
|
||||||
|
await pick_all(easy)
|
||||||
opp = easy.game.opponent
|
opp = easy.game.opponent
|
||||||
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
|
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
|
||||||
check(easy.game.players[0].max_energy == 1 and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
|
check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
|
||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
await click(find_button("Normal"))
|
await click(find_button("Normal"))
|
||||||
@@ -457,39 +487,47 @@ func test_kampf_steuerung() -> void:
|
|||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
g.human.energy = 20 # das Angebot ist zufällig, hier soll nur die Steuerung geprüft werden
|
g.human.energy = 20 # die Wahl ist zufällig, hier soll nur die Steuerung geprüft werden
|
||||||
|
g.human.hand = [Cards.by_id("f_wolf")]
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
check(scr.shop_views.size() == Game.SHOP_SIZE and g.human.hand.is_empty(), "voller Laden, leere Hand")
|
check(scr.hand_views.size() == 1 and scr.deck.enabled and scr.pick_views.is_empty(), "eine Handkarte, Stapel bereit, keine Wahl offen")
|
||||||
# Kaufen: Mana weg, Karte auf der Hand
|
# Ziehen: 1 Mana weg, drei zur Wahl, eine landet auf der Hand
|
||||||
var offer: Cards.Card = scr.shop_views[0].card
|
|
||||||
var mana: int = g.human.energy
|
var mana: int = g.human.energy
|
||||||
await click(scr.shop_views[0])
|
await click(scr.deck)
|
||||||
check(g.human.hand.size() == 1 and g.human.hand[0] == offer and g.human.energy == mana - offer.cost, "Angebot gekauft")
|
check(g.human.energy == mana - Game.DRAW_COST and scr.pick_views.size() == Game.OFFER_SIZE and scr.pick_layer.visible, "gezogen: drei Karten zur Wahl")
|
||||||
# Ausspielen kostet nichts
|
var wanted: Cards.Card = scr.pick_views[1].card
|
||||||
|
await click(scr.pick_views[1])
|
||||||
|
check(g.human.hand.size() == 2 and g.human.hand[1].id == wanted.id and not scr.pick_layer.visible, "gewählt, liegt auf der Hand")
|
||||||
|
# Ausspielen kostet die Kartenkosten
|
||||||
var board_before: int = g.human.board.size()
|
var board_before: int = g.human.board.size()
|
||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == board_before + 1 and g.human.energy == mana, "Wesen per Taste gespielt, ohne Mana")
|
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_wolf").cost, "Wesen per Taste gespielt, Kosten bezahlt")
|
||||||
# Rückgängig: Wesen zurück auf die Hand
|
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
|
||||||
check(scr.b_undo.visible, "Rückgängig angeboten")
|
check(scr.b_undo.visible, "Rückgängig angeboten")
|
||||||
await click(scr.b_undo)
|
await click(scr.b_undo)
|
||||||
g = scr.game
|
g = scr.game
|
||||||
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == offer.id), "Rückgängig stellt die Hand her")
|
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == "f_wolf") and g.human.energy == mana, "Rückgängig stellt Hand und Mana her")
|
||||||
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
||||||
# Neu würfeln kostet Mana
|
# Ziehen per Taste
|
||||||
var shop_before: Array = g.human.shop.duplicate()
|
|
||||||
mana = g.human.energy
|
mana = g.human.energy
|
||||||
await click(scr.b_reroll)
|
await key(KEY_Z)
|
||||||
check(g.human.energy == mana - Game.REROLL_COST and g.human.shop != shop_before, "Laden neu gewürfelt")
|
check(g.human.energy == mana - Game.DRAW_COST and scr.pick_layer.visible, "Z zieht")
|
||||||
|
await key(KEY_1)
|
||||||
|
check(not scr.pick_layer.visible, "1 wählt die erste Karte")
|
||||||
await shot("22_kampf_steuerung")
|
await shot("22_kampf_steuerung")
|
||||||
# Gamepad: Zeiger im Laden bewegen, A kauft
|
# Gamepad: A auf dem Stapel zieht, Steuerkreuz wählt die zweite Karte, A nimmt sie
|
||||||
await pad(JOY_BUTTON_DPAD_RIGHT)
|
await pad(JOY_BUTTON_DPAD_LEFT)
|
||||||
check(scr.gp_zone == "shop" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweitem Angebot")
|
check(scr.gp_zone == "deck" and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf dem Stapel")
|
||||||
var hand_before: int = g.human.hand.size()
|
var hand_before: int = g.human.hand.size()
|
||||||
await pad(JOY_BUTTON_A)
|
await pad(JOY_BUTTON_A)
|
||||||
check(g.human.hand.size() == hand_before + 1, "Gamepad A kauft das Angebot")
|
check(scr.pick_layer.visible, "Gamepad A zieht")
|
||||||
|
await pad(JOY_BUTTON_DPAD_RIGHT)
|
||||||
|
check(scr.gp_zone == "pick" and scr.gp_index == 1, "Zeiger auf der zweiten Karte der Wahl")
|
||||||
|
await pad(JOY_BUTTON_A)
|
||||||
|
check(g.human.hand.size() == hand_before + 1 and not scr.pick_layer.visible, "Gamepad A nimmt die Karte")
|
||||||
scr.ai_delay = 0.75
|
scr.ai_delay = 0.75
|
||||||
await key(KEY_SPACE)
|
await key(KEY_SPACE)
|
||||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||||
@@ -519,31 +557,28 @@ func test_kampf_grenzen() -> void:
|
|||||||
g.human.energy = 100
|
g.human.energy = 100
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
# Hand voll kaufen
|
# Hand voll ziehen
|
||||||
var guard := 0
|
var guard := 0
|
||||||
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
if scr.shop_views.is_empty():
|
await click(scr.deck)
|
||||||
await click(scr.b_reroll)
|
await pick_all(scr)
|
||||||
continue
|
|
||||||
await click(scr.shop_views[0])
|
|
||||||
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
||||||
check(scr.shop_views.all(func(v): return v.dim), "bei voller Hand ist nichts mehr kaufbar")
|
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
|
||||||
if not scr.shop_views.is_empty():
|
await click(scr.deck)
|
||||||
await click(scr.shop_views[0])
|
check(scr.msg_label.text == tr("Deine Hand ist voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
||||||
check(scr.msg_label.text == tr("Hand voll"), "Meldung bei voller Hand: " + scr.msg_label.text)
|
|
||||||
# Feld voll spielen
|
# Feld voll spielen
|
||||||
guard = 0
|
guard = 0
|
||||||
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
|
||||||
guard += 1
|
guard += 1
|
||||||
|
if g.human.hand.is_empty():
|
||||||
|
await click(scr.deck)
|
||||||
|
await pick_all(scr)
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
if scr.pending != null: # Karte will ein Ziel
|
|
||||||
var valid: Array = g.valid_targets(g.human, scr.pending.target_type())
|
|
||||||
if valid.is_empty():
|
|
||||||
await right_click()
|
|
||||||
else:
|
|
||||||
await click(target_view(scr, valid))
|
|
||||||
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
|
||||||
|
if g.human.hand.is_empty():
|
||||||
|
await click(scr.deck)
|
||||||
|
await pick_all(scr)
|
||||||
await key(KEY_1)
|
await key(KEY_1)
|
||||||
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
||||||
await shot("25_kampf_voll")
|
await shot("25_kampf_voll")
|
||||||
@@ -553,16 +588,16 @@ func test_kampf_grenzen() -> void:
|
|||||||
await click(scr.own_views[0])
|
await click(scr.own_views[0])
|
||||||
await click(scr.own_views[1])
|
await click(scr.own_views[1])
|
||||||
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
||||||
# Ohne Mana kein Neuwürfeln
|
# Ohne Mana kein Ziehen
|
||||||
g.human.energy = 0
|
g.human.energy = 0
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(1)
|
await frames(1)
|
||||||
check(scr.b_reroll.disabled, "ohne Mana kein Neuwürfeln")
|
check(not scr.deck.enabled, "ohne Mana kein Ziehen")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
## Feldanzeige und Verschmelzen im Kampf.
|
## Elementregel, verdeckt legen, Kartenmenü und Verschmelzen im Kampf.
|
||||||
func test_kampf_feld_und_verschmelzen() -> void:
|
func test_kampf_elemente_und_verschmelzen() -> void:
|
||||||
await start_battle()
|
await start_battle()
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BattleScreen, "Kampf gestartet")
|
check(scr is BattleScreen, "Kampf gestartet")
|
||||||
@@ -570,47 +605,71 @@ func test_kampf_feld_und_verschmelzen() -> void:
|
|||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
g.human.board.clear()
|
g.human.board.clear()
|
||||||
var put := func(p: Game.Player, id: String) -> Game.Creature:
|
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Zähler
|
||||||
var cr := Game.Creature.new(Cards.by_id(id), p, g.next_uid)
|
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
|
||||||
|
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
|
||||||
g.next_uid += 1
|
g.next_uid += 1
|
||||||
cr.ready = true
|
|
||||||
cr.attacks_left = 1
|
cr.attacks_left = 1
|
||||||
|
cr.hidden = hidden
|
||||||
p.board.append(cr)
|
p.board.append(cr)
|
||||||
return cr
|
return cr
|
||||||
put.call(g.human, "f_wolf")
|
put.call(g.human, "f_wolf")
|
||||||
|
put.call(g.human, "f_asche")
|
||||||
put.call(g.human, "w_reiter")
|
put.call(g.human, "w_reiter")
|
||||||
var falke: Game.Creature = put.call(scr.others()[0], "l_falke")
|
var falke: Game.Creature = put.call(scr.opponent(), "l_falke")
|
||||||
g.human.hand = [Cards.by_id("f_kobold")]
|
var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true)
|
||||||
g.human.energy = 10
|
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
|
||||||
g.field_charge["Wasser"] = 2
|
g.human.energy = 20
|
||||||
|
g.apply_auras()
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
await frames(2)
|
await frames(2)
|
||||||
check(scr.field_view.text() == "Feuer 0/4 · Wasser 2/4 · Erde 0/4 · Luft 0/4", "neutrales Feld zeigt die Ladung: " + scr.field_view.text())
|
check(scr.element_view.text() == "Feuer 2 · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen: " + scr.element_view.text())
|
||||||
for i in Cards.FIELD_CHARGE:
|
check(scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 0, "fremdes verdecktes Wesen zeigt nur den Rücken, noch kein Bonus in Sicht")
|
||||||
g.charge_field(Cards.by_id("f_kobold"))
|
# verdeckt legen über das Kartenmenü (Rechtsklick auf die Handkarte)
|
||||||
scr.refresh()
|
var menu_text := "Verdeckt legen ◆ %d" % Cards.by_id("f_kobold").cost
|
||||||
await frames(2)
|
|
||||||
check(scr.field_view.text().begins_with("Vulkan: +1 Angriff"), "Feldanzeige nennt das Feld: " + scr.field_view.text())
|
|
||||||
check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff")
|
|
||||||
check(scr.hand_views[0].bonus == 2 and scr.hand_views[0].tag == "Dampf", "die Handkarte zeigt +2 (Resonanz und Feld) und die Kombo Dampf")
|
|
||||||
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
|
|
||||||
await shot("23_feld")
|
|
||||||
await hover(scr.hand_views[0])
|
await hover(scr.hand_views[0])
|
||||||
check(scr.tiles[g.human.board[1].uid].mark == "Dampf" and "Dampf" in scr.info_label.text, "beim Zeigen auf die Handkarte ist der Kombo-Partner markiert")
|
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
|
||||||
|
check(scr.menu_layer.visible and find_button(menu_text, scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit „Verdeckt legen“")
|
||||||
|
await shot("23_kartenmenue")
|
||||||
|
await click(find_button(menu_text, scr.menu_layer))
|
||||||
|
var mine_hidden: Game.Creature = g.human.board[3]
|
||||||
|
check(mine_hidden.hidden and not scr.menu_layer.visible and scr.element_view.text().begins_with("Feuer 2"), "verdeckt gelegt, zählt nicht")
|
||||||
|
# aufdecken per Klick auf das eigene verdeckte Wesen (erst, wenn es aus der Hand angekommen ist)
|
||||||
|
while scr.own_views[3].busy:
|
||||||
|
await frames(1)
|
||||||
|
await click(scr.own_views[3])
|
||||||
|
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3"), "aufgedeckt, zählt jetzt")
|
||||||
|
put.call(g.human, "f_kobold")
|
||||||
|
g.apply_auras()
|
||||||
|
scr.refresh()
|
||||||
|
await frames(2)
|
||||||
|
check(scr.element_view.text().begins_with("Feuer 4 (+1/+1)") and "Luft 1 (-1/-1)" in scr.element_view.text(), "vier Feuer: Bonus und Malus angezeigt: " + scr.element_view.text())
|
||||||
|
check(g.human.board[0].power() == 4 and falke.power() == 2, "Feuerwesen +1, Luftwesen -1")
|
||||||
|
check(scr.hand_views[0].bonus == -1, "die Luftkarte auf der Hand zeigt -1")
|
||||||
|
await shot("23b_vier_feuer")
|
||||||
|
# Verbessern über das Menü
|
||||||
|
var wolf: Game.Creature = g.human.board[0]
|
||||||
|
scr.open_menu(wolf, Rect2(scr.own_views[0].slot, scr.own_views[0].size))
|
||||||
|
var mana: int = g.human.energy
|
||||||
|
await click(find_button("Schild ◆ %d" % Cards.KEYWORD_COST["Schild"], scr.menu_layer))
|
||||||
|
check(wolf.shield and "Schild" in wolf.card.keywords and g.human.energy == mana - Cards.KEYWORD_COST["Schild"], "Schild gekauft")
|
||||||
|
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
||||||
await click(scr.own_views[0])
|
await click(scr.own_views[0])
|
||||||
check(scr.selected == g.human.board[0] and scr.tiles[falke.uid].matchup == 1, "Angreifer gewählt: das Luftwesen zeigt +1 für Feuer")
|
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")
|
||||||
await shot("23b_angriffsziel")
|
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
|
||||||
await right_click() # Auswahl wieder aufheben
|
await click(scr.tiles[secret.uid])
|
||||||
check(scr.selected == null and scr.tiles[falke.uid].matchup == 0, "Auswahl aufgehoben, Vorteil verschwindet")
|
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
|
||||||
|
await right_click()
|
||||||
|
# Verschmelzen
|
||||||
await click(scr.b_fuse)
|
await click(scr.b_fuse)
|
||||||
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
||||||
await click(scr.own_views[0])
|
await click(scr.own_views[1])
|
||||||
check(scr.fuse_first != null, "erstes Wesen gewählt")
|
check(scr.fuse_first != null, "erstes Wesen gewählt")
|
||||||
await shot("24_verschmelzen")
|
await shot("24_verschmelzen")
|
||||||
await click(scr.own_views[1])
|
mana = g.human.energy
|
||||||
check(g.human.board.size() == 1 and g.human.board[0].name == "Dampf", "die beiden Wesen sind zu Dampf verschmolzen")
|
await click(scr.own_views[2])
|
||||||
check(not scr.fusing and g.human.energy == 8, "Modus beendet und Mana bezahlt")
|
check(g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
|
||||||
check(scr.own_views.size() == 1, "nur noch eine eigene Kachel")
|
check(not scr.fusing, "Modus beendet")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
@@ -642,6 +701,7 @@ func test_netzwerk() -> void:
|
|||||||
var b = screen()
|
var b = screen()
|
||||||
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
|
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
|
||||||
b.ai_delay = 0.0
|
b.ai_delay = 0.0
|
||||||
|
await pick_all(b)
|
||||||
var rounds := 0
|
var rounds := 0
|
||||||
while b.game != null and b.game.winner == "" and rounds < 2:
|
while b.game != null and b.game.winner == "" and rounds < 2:
|
||||||
if b.game.current() == b.perspective:
|
if b.game.current() == b.perspective:
|
||||||
@@ -747,6 +807,7 @@ func test_host_wechsel() -> void:
|
|||||||
await frames(1)
|
await frames(1)
|
||||||
w += 1
|
w += 1
|
||||||
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
|
check(b.game != null and main.net.i_am_successor and not main.net.backup.is_empty(), "Nachfolger kennt den vollen Spielstand")
|
||||||
|
await pick_all(b)
|
||||||
main.net.rejoin_seconds = 3.0
|
main.net.rejoin_seconds = 3.0
|
||||||
_bot_quit(true)
|
_bot_quit(true)
|
||||||
w = 0
|
w = 0
|
||||||
@@ -820,6 +881,7 @@ func test_kampagne() -> void:
|
|||||||
await click(find_button("Kämpfen"))
|
await click(find_button("Kämpfen"))
|
||||||
var battle = screen()
|
var battle = screen()
|
||||||
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
||||||
|
await pick_all(battle)
|
||||||
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
|
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
|
||||||
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
||||||
await shot("16_kampagne_kampf")
|
await shot("16_kampagne_kampf")
|
||||||
@@ -835,7 +897,8 @@ func test_kampagne() -> void:
|
|||||||
await shot("27_kampagne_kapitel2")
|
await shot("27_kampagne_kapitel2")
|
||||||
await click(find_button("Kämpfen"))
|
await click(find_button("Kämpfen"))
|
||||||
var b2 = screen()
|
var b2 = screen()
|
||||||
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 30, "Kapitel-2-Boss mit zwei Merkmalen und 30 Leben")
|
await pick_all(b2)
|
||||||
|
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 22, "Kapitel-2-Boss mit zwei Merkmalen und 22 Leben")
|
||||||
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
|
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
main.profile.campaign_stage = 0
|
main.profile.campaign_stage = 0
|
||||||
@@ -884,7 +947,7 @@ func test_kampf_abbrechen() -> void:
|
|||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
await wait_for_human(scr)
|
await wait_for_human(scr)
|
||||||
check(g.opponent != null and g.human.max_energy == 1, "Duell läuft")
|
check(g.opponent != null and g.human.max_energy == Game.START_MANA, "Duell läuft")
|
||||||
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
||||||
g.human.energy = 10
|
g.human.energy = 10
|
||||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
||||||
@@ -894,18 +957,15 @@ func test_kampf_abbrechen() -> void:
|
|||||||
scr.refresh()
|
scr.refresh()
|
||||||
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
||||||
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
|
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
|
||||||
await hover(scr.field_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
||||||
await hover(reiter_view)
|
await hover(reiter_view)
|
||||||
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
|
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
|
||||||
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
|
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
|
||||||
await shot("21_kampf_vorschau")
|
await shot("21_kampf_vorschau")
|
||||||
var targeted: Array = scr.hand_views.filter(func(v): return v.card.target_type() != "" and g.can_play(g.human, v.card)[0])
|
if not scr.own_views.is_empty():
|
||||||
if not targeted.is_empty():
|
await click(scr.own_views[0])
|
||||||
var card: Cards.Card = targeted[0].card
|
await right_click()
|
||||||
await click(targeted[0])
|
check(scr.selected == null, "Rechtsklick bricht ab")
|
||||||
check(scr.pending == card, "Ziel wird erwartet")
|
|
||||||
await right_click()
|
|
||||||
check(scr.pending == null and card in g.human.hand, "Rechtsklick bricht ab")
|
|
||||||
await hover(scr.hand_views[0])
|
await hover(scr.hand_views[0])
|
||||||
if scr.preview_holder.get_child_count() == 0:
|
if scr.preview_holder.get_child_count() == 0:
|
||||||
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ Kapitel 2,Chapter 2
|
|||||||
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
|
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
|
||||||
Zug %d · Du bist dran,Turn %d · Your turn
|
Zug %d · Du bist dran,Turn %d · Your turn
|
||||||
Zug %d · %s ist dran …,Turn %d · %s is playing …
|
Zug %d · %s ist dran …,Turn %d · %s is playing …
|
||||||
Wähle ein Ziel,Choose a target
|
|
||||||
Wähle ein Angriffsziel,Choose an attack target
|
Wähle ein Angriffsziel,Choose an attack target
|
||||||
Abgebrochen,Cancelled
|
Abgebrochen,Cancelled
|
||||||
%s kann nicht angreifen,%s cannot attack
|
%s kann nicht angreifen,%s cannot attack
|
||||||
@@ -65,26 +64,6 @@ Große Schrift,Large text
|
|||||||
%d Münzen,%d coins
|
%d Münzen,%d coins
|
||||||
Abbrechen,Cancel
|
Abbrechen,Cancel
|
||||||
"Ja, zurücksetzen","Yes, reset"
|
"Ja, zurücksetzen","Yes, reset"
|
||||||
%s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature
|
|
||||||
%s Schaden an ein Ziel,%s damage to a target
|
|
||||||
%s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures
|
|
||||||
%s Schaden an den Gegner,%s damage to the opponent
|
|
||||||
%s Schaden an den Gegner und seine Wesen,%s damage to the opponent and their creatures
|
|
||||||
Heile dich um %s,Heal yourself for %s
|
|
||||||
Erhalte 1 Mana,Gain 1 mana
|
|
||||||
Erhalte %s Mana,Gain %s mana
|
|
||||||
Friere ein gegnerisches Wesen ein,Freeze an enemy creature
|
|
||||||
Friere das stärkste gegnerische Wesen ein,Freeze the strongest enemy creature
|
|
||||||
Friere alle gegnerischen Wesen ein,Freeze all enemy creatures
|
|
||||||
Ein gegnerisches Wesen zurück auf die Hand,Return an enemy creature to its owner's hand
|
|
||||||
Alle gegnerischen Wesen zurück auf die Hand,Return all enemy creatures to their owners' hands
|
|
||||||
Ein befreundetes Wesen erhält %s,A friendly creature gets %s
|
|
||||||
Alle befreundeten Wesen erhalten %s,All friendly creatures get %s
|
|
||||||
Ein befreundetes Wesen erhält Schild,A friendly creature gets Shield
|
|
||||||
Erhält Schild,Gets Shield
|
|
||||||
Alle befreundeten Wesen erhalten Schild,All friendly creatures get Shield
|
|
||||||
Beim Ausspielen: %s,On play: %s
|
|
||||||
Wächter,Guardian
|
|
||||||
Flink,Swift
|
Flink,Swift
|
||||||
Schild,Shield
|
Schild,Shield
|
||||||
Doppelschlag,Double Strike
|
Doppelschlag,Double Strike
|
||||||
@@ -97,16 +76,6 @@ Feuer,Fire
|
|||||||
Wasser,Water
|
Wasser,Water
|
||||||
Erde,Earth
|
Erde,Earth
|
||||||
Luft,Air
|
Luft,Air
|
||||||
Inferno,Inferno
|
|
||||||
Tiefsee,Deep Sea
|
|
||||||
Berg,Mountain
|
|
||||||
Orkan,Hurricane
|
|
||||||
Dampf,Steam
|
|
||||||
Lava,Lava
|
|
||||||
Blitz,Lightning
|
|
||||||
Schlamm,Mud
|
|
||||||
Sturm,Storm
|
|
||||||
Sand,Sand
|
|
||||||
%s zerlegt (+%d Staub),%s dismantled (+%d dust)
|
%s zerlegt (+%d Staub),%s dismantled (+%d dust)
|
||||||
Kapitel %d,Chapter %d
|
Kapitel %d,Chapter %d
|
||||||
Zuschauen,Watch
|
Zuschauen,Watch
|
||||||
@@ -119,14 +88,6 @@ Zwei eigene Wesen wählen,Pick two of your creatures
|
|||||||
Zweites Wesen wählen,Pick the second creature
|
Zweites Wesen wählen,Pick the second creature
|
||||||
Zwei verschiedene Wesen wählen,Pick two different creatures
|
Zwei verschiedene Wesen wählen,Pick two different creatures
|
||||||
Nur eigene Wesen,Only your own creatures
|
Nur eigene Wesen,Only your own creatures
|
||||||
Vulkan,Volcano
|
|
||||||
Flut,Flood
|
|
||||||
Fels,Rock
|
|
||||||
Wirbel,Vortex
|
|
||||||
Feuerwesen haben +1 Angriff,Fire creatures get +1 attack
|
|
||||||
Wasserwesen haben +1 Angriff,Water creatures get +1 attack
|
|
||||||
Erdwesen haben +1 Angriff,Earth creatures get +1 attack
|
|
||||||
Luftwesen haben +1 Angriff,Air creatures get +1 attack
|
|
||||||
Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d
|
Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d
|
||||||
%s ist jetzt %s,%s is now %s
|
%s ist jetzt %s,%s is now %s
|
||||||
Alle Kapitel abgeschlossen,All chapters completed
|
Alle Kapitel abgeschlossen,All chapters completed
|
||||||
@@ -141,27 +102,53 @@ Schon auf der höchsten Stufe,Already at the top tier
|
|||||||
Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
||||||
Aufgewertet,Upgraded
|
Aufgewertet,Upgraded
|
||||||
+%d Staub,+%d dust
|
+%d Staub,+%d dust
|
||||||
+%d je Zug,+%d per turn
|
|
||||||
Neu würfeln ◆ %d,Reroll ◆ %d
|
|
||||||
Dein Zug,Your turn
|
Dein Zug,Your turn
|
||||||
Beim Ausspielen,On play
|
|
||||||
Nicht genug Mana,Not enough mana
|
Nicht genug Mana,Not enough mana
|
||||||
Kein Angebot,No offer
|
|
||||||
Hand voll,Hand full
|
|
||||||
Profil wirklich zurücksetzen?,Really reset your profile?
|
Profil wirklich zurücksetzen?,Really reset your profile?
|
||||||
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
||||||
Chimäre,Chimera
|
|
||||||
Angriff,attack
|
|
||||||
Eis,Ice
|
|
||||||
Resonanz +%d,Resonance +%d
|
|
||||||
Feld +%d,Field +%d
|
|
||||||
Merkmal +%d,Perk +%d
|
|
||||||
Feldkombo %s mit %s: %s,Field combo %s with %s: %s
|
|
||||||
1 Schaden an das stärkste gegnerische Wesen,1 damage to the strongest enemy creature
|
|
||||||
2 Schaden an den Gegner,2 damage to the opponent
|
|
||||||
Heile dich um 3,Heal yourself for 3
|
|
||||||
Das neue Wesen erhält +1/+1,The new creature gets +1/+1
|
|
||||||
%d Leben,%d life
|
%d Leben,%d life
|
||||||
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win
|
Gegner mit %d Leben · %d Münzen bei Sieg,Opponent with %d life · %d coins for a win
|
||||||
Start gegen die KI,Start against the AI
|
Start gegen die KI,Start against the AI
|
||||||
|
Wähle %d von %d,Pick %d of %d
|
||||||
|
Ziehen,Draw
|
||||||
|
Erst wählen,Pick first
|
||||||
|
Deine Hand ist voll,Your hand is full
|
||||||
|
Keine Wahl offen,Nothing to pick
|
||||||
|
Keine solche Karte,No such card
|
||||||
|
Vorbei,Over
|
||||||
|
Verdeckt legen ◆ %d,Set face-down ◆ %d
|
||||||
|
Aufdecken,Reveal
|
||||||
|
verdeckt,face-down
|
||||||
|
Aufgedeckt!,Revealed!
|
||||||
|
Elementbonus +%d/+%d,Element bonus +%d/+%d
|
||||||
|
Elementbonus %d/%d,Element bonus %d/%d
|
||||||
|
Merkmal +%d,Perk +%d
|
||||||
|
nächster Zug ◆ %d,next turn ◆ %d
|
||||||
|
Erst die Wesen,Creatures first
|
||||||
|
%s liegt verdeckt,%s is face-down
|
||||||
|
%s liegt schon offen,%s is already face-up
|
||||||
|
Unbekannte Eigenschaft,Unknown trait
|
||||||
|
%s hat schon %s,%s already has %s
|
||||||
|
Wesen kommen mit +1 Angriff,Creatures enter with +1 attack
|
||||||
|
Hat im ersten Zug 2 Mana mehr,Has 2 extra mana on the first turn
|
||||||
|
Elementbonus zählt doppelt,Element bonus counts twice
|
||||||
Beginnt mit 5 Leben mehr,Starts with 5 extra life
|
Beginnt mit 5 Leben mehr,Starts with 5 extra life
|
||||||
|
Blockt den ersten Schaden,Blocks the first damage
|
||||||
|
Greift zweimal je Zug,Attacks twice per turn
|
||||||
|
Verdeckt,Face-down
|
||||||
|
Inferno,Inferno
|
||||||
|
Tiefsee,Deep Sea
|
||||||
|
Berg,Mountain
|
||||||
|
Orkan,Hurricane
|
||||||
|
Dampf,Steam
|
||||||
|
Lava,Lava
|
||||||
|
Blitz,Lightning
|
||||||
|
Schlamm,Mud
|
||||||
|
Sturm,Storm
|
||||||
|
Sand,Sand
|
||||||
|
Feuerwesen haben +1 Angriff,Fire creatures have +1 attack
|
||||||
|
Wesen kommen mit +1 Leben,Creatures enter with +1 health
|
||||||
|
Bekommt jeden Zug 1 Mana mehr,Gets 1 extra mana each turn
|
||||||
|
Wesen kommen mit Schild,Creatures enter with a shield
|
||||||
|
Ziehen kostet kein Mana,Drawing costs no mana
|
||||||
|
Eigenschaften kosten 1 weniger,Traits cost 1 less
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user