update
This commit is contained in:
22
README.md
22
README.md
@@ -21,17 +21,17 @@ Der Spielstand liegt unter `user://spielstand.json`
|
||||
## Spielsysteme
|
||||
|
||||
- 77 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
|
||||
- Karten haben nur Element, Angriff und Leben; die Kosten sind Angriff plus Leben, halbiert und aufgerundet
|
||||
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1
|
||||
- Mana: 5 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen
|
||||
- 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
|
||||
- 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
|
||||
- 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)
|
||||
- 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
|
||||
- Angriffe treffen erst Wesen; das Leben ist nur ohne Wesen angreifbar. Schaden über das Leben eines Wesens hinaus schlägt auf den Besitzer durch
|
||||
- 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
|
||||
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, Ziehen kostet 1 Mana und bringt eine; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
|
||||
- Eigenschaften: Spott (2, muss zuerst angegriffen werden), Flink (2, greift sofort an), Regeneration (3, heilt am Zugende ganz) und Doppelschlag (4) lassen sich für Mana auf jede Hand- oder Brettkarte kaufen (Rechtsklick oder Y). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
|
||||
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementboni; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
|
||||
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
|
||||
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
|
||||
- Elementboni: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); drei offene Wesen eines Elements auf dem Tisch geben dem Element +1/+1 (fällt der Bonus weg, sinkt nur das Höchstleben). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni
|
||||
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): 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
|
||||
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar. Nichts schlägt durch
|
||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
|
||||
- Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
|
||||
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
|
||||
@@ -70,7 +70,7 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
||||
## Aufbau
|
||||
|
||||
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
||||
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementregel, Kampf, Verbessern, Verschmelzen
|
||||
- `scripts/game.gd` – Spielregeln: Züge, Mana, Ziehen, verdeckte Wesen, Elementboni, Kampf, Verbessern, Verschmelzen
|
||||
- `scripts/deckgen.gd` – gemeinsamer Stapel und Gegner
|
||||
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||
|
||||
@@ -12,20 +12,22 @@
|
||||
|
||||
## Sammeln
|
||||
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
|
||||
- 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
|
||||
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
|
||||
- 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
|
||||
- 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
|
||||
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
|
||||
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
|
||||
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
|
||||
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten; Ziehen kostet 1 Mana und bringt eine Karte; es kommt nur, was man sich in diesem Zug leisten kann
|
||||
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, mindestens 1), 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 kauft man per Rechtsklick für Mana dazu, auf Hand und Brett: Spott 2 (muss zuerst angegriffen werden), Flink 2 (greift sofort an), Regeneration 3 (heilt sich am Zugende ganz), Doppelschlag 4
|
||||
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
|
||||
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
|
||||
- Leben erst ohne Wesen angreifbar; Wesen mit Spott zuerst; nichts schlägt durch
|
||||
- Kartenvorteil ist Manavorteil: Effekte geben Mana statt Nachziehen
|
||||
|
||||
## Elemente
|
||||
- Jede Karte gehört zu genau einem Element
|
||||
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
|
||||
- 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 (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)
|
||||
- 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 offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
|
||||
- Drei offene Wesen eines Elements auf dem Tisch: +1/+1 für das Element, kein Malus; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
|
||||
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (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)
|
||||
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
|
||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||
|
||||
45
design.md
45
design.md
@@ -8,10 +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
|
||||
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.
|
||||
- **Hearthstones Entdecken** – drei Karten sehen, eine nehmen: Zufall mit Entscheidung. Jede
|
||||
Wahl zeigt an, was zum eigenen Brett passt (Elementbonus als „+1/+1“ oder „−1/−1“).
|
||||
- **Yu-Gi-Oh** – verdeckt gelegte Monster, Angriffe erst auf Monster, Kampfschaden schlägt
|
||||
durch, Fusion.
|
||||
- **Teamfight Tactics** – Synergien durch gleiche Elemente: zwei eigene geben den Effekt des
|
||||
Elements, drei auf dem Tisch den Wertebonus; die Elementleiste zeigt den Stand.
|
||||
- **Yu-Gi-Oh** – verdeckt gelegte Monster, Angriffe erst auf Monster, Fusion durch Übereinander-
|
||||
legen (hier: ziehen und halten).
|
||||
- **Pokémon TCG** – Seltenheit ist Optik: Folienglanz, Funkeln, Schillern. Werte und Symbole
|
||||
statt Sätze.
|
||||
- **Allgemeine Regeln für Kartenspiel-Oberflächen** (GDKeys, Medium): klar getrennte Zonen,
|
||||
@@ -22,12 +22,16 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
|
||||
## Karte
|
||||
|
||||
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
|
||||
gekaufte Eigenschaften als Symbole (Schild, gekreuzte Klingen = Doppelschlag), unten Angriff (gelb) und Leben (rot) als Kreise, dazwischen der Edelstein der
|
||||
Eigenschaften als Symbole (Ausrufezeichen = Spott, Doppelpfeil = Flink, Kreuz = Regeneration, gekreuzte Klingen = Doppelschlag; gekaufte weiß, vom Element in dessen Farbe), unten Angriff (gelb) und Leben (rot) als Kreise, dazwischen der Edelstein der
|
||||
Stufe. Karten haben keinen Text: nur Element, Angriff, Leben.
|
||||
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol.
|
||||
- Die Hand sind bis zu fünf große Karten; Details kommen aus der schwebenden Vorschau.
|
||||
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen;
|
||||
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben.
|
||||
- Seltenheit nach Yu-Gi-Oh: Common matt; Rare silberner Name und Rahmen, stehender Glanzstreifen,
|
||||
Edelstein; Super Rare Holo-Bild (wandernder Regenbogenschimmer über dem Symbol); Ultra Rare
|
||||
goldener Name und Rahmen plus Holo-Bild; Ultimate Rare Relief am Symbol, Holo auf Bild und
|
||||
Rahmen, Funkeln, schillernde Farben. Fusionen tragen die höhere Stufe ihrer Eltern.
|
||||
- Mehrere Elemente: der Hintergrund trägt das erste Element, ein rundes Medaillon hinter dem
|
||||
Symbol zeigt alle Elemente als Tortenstücke.
|
||||
- Verdeckte fremde Wesen sind ein Kartenrücken mit „?“; eigene verdeckte zeigen die Karte mit
|
||||
dem Vermerk „verdeckt“.
|
||||
|
||||
@@ -37,33 +41,34 @@ Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
|
||||
|
||||
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 1“ (Klick zieht), rechts
|
||||
„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
|
||||
die eigene Leiste (Leben, Mana, Einkommen, Zug), rechts „Verschmelzen“ und „Rückgängig“, nur
|
||||
wenn sie gerade möglich sind.
|
||||
- **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.
|
||||
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
|
||||
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über
|
||||
das Y-Menü („Verschmelzen mit …“, dann A auf dem Partner).
|
||||
- **Eigenes Brett** mit fünf sichtbaren Plätzen; links daneben die eigene Leiste (Leben, Mana,
|
||||
Einkommen, Zug), rechts „Rückgängig“, nur wenn es gerade möglich ist.
|
||||
- **Elementleiste** mit drei Punkten je Element: offene Wesen auf dem Tisch; voll heißt +1/+1.
|
||||
Daneben das Symbol des Sondereffekts, sobald zwei eigene Wesen des Elements liegen.
|
||||
- **Mittlere Maustaste** auf eine Handkarte legt sie verdeckt. **Kartenmenü** per Rechtsklick
|
||||
oder Y auf Hand- und Brettkarten: verdeckt legen, aufdecken, Spott, Flink, Regeneration und
|
||||
Doppelschlag kaufen; per Y zusätzlich verschmelzen.
|
||||
- **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
|
||||
macht Ziele, Texte und Balance eindeutig.
|
||||
- **Vorschau** schwebt neben der Karte, auf die man zeigt (über Ladenkarten), mit Resonanz, Feld
|
||||
und Feldkombo darunter.
|
||||
- **Verlauf** und **Aufgeben** als kleine Knöpfe oben links; der Verlauf öffnet als Überlagerung.
|
||||
- Hand- und Wahlkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||
- Handkarten tragen den Elementbonus, den sie offen gelegt bekämen; beim Angreifen
|
||||
steht der Elementkonter (+1/−1) am Ziel, bei Verdeckten nicht.
|
||||
|
||||
## Bewegung und Ton
|
||||
|
||||
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Ton.
|
||||
- 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.
|
||||
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
|
||||
- Aufdecken: „Aufgedeckt!“ am Wesen; Verbessern: die Eigenschaft steigt grün auf.
|
||||
- Aufdecken: „Aufgedeckt!“ am Wesen; Verbessern: die Eigenschaft steigt grün auf; Regeneration am
|
||||
Zugende ebenso.
|
||||
- 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, Stufen ab Rare blitzen in ihrer Farbe auf.
|
||||
- Alles hängt an der Einstellung „Animationen“.
|
||||
|
||||
## Menüs
|
||||
|
||||
@@ -10,10 +10,10 @@ class_name AI
|
||||
|
||||
static func creature_value(c: Game.Creature) -> float:
|
||||
var v := float(c.power() + c.health)
|
||||
if c.shield:
|
||||
v += 1.0
|
||||
if c.attacks_per_turn() == 2:
|
||||
v += c.power() * 0.5
|
||||
if c.has("Regeneration"):
|
||||
v += 1.0
|
||||
return v
|
||||
|
||||
|
||||
@@ -61,26 +61,25 @@ static func evaluate(g: Game, p: Game.Player) -> float:
|
||||
else:
|
||||
score -= threat * 0.3
|
||||
else:
|
||||
score -= threat * 0.12 # Durchschlag: auch hinter Wesen tut Angriffskraft weh
|
||||
score -= threat * 0.12 # auch hinter Wesen tut gegnerische Angriffskraft weh
|
||||
return score
|
||||
|
||||
|
||||
# --- Vorausschau ---------------------------------------------------------------
|
||||
|
||||
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 FOLLOW_UPS := 10 # Folgeschritte je vertieftem Kandidat
|
||||
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
|
||||
|
||||
|
||||
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest; Durchschlag zählt mit.
|
||||
## Rangfolge eines Angriffsziels: sichere Abtausche zuerst, dann der Rest.
|
||||
static func _attack_rank(cr: Game.Creature, t) -> float:
|
||||
if t is Game.Player:
|
||||
return 50.0 - t.life
|
||||
if _kills(cr, t):
|
||||
return (100.0 if not _kills(t, cr) else 60.0) + _value(t) + _overflow(cr, t)
|
||||
return (100.0 if not _kills(t, cr) else 60.0) + _value(t)
|
||||
return -float(t.health)
|
||||
|
||||
|
||||
@@ -98,22 +97,20 @@ static func _top(list: Array, rank: Callable, n: int) -> Array:
|
||||
static func candidate_actions(game: Game, p: Game.Player, prune: bool = false) -> Array:
|
||||
var out := []
|
||||
for card in p.hand:
|
||||
if not game.can_play(p, card)[0]:
|
||||
continue
|
||||
out.append({"type": "play", "card": card, "face_down": false})
|
||||
if allow_hidden and game.forecast(p, card) < 0:
|
||||
out.append({"type": "play", "card": card, "face_down": true})
|
||||
if game.can_play(p, card)[0]:
|
||||
out.append({"type": "play", "card": card, "face_down": false})
|
||||
var has_target := not game.enemies_of(p).is_empty()
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
out.append({"type": "reveal", "creature": cr})
|
||||
elif not cr.hidden and allow_upgrade:
|
||||
if cr.hidden:
|
||||
if game.can_reveal(p, cr)[0]:
|
||||
out.append({"type": "reveal", "creature": cr})
|
||||
elif allow_upgrade and has_target:
|
||||
# Nur Verbesserungen, die sich noch in diesem Zug auszahlen können: Doppelschlag für
|
||||
# ein Wesen, das gleich angreift; Schild nur, wenn kein Wesen mehr spielbar ist.
|
||||
var has_target := not game.enemies_of(p).is_empty()
|
||||
if cr.attacks_left > 0 and has_target and game.can_upgrade(p, cr, "Doppelschlag")[0]:
|
||||
# ein Wesen, das gleich angreift; Flink für eines, das sonst erst nächsten Zug dürfte.
|
||||
if cr.can_attack() and game.can_upgrade(p, cr, "Doppelschlag")[0]:
|
||||
out.append({"type": "upgrade", "creature": cr, "keyword": "Doppelschlag"})
|
||||
if not p.hand.any(func(c): return game.can_play(p, c)[0]) and game.can_upgrade(p, cr, "Schild")[0]:
|
||||
out.append({"type": "upgrade", "creature": cr, "keyword": "Schild"})
|
||||
if not cr.ready and not cr.has("Flink") and cr.power() > 0 and game.can_upgrade(p, cr, "Flink")[0]:
|
||||
out.append({"type": "upgrade", "creature": cr, "keyword": "Flink"})
|
||||
var pairs := []
|
||||
for i in p.board.size():
|
||||
for j in range(i + 1, p.board.size()):
|
||||
@@ -178,8 +175,6 @@ static func map_action(game: Game, copy: Game, action: Dictionary) -> Dictionary
|
||||
static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||
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()
|
||||
@@ -228,18 +223,9 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
static func _kills(attacker, defender) -> bool:
|
||||
if defender is Game.Player:
|
||||
return attacker.power() >= defender.life
|
||||
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:
|
||||
if c is Game.Player:
|
||||
return 1000
|
||||
@@ -257,40 +243,11 @@ static func _best(list: Array, key: Callable):
|
||||
return best
|
||||
|
||||
|
||||
## Wert einer angebotenen Karte für p: Werte je Mana, jetzt spielbar zählt extra, der
|
||||
## Elementbonus und ein Vorteil gegen das gegnerische Brett auch; viel zu teuer zählt ab.
|
||||
static func offer_score(game: Game, p: Game.Player, c: Cards.Card) -> float:
|
||||
var score := float(c.attack + c.health) / maxf(1.0, c.cost) * 2.0 + c.attack + c.health
|
||||
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
|
||||
|
||||
|
||||
## Welche der angebotenen Karten p nimmt.
|
||||
static func pick_index(game: Game, p: Game.Player) -> int:
|
||||
var best := 0
|
||||
var best_score := -INF
|
||||
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
|
||||
|
||||
|
||||
static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if game.winner != "" or game.enemies_of(p).is_empty():
|
||||
return {"type": "end"}
|
||||
if not p.offer.is_empty():
|
||||
return {"type": "pick", "index": pick_index(game, p)}
|
||||
# Gierig: erst das teuerste Spielbare legen, dann aufdecken und angreifen, dann ziehen.
|
||||
# Gierig: erst das teuerste Spielbare legen (bei vollem Feld die zwei schwächsten Wesen
|
||||
# verschmelzen), dann aufdecken und angreifen, dann Doppelschlag kaufen, dann ziehen.
|
||||
var playable := p.hand.filter(func(c): return game.can_play(p, c)[0])
|
||||
if not playable.is_empty():
|
||||
Game.shuffle(playable, game.rng)
|
||||
@@ -299,6 +256,11 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if c.cost > card.cost:
|
||||
card = c
|
||||
return {"type": "play", "card": card, "face_down": false}
|
||||
if p.board.size() >= Game.MAX_BOARD and p.hand.any(func(c): return c.cost <= p.energy - 1):
|
||||
var weakest: Array = p.board.duplicate()
|
||||
weakest.sort_custom(func(a, b): return _value(a) < _value(b))
|
||||
if game.can_fuse(p, weakest[0], weakest[1])[0]:
|
||||
return {"type": "fuse", "creature": weakest[0], "other": weakest[1]}
|
||||
for cr in p.board:
|
||||
if cr.hidden and game.can_reveal(p, cr)[0]:
|
||||
return {"type": "reveal", "creature": cr}
|
||||
@@ -307,6 +269,10 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
var t = pick_attack_target(game, p, cr)
|
||||
if t != null:
|
||||
return {"type": "attack", "creature": cr, "target": t}
|
||||
if not game.enemies_of(p).is_empty():
|
||||
var strongest = _best(p.board.filter(func(c): return not c.hidden and game.can_upgrade(p, c, "Doppelschlag")[0]), func(c): return c.power())
|
||||
if strongest != null:
|
||||
return {"type": "upgrade", "creature": strongest, "keyword": "Doppelschlag"}
|
||||
if game.can_draw(p)[0]:
|
||||
return {"type": "draw"}
|
||||
return {"type": "end"}
|
||||
@@ -337,8 +303,6 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
|
||||
match action.type:
|
||||
"play":
|
||||
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||
"pick":
|
||||
game.pick(p, int(action.index))
|
||||
"draw":
|
||||
game.draw(p)
|
||||
"reveal":
|
||||
|
||||
@@ -10,7 +10,7 @@ const STAGES := [
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "life": 24, "reward": 200, "perk": "wesen_leben"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "life": 26, "reward": 200, "perk": "extra_mana"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "life": 26, "reward": 250, "perk": "element_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "schild_wesen"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "life": 26, "reward": 250, "perk": "alle_flink"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 300, "perk": "verbessern_billig"},
|
||||
{"name": "Archimagier", "title": "Meister der Elemente", "life": 28, "reward": 500, "perk": "start_energie"},
|
||||
]
|
||||
@@ -20,12 +20,12 @@ const STAGES := [
|
||||
const STAGES2 := [
|
||||
{"name": "Fenn", "title": "Flammenzähmer", "life": 22, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||
{"name": "Mara", "title": "Herrin der Gezeiten", "life": 22, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "schild_wesen"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "life": 24, "reward": 350, "perks": ["wesen_angriff", "alle_flink"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "life": 24, "reward": 350, "perks": ["ziehen_gratis", "verbessern_billig"]},
|
||||
{"name": "Vulkar", "title": "Aschekönig", "life": 24, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
||||
{"name": "Nyx", "title": "Gewitterruferin", "life": 26, "reward": 400, "perks": ["extra_mana", "ziehen_gratis"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "life": 26, "reward": 450, "perks": ["element_doppelt", "wesen_leben"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "life": 26, "reward": 450, "perks": ["alle_flink", "leben_35"]},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "life": 28, "reward": 500, "perks": ["verbessern_billig", "start_energie"]},
|
||||
{"name": "Archimagier", "title": "Herr aller Elemente", "life": 28, "reward": 800, "perks": ["start_energie", "element_doppelt"]},
|
||||
]
|
||||
@@ -34,13 +34,13 @@ const STAGES2 := [
|
||||
## Kapitel 3: drei Merkmale und volle Tische.
|
||||
const STAGES3 := [
|
||||
{"name": "Fenn", "title": "Aschefürst", "life": 20, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
||||
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
||||
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "schild_wesen", "leben_35"]},
|
||||
{"name": "Mara", "title": "Sturmflut", "life": 20, "reward": 600, "perks": ["leben_35", "extra_mana", "alle_flink"]},
|
||||
{"name": "Borin", "title": "Berg selbst", "life": 22, "reward": 700, "perks": ["wesen_angriff", "alle_flink", "leben_35"]},
|
||||
{"name": "Liska", "title": "Windbraut", "life": 22, "reward": 700, "perks": ["ziehen_gratis", "verbessern_billig", "extra_mana"]},
|
||||
{"name": "Vulkar", "title": "Magmakaiser", "life": 22, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "wesen_angriff"]},
|
||||
{"name": "Nyx", "title": "Orkanmutter", "life": 24, "reward": 800, "perks": ["extra_mana", "ziehen_gratis", "element_doppelt"]},
|
||||
{"name": "Ignis", "title": "Sonnenpriester", "life": 24, "reward": 900, "perks": ["element_doppelt", "wesen_leben", "start_energie"]},
|
||||
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["schild_wesen", "leben_35", "element_doppelt"]},
|
||||
{"name": "Thalassa", "title": "Herz der Tiefe", "life": 24, "reward": 900, "perks": ["alle_flink", "leben_35", "element_doppelt"]},
|
||||
{"name": "Zephyra", "title": "Gewitterkaiserin", "life": 26, "reward": 1000, "perks": ["verbessern_billig", "start_energie", "wesen_leben"]},
|
||||
{"name": "Archimagier", "title": "Ewiger Meister", "life": 26, "reward": 1200, "perks": ["start_energie", "element_doppelt", "leben_35"]},
|
||||
]
|
||||
|
||||
@@ -24,18 +24,27 @@ const COMBOS := {
|
||||
# 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"}
|
||||
# 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"]
|
||||
# Stufen wie bei Yu-Gi-Oh: Common grau, Rare silber, Super Rare blau, Ultra Rare gold,
|
||||
# Ultimate Rare schillert durch alle Farben.
|
||||
const RARITIES: Array[String] = ["Common", "Rare", "Super Rare", "Ultra Rare", "Ultimate Rare"]
|
||||
const RARITY_COLORS := {
|
||||
"gewöhnlich": Color("aaaaaa"),
|
||||
"selten": Color("4682e6"),
|
||||
"episch": Color("aa46dc"),
|
||||
"legendär": Color("f0aa28"),
|
||||
"mythisch": Color("ff5a8c"),
|
||||
"Common": Color("aaaaaa"),
|
||||
"Rare": Color("c8d0dc"),
|
||||
"Super Rare": Color("4682e6"),
|
||||
"Ultra Rare": Color("f0aa28"),
|
||||
"Ultimate Rare": Color("ff5a8c"),
|
||||
}
|
||||
# Schlüsselwörter haben Karten nicht von Haus aus; sie werden für Mana dazugekauft (Game.upgrade).
|
||||
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"}
|
||||
# Eigenschaften haben Karten nicht von Haus aus; sie werden für Mana dazugekauft (Game.upgrade)
|
||||
# oder kommen vom Elementpaar der eigenen Seite (ELEMENT_KEYWORD, siehe Game.Creature.has).
|
||||
const KEYWORDS: Array[String] = ["Spott", "Flink", "Regeneration", "Doppelschlag"]
|
||||
const KEYWORD_COST := {"Spott": 2, "Flink": 2, "Regeneration": 3, "Doppelschlag": 4}
|
||||
const KEYWORD_TEXT := {
|
||||
"Spott": "Muss zuerst angegriffen werden",
|
||||
"Flink": "Greift sofort an",
|
||||
"Regeneration": "Heilt sich am Zugende ganz",
|
||||
"Doppelschlag": "Greift zweimal je Zug",
|
||||
}
|
||||
const ELEMENT_KEYWORD := {"Erde": "Spott", "Luft": "Flink", "Wasser": "Regeneration", "Feuer": "Doppelschlag"}
|
||||
const DATA_PATH := "res://data/karten.json"
|
||||
|
||||
|
||||
@@ -85,17 +94,16 @@ static func best_element(own: Array, other: Array) -> String:
|
||||
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.
|
||||
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1.
|
||||
static func cost_of(attack: int, health: int) -> int:
|
||||
return ceili((attack + health) / 2.0)
|
||||
return maxi(1, (2 * attack + health) / 3)
|
||||
|
||||
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren,
|
||||
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
||||
## als das stärkere Wesen, und jeder Partner ab 1 bringt mindestens +1.
|
||||
static func fusion_value(a: int, b: int) -> int:
|
||||
return maxi(a, b) + ceili(mini(a, b) / 3.0)
|
||||
|
||||
|
||||
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
||||
@@ -103,9 +111,13 @@ static func fusion_name(elements: Array) -> String:
|
||||
return combo_name(elements if elements.size() >= 2 else [elements[0], elements[0]])
|
||||
|
||||
|
||||
## Karte des verschmolzenen Wesens: Elemente und Schlüsselwörter beider Karten, die Werte
|
||||
## bestimmt Game.fuse, eine Verschmelzung mehr als der erfahrenere Elternteil.
|
||||
static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
|
||||
## Karte des verschmolzenen Wesens: Elemente und Eigenschaften beider Karten, Werte nach
|
||||
## fusion_value (oder die übergebenen), eine Verschmelzung mehr als der erfahrenere Elternteil.
|
||||
static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
||||
if attack < 0:
|
||||
attack = fusion_value(a.attack, b.attack)
|
||||
if health < 0:
|
||||
health = fusion_value(a.health, b.health)
|
||||
var els := []
|
||||
for e in a.elements + b.elements:
|
||||
if not (e in els):
|
||||
@@ -122,6 +134,7 @@ static func fused(a: Card, b: Card, attack: int, health: int) -> Card:
|
||||
if k in a.keywords or k in b.keywords:
|
||||
c.keywords.append(k)
|
||||
c.fusions = maxi(a.fusions, b.fusions) + 1
|
||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt; Game.fuse rechnet die Sammlung ein
|
||||
return c
|
||||
|
||||
|
||||
@@ -145,9 +158,10 @@ class Card extends RefCounted:
|
||||
var elements: Array
|
||||
var attack: int = 0
|
||||
var health: int = 0
|
||||
var cost: int = 0 # abgeleitet: (Angriff + Leben) / 2, aufgerundet
|
||||
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, mindestens 1
|
||||
var keywords: Array = [] # dazugekauft, je Exemplar
|
||||
var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …)
|
||||
var look := -1 # Seltenheitsstufe verschmolzener Karten (die höhere der Eltern); -1: aus der Sammlung des Besitzers
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
||||
@@ -155,8 +169,14 @@ class Card extends RefCounted:
|
||||
d["keywords"] = keywords
|
||||
if fusions > 0:
|
||||
d["fusions"] = fusions
|
||||
if look >= 0:
|
||||
d["look"] = look
|
||||
return d
|
||||
|
||||
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
|
||||
func rarity_in(looks: Dictionary) -> String:
|
||||
return Cards.RARITIES[look if look >= 0 else int(looks.get(id, 0))]
|
||||
|
||||
static func from_dict(d: Dictionary) -> Card:
|
||||
var c := Card.new()
|
||||
c.id = d.id
|
||||
@@ -168,6 +188,7 @@ class Card extends RefCounted:
|
||||
c.cost = Cards.cost_of(c.attack, c.health)
|
||||
c.keywords = Array(d.get("keywords", []))
|
||||
c.fusions = int(d.get("fusions", 0))
|
||||
c.look = int(d.get("look", -1))
|
||||
return c
|
||||
|
||||
## Eigenes Exemplar (Hand, Brett): Schlüsselwörter gehören dem Exemplar, nicht dem Pool.
|
||||
@@ -190,7 +211,7 @@ class Card extends RefCounted:
|
||||
func fusion_cost() -> int:
|
||||
return 1 << fusions
|
||||
|
||||
## Kartentext: die gekauften Schlüsselwörter.
|
||||
## Kartentext: die gekauften Eigenschaften.
|
||||
func text() -> String:
|
||||
if keywords.is_empty():
|
||||
return ""
|
||||
|
||||
287
scripts/game.gd
287
scripts/game.gd
@@ -1,17 +1,16 @@
|
||||
class_name Game
|
||||
extends RefCounted
|
||||
## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen,
|
||||
## Vier-Elemente-Regel, Verschmelzen und Verbessern.
|
||||
## Elementboni, Verschmelzen und Verbessern.
|
||||
|
||||
const MAX_BOARD := 5
|
||||
const MAX_HAND := 5
|
||||
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||
const DRAW_COST := 1 # Ziehen: drei Karten sehen, eine behalten
|
||||
const OFFER_SIZE := 3
|
||||
const START_OFFER := 5 # zu Beginn fünf sehen, drei behalten
|
||||
const START_PICKS := 3
|
||||
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
||||
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||
const DRAW_COST := 1 # Ziehen: eine bezahlbare Karte vom Stapel
|
||||
const START_LIFE := 30
|
||||
const ELEMENT_STACK := 4 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1, das geschlagene Element -1/-1
|
||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||
const ELEMENT_STACK := 3 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
const PERKS := {
|
||||
@@ -21,7 +20,7 @@ const PERKS := {
|
||||
"wesen_leben": "Wesen kommen mit +1 Leben",
|
||||
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
||||
"leben_35": "Beginnt mit 5 Leben mehr",
|
||||
"schild_wesen": "Wesen kommen mit Schild",
|
||||
"alle_flink": "Wesen greifen sofort an",
|
||||
"verbessern_billig": "Eigenschaften kosten 1 weniger",
|
||||
"start_energie": "Hat im ersten Zug 2 Mana mehr",
|
||||
"element_doppelt": "Elementbonus zählt doppelt",
|
||||
@@ -49,10 +48,10 @@ class Creature extends RefCounted:
|
||||
var attack: int
|
||||
var health: int
|
||||
var max_health: int
|
||||
var shield: bool
|
||||
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)
|
||||
var attacks_done := 0 # Angriffe in diesem Zug
|
||||
var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden
|
||||
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementboni
|
||||
var aura := 0 # Elementbonus (+1 je Element mit vollem Stapel); das Leben ist schon angepasst
|
||||
|
||||
func _init(c: Cards.Card, o, id: int) -> void:
|
||||
card = c
|
||||
@@ -62,12 +61,35 @@ class Creature extends RefCounted:
|
||||
attack = c.attack
|
||||
health = c.health
|
||||
max_health = c.health
|
||||
shield = "Schild" in c.keywords
|
||||
attacks_left = attacks_per_turn() # frisch gelegte Wesen greifen sofort an
|
||||
|
||||
func owner():
|
||||
return owner_ref.get_ref()
|
||||
|
||||
## Eigenschaft: gekauft, als Sondereffekt des Elementpaars der eigenen Seite oder als
|
||||
## Boss-Merkmal. Verdeckte Wesen haben keine.
|
||||
func has(kw: String) -> bool:
|
||||
if hidden:
|
||||
return false
|
||||
if kw in card.keywords:
|
||||
return true
|
||||
var o = owner()
|
||||
if o == null:
|
||||
return false
|
||||
if kw == "Flink" and "alle_flink" in o.perks:
|
||||
return true
|
||||
for e in card.elements:
|
||||
if Cards.ELEMENT_KEYWORD.get(e, "") == kw and o.element_count(e) >= Game.ELEMENT_PAIR:
|
||||
return true
|
||||
return false
|
||||
|
||||
## Alle wirksamen Eigenschaften in fester Reihenfolge.
|
||||
func keywords() -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if has(k):
|
||||
out.append(k)
|
||||
return out
|
||||
|
||||
## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
|
||||
func power() -> int:
|
||||
var v := attack + aura
|
||||
@@ -78,18 +100,22 @@ class Creature extends RefCounted:
|
||||
return v
|
||||
|
||||
func attacks_per_turn() -> int:
|
||||
return 2 if "Doppelschlag" in card.keywords else 1
|
||||
return 2 if has("Doppelschlag") else 1
|
||||
|
||||
func attacks_left() -> int:
|
||||
return maxi(0, attacks_per_turn() - attacks_done)
|
||||
|
||||
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
|
||||
func can_attack() -> bool:
|
||||
return attacks_left > 0 and not hidden and power() > 0
|
||||
return not hidden and attacks_left() > 0 and power() > 0 and (ready or has("Flink"))
|
||||
|
||||
func clone(new_owner) -> Creature:
|
||||
var c := Creature.new(card, new_owner, uid)
|
||||
c.attack = attack
|
||||
c.health = health
|
||||
c.max_health = max_health
|
||||
c.shield = shield
|
||||
c.attacks_left = attacks_left
|
||||
c.attacks_done = attacks_done
|
||||
c.ready = ready
|
||||
c.hidden = hidden
|
||||
c.aura = aura
|
||||
return c
|
||||
@@ -101,8 +127,6 @@ class Player extends RefCounted:
|
||||
var is_human: bool
|
||||
var life: int
|
||||
var max_life: int
|
||||
var offer: Array = [] # offene Wahl: angebotene Karten (Start: 5, Ziehen: 3)
|
||||
var picks_left := 0 # wie viele davon noch zu nehmen sind
|
||||
var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
|
||||
var board: Array = []
|
||||
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
||||
@@ -123,11 +147,17 @@ class Player extends RefCounted:
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
|
||||
## Offene eigene Wesen mit diesem Element.
|
||||
func element_count(e: String) -> int:
|
||||
var n := 0
|
||||
for c in board:
|
||||
if not c.hidden and e in c.card.elements:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func clone() -> Player:
|
||||
var p := Player.new(name, is_human, life, title)
|
||||
p.max_life = max_life
|
||||
p.offer = offer.duplicate()
|
||||
p.picks_left = picks_left
|
||||
p.hand = hand.duplicate()
|
||||
p.energy = energy
|
||||
p.max_energy = max_energy
|
||||
@@ -235,7 +265,6 @@ func masked_clone(viewer: Player) -> Game:
|
||||
c.attack = c.card.attack
|
||||
c.health = c.card.health
|
||||
c.max_health = c.card.health
|
||||
c.shield = false
|
||||
return g
|
||||
|
||||
|
||||
@@ -250,8 +279,8 @@ func start(fixed_order: bool = false) -> void:
|
||||
p.order = i
|
||||
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
|
||||
p.start_bonus = 0 if i == 0 else 1
|
||||
p.offer = draw_cards(START_OFFER, START_MANA + p.start_bonus)
|
||||
p.picks_left = START_PICKS
|
||||
for c in draw_cards(START_HAND, START_MANA + p.start_bonus):
|
||||
p.hand.append(c.copy())
|
||||
turn = 1
|
||||
current_idx = 0
|
||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||
@@ -282,17 +311,10 @@ func draw_cards(n: int, max_cost: int) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Solange eine Wahl offen ist, geht nichts anderes.
|
||||
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).
|
||||
## Ziehen: kostet DRAW_COST, eine Karte kommt auf die Hand.
|
||||
func can_draw(p: Player) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if blocked(p) != "":
|
||||
return [false, blocked(p)]
|
||||
if p.hand.size() >= MAX_HAND:
|
||||
return [false, "Deine Hand ist voll"]
|
||||
if p.energy < draw_cost(p):
|
||||
@@ -315,36 +337,13 @@ func draw(p: Player) -> String:
|
||||
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
|
||||
var card: Cards.Card = draw_cards(1, p.max_energy)[0]
|
||||
p.hand.append(card.copy())
|
||||
say("%s zieht." % p.name)
|
||||
emit({"type": "draw", "player": players.find(p)})
|
||||
return ""
|
||||
|
||||
|
||||
## Nimmt eine der angebotenen Karten auf die Hand; jederzeit erlaubt, auch außerhalb des Zugs.
|
||||
## Was liegen bleibt, wandert unter den Stapel.
|
||||
func pick(p: Player, index: int) -> String:
|
||||
if winner != "":
|
||||
return "Vorbei"
|
||||
if p.offer.is_empty():
|
||||
return "Keine Wahl offen"
|
||||
if index < 0 or index >= p.offer.size():
|
||||
return "Keine solche Karte"
|
||||
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 ""
|
||||
|
||||
|
||||
func begin_turn(p: Player) -> void:
|
||||
p.max_energy = START_MANA + p.turns
|
||||
p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen
|
||||
@@ -355,18 +354,22 @@ func begin_turn(p: Player) -> void:
|
||||
say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus])
|
||||
p.start_bonus = 0
|
||||
for c in p.board:
|
||||
c.health = c.max_health # Wesen erholen sich zu Zugbeginn ganz
|
||||
c.attacks_left = c.attacks_per_turn()
|
||||
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
||||
c.attacks_done = 0
|
||||
p.turns += 1
|
||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||
emit({"type": "turn", "player": players.find(p)})
|
||||
|
||||
|
||||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||||
func end_turn() -> void:
|
||||
if winner != "":
|
||||
return
|
||||
while not current().offer.is_empty(): # offene Wahl: die erste Karte, damit nichts verfällt
|
||||
pick(current(), 0)
|
||||
for c in current().board:
|
||||
if c.has("Regeneration") and c.health < c.max_health:
|
||||
c.health = c.max_health
|
||||
say("%s regeneriert sich." % c.name)
|
||||
emit({"type": "heal", "uid": c.uid})
|
||||
var n := players.size()
|
||||
for i in range(1, n + 1):
|
||||
var idx := (current_idx + i) % n
|
||||
@@ -384,8 +387,6 @@ func end_turn() -> void:
|
||||
func can_play(p: Player, card: Cards.Card) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if blocked(p) != "":
|
||||
return [false, blocked(p)]
|
||||
if not (card in p.hand):
|
||||
return [false, "Nicht auf der Hand"]
|
||||
if p.board.size() >= MAX_BOARD:
|
||||
@@ -395,7 +396,8 @@ func can_play(p: Player, card: Cards.Card) -> Array:
|
||||
return [true, ""]
|
||||
|
||||
|
||||
## Spielt eine Karte, offen oder verdeckt. Gibt "" zurück oder eine Fehlermeldung.
|
||||
## Spielt eine Karte, offen oder verdeckt. Das Wesen greift erst im nächsten Zug an (außer flink).
|
||||
## Gibt "" zurück oder eine Fehlermeldung.
|
||||
func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
var ok := can_play(p, card)
|
||||
if not ok[0]:
|
||||
@@ -410,8 +412,6 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
if "wesen_leben" in p.perks:
|
||||
cr.health += 1
|
||||
cr.max_health += 1
|
||||
if "schild_wesen" in p.perks:
|
||||
cr.shield = true
|
||||
p.board.append(cr)
|
||||
if face_down:
|
||||
say("%s legt ein Wesen verdeckt." % p.name)
|
||||
@@ -424,16 +424,17 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug.
|
||||
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
|
||||
## nach dem Legen. Aufgedeckt greift es sofort an.
|
||||
func can_reveal(p: Player, cr: Creature) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if blocked(p) != "":
|
||||
return [false, blocked(p)]
|
||||
if cr == null or not (cr in p.board):
|
||||
return [false, "Nur eigene Wesen"]
|
||||
if not cr.hidden:
|
||||
return [false, "%s liegt schon offen" % cr.name]
|
||||
if not cr.ready:
|
||||
return [false, "%s kann erst im nächsten Zug aufgedeckt werden" % cr.name]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
@@ -447,7 +448,7 @@ func reveal(p: Player, cr: Creature) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Deckt ein Wesen auf; die Elementregel gilt sofort.
|
||||
## Deckt ein Wesen auf; die Elementboni gelten sofort.
|
||||
func _flip(cr: Creature) -> void:
|
||||
cr.hidden = false
|
||||
say("%s wird aufgedeckt." % cr.name)
|
||||
@@ -455,14 +456,12 @@ func _flip(cr: Creature) -> void:
|
||||
apply_auras()
|
||||
|
||||
|
||||
# --- Verbessern: Schlüsselwörter für Mana -----------------------------------
|
||||
# --- Verbessern: Eigenschaften 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
|
||||
@@ -489,12 +488,6 @@ func upgrade(p: Player, target, keyword: String) -> String:
|
||||
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 ""
|
||||
@@ -502,40 +495,69 @@ func upgrade(p: Player, target, keyword: String) -> String:
|
||||
|
||||
# --- 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)
|
||||
static func card_of(x) -> Cards.Card:
|
||||
return x.card if x is Creature else x
|
||||
|
||||
|
||||
func can_fuse(p: Player, a: Creature, b: Creature) -> Array:
|
||||
## Seltenheitsstufe einer Karte von p: bei verschmolzenen die geerbte, sonst die der Sammlung.
|
||||
static func look_of(p: Player, c: Cards.Card) -> int:
|
||||
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
||||
|
||||
|
||||
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einer der beiden steckt.
|
||||
static func fusion_cost(a, b) -> int:
|
||||
return 1 << maxi(card_of(a).fusions, card_of(b).fusions)
|
||||
|
||||
|
||||
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
|
||||
func can_fuse(p: Player, a, b) -> 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"]
|
||||
return [false, "Zwei verschiedene Karten wählen"]
|
||||
if a is Creature and b is Creature:
|
||||
if not (a in p.board) or not (b in p.board):
|
||||
return [false, "Nur eigene Wesen"]
|
||||
elif a is Creature or b is Creature:
|
||||
return [false, "Nur Hand mit Hand oder Brett mit Brett"]
|
||||
elif not (a in p.hand) or not (b in p.hand):
|
||||
return [false, "Nicht auf der Hand"]
|
||||
if p.energy < fusion_cost(a, b):
|
||||
return [false, "Nicht genug Mana"]
|
||||
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:
|
||||
## Verschmilzt zwei Handkarten zu einer oder zwei eigene Wesen zu einem offenen: Elemente und
|
||||
## Eigenschaften beider, Werte nach Cards.fusion_value, Kosten folgen den Werten.
|
||||
func fuse(p: Player, a, b) -> String:
|
||||
var ok := can_fuse(p, a, b)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
p.energy -= fusion_cost(a, b)
|
||||
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)
|
||||
if a is Creature:
|
||||
return _fuse_board(p, a, b)
|
||||
var card := Cards.fused(a, b)
|
||||
card.look = maxi(look_of(p, a), look_of(p, b))
|
||||
var at := p.hand.find(a)
|
||||
p.hand.erase(a)
|
||||
p.hand.erase(b)
|
||||
p.hand.insert(mini(at, p.hand.size()), card)
|
||||
say("%s und %s verschmelzen auf der Hand zu %s (%d/%d)." % [a.name, b.name, card.name, card.attack, card.health])
|
||||
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": []})
|
||||
return ""
|
||||
|
||||
|
||||
## Auf dem Brett zählen die Werte der Wesen (ohne Elementbonus, den bekommt das neue selbst);
|
||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||
var cr := Creature.new(card, p, next_uid)
|
||||
next_uid += 1
|
||||
cr.health = maxi(a.health - a.aura, b.health - b.aura) + 1
|
||||
cr.shield = cr.shield or a.shield or b.shield
|
||||
cr.attacks_left = mini(a.attacks_left, b.attacks_left)
|
||||
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
||||
cr.ready = a.ready and b.ready
|
||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||
var at := p.board.find(a)
|
||||
p.board.erase(a)
|
||||
p.board.erase(b)
|
||||
@@ -548,7 +570,7 @@ func fuse(p: Player, a: Creature, b: Creature) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# --- Vier-Elemente-Regel -----------------------------------------------------
|
||||
# --- Elementboni ---------------------------------------------------------------
|
||||
|
||||
## Offene Wesen je Element auf dem ganzen Tisch.
|
||||
func element_counts() -> Dictionary:
|
||||
@@ -563,24 +585,16 @@ func element_counts() -> Dictionary:
|
||||
return counts
|
||||
|
||||
|
||||
## Bonus aus der Elementregel für diese Elemente: +1, wenn ELEMENT_STACK offene Wesen des
|
||||
## Elements stehen, -1, wenn das Element, das es schlägt, den Stapel voll hat.
|
||||
## Mehrere Elemente nehmen den besten Wert.
|
||||
## +1 je Element mit ELEMENT_STACK offenen Wesen auf dem Tisch; mehrere Elemente zählen alle.
|
||||
static func aura_of(elements: Array, counts: Dictionary) -> int:
|
||||
var best := 0
|
||||
for i in elements.size():
|
||||
var e: String = elements[i]
|
||||
var a := 0
|
||||
var a := 0
|
||||
for e in elements:
|
||||
if int(counts[e]) >= ELEMENT_STACK:
|
||||
a += 1
|
||||
var predator := Cards.beaten_by(e)
|
||||
if predator != "" and int(counts[predator]) >= ELEMENT_STACK:
|
||||
a -= 1
|
||||
best = a if i == 0 else maxi(best, a)
|
||||
return best
|
||||
return a
|
||||
|
||||
|
||||
## Bonus eines offenen Wesens aus der Elementregel; verdeckte zählen nicht.
|
||||
## Bonus eines offenen Wesens; verdeckte zählen nicht und bekommen nichts.
|
||||
func aura_for(c: Creature, counts: Dictionary) -> int:
|
||||
if c.hidden:
|
||||
return 0
|
||||
@@ -591,7 +605,8 @@ func aura_for(c: Creature, counts: Dictionary) -> int:
|
||||
return a
|
||||
|
||||
|
||||
## Rechnet die Elementregel neu; Leben folgt dem Bonus direkt (+1/+1 bzw. -1/-1).
|
||||
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er,
|
||||
## sinkt nur das Höchstleben, das Leben wird höchstens darauf gekappt.
|
||||
func apply_auras() -> void:
|
||||
var counts := element_counts()
|
||||
for p in players:
|
||||
@@ -599,8 +614,8 @@ func apply_auras() -> void:
|
||||
var a := aura_for(c, counts)
|
||||
if a != c.aura:
|
||||
var delta: int = a - c.aura
|
||||
c.health += delta
|
||||
c.max_health += delta
|
||||
c.health = mini(c.health + maxi(delta, 0), c.max_health)
|
||||
c.aura = a
|
||||
|
||||
|
||||
@@ -614,14 +629,15 @@ func forecast(p: Player, card: Cards.Card) -> int:
|
||||
|
||||
# --- Kampf -------------------------------------------------------------------
|
||||
|
||||
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat.
|
||||
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott
|
||||
## müssen vor den anderen angegriffen werden.
|
||||
func can_attack(cr: Creature, target) -> Array:
|
||||
if winner != "" or cr.owner() != current():
|
||||
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.ready and not cr.has("Flink") and cr.attacks_left() > 0 and cr.power() > 0:
|
||||
return [false, "%s greift erst im nächsten Zug an" % cr.name]
|
||||
if not cr.can_attack():
|
||||
return [false, "%s kann nicht angreifen" % cr.name]
|
||||
var tgt_owner = target.owner() if target is Creature else target
|
||||
@@ -629,40 +645,24 @@ func can_attack(cr: Creature, target) -> Array:
|
||||
return [false, "Kein gültiges Ziel"]
|
||||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||
return [false, "Erst die Wesen"]
|
||||
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
|
||||
return [false, "Erst die Wesen mit Spott"]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
## 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.
|
||||
## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func attack(cr: Creature, target) -> String:
|
||||
var ok := can_attack(cr, target)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
cr.attacks_left -= 1
|
||||
cr.attacks_done += 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])
|
||||
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
||||
var dealt := cr.power() + matchup_bonus(cr, target)
|
||||
if target is Creature:
|
||||
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)
|
||||
damage(target, cr.power() + matchup_bonus(cr, target))
|
||||
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil
|
||||
damage(cr, target.power())
|
||||
cleanup()
|
||||
check_end()
|
||||
return ""
|
||||
@@ -680,11 +680,6 @@ func damage(target, amount: int) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
if target is Creature:
|
||||
if target.shield:
|
||||
target.shield = false
|
||||
say("Schild von %s blockt den Schaden." % target.name)
|
||||
emit({"type": "blocked", "target": ref_of(target)})
|
||||
return
|
||||
target.health -= amount
|
||||
else:
|
||||
target.life -= amount
|
||||
@@ -692,7 +687,7 @@ func damage(target, amount: int) -> void:
|
||||
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.
|
||||
## Räumt Tote ab; danach gelten die Elementboni neu, was weitere Tote nach sich ziehen kann.
|
||||
func cleanup() -> void:
|
||||
for round in 8:
|
||||
var died := false
|
||||
|
||||
@@ -386,10 +386,6 @@ func _apply(id: int, action: Dictionary) -> void:
|
||||
var p: Game.Player = game.players[seats[id]]
|
||||
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
|
||||
match str(action.get("type", "")):
|
||||
@@ -415,9 +411,14 @@ func _apply(id: int, action: Dictionary) -> void:
|
||||
game.attack(cr, t)
|
||||
"draw":
|
||||
game.draw(p)
|
||||
"fuse":
|
||||
var a := game.find_creature(int(action.get("uid", -1)))
|
||||
var b := game.find_creature(int(action.get("other", -1)))
|
||||
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
|
||||
var a = game.find_creature(int(action.get("uid", -1)))
|
||||
var b = game.find_creature(int(action.get("other", -1)))
|
||||
var i := int(action.get("hand", -1))
|
||||
var j := int(action.get("other_hand", -1))
|
||||
if a == null and i >= 0 and j >= 0 and i < p.hand.size() and j < p.hand.size():
|
||||
a = p.hand[i]
|
||||
b = p.hand[j]
|
||||
if a != null and b != null:
|
||||
game.fuse(p, a, b)
|
||||
"end":
|
||||
|
||||
@@ -11,7 +11,7 @@ static func card_of(ref) -> Cards.Card:
|
||||
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
|
||||
## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde
|
||||
## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer.
|
||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||
var ps := []
|
||||
@@ -23,14 +23,12 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||
var masked: bool = c.hidden and not mine
|
||||
board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card),
|
||||
"attack": 0 if masked else c.attack, "health": 0 if masked else c.health,
|
||||
"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})
|
||||
"max_health": 0 if masked else c.max_health, "attacks_done": c.attacks_done,
|
||||
"ready": c.ready, "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,
|
||||
"energy": p.energy, "max_energy": p.max_energy, "start_bonus": p.start_bonus,
|
||||
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns,
|
||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(),
|
||||
"offer": p.offer.map(func(c): return c.id) if mine else [], "picks_left": p.picks_left,
|
||||
"board": board})
|
||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board})
|
||||
var evs := []
|
||||
for ev in g.events:
|
||||
var e: Dictionary = ev.duplicate()
|
||||
@@ -64,17 +62,14 @@ static func from_snapshot(d: Dictionary) -> Game:
|
||||
else:
|
||||
for i in int(pd.hand):
|
||||
p.hand.append(placeholder)
|
||||
for cid in pd.get("offer", []):
|
||||
p.offer.append(Cards.by_id(cid))
|
||||
p.picks_left = int(pd.get("picks_left", 0))
|
||||
for cd in pd.board:
|
||||
var card := card_of(cd.card)
|
||||
var c := Game.Creature.new(card, p, int(cd.uid))
|
||||
c.attack = int(cd.attack)
|
||||
c.health = int(cd.health)
|
||||
c.max_health = int(cd.max_health)
|
||||
c.shield = bool(cd.shield)
|
||||
c.attacks_left = int(cd.attacks_left)
|
||||
c.attacks_done = int(cd.get("attacks_done", 0))
|
||||
c.ready = bool(cd.get("ready", false))
|
||||
c.hidden = bool(cd.get("hidden", false))
|
||||
c.aura = int(cd.get("aura", 0))
|
||||
p.board.append(c)
|
||||
|
||||
@@ -8,7 +8,7 @@ const SAVE_VERSION := 6
|
||||
const SAVE_PATH := "user://spielstand.json"
|
||||
const BOOSTER_COST := 100
|
||||
const BOOSTER_SIZE := 5
|
||||
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
||||
const RARITY_WEIGHTS := {"Common": 50.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 8.0, "Ultimate Rare": 2.0}
|
||||
const START_COINS := 150
|
||||
const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
@@ -19,12 +19,12 @@ const DIFFICULTIES := {
|
||||
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
|
||||
}
|
||||
# Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub.
|
||||
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600}
|
||||
const DUST_UPGRADE := {"selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200}
|
||||
const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
|
||||
const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000}
|
||||
|
||||
var coins := 0
|
||||
var dust := 0
|
||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = gewöhnlich
|
||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
||||
@@ -116,7 +116,7 @@ func looks() -> Dictionary:
|
||||
return out
|
||||
|
||||
|
||||
## Anzahl Karten über der gewöhnlichen Stufe.
|
||||
## Anzahl Karten über Common.
|
||||
func upgraded_count() -> int:
|
||||
var n := 0
|
||||
for cid in collection:
|
||||
@@ -142,7 +142,7 @@ func upgrade(cid: String) -> String:
|
||||
func dismantle(cid: String) -> String:
|
||||
var lvl := level(cid)
|
||||
if lvl == 0:
|
||||
return "Gewöhnliche Karten lassen sich nicht zerlegen"
|
||||
return "Common-Karten lassen sich nicht zerlegen"
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
@@ -158,8 +158,8 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
var out := []
|
||||
for i in BOOSTER_SIZE:
|
||||
var rar: String = rarities[r.rand_weighted(weights)]
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "gewöhnlich"):
|
||||
rar = "selten" # mindestens eine seltene Karte pro Booster
|
||||
if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"):
|
||||
rar = "Rare" # mindestens eine Rare pro Booster
|
||||
out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar})
|
||||
return out
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
"damage":
|
||||
sfx("damage")
|
||||
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
|
||||
"blocked":
|
||||
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay)
|
||||
"heal":
|
||||
sfx("heal")
|
||||
popup(tr("Regeneration"), Style.GREEN, screen.target_center({"kind": "creature", "uid": ev.uid}), 0.0)
|
||||
"play":
|
||||
sfx("click" if ev.get("hidden", false) else "play")
|
||||
"reveal":
|
||||
@@ -174,33 +175,6 @@ func fly_in(tile: CreatureView, from: Rect2) -> void:
|
||||
tile.position = tile.slot)
|
||||
|
||||
|
||||
## Eine gekaufte Karte fliegt aus dem Laden auf die Hand.
|
||||
func fly_card(card: Cards.Card, from: Rect2, to: Rect2, rarity: String) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
var v := CardView.new(card, from.size, false, rarity)
|
||||
v.position = from.position
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
v.pivot_offset = Vector2.ZERO
|
||||
screen.fx_layer.add_child(v)
|
||||
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||
tw.tween_property(v, "position", to.position, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(v, "scale", to.size / from.size, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(v, "modulate:a", 0.0, 0.1).set_delay(0.2)
|
||||
tw.chain().tween_callback(v.queue_free)
|
||||
|
||||
|
||||
## Ladenkarten werden nacheinander ausgeteilt.
|
||||
func deal(v: CardView, i: int) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
v.modulate.a = 0.0
|
||||
v.position.y += 24
|
||||
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||
tw.tween_property(v, "modulate:a", 1.0, 0.15).set_delay(i * 0.05)
|
||||
tw.tween_property(v, "position:y", v.position.y - 24, 0.2).set_delay(i * 0.05).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Kurzes Zittern des Bretts beim Treffer.
|
||||
func shake() -> void:
|
||||
if not enabled:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
class_name BattleGamepad
|
||||
extends RefCounted
|
||||
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Stapel, Hand, eigene Wesen
|
||||
## und Ziele (bei offener Wahl über deren Karten), A bestätigt, B bricht ab, Start beendet den
|
||||
## Zug, Y öffnet das Kartenmenü (im Gegnerzug: überspringen), X macht rückgängig.
|
||||
## und Ziele, A bestätigt, B bricht ab, Start beendet den Zug, Y öffnet das Kartenmenü (im
|
||||
## Gegnerzug: überspringen), X macht rückgängig. Verschmelzen: im Menü „Verschmelzen mit …“,
|
||||
## dann A auf dem Partner.
|
||||
|
||||
var screen # BattleScreen
|
||||
var active := false
|
||||
@@ -19,12 +20,6 @@ func button(button: int) -> void:
|
||||
active = true
|
||||
if screen.game == null:
|
||||
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:
|
||||
JOY_BUTTON_START:
|
||||
if screen.my_turn() and not screen.any_overlay():
|
||||
@@ -44,9 +39,9 @@ func button(button: int) -> void:
|
||||
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))
|
||||
screen.open_menu(item.card, Rect2(item.position, item.size), true)
|
||||
else:
|
||||
screen.open_menu(item.creature, Rect2(item.slot, item.size))
|
||||
screen.open_menu(item.creature, Rect2(item.slot, item.size), true)
|
||||
JOY_BUTTON_X:
|
||||
if screen.b_undo.visible:
|
||||
screen.undo()
|
||||
@@ -82,14 +77,10 @@ func items() -> Array:
|
||||
match zone:
|
||||
"deck":
|
||||
return [screen.deck] if screen.deck.visible else []
|
||||
"pick":
|
||||
return screen.pick_views
|
||||
"hand":
|
||||
return screen.hand_views
|
||||
"own":
|
||||
if screen.fusing: # zum Verschmelzen zählt jedes eigene Wesen, nicht nur angriffsbereite
|
||||
return screen.own_views
|
||||
return screen.own_views.filter(func(v): return v.creature.can_attack())
|
||||
return screen.own_views
|
||||
"target":
|
||||
return targets
|
||||
return []
|
||||
@@ -115,9 +106,6 @@ func confirm() -> void:
|
||||
match zone:
|
||||
"deck":
|
||||
screen.on_click("deck", null)
|
||||
"pick":
|
||||
screen.do_pick(index)
|
||||
index = 0
|
||||
"hand":
|
||||
screen.on_click("hand", item.card)
|
||||
"own":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name BattleLayout
|
||||
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem
|
||||
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links),
|
||||
## Feldband, oben der Gegner mit seinem Brett. Eine offene Wahl liegt als Überlagerung darüber.
|
||||
## Elementband, oben der Gegner mit seinem Brett.
|
||||
|
||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||
const B_LOG := Rect2(112, 10, 96, 32)
|
||||
@@ -10,8 +10,7 @@ const FIELD := Rect2(240, 226, 800, 32)
|
||||
const MSG := Rect2(240, 262, 800, 22)
|
||||
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
||||
const STATUS := Rect2(20, 288, 210, 160)
|
||||
const B_FUSE := Rect2(1050, 298, 210, 40)
|
||||
const B_UNDO := Rect2(1050, 346, 210, 40)
|
||||
const B_UNDO := Rect2(1050, 298, 210, 40)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 150
|
||||
const HAND_H := 205
|
||||
@@ -19,8 +18,6 @@ const HAND_GAP := 10
|
||||
const HAND_Y := 532
|
||||
const DECK := Rect2(50, 540, 130, 182)
|
||||
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 PREVIEW_SIZE := Vector2(180, 252)
|
||||
const CENTER_X := 240
|
||||
@@ -36,14 +33,6 @@ static func opp_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||
|
||||
|
||||
## Angebotene Karten einer Wahl, mittig im Wahlfenster.
|
||||
static func pick_rect(count: int, i: int) -> Rect2:
|
||||
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: bis zu fünf große Karten unten, mittig.
|
||||
static func hand_rect(count: int, i: int) -> Rect2:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
@@ -51,7 +40,7 @@ static func hand_rect(count: int, i: int) -> Rect2:
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Schwebende Vorschau neben der gezeigten Karte: über Hand- und Ladenkarten, sonst rechts
|
||||
## Schwebende Vorschau neben der gezeigten Karte: über Handkarten, sonst rechts
|
||||
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
|
||||
static func preview_rect(anchor: Rect2) -> Rect2:
|
||||
if anchor.position.y >= HAND_Y - 10:
|
||||
@@ -64,7 +53,7 @@ static func preview_rect(anchor: Rect2) -> Rect2:
|
||||
return Rect2(Vector2(x, y), PREVIEW_SIZE)
|
||||
|
||||
|
||||
## Zusatzzeile (Resonanz, Feld, Feldkombo) neben der Vorschau, rechts davon oder links.
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) neben der Vorschau, rechts davon oder links.
|
||||
static func info_rect(preview: Rect2) -> Rect2:
|
||||
var w := 230.0
|
||||
var x := preview.end.x + 8
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
class_name BattleScreen
|
||||
extends Control
|
||||
## 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.
|
||||
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
||||
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
||||
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
|
||||
var app
|
||||
@@ -14,8 +16,16 @@ var game: Game
|
||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||
var fx: BattleFx
|
||||
var selected = null # Game.Creature
|
||||
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken
|
||||
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung
|
||||
var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
|
||||
var hold_time := 1.0 # so lange still über dem Ziel halten, dann verschmelzen die beiden
|
||||
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
|
||||
var press_view: Control = null
|
||||
var press_pos := Vector2.ZERO
|
||||
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
|
||||
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
|
||||
var drag_target_view: Control = null
|
||||
var hold_t := 0.0
|
||||
var hold_pos := Vector2.ZERO
|
||||
var msg_t := 0.0
|
||||
var ai_delay := 0.75
|
||||
var ai_timer := 0.0
|
||||
@@ -40,7 +50,6 @@ var undo_t := 0.0
|
||||
# Ansichten (auch für Tests)
|
||||
var tiles := {}
|
||||
var hand_views: Array = []
|
||||
var pick_views: Array = [] # Karten der offenen Wahl
|
||||
var own_views: Array = []
|
||||
var enemy_views: Array = []
|
||||
var hero_panels := {} # Player -> Panel
|
||||
@@ -50,9 +59,6 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_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 b_log: Button
|
||||
@@ -67,7 +73,6 @@ var income_label: Label
|
||||
var status_label: Label
|
||||
var msg_label: Label
|
||||
var b_end: Button
|
||||
var b_fuse: Button
|
||||
var b_giveup: Button
|
||||
var element_view: ElementView
|
||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
@@ -197,9 +202,6 @@ func _build_static() -> void:
|
||||
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
|
||||
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left")
|
||||
status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
|
||||
b_fuse = Style.button(self, "Verschmelzen", L.B_FUSE, toggle_fuse, Color("3c2858"))
|
||||
b_fuse.add_theme_font_size_override("font_size", 15)
|
||||
b_fuse.visible = false
|
||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||
b_undo.add_theme_font_size_override("font_size", 15)
|
||||
b_undo.visible = false
|
||||
@@ -222,11 +224,6 @@ func _build_static() -> void:
|
||||
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
info_label.visible = false
|
||||
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)
|
||||
@@ -372,6 +369,20 @@ func _input(event: InputEvent) -> void:
|
||||
cancel_all()
|
||||
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
||||
return
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
var under := get_viewport().gui_get_hovered_control()
|
||||
if under is CardView and under in hand_views and my_turn() and not any_overlay():
|
||||
cancel()
|
||||
play(under.card, true)
|
||||
after_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
_drag_motion(event.position)
|
||||
return
|
||||
if event is InputEventMouseButton and not event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_release()
|
||||
return
|
||||
if event is InputEventJoypadButton and event.pressed:
|
||||
pad.button(event.button_index)
|
||||
return
|
||||
@@ -385,7 +396,7 @@ func _input(event: InputEvent) -> void:
|
||||
close_menu()
|
||||
elif log_overlay.visible:
|
||||
log_overlay.visible = false
|
||||
elif selected != null or fusing:
|
||||
elif selected != null or fuse_first != null:
|
||||
cancel_all()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
@@ -401,11 +412,9 @@ func _input(event: InputEvent) -> void:
|
||||
if overlay.visible:
|
||||
leave()
|
||||
_:
|
||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and not spectating() and not any_overlay():
|
||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
||||
var idx: int = event.keycode - KEY_1
|
||||
if not perspective.offer.is_empty():
|
||||
do_pick(idx)
|
||||
elif my_turn() and idx < perspective.hand.size():
|
||||
if idx < perspective.hand.size():
|
||||
on_click("hand", perspective.hand[idx])
|
||||
elif event.keycode == KEY_Z and my_turn() and not any_overlay():
|
||||
on_click("deck", null)
|
||||
@@ -429,6 +438,15 @@ func _process(delta: float) -> void:
|
||||
undo_t -= delta
|
||||
if undo_t <= 0.0:
|
||||
clear_undo()
|
||||
if drag_ghost != null and drag_target != null:
|
||||
_set_hold(hold_t + delta)
|
||||
if hold_t >= hold_time:
|
||||
var a = press_target
|
||||
var b = drag_target
|
||||
press_target = null
|
||||
_end_drag()
|
||||
do_fuse(a, b)
|
||||
after_action()
|
||||
if is_online() and net.is_host:
|
||||
net.tick()
|
||||
if game.winner != "":
|
||||
@@ -503,8 +521,109 @@ func say(m: String) -> void:
|
||||
|
||||
func cancel() -> void:
|
||||
selected = null
|
||||
fusing = false
|
||||
fuse_first = null
|
||||
press_target = null
|
||||
_end_drag()
|
||||
|
||||
|
||||
# --- Ziehen mit der Maus: Klick beim Loslassen, Verschmelzen nach einer Sekunde über dem Ziel ---
|
||||
|
||||
func _press(obj, view: Control, pos: Vector2) -> void:
|
||||
_end_drag()
|
||||
press_target = obj
|
||||
press_view = view
|
||||
press_pos = pos
|
||||
|
||||
|
||||
func _drag_motion(pos: Vector2) -> void:
|
||||
if press_target == null:
|
||||
return
|
||||
if drag_ghost == null:
|
||||
if pos.distance_to(press_pos) < 8.0 or not my_turn() or any_overlay():
|
||||
return
|
||||
_start_drag(pos)
|
||||
drag_ghost.position = pos - drag_ghost.size / 2
|
||||
var t = _fusion_target_at(pos)
|
||||
if t != drag_target or pos.distance_to(hold_pos) > 6.0:
|
||||
_set_hold(0.0)
|
||||
drag_target = t
|
||||
drag_target_view = _view_of(t)
|
||||
hold_pos = pos
|
||||
|
||||
|
||||
## Loslassen ohne Ziehen ist der Klick.
|
||||
func _release() -> void:
|
||||
var obj = press_target
|
||||
var dragged := drag_ghost != null
|
||||
press_target = null
|
||||
_end_drag()
|
||||
if obj == null:
|
||||
return
|
||||
if dragged:
|
||||
refresh()
|
||||
elif obj is Cards.Card:
|
||||
on_click("hand", obj)
|
||||
else:
|
||||
on_tile_click(obj)
|
||||
|
||||
|
||||
func _start_drag(pos: Vector2) -> void:
|
||||
show_preview(null, false)
|
||||
if press_target is Cards.Card:
|
||||
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H) * 0.8
|
||||
drag_ghost = CardView.new(press_target, size, false, press_target.rarity_in(perspective.looks))
|
||||
else:
|
||||
drag_ghost = CreatureView.new(press_target, BattleLayout.own_slot(0).size)
|
||||
drag_ghost.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
drag_ghost.modulate.a = 0.85
|
||||
drag_ghost.position = pos - drag_ghost.size / 2
|
||||
fx_layer.add_child(drag_ghost)
|
||||
if press_view != null:
|
||||
press_view.modulate.a = 0.4
|
||||
|
||||
|
||||
func _end_drag() -> void:
|
||||
if drag_ghost != null:
|
||||
drag_ghost.queue_free()
|
||||
drag_ghost = null
|
||||
if is_instance_valid(press_view):
|
||||
press_view.modulate.a = 1.0
|
||||
_set_hold(0.0)
|
||||
drag_target = null
|
||||
drag_target_view = null
|
||||
|
||||
|
||||
func _set_hold(v: float) -> void:
|
||||
hold_t = v
|
||||
if is_instance_valid(drag_target_view):
|
||||
drag_target_view.hold = clampf(v / hold_time, 0.0, 1.0) if v > 0.0 else 0.0
|
||||
drag_target_view.queue_redraw()
|
||||
|
||||
|
||||
## Karte oder Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
|
||||
func _fusion_target_at(pos: Vector2):
|
||||
for v in (hand_views if press_target is Cards.Card else own_views):
|
||||
var obj = v.card if v is CardView else v.creature
|
||||
var r: Rect2 = Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect() # Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten
|
||||
if obj != press_target and r.has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||||
return obj
|
||||
return null
|
||||
|
||||
|
||||
func _view_of(obj) -> Control:
|
||||
if obj is Cards.Card:
|
||||
for v in hand_views:
|
||||
if v.card == obj:
|
||||
return v
|
||||
elif obj is Game.Creature and tiles.has(obj.uid):
|
||||
return tiles[obj.uid]
|
||||
return null
|
||||
|
||||
|
||||
## Mögliche Partner für „Verschmelzen mit …“: andere Handkarten bzw. andere eigene Wesen.
|
||||
func fusion_partners(target) -> Array:
|
||||
var pool: Array = perspective.hand if target is Cards.Card else perspective.board
|
||||
return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0])
|
||||
|
||||
|
||||
func play(card: Cards.Card, face_down: bool = false) -> void:
|
||||
@@ -556,10 +675,12 @@ func do_upgrade(target, keyword: String) -> void:
|
||||
after_action()
|
||||
|
||||
|
||||
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen.
|
||||
func open_menu(target, anchor: Rect2) -> void:
|
||||
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
|
||||
## (Gamepad, das nicht ziehen kann).
|
||||
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
if spectating() or not my_turn():
|
||||
return
|
||||
cancel()
|
||||
Style.clear(menu_layer)
|
||||
menu_target = target
|
||||
var entries := [] # [Text, Callable, aktiv]
|
||||
@@ -570,8 +691,14 @@ func open_menu(target, anchor: Rect2) -> void:
|
||||
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
||||
play(card, true)
|
||||
after_action(), ok[0]])
|
||||
elif target.hidden:
|
||||
elif not is_card and target.hidden:
|
||||
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||
if full:
|
||||
var partners := fusion_partners(target)
|
||||
entries.append([tr("Verschmelzen mit … ◆ %d") % card.fusion_cost(), func():
|
||||
fuse_first = target
|
||||
say(tr("Ziel wählen"))
|
||||
refresh(), not partners.is_empty()])
|
||||
for kw in Cards.KEYWORDS:
|
||||
var k: String = kw
|
||||
var ok := game.can_upgrade(perspective, target, k)
|
||||
@@ -607,16 +734,7 @@ func close_menu() -> void:
|
||||
menu_target = null
|
||||
|
||||
|
||||
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse).
|
||||
func toggle_fuse() -> void:
|
||||
var on := not fusing
|
||||
cancel()
|
||||
fusing = on
|
||||
say(tr("Zwei eigene Wesen wählen") if on else tr("Abgebrochen"))
|
||||
after_action()
|
||||
|
||||
|
||||
## Ziehen: kostet Mana, öffnet die Wahl aus drei Karten.
|
||||
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
||||
func do_draw() -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
@@ -629,31 +747,15 @@ func do_draw() -> void:
|
||||
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()
|
||||
|
||||
|
||||
## Verschmilzt zwei Handkarten oder zwei eigene Wesen (siehe Game.fuse).
|
||||
func do_fuse(a, b) -> void:
|
||||
cancel()
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
|
||||
if a is Cards.Card:
|
||||
net.send_action({"type": "fuse", "hand": perspective.hand.find(a), "other_hand": perspective.hand.find(b)})
|
||||
else:
|
||||
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
|
||||
return
|
||||
var err := game.fuse(perspective, a, b)
|
||||
if err != "":
|
||||
@@ -703,17 +805,13 @@ func on_click(kind: String, obj) -> void:
|
||||
var g := game
|
||||
if not my_turn() or any_overlay():
|
||||
return
|
||||
if fusing:
|
||||
if kind != "own":
|
||||
cancel()
|
||||
say(tr("Abgebrochen"))
|
||||
elif fuse_first == null:
|
||||
fuse_first = obj
|
||||
say(tr("Zweites Wesen wählen"))
|
||||
elif obj == fuse_first:
|
||||
fuse_first = null
|
||||
if fuse_first != null:
|
||||
var first = fuse_first
|
||||
fuse_first = null
|
||||
if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]:
|
||||
do_fuse(first, obj)
|
||||
else:
|
||||
do_fuse(fuse_first, obj)
|
||||
say(tr("Abgebrochen"))
|
||||
after_action()
|
||||
return
|
||||
if selected != null:
|
||||
@@ -745,6 +843,8 @@ func on_click(kind: String, obj) -> void:
|
||||
if obj.can_attack():
|
||||
selected = obj
|
||||
say(tr("Wähle ein Angriffsziel"))
|
||||
elif not obj.ready:
|
||||
say(tr("%s greift erst im nächsten Zug an") % obj.card.title())
|
||||
else:
|
||||
say(tr("%s kann nicht angreifen") % obj.card.title())
|
||||
after_action()
|
||||
@@ -783,7 +883,7 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
|
||||
info_label.text = info if entered else ""
|
||||
if entered and card != null:
|
||||
var r := BattleLayout.preview_rect(anchor)
|
||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||
var v := CardView.new(card, r.size, false, card.rarity_in(owner_looks))
|
||||
v.position = r.position
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
@@ -792,69 +892,32 @@ func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: Strin
|
||||
info_label.size = ir.size
|
||||
|
||||
|
||||
## Eigene Hand- und Wahlkarte: zeigt den Elementbonus, den sie offen gelegt bekäme.
|
||||
## Eigene Handkarte: zeigt den Elementbonus, den sie offen gelegt bekäme.
|
||||
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||||
if drag_ghost != null:
|
||||
return
|
||||
var a := game.forecast(perspective, card)
|
||||
var info := ""
|
||||
if a > 0:
|
||||
info = tr("Elementbonus +%d/+%d") % [a, a]
|
||||
elif a < 0:
|
||||
info = tr("Elementbonus %d/%d") % [a, a]
|
||||
show_preview(card, entered, perspective.looks, info, anchor)
|
||||
|
||||
|
||||
## 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:
|
||||
v.bonus = game.forecast(perspective, v.card)
|
||||
|
||||
|
||||
## Was den Angriff eines Wesens gerade hebt oder senkt.
|
||||
## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
|
||||
func creature_info(cr: Game.Creature) -> String:
|
||||
var parts: Array[String] = []
|
||||
if cr.aura > 0:
|
||||
parts.append(tr("Elementbonus +%d/+%d") % [cr.aura, cr.aura])
|
||||
elif cr.aura < 0:
|
||||
parts.append(tr("Elementbonus %d/%d") % [cr.aura, cr.aura])
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
for k in cr.keywords():
|
||||
if not (k in cr.card.keywords):
|
||||
parts.append(tr("%s durch das Element") % tr(k))
|
||||
if cr.hidden:
|
||||
parts.append(tr("verdeckt"))
|
||||
elif not cr.ready and not cr.has("Flink") and cr.owner() == perspective:
|
||||
parts.append(tr("greift erst im nächsten Zug an"))
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
@@ -1020,10 +1083,10 @@ func refresh() -> void:
|
||||
else:
|
||||
show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
||||
var uid := cr.uid
|
||||
tile.clicked.connect(func(_v):
|
||||
tile.pressed.connect(func(_v, pos):
|
||||
var c := game.find_creature(uid)
|
||||
if c != null:
|
||||
on_tile_click(c))
|
||||
_press(c, tile, pos))
|
||||
if arrivals.has(cr.uid):
|
||||
fx.fly_in(tile, arrivals[cr.uid])
|
||||
arrivals.erase(cr.uid)
|
||||
@@ -1037,7 +1100,7 @@ func refresh() -> void:
|
||||
if cr == selected or cr == fuse_first:
|
||||
b = Style.YELLOW
|
||||
w = 4
|
||||
elif fusing and p == h and mine:
|
||||
elif fuse_first != null and p == h and mine and g.can_fuse(h, fuse_first, cr)[0]:
|
||||
b = Style.PURPLE
|
||||
w = 3
|
||||
elif selected != null and g.can_attack(selected, cr)[0]:
|
||||
@@ -1060,7 +1123,6 @@ func refresh() -> void:
|
||||
var t: CreatureView = tiles[uid]
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
build_pick()
|
||||
deck.visible = not spectating()
|
||||
deck.enabled = mine and g.can_draw(h)[0]
|
||||
deck.count = g.deck.size()
|
||||
@@ -1075,15 +1137,20 @@ func refresh() -> void:
|
||||
var card: Cards.Card = h.hand[i]
|
||||
var playable: bool = mine and g.can_play(h, card)[0]
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
||||
var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
|
||||
v.position = r.position
|
||||
v.lift = mine
|
||||
apply_forecast(v)
|
||||
if playable:
|
||||
v.bonus = g.forecast(h, card)
|
||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||
v.border = Style.PURPLE
|
||||
elif card == fuse_first:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
v.dim = not playable
|
||||
v.dim = not playable and card != fuse_first
|
||||
v.drag_enabled = true
|
||||
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
||||
v.clicked.connect(func(_view): on_click("hand", card))
|
||||
v.pressed.connect(func(_view, pos): _press(card, v, pos))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
|
||||
@@ -1103,18 +1170,16 @@ func refresh() -> void:
|
||||
b_skip.visible = ai_running
|
||||
b_end.visible = not ai_running and not spectating()
|
||||
b_undo.visible = undo_state != null and mine
|
||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||
element_view.update_from(g)
|
||||
element_view.update_from(g, h)
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
var line := g.log[i]
|
||||
if line.begins_with("---"):
|
||||
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
||||
elif " verschmelzen zu " in line or " wird aufgedeckt." in line:
|
||||
elif " verschmelzen " in line or " wird aufgedeckt." in line:
|
||||
lines.append("[color=#8c78dc]%s[/color]" % line)
|
||||
elif " bekommt " in line:
|
||||
elif " bekommt " in line or " regeneriert sich." in line:
|
||||
lines.append("[color=#5ad282]%s[/color]" % line)
|
||||
else:
|
||||
lines.append(line)
|
||||
|
||||
@@ -87,7 +87,7 @@ func buy() -> void:
|
||||
func reveal(i: int) -> void:
|
||||
revealed[i] = true
|
||||
fresh = {i: 0.0}
|
||||
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click")
|
||||
app.sfx.play("combo" if cards[i].rarity != "Common" else "click")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||
var tw := create_tween()
|
||||
tw.tween_interval(delay)
|
||||
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
if rarity == "gewöhnlich":
|
||||
if rarity == "Common":
|
||||
return
|
||||
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
|
||||
burst.pivot_offset = burst.size / 2
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
class_name CardView
|
||||
extends Control
|
||||
## Zeichnet eine Karte (Laden, Hand, Sammlung, Booster, Vorschau).
|
||||
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Schlüsselwörter als
|
||||
## Symbole, Effekttext, Angriff und Leben als Kreise unten. Das Element steht im Hintergrund,
|
||||
## die Seltenheit in Rahmen, Glanz und Edelstein. Sehr flache Karten (Hand) zeigen nur das Nötigste.
|
||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
||||
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Eigenschaften als
|
||||
## Symbole, Angriff und Leben als Kreise unten. Das erste Element färbt den Hintergrund, weitere
|
||||
## Elemente zeigt ein Medaillon als Tortenstücke hinter dem Bild; die Seltenheit steckt in Name,
|
||||
## Rahmen, Glanz und Edelstein (siehe Style). Sehr flache Karten zeigen nur das Nötigste.
|
||||
|
||||
signal clicked(view: CardView)
|
||||
signal pressed(view: CardView, pos: Vector2) # nur mit drag_enabled: der Kampf macht daraus Klick oder Ziehen
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var card: Cards.Card
|
||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||
var rarity := "gewöhnlich" # nur Optik
|
||||
var rarity := "Common" # nur Optik
|
||||
var dim := false
|
||||
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme (+1 oder -1)
|
||||
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
|
||||
var bonus := 0 # Vorschau im Kampf: Elementbonus, den die Karte offen gelegt bekäme
|
||||
var lift := false # hebt sich beim Zeigen an (Hand)
|
||||
var drag_enabled := false # Hand im Kampf: Drücken meldet pressed statt clicked
|
||||
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
|
||||
|
||||
|
||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "Common") -> void:
|
||||
card = c
|
||||
rarity = rar
|
||||
set_process(Style.rarity_animated(rarity)) # bewegter Glanz
|
||||
@@ -55,7 +59,10 @@ func _process(_delta: float) -> void:
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
if drag_enabled:
|
||||
pressed.emit(self, event.global_position)
|
||||
else:
|
||||
clicked.emit(self)
|
||||
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
@@ -65,7 +72,7 @@ func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var seed := hash(card.id)
|
||||
var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
|
||||
if rarity != "gewöhnlich":
|
||||
if rarity != "Common":
|
||||
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
if size.y < 90:
|
||||
@@ -76,9 +83,14 @@ func _draw() -> void:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||
if hold > 0.0:
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
|
||||
|
||||
## Volle Karte: Laden, Sammlung, Booster, Vorschau.
|
||||
## Volle Karte: Sammlung, Booster, Vorschau.
|
||||
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 140
|
||||
@@ -90,14 +102,18 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
var icon := 11.0 if big else (9.0 if mid else 7.0)
|
||||
# Bild: das Elementsymbol füllt den oberen Teil
|
||||
var band_top: float = size.y * 0.36
|
||||
Style.glyph(self, card, Vector2(size.x / 2, band_top * 0.56), minf(size.x * 0.62, band_top * 0.95), 0.4)
|
||||
Style.rarity_sheen(self, rect, rarity, seed)
|
||||
var gc := Vector2(size.x / 2, band_top * 0.56)
|
||||
var gs := minf(size.x * 0.62, band_top * 0.95)
|
||||
if card.is_combo():
|
||||
Style.element_medallion(self, gc, gs * 0.6, card.elements)
|
||||
Style.glyph(self, card, gc, gs, 0.4, Style.rarity_relief(rarity))
|
||||
Style.rarity_sheen(self, rect, rarity, seed, Rect2(0, 0, size.x, band_top))
|
||||
# Namensband über die volle Breite
|
||||
var band_h: float = Style.fs(f_name) + 10
|
||||
var band := Rect2(0, band_top, size.x, band_h)
|
||||
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
|
||||
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.TEXT, Vector2(size.x / 2, band.get_center().y), "center", true)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.rarity_name_color(rarity, seed), Vector2(size.x / 2, band.get_center().y), "center", true)
|
||||
var y := band.end.y + 4
|
||||
# Kombo eines verschmolzenen Wesens
|
||||
if card.is_combo():
|
||||
@@ -146,9 +162,8 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
|
||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
|
||||
if bonus != 0:
|
||||
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)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d/+%d" % [bonus, bonus], f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||
@@ -160,7 +175,7 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
Style.rarity_sheen(self, rect, rarity, seed)
|
||||
Style.border(self, rect, frame, 2, 8)
|
||||
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
|
||||
var name_w: float = size.x - 30 - (12 if rarity != "gewöhnlich" else 0)
|
||||
var name_w: float = size.x - 30 - (12 if rarity != "Common" else 0)
|
||||
var nf := Style.fit_size(card.title(), true, 12, name_w)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
|
||||
@@ -174,6 +189,6 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
x += 18
|
||||
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
|
||||
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
|
||||
if bonus != 0:
|
||||
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)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||
|
||||
@@ -2,7 +2,7 @@ class_name CreatureView
|
||||
extends Control
|
||||
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
|
||||
|
||||
signal clicked(view: CreatureView)
|
||||
signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var creature: Game.Creature
|
||||
@@ -13,6 +13,7 @@ var slot: Vector2 # Zielposition im Raster
|
||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||
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
|
||||
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
|
||||
|
||||
|
||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
@@ -35,27 +36,31 @@ func _process(_delta: float) -> void:
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
pressed.emit(self, event.global_position)
|
||||
|
||||
|
||||
func rarity() -> String:
|
||||
var owner = creature.owner()
|
||||
return Cards.RARITIES[int(owner.looks.get(creature.card.id, 0)) if owner != null else 0]
|
||||
return creature.card.rarity_in(owner.looks if owner != null else {})
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
if secret:
|
||||
_draw_back(rect)
|
||||
_draw_hold()
|
||||
return
|
||||
var small := size.x < 100
|
||||
var rar := rarity()
|
||||
var seed := hash(creature.card.id) + creature.uid
|
||||
if rar != "gewöhnlich":
|
||||
if rar != "Common":
|
||||
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 6)
|
||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||
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)
|
||||
var gc := Vector2(size.x / 2, size.y * 0.5)
|
||||
if creature.card.is_combo():
|
||||
Style.element_medallion(self, gc, size.x * 0.42, creature.card.elements)
|
||||
Style.glyph(self, creature.card, gc, size.x * 0.75, 0.3, Style.rarity_relief(rar))
|
||||
Style.rarity_sheen(self, rect, rar, seed, rect)
|
||||
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
@@ -64,19 +69,19 @@ func _draw() -> void:
|
||||
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)
|
||||
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)
|
||||
# Schlüsselwörter als Symbole (Schild nur, solange er steht)
|
||||
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.rarity_name_color(rar, seed), Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||
# Eigenschaften als Symbole: gekaufte weiß, die vom Elementpaar in der Elementfarbe
|
||||
var icon := 7.0 if small else 9.0
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in creature.card.keywords and (k != "Schild" or creature.shield):
|
||||
kws.append(k)
|
||||
if creature.shield and not ("Schild" in kws):
|
||||
kws.append("Schild")
|
||||
var kws := creature.keywords()
|
||||
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
|
||||
var y := Style.fs(fs) + 14 + icon
|
||||
for k in kws:
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon)
|
||||
var col := Color("f2f2f6")
|
||||
if not (k in creature.card.keywords):
|
||||
for e in creature.card.elements:
|
||||
if Cards.ELEMENT_KEYWORD.get(e, "") == k:
|
||||
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col)
|
||||
x += icon * 2 + 6
|
||||
_draw_stats(15 if small else 17, 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)
|
||||
@@ -84,6 +89,17 @@ func _draw() -> void:
|
||||
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
|
||||
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
_draw_hold()
|
||||
|
||||
|
||||
## Ring, der sich füllt, solange eine Karte zum Verschmelzen darüber gehalten wird.
|
||||
func _draw_hold() -> void:
|
||||
if hold <= 0.0:
|
||||
return
|
||||
var c := size / 2
|
||||
var r := minf(size.x, size.y) * 0.3
|
||||
draw_circle(c, r + 6, Color(0, 0, 0, 0.55))
|
||||
draw_arc(c, r, -PI / 2, -PI / 2 + TAU * hold, 40, Style.GOLD, 5.0, true)
|
||||
|
||||
|
||||
## Verdecktes fremdes Wesen: ein Kartenrücken.
|
||||
@@ -98,7 +114,7 @@ func _draw_back(rect: Rect2) -> void:
|
||||
pass # unbekanntes Element: kein Vorteil anzeigen
|
||||
|
||||
|
||||
## Angriff (grün oder rot, wenn die Elementregel ihn hebt oder senkt), Leben, Elementvorteil.
|
||||
## Angriff (grün, wenn der Elementbonus ihn hebt), Leben, Elementvorteil des Angreifers.
|
||||
func _draw_stats(f: int, f_small: int, r: float) -> void:
|
||||
var extra := creature.power() - creature.attack
|
||||
var atk_col := Style.GREEN if extra > 0 else (Style.RED if extra < 0 else Style.AMBER)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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.
|
||||
## Elementboni auf einen Blick: offene Wesen je Element auf dem Tisch (ab drei +1/+1) und der
|
||||
## Sondereffekt der eigenen Seite (ab zwei eigenen: Erde Spott, Luft Flink, Wasser Regeneration,
|
||||
## Feuer Doppelschlag), als Symbol in der Elementfarbe.
|
||||
|
||||
var counts := {}
|
||||
var counts := {} # Tisch
|
||||
var own := {} # eigene Seite
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
@@ -12,10 +14,13 @@ func _init(rect: Rect2) -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in Cards.ELEMENTS:
|
||||
counts[e] = 0
|
||||
own[e] = 0
|
||||
|
||||
|
||||
func update_from(g: Game) -> void:
|
||||
func update_from(g: Game, me: Game.Player) -> void:
|
||||
counts = g.element_counts()
|
||||
for e in Cards.ELEMENTS:
|
||||
own[e] = me.element_count(e) if me != null else 0
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -23,9 +28,8 @@ 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)
|
||||
func paired(e: String) -> bool:
|
||||
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll).
|
||||
@@ -35,8 +39,8 @@ func text() -> String:
|
||||
var s := "%s %d" % [tr(e), int(counts.get(e, 0))]
|
||||
if strong(e):
|
||||
s += " (+1/+1)"
|
||||
if weak(e):
|
||||
s += " (-1/-1)"
|
||||
if paired(e):
|
||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||
parts.append(s)
|
||||
return " · ".join(parts)
|
||||
|
||||
@@ -51,9 +55,10 @@ func _draw() -> void:
|
||||
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))
|
||||
elif paired(e):
|
||||
draw_style_box(Style.box(Color(col, 0.18), 6, Color(col, 0.6), 1), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||
var active := strong(e) or paired(e)
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(x0 + 12, cy), "midleft", active)
|
||||
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)
|
||||
@@ -62,12 +67,11 @@ func _draw() -> void:
|
||||
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)
|
||||
px += Game.ELEMENT_STACK * 13 + 2
|
||||
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 := ""
|
||||
Style.text(self, "+%d" % (n - Game.ELEMENT_STACK), 11, Style.MUTED, Vector2(px, cy), "midleft")
|
||||
px += 18
|
||||
if paired(e):
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
|
||||
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)
|
||||
Style.text(self, "+1/+1", 13, Style.GREEN, Vector2(x0 + w - 12, cy), "midright", true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||
## per Ziehen (siehe DeckGen.offer_cards).
|
||||
## vom gemeinsamen Stapel (siehe Game.start).
|
||||
|
||||
var app
|
||||
var diff_buttons := {}
|
||||
|
||||
@@ -309,43 +309,79 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
|
||||
|
||||
# --- Seltenheit als Aussehen ------------------------------------------------------
|
||||
# Common matt; Rare silberner Name und Rahmen mit stehendem Glanzstreifen; Super Rare Holo-Bild
|
||||
# (wandernder Regenbogenschimmer); Ultra Rare goldener Name und Rahmen plus Holo-Bild;
|
||||
# Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln, schillernde Farben.
|
||||
|
||||
## Ab episch bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||
## Ab Super Rare bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||
static func rarity_animated(rar: String) -> bool:
|
||||
return animations and Cards.RARITIES.find(rar) >= 2
|
||||
|
||||
|
||||
## Farbe einer Stufe; mythisch schillert über die Zeit durch alle Farben.
|
||||
## Farbe einer Stufe; Ultimate Rare schillert über die Zeit durch alle Farben.
|
||||
static func rarity_color(rar: String, seed: int = 0) -> Color:
|
||||
if rar == "mythisch" and animations:
|
||||
if rar == "Ultimate Rare" and animations:
|
||||
return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0)
|
||||
return Cards.RARITY_COLORS[rar]
|
||||
|
||||
|
||||
## Rahmenfarbe einer Stufe: gewöhnlich bleibt schlicht, alles darüber trägt seine Farbe.
|
||||
## Rahmenfarbe einer Stufe: Common bleibt schlicht, alles darüber trägt seine Farbe.
|
||||
static func rarity_frame(rar: String, seed: int = 0) -> Color:
|
||||
return Color("3a3e4c") if rar == "gewöhnlich" else rarity_color(rar, seed)
|
||||
return Color("3a3e4c") if rar == "Common" else rarity_color(rar, seed)
|
||||
|
||||
|
||||
## Lichtstreifen wie auf einer Folienkarte: selten steht er, ab episch wandert er über die Karte.
|
||||
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||
## Farbe des Kartennamens: Rare silbern, Ultra Rare golden, Ultimate Rare schillernd, sonst weiß.
|
||||
static func rarity_name_color(rar: String, seed: int = 0) -> Color:
|
||||
match rar:
|
||||
"Rare", "Ultra Rare", "Ultimate Rare":
|
||||
return lighter(rarity_color(rar, seed), 0.25)
|
||||
return TEXT
|
||||
|
||||
|
||||
## Ultimate Rare zeigt das Bild als Relief (Licht- und Schattenkante).
|
||||
static func rarity_relief(rar: String) -> bool:
|
||||
return Cards.RARITIES.find(rar) >= 4
|
||||
|
||||
|
||||
## Glanz wie auf einer Folienkarte. Rare: ein stehender weißer Streifen über die Karte.
|
||||
## Ab Super Rare: Holo, ein wandernder Regenbogenschimmer über das Bild (picture); Ultimate Rare
|
||||
## zusätzlich schwach über die ganze Karte.
|
||||
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int, picture: Rect2 = Rect2()) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 1:
|
||||
return
|
||||
if picture.size.x <= 0:
|
||||
picture = rect
|
||||
if lvl == 1:
|
||||
_sheen_bands(ci, rect, (seed % 100) / 100.0, [[1.0, Color(1, 1, 1, 0.07)], [0.55, Color(1, 1, 1, 0.09)], [0.22, Color(1, 1, 1, 0.13)]])
|
||||
return
|
||||
var phase := (seed % 100) / 100.0
|
||||
if lvl >= 2 and animations:
|
||||
if animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||
var holo := []
|
||||
for i in 4:
|
||||
holo.append([1.0 - i * 0.22, Color.from_hsv(fmod(phase + i * 0.17, 1.0), 0.6, 1.0, 0.16)])
|
||||
_sheen_bands(ci, picture, phase, holo)
|
||||
if lvl >= 4:
|
||||
var soft := []
|
||||
for i in 3:
|
||||
soft.append([1.0 - i * 0.3, Color.from_hsv(fmod(phase + 0.5 + i * 0.2, 1.0), 0.5, 1.0, 0.07)])
|
||||
_sheen_bands(ci, rect, fmod(phase + 0.5, 1.0), soft)
|
||||
|
||||
|
||||
## Schräge Streifen über rect, auf rect beschnitten. layers: [Breitenanteil, Farbe].
|
||||
static func _sheen_bands(ci: CanvasItem, rect: Rect2, phase: float, layers: Array) -> void:
|
||||
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
var w := rect.size.x * 0.34
|
||||
var k := rect.size.y * 0.75 # Schräge
|
||||
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
|
||||
for layer in [[1.0, 0.07], [0.55, 0.09], [0.22, 0.13]]:
|
||||
for layer in layers:
|
||||
var lw: float = w * layer[0]
|
||||
var lx := x0 + (w - lw) / 2
|
||||
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
|
||||
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||
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]))
|
||||
ci.draw_colored_polygon(poly, layer[1])
|
||||
|
||||
|
||||
static func _area(poly: PackedVector2Array) -> float:
|
||||
@@ -357,15 +393,13 @@ static func _area(poly: PackedVector2Array) -> float:
|
||||
return absf(a) / 2.0
|
||||
|
||||
|
||||
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||
## Funkeln bei Ultimate Rare: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||
## avoid: Bereich, der frei bleibt (der Kartentext).
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 3:
|
||||
if Cards.RARITIES.find(rar) < 4:
|
||||
return
|
||||
var t := Time.get_ticks_msec() / 1000.0
|
||||
var n := 5 if lvl == 3 else 9
|
||||
for i in n:
|
||||
for i in 9:
|
||||
var h := absi(hash(Vector2i(seed, i)))
|
||||
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
|
||||
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
|
||||
@@ -388,9 +422,9 @@ static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spr
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
## Edelstein in der Farbe der Stufe (ab selten).
|
||||
## Edelstein in der Farbe der Stufe (ab Rare).
|
||||
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
|
||||
if rar == "gewöhnlich":
|
||||
if rar == "Common":
|
||||
return
|
||||
var col := rarity_color(rar, seed)
|
||||
var diamond := func(h: float, k: Color) -> void:
|
||||
@@ -400,23 +434,28 @@ static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed:
|
||||
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
|
||||
|
||||
|
||||
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
|
||||
## Hintergrund in der Farbe des ersten Elements; weitere Elemente zeigt element_medallion.
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
if elements.size() == 1:
|
||||
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
|
||||
return
|
||||
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
|
||||
|
||||
|
||||
## Medaillon eines verschmolzenen Wesens: ein Kreis hinter dem Bild, als Tortenstücke in den
|
||||
## Farben seiner Elemente, mit dunklem Rand.
|
||||
static func element_medallion(ci: CanvasItem, c: Vector2, r: float, elements: Array) -> void:
|
||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.75))
|
||||
var n := elements.size()
|
||||
var w := rect.size.x / n
|
||||
for i in n:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Cards.ELEMENT_COLORS[elements[i]]
|
||||
if i == 0:
|
||||
sb.corner_radius_top_left = radius
|
||||
sb.corner_radius_bottom_left = radius
|
||||
if i == n - 1:
|
||||
sb.corner_radius_top_right = radius
|
||||
sb.corner_radius_bottom_right = radius
|
||||
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
|
||||
var from := -PI / 2 + TAU * i / n
|
||||
var to := -PI / 2 + TAU * (i + 1) / n
|
||||
var pts := PackedVector2Array([c])
|
||||
var steps := 24
|
||||
for k in steps + 1:
|
||||
var ang := lerpf(from, to, float(k) / steps)
|
||||
pts.append(c + Vector2(cos(ang), sin(ang)) * r)
|
||||
ci.draw_colored_polygon(pts, Cards.ELEMENT_COLORS[elements[i]])
|
||||
for i in n:
|
||||
var ang := -PI / 2 + TAU * i / n
|
||||
ci.draw_line(c, c + Vector2(cos(ang), sin(ang)) * r, Color(0.05, 0.05, 0.08, 0.75), 2.0)
|
||||
|
||||
|
||||
# --- Kartenteile ------------------------------------------------------
|
||||
@@ -444,20 +483,18 @@ static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, col
|
||||
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
|
||||
|
||||
|
||||
## Schlüsselwort als Symbol auf dunklem Kreis: Schild Schild, Doppelschlag gekreuzte Klingen.
|
||||
## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil,
|
||||
## Regeneration Kreuz, Doppelschlag gekreuzte Klingen.
|
||||
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))
|
||||
var w := maxf(1.5, r * 0.22)
|
||||
match kw:
|
||||
"Wächter":
|
||||
ci.draw_rect(Rect2(c + Vector2(-r * 0.75, -r * 0.15), Vector2(r * 1.5, r * 0.85)), col)
|
||||
for i in 3:
|
||||
ci.draw_rect(Rect2(c + Vector2(-r * 0.75 + i * r * 0.6, -r * 0.6), Vector2(r * 0.3, r * 0.5)), col)
|
||||
"Schild":
|
||||
var pts := PackedVector2Array([c + Vector2(-r * 0.7, -r * 0.65), c + Vector2(r * 0.7, -r * 0.65), c + Vector2(r * 0.7, r * 0.05),
|
||||
c + Vector2(0, r * 0.8), c + Vector2(-r * 0.7, r * 0.05)])
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
ci.draw_line(c + Vector2(0, -r * 0.55), c + Vector2(0, r * 0.6), Color(0.05, 0.05, 0.08, 0.6), w)
|
||||
"Spott":
|
||||
ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.2), col, w * 1.6)
|
||||
ci.draw_circle(c + Vector2(0, r * 0.6), w * 0.95, col)
|
||||
"Regeneration":
|
||||
ci.draw_line(c + Vector2(0, -r * 0.7), c + Vector2(0, r * 0.7), col, w * 1.5)
|
||||
ci.draw_line(c + Vector2(-r * 0.7, 0), c + Vector2(r * 0.7, 0), col, w * 1.5)
|
||||
"Flink":
|
||||
for i in 2:
|
||||
var x := c.x - r * 0.55 + i * r * 0.55
|
||||
@@ -471,10 +508,24 @@ static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col:
|
||||
|
||||
# --- Kartensymbole (prozedural, je Element) --------------------------
|
||||
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95) -> void:
|
||||
## relief: Schatten- und Lichtkante unter dem Symbol (Ultimate Rare).
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95, relief: bool = false) -> void:
|
||||
if relief:
|
||||
ci.draw_set_transform(Vector2(2, 2))
|
||||
_glyph_plain(ci, card, center, size, Color(0, 0, 0, alpha * 0.6))
|
||||
ci.draw_set_transform(Vector2(-1, -1))
|
||||
_glyph_plain(ci, card, center, size, Color(1, 1, 1, alpha * 0.5))
|
||||
ci.draw_set_transform(Vector2.ZERO)
|
||||
_glyph_plain(ci, card, center, size, Color(0, 0, 0, 0), alpha)
|
||||
|
||||
|
||||
## Symbol in einer festen Farbe (Relief) oder, bei durchsichtigem tint, in Elementfarben.
|
||||
static func _glyph_plain(ci: CanvasItem, card, center: Vector2, size: float, tint: Color, alpha: float = 0.95) -> void:
|
||||
var variant := absi(hash(card.id)) % 3
|
||||
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||
col.a = alpha
|
||||
if tint.a > 0:
|
||||
col = tint
|
||||
var elems: Array = card.elements
|
||||
if elems.size() == 1:
|
||||
_element_glyph(ci, elems[0], center, size, variant, col)
|
||||
@@ -483,6 +534,8 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: flo
|
||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||
col2.a = alpha
|
||||
if tint.a > 0:
|
||||
col2 = tint
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
|
||||
@@ -19,15 +19,14 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var h: Game.Player = scr.game.human
|
||||
for i in 3:
|
||||
scr.game.pick(h, 0)
|
||||
h.energy = 10
|
||||
scr.game.draw(h)
|
||||
scr.game.pick(h, 0)
|
||||
h.hand.pop_back()
|
||||
if scr.game.current() == h:
|
||||
scr.game.draw(h)
|
||||
var t0 := Time.get_ticks_usec()
|
||||
for i in 30:
|
||||
scr.refresh()
|
||||
print("refresh() mit 4 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
||||
print("refresh() mit 5 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
|
||||
scr.ai_delay = 0.2
|
||||
measuring = true
|
||||
var start := Time.get_ticks_msec()
|
||||
|
||||
@@ -73,16 +73,13 @@ func _act_host() -> void:
|
||||
_decide(game.players[seat])
|
||||
|
||||
|
||||
## Einfache Züge mit kurzer Bedenkzeit: erst eine offene Wahl treffen, dann das erste
|
||||
## spielbare Wesen ohne Ziel legen, dann angreifen, dann ziehen, sonst Zug beenden.
|
||||
## Einfache Züge mit kurzer Bedenkzeit: das erste spielbare Wesen legen, dann angreifen,
|
||||
## dann ziehen, sonst Zug beenden.
|
||||
func _decide(me: Game.Player) -> void:
|
||||
var g := game
|
||||
await get_tree().create_timer(0.25).timeout
|
||||
if game != g or (hosting and (net.game == null or net.game.current_idx != seat)):
|
||||
return # inzwischen ein neuer Zustand
|
||||
if not me.offer.is_empty():
|
||||
net.send_action({"type": "pick", "index": 0})
|
||||
return
|
||||
for i in me.hand.size():
|
||||
var card: Cards.Card = me.hand[i]
|
||||
if game.can_play(me, card)[0]:
|
||||
|
||||
@@ -25,7 +25,6 @@ func _initialize() -> void:
|
||||
var c := Game.Creature.new(Cards.by_id("f_wolf"), p, uid)
|
||||
uid += 1
|
||||
c.ready = true
|
||||
c.attacks_left = 1
|
||||
p.board.append(c)
|
||||
var h: Game.Player = g.players[0]
|
||||
h.energy = 10
|
||||
|
||||
@@ -43,7 +43,7 @@ var next_uid := 9000
|
||||
func put(p: Game.Player, cid: String, ready: bool = true, hidden: bool = false) -> Game.Creature:
|
||||
next_uid += 1
|
||||
var cr := Game.Creature.new(C(cid), p, next_uid)
|
||||
cr.attacks_left = cr.attacks_per_turn() if ready else 0
|
||||
cr.ready = ready
|
||||
cr.hidden = hidden
|
||||
p.board.append(cr)
|
||||
return cr
|
||||
@@ -58,7 +58,8 @@ func _initialize() -> void:
|
||||
test_wesen_und_angriff()
|
||||
test_elementvorteil()
|
||||
test_verdeckt()
|
||||
test_vier_elemente()
|
||||
test_elementboni()
|
||||
test_spott_und_regeneration()
|
||||
test_verbessern()
|
||||
test_verschmelzen()
|
||||
test_limits()
|
||||
@@ -94,12 +95,12 @@ func test_kartenpool() -> void:
|
||||
for e in Cards.ELEMENTS:
|
||||
check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e)
|
||||
for c in Cards.all():
|
||||
check(c.health > 0 and c.cost == ceili((c.attack + c.health) / 2.0), "Kosten = (Angriff + Leben) / 2: " + c.id)
|
||||
check(c.health > 0 and c.cost == maxi(1, floori((2 * c.attack + c.health) / 3.0)), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
|
||||
check(c.keywords.is_empty() and c.text() == "", "Karten kommen ohne Eigenschaften: " + c.id)
|
||||
check(C("f_kobold").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 2, "Kostenbeispiele")
|
||||
check(C("f_kobold").cost == 1 and C("f_wolf").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 1 and C("f_drache").cost == 5, "Kostenbeispiele")
|
||||
var a := C("f_wolf")
|
||||
var b := C("f_wolf")
|
||||
a.keywords.append("Schild")
|
||||
a.keywords.append("Spott")
|
||||
check(b.keywords.is_empty() and Cards.by_id("f_wolf").keywords.is_empty(), "Exemplare teilen sich keine Eigenschaften")
|
||||
|
||||
|
||||
@@ -108,7 +109,6 @@ func test_kombos() -> void:
|
||||
check(Cards.combo_name(["Luft", "Feuer"]) == "Blitz", "Luft+Feuer = Blitz")
|
||||
check(not C("ff_phoenix").is_combo() and Cards.fusion_name(["Feuer"]) == "Inferno", "Kombos gibt es nur beim Verschmelzen")
|
||||
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
|
||||
check(Cards.beaten_by("Feuer") == "Wasser" and Cards.beaten_by("Erde") == "Luft", "wer wen schlägt")
|
||||
|
||||
|
||||
## Mana: 5 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
|
||||
@@ -129,101 +129,78 @@ func test_mana_und_kosten() -> void:
|
||||
check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
|
||||
h.energy = 5
|
||||
check(g.play_card(h, h.hand[0]) == "" and h.energy == 5 - C("f_wolf").cost, "Ausspielen kostet die Kartenkosten")
|
||||
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (6) zu teuer bei %d Mana" % h.energy)
|
||||
check(g.can_play(h, h.hand[0])[1] == "Nicht genug Mana", "Drache (5) zu teuer bei %d Mana" % h.energy)
|
||||
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 4 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren")
|
||||
|
||||
|
||||
## Ziehen: 1 Mana, drei vom gemeinsamen Stapel, eine behalten; der Rest unter den Stapel.
|
||||
## Start: fünf bezahlbare Karten, keine Wahl. Ziehen: 1 Mana, eine bezahlbare Karte vom Stapel.
|
||||
func test_ziehen() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 1
|
||||
var g := Game.new_battle(r)
|
||||
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_OFFER, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
|
||||
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
|
||||
for p in g.players:
|
||||
check(p.offer.size() == Game.START_OFFER and p.picks_left == Game.START_PICKS and p.hand.is_empty(), "Start: fünf zur Wahl, drei zu nehmen")
|
||||
check(p.offer.all(func(c): return c.cost <= Game.START_MANA + p.start_bonus), "nur Karten, die im ersten Zug spielbar sind")
|
||||
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
||||
check(p.hand.all(func(c): return c.cost <= Game.START_MANA + p.start_bonus), "nur Karten, die im ersten Zug spielbar sind")
|
||||
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
|
||||
var first: Game.Player = g.current()
|
||||
var deck_before := g.deck.size()
|
||||
var wanted: Cards.Card = first.offer[4]
|
||||
check(g.pick(first, 4) == "" and first.hand.size() == 1 and first.hand[0].id == wanted.id and first.hand[0] != wanted, "erste Startkarte genommen, als eigenes Exemplar")
|
||||
check(first.offer.size() == 4 and first.picks_left == 2, "vier bleiben, zwei noch zu nehmen")
|
||||
check(g.draw(first) == "Erst wählen" and g.can_play(first, first.hand[0])[1] == "Erst wählen", "bei offener Wahl geht nichts anderes")
|
||||
g.pick(first, 0)
|
||||
g.pick(first, 0)
|
||||
check(first.hand.size() == 3 and first.offer.is_empty() and g.deck.size() == deck_before + 2, "drei behalten, die anderen zwei liegen unter dem Stapel")
|
||||
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
||||
first.hand.pop_back()
|
||||
var mana := first.energy
|
||||
check(g.draw(first) == "" and first.energy == mana - 1 and first.offer.size() == 3 and first.picks_left == 1, "gezogen: 1 Mana, drei zur Wahl")
|
||||
check(first.offer.all(func(c): return c.cost <= first.max_energy), "nur Karten bis zum Mana dieses Zugs")
|
||||
check(first.offer[0] != first.offer[1] and first.offer[1] != first.offer[2], "drei verschiedene Karten")
|
||||
check(g.pick(first, 1) == "" and first.hand.size() == 4 and first.offer.is_empty(), "eine genommen, Wahl zu")
|
||||
check(g.pick(first, 0) == "Keine Wahl offen", "ohne Wahl nichts zu nehmen")
|
||||
var deck_before := g.deck.size()
|
||||
check(g.draw(first) == "" and first.energy == mana - 1 and first.hand.size() == Game.START_HAND, "gezogen: 1 Mana, eine Karte")
|
||||
check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel")
|
||||
first.energy = 0
|
||||
first.hand.pop_back()
|
||||
check(g.draw(first) == "Nicht genug Mana", "ohne Mana kein Ziehen")
|
||||
first.energy = 5
|
||||
g.draw(first)
|
||||
g.pick(first, 0)
|
||||
check(first.hand.size() == Game.MAX_HAND and g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
|
||||
var second: Game.Player = g.enemy_of(first)
|
||||
check(g.pick(second, 0) == "" and second.hand.size() == 1, "wählen geht auch außerhalb des Zugs")
|
||||
g.end_turn() # jetzt ist der Zweite dran, seine Wahl ist noch offen
|
||||
g.end_turn() # sein Zugende nimmt den Rest von vorn
|
||||
check(second.hand.size() == 3 and second.offer.is_empty(), "Zugende nimmt offene Karten von vorn")
|
||||
check(g.draw(second) == "Nicht dein Zug", "nur im eigenen Zug")
|
||||
# Leerer Stapel wird neu gemischt
|
||||
g.deck = []
|
||||
var cur: Game.Player = g.current()
|
||||
cur.energy = 3
|
||||
cur.hand.pop_back() # Platz auf der Hand
|
||||
check(g.draw(cur) == "" and cur.offer.size() == 3 and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
|
||||
# Mit viel Mana tauchen auch teure Karten auf; zu teure wandern unter den Stapel
|
||||
g.pick(cur, 0)
|
||||
cur.max_energy = 20
|
||||
cur.energy = 20
|
||||
var expensive_before: int = g.deck.filter(func(c): return c.cost > 8).size()
|
||||
g.draw(cur)
|
||||
g.pick(cur, 0)
|
||||
var bottom: Array = g.deck.slice(0, 5)
|
||||
check(expensive_before > 0 and (g.deck.size() + 1 + cur.offer.size()) > 0 and bottom.size() == 5, "Stapel bleibt vollständig")
|
||||
first.energy = 3
|
||||
check(g.draw(first) == "" and g.deck.size() > 0 and g.log.any(func(l): return l.begins_with("Der Stapel wird neu gemischt")), "leerer Stapel: neu gemischt")
|
||||
# Mit viel Mana tauchen auch teure Karten auf
|
||||
first.max_energy = 20
|
||||
first.energy = 20
|
||||
var seen_expensive := false
|
||||
for i in 12:
|
||||
first.hand = []
|
||||
g.draw(first)
|
||||
if first.hand[0].cost > 5:
|
||||
seen_expensive = true
|
||||
check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
|
||||
|
||||
|
||||
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
|
||||
func test_wesen_und_angriff() -> void:
|
||||
var s := setup(["f_wolf"])
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
check(g.play_card(h, h.hand[0]) == "" and h.board.size() == 1 and h.board[0].can_attack(), "gespielt und sofort angriffsbereit")
|
||||
check(g.play_card(h, h.hand[0]) == "" and h.board.size() == 1 and not h.board[0].can_attack(), "gespielt, greift noch nicht an")
|
||||
var wolf: Game.Creature = h.board[0]
|
||||
check(g.can_attack(wolf, o)[1] == "Flammenwolf greift erst im nächsten Zug an", "Meldung: erst nächsten Zug")
|
||||
g.begin_turn(h)
|
||||
check(wolf.ready and wolf.can_attack(), "nach einer Runde angriffsbereit")
|
||||
var e := put(o, "e_wall") # 0/4
|
||||
check(g.can_attack(wolf, o)[1] == "Erst die Wesen", "Leben erst ohne Wesen angreifbar")
|
||||
check(g.attack(wolf, e) == "" and e.health == 4 - 3 and wolf.health == 2, "Angriff: Schaden, kein Konter von 0 Angriff")
|
||||
check(g.can_attack(wolf, e)[1].ends_with("kann nicht angreifen"), "nur ein Angriff je Zug")
|
||||
g.begin_turn(o)
|
||||
check(e.health == 4, "zu Zugbeginn erholt sich das Wesen seines Besitzers ganz")
|
||||
check(e.health == 1, "keine Heilung zu Zugbeginn")
|
||||
g.begin_turn(h)
|
||||
var life := o.life
|
||||
g.attack(wolf, e)
|
||||
check(e.health == 1 and o.board.size() == 1, "Angriff: wieder nur 3 Schaden")
|
||||
e.health = 1
|
||||
check(o.board.is_empty() and g.log.any(func(l): return l == "Steinwall stirbt.") and o.life == life, "Wesen stirbt, nichts schlägt durch")
|
||||
g.begin_turn(h)
|
||||
g.attack(wolf, e)
|
||||
check(o.board.is_empty() and g.log.any(func(l): return l == "Steinwall stirbt."), "Wesen stirbt, Überschuss schlägt durch")
|
||||
g.begin_turn(h)
|
||||
var life0 := o.life
|
||||
check(g.attack(wolf, o) == "" and o.life == life0 - 3, "ohne Wesen trifft es den Helden")
|
||||
check(g.attack(wolf, o) == "" and o.life == life - 3, "ohne Wesen trifft es den Helden")
|
||||
# Gegenschlag
|
||||
var troll := put(o, "e_troll") # 3/6
|
||||
g.begin_turn(h)
|
||||
g.attack(wolf, troll)
|
||||
check(troll.health == 3 and wolf.health <= 0 and not (wolf in h.board), "Konter tötet den Wolf")
|
||||
# Durchschlag: was über das Leben des Wesens hinausgeht, trifft den Besitzer
|
||||
var drache := put(h, "f_drache") # 6/5
|
||||
var kobold := put(o, "f_kobold") # 2/1
|
||||
var life := o.life
|
||||
g.attack(drache, kobold)
|
||||
check(not (kobold in o.board) and o.life == life - 5 and g.log.any(func(l): return l == "5 Schaden schlagen durch auf Gegner."), "Durchschlag: 6 gegen 1 Leben, 5 gehen durch")
|
||||
var sch := put(o, "f_kobold")
|
||||
sch.shield = true
|
||||
life = o.life
|
||||
g.begin_turn(h)
|
||||
g.attack(drache, sch)
|
||||
check(sch.health == 1 and o.life == life, "Schild blockt alles, nichts schlägt durch")
|
||||
g.begin_turn(o)
|
||||
check(troll.health == 3, "der Troll bleibt verletzt")
|
||||
|
||||
|
||||
func test_elementvorteil() -> void:
|
||||
@@ -242,7 +219,7 @@ func test_elementvorteil() -> void:
|
||||
var wolf := put(o, "f_wolf") # Feuer 3/2
|
||||
var life: int = o.life
|
||||
g.attack(tiefen, wolf)
|
||||
check(wolf.health <= 0 and o.life == life - 3 and tiefen.health == 6 - 3, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter bleibt gleich")
|
||||
check(wolf.health <= 0 and o.life == life and tiefen.health == 6 - 3, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter bleibt gleich, nichts schlägt durch")
|
||||
var s3 := setup()
|
||||
var w3 := put(s3[1], "f_wolf")
|
||||
var t3 := put(s3[2], "w_tiefen")
|
||||
@@ -255,7 +232,8 @@ func test_elementvorteil() -> void:
|
||||
check(s2[2].life == 13, "gegen Helden gibt es keinen Vorteil")
|
||||
|
||||
|
||||
## Verdeckt: greift nicht an, zählt nicht für die Elementregel, wird beim Angriff aufgedeckt.
|
||||
## Verdeckt: greift nicht an, zählt nicht für die Elementboni, darf erst nach einer Runde aufgedeckt
|
||||
## werden, greift dann sofort an; ein Angriff deckt es auf.
|
||||
func test_verdeckt() -> void:
|
||||
var s := setup(["f_wolf", "f_kobold"])
|
||||
var g: Game = s[0]
|
||||
@@ -265,12 +243,13 @@ func test_verdeckt() -> void:
|
||||
check(g.play_card(h, h.hand[0], true) == "" and h.board[0].hidden and h.energy == mana - C("f_wolf").cost, "verdeckt gelegt, gleich teuer")
|
||||
var wolf: Game.Creature = h.board[0]
|
||||
check(g.log.back() == "Spieler legt ein Wesen verdeckt." and g.events.back().hidden, "Verlauf verrät die Karte nicht")
|
||||
check(g.reveal(h, wolf) == "Flammenwolf kann erst im nächsten Zug aufgedeckt werden" and wolf.hidden, "im Legezug nicht aufdecken")
|
||||
g.begin_turn(h)
|
||||
check(not wolf.can_attack() and g.can_attack(wolf, o)[1] == "Flammenwolf liegt verdeckt", "verdeckt greift nicht an")
|
||||
check(g.reveal(h, wolf) == "" and not wolf.hidden and wolf.can_attack(), "aufgedeckt: kann sofort angreifen")
|
||||
check(g.reveal(h, wolf) != "", "offen bleibt offen")
|
||||
# Der Gegner deckt beim Angriff auf
|
||||
var kobold := put(o, "f_kobold", true, true)
|
||||
var kobold := put(o, "f_kobold", false, true)
|
||||
o.hand = []
|
||||
check(g.attack(wolf, kobold) == "" and not kobold.hidden and kobold.health <= 0, "Angriff deckt auf und tötet")
|
||||
check(g.log.any(func(l): return l == "Feuerkobold wird aufgedeckt."), "Aufdecken steht im Verlauf")
|
||||
@@ -288,45 +267,104 @@ func test_verdeckt() -> void:
|
||||
check(own.card == "f_asche", "der Besitzer sieht seine verdeckte Karte")
|
||||
|
||||
|
||||
## Vier offene Wesen eines Elements: +1/+1 für das Element, -1/-1 für das, das es schlägt.
|
||||
func test_vier_elemente() -> void:
|
||||
## Elementboni: zwei eigene offene Wesen eines Elements geben den Sondereffekt (Erde Spott, Luft
|
||||
## Flink, Wasser Regeneration, Feuer Doppelschlag); drei auf dem Tisch geben dem Element +1/+1.
|
||||
func test_elementboni() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
var fire := []
|
||||
for i in 3:
|
||||
fire.append(put(h, "f_wolf"))
|
||||
var fire := [put(h, "f_wolf"), put(h, "f_kobold")]
|
||||
var bird := put(o, "l_wind") # Luft 1/1
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 0 and bird.aura == 0, "drei reichen nicht")
|
||||
var hidden_fire := put(o, "f_kobold", true, true)
|
||||
check(fire[0].aura == 0 and fire[1].aura == 0, "zwei reichen nicht für +1/+1")
|
||||
check(fire[0].has("Doppelschlag") and fire[0].attacks_per_turn() == 2 and fire[0].keywords() == ["Doppelschlag"], "zwei eigene Feuer: Doppelschlag für beide")
|
||||
var hidden_fire := put(o, "f_asche", true, true)
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 0, "verdeckte zählen nicht")
|
||||
check(g.element_counts()["Feuer"] == 3, "Zähler ohne Verdeckte")
|
||||
var fourth := put(o, "f_asche") # Feuer beim Gegner zählt auch
|
||||
check(fire[0].aura == 0 and g.element_counts()["Feuer"] == 2 and not hidden_fire.has("Doppelschlag"), "verdeckte zählen nicht und bekommen nichts")
|
||||
var third := put(o, "f_ritter") # Feuer beim Gegner zählt für den Tisch
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and fourth.aura == 1, "vier Feuer: +1/+1 für alle Feuerwesen")
|
||||
check(bird.aura == -1 and bird.power() == 0 and bird.health == 0, "Luft wird geschlagen: -1/-1")
|
||||
g.cleanup()
|
||||
check(not (bird in o.board), "das 1/1 ist daran gestorben")
|
||||
g.reveal(o, hidden_fire) if g.current() == o else g._flip(hidden_fire)
|
||||
check(hidden_fire.aura == 1, "aufgedeckt zählt es und bekommt den Bonus sofort")
|
||||
o.board.erase(fourth)
|
||||
check(fire[0].aura == 1 and fire[0].power() == 4 and fire[0].health == 3 and third.aura == 1, "drei Feuer auf dem Tisch: +1/+1 für alle Feuerwesen")
|
||||
check(bird.aura == 0 and bird.health == 1, "kein Malus für das geschlagene Element")
|
||||
check(not third.has("Doppelschlag"), "der Gegner hat nur ein offenes Feuer: kein Sondereffekt")
|
||||
g._flip(hidden_fire)
|
||||
check(hidden_fire.aura == 1 and third.has("Doppelschlag") and hidden_fire.has("Doppelschlag"), "aufgedeckt zählt es sofort: Bonus und Paar")
|
||||
fire[0].health = 1
|
||||
o.board.erase(third)
|
||||
o.board.erase(hidden_fire)
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 0 and fire[0].health == 2, "Bonus geht wieder, Leben folgt")
|
||||
check(fire[0].aura == 0 and fire[0].max_health == 2 and fire[0].health == 1 and fire[1].health == 1, "Bonus geht wieder: nur das Höchstleben sinkt, Leben wird höchstens gekappt")
|
||||
# Vorschau
|
||||
check(g.forecast(h, C("f_kobold")) == 1 and g.forecast(h, C("l_wind")) == 0 and g.forecast(h, C("w_geist")) == 0, "Vorschau: Feuer würde den Stapel voll machen")
|
||||
check(g.forecast(h, C("f_asche")) == 1 and g.forecast(h, C("l_wind")) == 0, "Vorschau: Feuer würde den Stapel voll machen")
|
||||
# Merkmal: doppelt
|
||||
h.perks = ["element_doppelt"]
|
||||
put(h, "f_kobold")
|
||||
put(h, "f_asche")
|
||||
g.apply_auras()
|
||||
check(fire[0].aura == 2 and fire[0].power() == 5, "Elementbonus zählt doppelt")
|
||||
check(g.forecast(h, C("l_wind")) == -2, "Vorschau: Luft bekäme jetzt -2/-2")
|
||||
h.perks = []
|
||||
# Die anderen Paare
|
||||
var walls := [put(h, "e_wall"), put(h, "e_wall")]
|
||||
check(walls[0].has("Spott") and not fire[0].has("Spott"), "zwei Erde: Spott nur für Erdwesen")
|
||||
var winds := [put(h, "l_wind", false), put(h, "l_wind", false)]
|
||||
check(winds[0].has("Flink") and winds[0].can_attack(), "zwei Luft: Flink, frisch gelegt greift an")
|
||||
var geister := [put(h, "w_geist"), put(h, "w_geist")]
|
||||
check(geister[0].has("Regeneration"), "zwei Wasser: Regeneration")
|
||||
# Verschmolzene Wesen zählen für beide Elemente und bekommen beide Effekte und Boni
|
||||
var s2 := setup()
|
||||
var g2: Game = s2[0]
|
||||
var h2: Game.Player = s2[1]
|
||||
var dampf := put(h2, "f_wolf")
|
||||
dampf.card = Cards.fused(C("f_wolf"), C("w_geist"))
|
||||
var fw := put(h2, "f_kobold")
|
||||
var ww := put(h2, "w_geist")
|
||||
g2.apply_auras()
|
||||
check(dampf.has("Doppelschlag") and dampf.has("Regeneration") and dampf.keywords() == ["Regeneration", "Doppelschlag"], "Dampf hat beide Sondereffekte")
|
||||
check(fw.has("Doppelschlag") and ww.has("Regeneration"), "und zählt für beide Paare")
|
||||
put(s2[2], "f_asche")
|
||||
put(s2[2], "w_reiter")
|
||||
g2.apply_auras()
|
||||
check(dampf.aura == 2 and fw.aura == 1 and ww.aura == 1, "drei Feuer und drei Wasser: Dampf bekommt +2/+2")
|
||||
|
||||
|
||||
## Verbessern: Eigenschaften für Mana, je Exemplar.
|
||||
## Spott zieht Angriffe auf sich; Regeneration heilt am Ende des eigenen Zugs.
|
||||
func test_spott_und_regeneration() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
var wolf := put(h, "f_wolf")
|
||||
var wall := put(o, "e_wall")
|
||||
var kobold := put(o, "f_kobold")
|
||||
wall.card.keywords.append("Spott")
|
||||
check(g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott" and g.can_attack(wolf, wall)[0], "Spott muss zuerst angegriffen werden")
|
||||
var secret := put(o, "e_troll", true, true)
|
||||
secret.card.keywords.append("Spott")
|
||||
check(not secret.has("Spott") and g.can_attack(wolf, kobold)[1] == "Erst die Wesen mit Spott", "verdeckter Spott zählt nicht")
|
||||
o.board.erase(secret)
|
||||
g.attack(wolf, wall)
|
||||
wall.health = 0
|
||||
g.cleanup()
|
||||
g.begin_turn(h)
|
||||
check(g.can_attack(wolf, kobold)[0], "ohne Spötter ist alles angreifbar")
|
||||
# Regeneration
|
||||
var s2 := setup()
|
||||
var g2: Game = s2[0]
|
||||
var h2: Game.Player = s2[1]
|
||||
var reg := put(h2, "w_tiefen")
|
||||
reg.card.keywords.append("Regeneration")
|
||||
var plain := put(h2, "e_troll")
|
||||
var enemy := put(s2[2], "w_reiter")
|
||||
enemy.card.keywords.append("Regeneration")
|
||||
reg.health = 1
|
||||
plain.health = 1
|
||||
enemy.health = 1
|
||||
g2.end_turn()
|
||||
check(reg.health == reg.max_health and g2.log.any(func(l): return l == "Tiefenwächter regeneriert sich."), "Regeneration heilt am Zugende ganz")
|
||||
check(plain.health == 1 and enemy.health == 1, "ohne Regeneration und beim Gegner heilt nichts")
|
||||
check(g2.events.any(func(e): return e.type == "heal" and e.uid == reg.uid), "Ereignis fürs Heilen")
|
||||
|
||||
|
||||
## Verbessern: Eigenschaften für Mana, je Exemplar, auf Hand und Brett.
|
||||
func test_verbessern() -> void:
|
||||
var s := setup(["f_wolf"])
|
||||
var g: Game = s[0]
|
||||
@@ -334,24 +372,26 @@ func test_verbessern() -> void:
|
||||
var o: Game.Player = s[2]
|
||||
h.energy = 20
|
||||
var card: Cards.Card = h.hand[0]
|
||||
check(g.upgrade(h, card, "Schild") == "" and h.energy == 20 - 3 and "Schild" in h.hand[0].keywords, "Schild für die Handkarte gekauft")
|
||||
check(g.upgrade(h, card, "Spott") == "" and h.energy == 20 - 2 and "Spott" in h.hand[0].keywords, "Spott für die Handkarte gekauft")
|
||||
check(h.hand[0] != card and Cards.by_id("f_wolf").keywords.is_empty(), "eigenes Exemplar, der Pool bleibt sauber")
|
||||
check(g.upgrade(h, h.hand[0], "Schild") != "", "nicht doppelt")
|
||||
check(g.upgrade(h, h.hand[0], "Spott") != "", "nicht doppelt")
|
||||
g.play_card(h, h.hand[0])
|
||||
var wolf: Game.Creature = h.board[0]
|
||||
var after_play := h.energy
|
||||
check(wolf.shield and wolf.can_attack(), "mit Schild gelegt, greift sofort an")
|
||||
check(g.upgrade(h, wolf, "Doppelschlag") == "" and wolf.attacks_per_turn() == 2 and wolf.attacks_left == 2 and h.energy == after_play - 4, "Doppelschlag: zwei Angriffe")
|
||||
check(wolf.card.text() == "Schild, Doppelschlag.", "Kartentext zeigt die Eigenschaften")
|
||||
check(g.upgrade(h, wolf, "Doppelschlag") != "" and g.upgrade(h, put(o, "e_wall"), "Schild") != "" and g.upgrade(h, wolf, "Flink") != "", "nicht doppelt, nicht fremd, nicht unbekannt")
|
||||
h.energy = 2
|
||||
check(wolf.has("Spott") and not wolf.can_attack(), "mit Spott gelegt, greift noch nicht an")
|
||||
check(g.upgrade(h, wolf, "Flink") == "" and wolf.can_attack() and h.energy == after_play - 2, "Flink: greift sofort an")
|
||||
check(g.upgrade(h, wolf, "Doppelschlag") == "" and wolf.attacks_per_turn() == 2 and wolf.attacks_left() == 2 and h.energy == after_play - 6, "Doppelschlag: zwei Angriffe")
|
||||
check(g.upgrade(h, wolf, "Regeneration") == "" and h.energy == after_play - 9, "Regeneration kostet 3")
|
||||
check(wolf.card.text() == "Spott, Flink, Regeneration, Doppelschlag.", "Kartentext zeigt die Eigenschaften")
|
||||
check(g.upgrade(h, wolf, "Doppelschlag") != "" and g.upgrade(h, put(o, "e_wall"), "Spott") != "" and g.upgrade(h, wolf, "Schild") != "", "nicht doppelt, nicht fremd, nicht unbekannt")
|
||||
h.energy = 1
|
||||
var k2 := put(h, "f_kobold")
|
||||
check(g.upgrade(h, k2, "Schild") == "Nicht genug Mana", "zu teuer")
|
||||
check(g.upgrade(h, k2, "Spott") == "Nicht genug Mana", "zu teuer")
|
||||
h.perks = ["verbessern_billig"]
|
||||
check(g.upgrade(h, k2, "Schild") == "" and h.energy == 0, "Merkmal: Eigenschaften kosten 1 weniger")
|
||||
check(g.upgrade(h, k2, "Spott") == "" and h.energy == 0, "Merkmal: Eigenschaften kosten 1 weniger")
|
||||
|
||||
|
||||
## Verschmelzen: der bessere Wert von beidem plus 1, Kosten 1, 2, 4, 8 …
|
||||
## Verschmelzen: besserer Wert plus ein Drittel des schwächeren, Eigenschaften beider, Kosten 1, 2, 4, 8 …; auch auf der Hand.
|
||||
func test_verschmelzen() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
@@ -359,46 +399,60 @@ func test_verschmelzen() -> void:
|
||||
var a := put(h, "f_wolf") # 3/2
|
||||
var b := put(h, "w_tiefen") # 4/6
|
||||
b.health = 4
|
||||
a.card.keywords.append("Schild")
|
||||
a.card.keywords.append("Spott")
|
||||
check(Game.fusion_cost(a, b) == 1, "erste Verschmelzung kostet 1")
|
||||
check(g.fuse(h, a, b) == "" and h.board.size() == 1 and h.energy == 9, "verschmolzen")
|
||||
var f: Game.Creature = h.board[0]
|
||||
check(f.name == "Dampf" and f.attack == 5 and f.max_health == 7 and f.health == 5, "Dampf mit dem besseren Angriff und Leben plus 1")
|
||||
check("Schild" in f.card.keywords and f.card.fusions == 1 and f.card.cost == Cards.cost_of(5, 7), "Eigenschaften geerbt, einmal verschmolzen")
|
||||
check(f.card.elements == ["Feuer", "Wasser"], "beide Elemente")
|
||||
var c := put(h, "e_wall")
|
||||
check(f.name == "Dampf" and f.attack == 5 and f.max_health == 7 and f.health == 5, "Dampf: 4+1 Angriff, 6+1 Leben, aktuell 4+1")
|
||||
check("Spott" in f.card.keywords and f.card.fusions == 1 and f.card.cost == 5, "Eigenschaften geerbt, einmal verschmolzen, Kosten 5")
|
||||
check(f.card.elements == ["Feuer", "Wasser"] and f.ready, "beide Elemente, beide waren bereit")
|
||||
var c := put(h, "e_wall", false)
|
||||
check(Game.fusion_cost(f, c) == 2, "zweite Verschmelzung kostet 2")
|
||||
g.fuse(h, f, c)
|
||||
var f2: Game.Creature = h.board[0]
|
||||
check(f2.name == "Chimäre" and f2.card.fusions == 2 and h.energy == 7, "Chimäre aus drei Elementen")
|
||||
check(f2.name == "Chimäre" and f2.card.fusions == 2 and h.energy == 7 and not f2.ready, "Chimäre aus drei Elementen, nicht bereit, weil der Wall es nicht war")
|
||||
var d := put(h, "l_wind")
|
||||
check(Game.fusion_cost(f2, d) == 4, "dritte kostet 4")
|
||||
h.energy = 3
|
||||
check(g.fuse(h, f2, d) == "Nicht genug Mana", "zu teuer")
|
||||
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Wesen wählen", "nicht mit sich selbst")
|
||||
check(g.can_fuse(h, f2, f2)[1] == "Zwei verschiedene Karten wählen", "nicht mit sich selbst")
|
||||
# Verdeckte verschmelzen offen
|
||||
var s2 := setup()
|
||||
var x := put(s2[1], "f_wolf", true, true)
|
||||
var y := put(s2[1], "f_kobold", true, true)
|
||||
s2[0].fuse(s2[1], x, y)
|
||||
check(not s2[1].board[0].hidden, "das Ergebnis liegt offen")
|
||||
# Elementbonus der Eltern zählt nicht doppelt; Dampf nimmt den besseren Elementwert
|
||||
check(not s2[1].board[0].hidden and s2[1].board[0].name == "Inferno", "das Ergebnis liegt offen")
|
||||
# Elementbonus der Eltern zählt nicht doppelt; Dampf bekommt die Boni beider Elemente
|
||||
var s3 := setup()
|
||||
var g3: Game = s3[0]
|
||||
var h3: Game.Player = s3[1]
|
||||
var fa := put(h3, "f_wolf")
|
||||
var fb := put(h3, "w_tiefen")
|
||||
for i in 3:
|
||||
for i in 2:
|
||||
put(h3, "f_kobold")
|
||||
g3.apply_auras()
|
||||
check(fa.aura == 1 and fa.max_health == 3, "vier Feuer vor dem Verschmelzen")
|
||||
check(fa.aura == 1 and fa.max_health == 3, "drei Feuer vor dem Verschmelzen")
|
||||
g3.fuse(h3, fa, fb)
|
||||
var ff: Game.Creature = h3.board[0]
|
||||
check(ff.aura == 1 and ff.max_health == 8 and ff.health == 8 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal")
|
||||
for i in 3:
|
||||
check(ff.aura == 1 and ff.max_health == 8 and ff.health == 8 and ff.power() == 6, "nach dem Verschmelzen zählt der Bonus nur einmal (7 + 1)")
|
||||
for i in 2:
|
||||
put(h3, "w_geist")
|
||||
g3.apply_auras()
|
||||
check(g3.element_counts()["Wasser"] == 4 and ff.aura == 1, "Wasser +1 und Feuer -1: Dampf nimmt den besseren Wert")
|
||||
check(g3.element_counts()["Wasser"] == 3 and ff.aura == 2 and ff.max_health == 9, "Feuer und Wasser voll: Dampf bekommt +2/+2")
|
||||
# Auf der Hand
|
||||
var s4 := setup(["f_wolf", "w_tiefen", "l_wind"])
|
||||
var g4: Game = s4[0]
|
||||
var h4: Game.Player = s4[1]
|
||||
var hand_a: Cards.Card = h4.hand[0]
|
||||
var hand_b: Cards.Card = h4.hand[1]
|
||||
check(g4.can_fuse(h4, hand_a, put(h4, "e_wall"))[1] == "Nur Hand mit Hand oder Brett mit Brett", "Hand und Brett mischen sich nicht")
|
||||
check(g4.fuse(h4, hand_a, hand_b) == "" and h4.hand.size() == 2 and h4.energy == 9, "zwei Handkarten verschmolzen, 1 Mana")
|
||||
var hc: Cards.Card = h4.hand[0]
|
||||
check(hc.name == "Dampf" and hc.attack == 5 and hc.health == 7 and hc.cost == 5 and hc.fusions == 1, "Dampf auf der Hand mit 5/7 und Kosten 5")
|
||||
var lava := Cards.fused(C("f_asche"), C("f_kobold"), 4, 8)
|
||||
check(Cards.fused(lava, C("f_kobold")).attack == 5 and Cards.fused(lava, C("f_kobold")).health == 9, "ein Kobold macht Lava 4/8 zu 5/9, nie schlechter")
|
||||
check(g4.play_card(h4, hc) == "" and h4.energy == 4 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
|
||||
check(g4.log.any(func(l): return l == "Flammenwolf und Tiefenwächter verschmelzen auf der Hand zu Dampf (5/7)."), "Verlauf")
|
||||
|
||||
|
||||
func test_limits() -> void:
|
||||
@@ -422,7 +476,7 @@ func test_gleiche_regeln() -> void:
|
||||
var g := Game.new_battle(r)
|
||||
var first: Game.Player = g.players[0]
|
||||
check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 5 Mana, kein Bonus")
|
||||
check(first.offer.size() == Game.START_OFFER and first.hand.is_empty(), "jeder beginnt mit einer Wahl aus fünf und leerer Hand")
|
||||
check(g.players.all(func(p): return p.hand.size() == Game.START_HAND), "jeder beginnt mit fünf Karten")
|
||||
for p in g.players:
|
||||
check(p.life == Game.START_LIFE, "gleiches Leben")
|
||||
if p != first:
|
||||
@@ -474,7 +528,7 @@ func test_booster() -> void:
|
||||
for i in 50:
|
||||
var rolled := Profile.roll_booster(r)
|
||||
check(rolled.size() == 5, "5 Karten")
|
||||
check(rolled.any(func(x): return x.rarity != "gewöhnlich"), "mindestens eine seltene")
|
||||
check(rolled.any(func(x): return x.rarity != "Common"), "mindestens eine Rare")
|
||||
var p := Profile.new()
|
||||
p.coins = Profile.BOOSTER_COST
|
||||
var got := p.open_booster(r)
|
||||
@@ -490,7 +544,23 @@ func test_booster() -> void:
|
||||
p.collection["f_wolf"] = Cards.RARITIES.size() - 1
|
||||
check(p.upgrade("f_wolf") != "", "über die höchste Stufe hinaus geht nicht")
|
||||
p.collection.erase("f_wolf")
|
||||
check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "gewöhnlich", "gewöhnliche Karten gehören allen")
|
||||
check(p.dismantle("f_wolf") != "" and p.rarity("f_wolf") == "Common", "Common-Karten gehören allen")
|
||||
check(Profile.RARITY_WEIGHTS.values().reduce(func(a, b): return a + b, 0.0) == 100.0, "Chancen ergeben 100 %")
|
||||
check(Profile.DUST_UPGRADE["Rare"] == 100 and Profile.DUST_UPGRADE["Ultimate Rare"] == 1000 and Profile.DUST_DISMANTLE["Common"] == 50 and Profile.DUST_DISMANTLE["Ultimate Rare"] == 1000, "Staubpreise")
|
||||
# Verschmolzene Karten tragen die höhere Stufe der Eltern
|
||||
var s := setup(["f_wolf", "w_tiefen"])
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
h.looks = {"w_tiefen": 3}
|
||||
check(h.hand[0].rarity_in(h.looks) == "Common" and h.hand[1].rarity_in(h.looks) == "Ultra Rare", "Stufe aus der Sammlung")
|
||||
g.fuse(h, h.hand[0], h.hand[1])
|
||||
check(h.hand[0].look == 3 and h.hand[0].rarity_in(h.looks) == "Ultra Rare", "Fusion auf der Hand: die höhere Stufe gewinnt")
|
||||
var kobold := put(h, "f_kobold")
|
||||
g.play_card(h, h.hand[0])
|
||||
var fused: Game.Creature = h.board[1]
|
||||
g.fuse(h, fused, kobold)
|
||||
check(h.board[0].card.look == 3 and h.board[0].card.rarity_in(h.looks) == "Ultra Rare", "auch auf dem Brett und über zwei Fusionen")
|
||||
check(Cards.Card.from_dict(h.board[0].card.to_dict()).look == 3, "die Stufe reist mit der Karte")
|
||||
|
||||
|
||||
func test_spielstand() -> void:
|
||||
@@ -500,7 +570,7 @@ func test_spielstand() -> void:
|
||||
if FileAccess.file_exists(path + suffix):
|
||||
DirAccess.remove_absolute(abs + suffix)
|
||||
var p := Profile.load_or_new(path)
|
||||
check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten gewöhnlich")
|
||||
check(p.coins == Profile.START_COINS and p.collection.is_empty(), "Starterprofil: alle Karten Common")
|
||||
check(FileAccess.file_exists(path), "Datei angelegt")
|
||||
p.coins = 999
|
||||
p.collection["f_wolf"] = 2
|
||||
@@ -525,8 +595,8 @@ func test_kopie() -> void:
|
||||
check(g2.players[1].life == 11 and o.life == 15, "Kopie verändert das Original nicht")
|
||||
g2.play_card(g2.players[0], g2.players[0].hand[0])
|
||||
check(h.hand.size() == 1 and g2.players[0].hand.is_empty(), "Hand der Kopie unabhängig")
|
||||
g2.upgrade(g2.players[0], a2, "Schild")
|
||||
check(not a.shield and a.card.keywords.is_empty(), "Verbessern in der Kopie lässt das Original unberührt")
|
||||
g2.upgrade(g2.players[0], a2, "Spott")
|
||||
check(not a.has("Spott") and a.card.keywords.is_empty() and a2.has("Spott"), "Verbessern in der Kopie lässt das Original unberührt")
|
||||
check(g2.log.is_empty() and g2.events.is_empty(), "Kopie schreibt keinen Verlauf")
|
||||
|
||||
|
||||
@@ -667,16 +737,15 @@ func test_merkmale() -> void:
|
||||
var s := setup(["f_wolf", "l_falke"])
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
h.perks = ["feuer_angriff", "wesen_angriff", "wesen_leben", "schild_wesen", "ziehen_gratis"]
|
||||
h.perks = ["feuer_angriff", "wesen_angriff", "wesen_leben", "alle_flink", "ziehen_gratis"]
|
||||
g.play_card(h, h.hand[0])
|
||||
var wolf: Game.Creature = h.board[0]
|
||||
check(wolf.power() == 3 + 1 + 1 and wolf.health == 3 and wolf.shield and wolf.can_attack(), "Feuer +1, Wesen +1/+1, Schild")
|
||||
check(wolf.power() == 3 + 1 + 1 and wolf.health == 3 and wolf.has("Flink") and wolf.can_attack(), "Feuer +1, Wesen +1/+1, flink")
|
||||
g.play_card(h, h.hand[0])
|
||||
check(h.board[1].power() == 3 + 1, "Luft: nur Wesen +1")
|
||||
g.deck = DeckGen.deck(g.rng)
|
||||
var mana := h.energy
|
||||
check(g.draw(h) == "" and h.energy == mana, "Merkmal: Ziehen kostet nichts")
|
||||
g.pick(h, 0)
|
||||
check(g.draw(h) == "" and h.energy == mana and h.hand.size() == 1, "Merkmal: Ziehen kostet nichts")
|
||||
for perk in Game.PERKS:
|
||||
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
|
||||
var r := RandomNumberGenerator.new()
|
||||
@@ -695,17 +764,18 @@ func test_merkmale() -> void:
|
||||
|
||||
|
||||
func test_ki_zwei_schritte() -> void:
|
||||
# Gegner hat 5 Leben und kein Wesen; der Wüstenkönig (5 Angriff) liegt auf der Hand:
|
||||
# erst ausspielen, dann mit ihm töten.
|
||||
var s := setup(["el_koenig"])
|
||||
# Gegner hat 5 Leben und kein Wesen; der Wüstenkönig (5 Angriff) liegt frisch auf dem Brett:
|
||||
# erst Flink kaufen, dann mit ihm töten.
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
h.turns = 2
|
||||
o.life = 5
|
||||
var king := put(h, "el_koenig", false)
|
||||
put(h, "f_wolf", false)
|
||||
var act := AI.choose_action(g, h)
|
||||
check(act.type == "play" and act.card == h.hand[0] and not act.face_down, "KI spielt zuerst den König, offen")
|
||||
check(act.type == "upgrade" and act.creature == king and act.keyword == "Flink", "KI kauft zuerst Flink für den König")
|
||||
AI.apply_action(g, h, act)
|
||||
var act2 := AI.choose_action(g, h)
|
||||
check(act2.type == "attack" and act2.target == o, "dann tödlicher Angriff")
|
||||
@@ -718,26 +788,26 @@ func test_snapshot() -> void:
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2]
|
||||
g.upgrade(h, h.hand[0], "Schild")
|
||||
g.upgrade(h, h.hand[0], "Spott")
|
||||
g.play_card(h, h.hand[0])
|
||||
put(o, "e_troll")
|
||||
var troll := put(o, "e_troll")
|
||||
troll.attacks_done = 1
|
||||
var hi := g.players.find(h)
|
||||
var oi := g.players.find(o)
|
||||
h.energy = 5
|
||||
o.energy = 5
|
||||
g.deck = DeckGen.deck(g.rng)
|
||||
g.draw(h)
|
||||
o.offer = g.draw_cards(3, 5)
|
||||
o.picks_left = 1
|
||||
var snap := g.snapshot(hi)
|
||||
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
||||
var g2 := Game.from_snapshot(json)
|
||||
check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx and g2.deck.size() == g.deck.size(), "Grunddaten und Stapelgröße")
|
||||
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar")
|
||||
check(g2.players[hi].hand.size() == 1 and g2.players[hi].hand[0].id == h.hand[0].id, "eigene Hand sichtbar")
|
||||
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0].id != "w_frost", "fremde Hand nur als Anzahl")
|
||||
check(g2.players[oi].offer.is_empty() and g2.players[hi].offer.size() == Game.OFFER_SIZE and g2.players[hi].picks_left == 1, "eigene Wahl sichtbar, fremde nicht")
|
||||
var cr2 := g2.find_creature(h.board[0].uid)
|
||||
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and cr2.shield and "Schild" in cr2.card.keywords, "Wesen mit Besitzer, Zustand und gekaufter Eigenschaft")
|
||||
check(cr2 != null and cr2.card.id == "f_wolf" and cr2.owner() == g2.players[hi] and cr2.has("Spott") and not cr2.ready, "Wesen mit Besitzer, Zustand und gekaufter Eigenschaft")
|
||||
var troll2 := g2.find_creature(troll.uid)
|
||||
check(troll2.ready and troll2.attacks_done == 1, "Bereitschaft und Angriffe reisen mit")
|
||||
check(g2.events.size() == g.events.size() and g2.events.back().type == g.events.back().type, "Ereignisse übertragen")
|
||||
check(g2.draw(g2.players[oi]) == "Nicht dein Zug", "Kopie kennt die Regeln")
|
||||
var snap_o := g.snapshot(oi)
|
||||
@@ -773,9 +843,6 @@ func test_ki_zeitbudget() -> void:
|
||||
r.seed = 12
|
||||
var g := Game.new_battle(r)
|
||||
var h: Game.Player = g.players[0]
|
||||
for p in g.players:
|
||||
p.offer = []
|
||||
p.picks_left = 0
|
||||
h.energy = 30
|
||||
for i in Game.MAX_HAND:
|
||||
h.hand.append(C("f_ritter"))
|
||||
@@ -796,11 +863,11 @@ func test_ki_zeitbudget() -> void:
|
||||
func test_englische_karten() -> void:
|
||||
check(Cards.all().all(func(c): return c.name_en != ""), "alle Karten mit englischem Namen")
|
||||
var ritter := C("f_ritter")
|
||||
ritter.keywords = ["Schild", "Doppelschlag"]
|
||||
ritter.keywords = ["Spott", "Doppelschlag"]
|
||||
TranslationServer.set_locale("en")
|
||||
check(ritter.title() == "Blazing Knight" and ritter.text() == "Shield, Double Strike.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()])
|
||||
check(ritter.title() == "Blazing Knight" and ritter.text() == "Taunt, Double Strike.", "Ritter auf Englisch: %s / %s" % [ritter.title(), ritter.text()])
|
||||
TranslationServer.set_locale("de")
|
||||
check(ritter.title() == "Lodernder Ritter" and ritter.text() == "Schild, Doppelschlag.", "zurück auf Deutsch")
|
||||
check(ritter.title() == "Lodernder Ritter" and ritter.text() == "Spott, Doppelschlag.", "zurück auf Deutsch")
|
||||
|
||||
|
||||
## Voller Spielstand (viewer -1) stellt Hände und Bretter wieder her; Zuschauer (-2) sehen nichts Verborgenes.
|
||||
@@ -826,7 +893,7 @@ func test_kampf_speichern() -> void:
|
||||
guard += 1
|
||||
check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar")
|
||||
var s2: Dictionary = g.snapshot(-2)
|
||||
check(s2.players.all(func(p): return not (p.hand is Array) and p.offer.is_empty()), "Zuschauer-Sicht ohne Hände und Wahlen")
|
||||
check(s2.players.all(func(p): return not (p.hand is Array)), "Zuschauer-Sicht ohne Hände")
|
||||
|
||||
|
||||
## Jeder tr("...")-Text in der Oberfläche hat einen englischen Eintrag.
|
||||
@@ -843,7 +910,7 @@ func test_uebersetzung_vollstaendig() -> void:
|
||||
key = line.substr(0, line.find(","))
|
||||
keys[key] = true
|
||||
var missing := []
|
||||
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + Cards.KEYWORD_TEXT.values() + Game.PERKS.values():
|
||||
for k in Cards.COMBOS.values() + Cards.ELEMENTS + Cards.KEYWORDS + Cards.RARITIES + Cards.KEYWORD_TEXT.values() + Game.PERKS.values() + Cards.ELEMENT_KEYWORD.values():
|
||||
if not keys.has(k):
|
||||
missing.append(k)
|
||||
var d := DirAccess.open("res://scripts/ui")
|
||||
|
||||
218
tests/ui_test.gd
218
tests/ui_test.gd
@@ -54,11 +54,11 @@ func right_click() -> void:
|
||||
await right_click_at(Vector2(5, 5))
|
||||
|
||||
|
||||
func right_click_at(pos: Vector2) -> void:
|
||||
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = pos
|
||||
down.global_position = pos
|
||||
down.button_index = MOUSE_BUTTON_RIGHT
|
||||
down.button_index = button
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
@@ -219,20 +219,42 @@ func target_view(scr, valid: Array) -> Control:
|
||||
return null
|
||||
|
||||
|
||||
## 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
|
||||
## Zieht eine Karte oder Kachel auf eine andere und hält sie dort, bis die beiden verschmelzen.
|
||||
func drag_fuse(scr, from: Control, to: Control) -> void:
|
||||
await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
|
||||
await settle(scr)
|
||||
var a := from.get_global_rect().get_center()
|
||||
var b := to.get_global_rect().get_center()
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = a
|
||||
down.global_position = a
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
get_viewport().push_input(down)
|
||||
await frames(1)
|
||||
for i in range(1, 6):
|
||||
var m := InputEventMouseMotion.new()
|
||||
m.position = a.lerp(b, i / 5.0)
|
||||
m.global_position = m.position
|
||||
m.button_mask = MOUSE_BUTTON_MASK_LEFT
|
||||
get_viewport().push_input(m)
|
||||
await frames(1)
|
||||
check(scr.drag_ghost != null and scr.drag_target != null, "Karte schwebt über einem Verschmelzungsziel")
|
||||
var waited := 0
|
||||
while scr.drag_ghost != null and waited < 240:
|
||||
var still := InputEventMouseMotion.new() # die Maus bleibt auf dem Ziel, auch wenn die echte zuckt
|
||||
still.position = b
|
||||
still.global_position = b
|
||||
still.button_mask = MOUSE_BUTTON_MASK_LEFT
|
||||
get_viewport().push_input(still)
|
||||
await frames(1)
|
||||
waited += 1
|
||||
var up: InputEventMouseButton = down.duplicate()
|
||||
up.pressed = false
|
||||
up.position = b
|
||||
up.global_position = b
|
||||
get_viewport().push_input(up)
|
||||
await frames(2)
|
||||
|
||||
|
||||
## Legt, was bezahlbar ist.
|
||||
@@ -257,10 +279,9 @@ func human_turn(scr) -> void:
|
||||
if g.winner != "" or not g.can_draw(g.human)[0]:
|
||||
break
|
||||
var mana: int = g.human.energy
|
||||
var hand: int = g.human.hand.size()
|
||||
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")
|
||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
|
||||
for i in 12:
|
||||
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack())
|
||||
if ready.is_empty() or g.winner != "":
|
||||
@@ -284,7 +305,7 @@ func human_turn(scr) -> void:
|
||||
targets.append(c)
|
||||
await settle(scr)
|
||||
await click(target_view(scr, targets))
|
||||
check(scr.selected == null and cr.attacks_left < cr.attacks_per_turn(), "Angriff ausgeführt")
|
||||
check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
|
||||
if g.winner == "":
|
||||
await click(scr.b_end)
|
||||
|
||||
@@ -293,7 +314,6 @@ func start_battle() -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||
await click(find_button("Starten"))
|
||||
await pick_all(screen())
|
||||
|
||||
|
||||
func test_gegnerauswahl() -> void:
|
||||
@@ -310,12 +330,9 @@ func test_gegnerauswahl() -> void:
|
||||
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
||||
var opp: Game.Player = hard.game.opponent
|
||||
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
||||
check(hard.game.human.offer.size() == Game.START_OFFER and hard.game.human.hand.is_empty() and hard.pick_layer.visible,
|
||||
"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.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
|
||||
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA + 1), "alle fünf sind im ersten Zug spielbar")
|
||||
await shot("05_starthand")
|
||||
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
|
||||
await shot("11_kampf")
|
||||
await give_up()
|
||||
@@ -323,7 +340,6 @@ func test_gegnerauswahl() -> void:
|
||||
await click(find_button("Leicht"))
|
||||
await click(find_button("Starten"))
|
||||
var easy = screen()
|
||||
await pick_all(easy)
|
||||
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(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
|
||||
@@ -393,21 +409,21 @@ func test_sammlung() -> void:
|
||||
await frames(1)
|
||||
var phoenix = scr.grid.views().filter(func(v): return v.card.id == "ff_phoenix")[0]
|
||||
await click(phoenix)
|
||||
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "gewöhnlich", "Karte gewählt, noch gewöhnlich")
|
||||
check(scr.selected.id == "ff_phoenix" and p.rarity("ff_phoenix") == "Common", "Karte gewählt, noch Common")
|
||||
await click(scr.b_upgrade)
|
||||
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["selten"], "auf selten aufgewertet")
|
||||
check(p.level("ff_phoenix") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
|
||||
scr.search_box.text = ""
|
||||
scr.search_box.text_changed.emit("")
|
||||
await frames(1)
|
||||
var first: CardView = scr.grid.views()[0]
|
||||
check(first.card.id == "ff_phoenix" and first.rarity == "selten", "die aufgewertete Karte steht vorn in der Sammlung")
|
||||
var front: Array = scr.grid.views().slice(0, p.upgraded_count())
|
||||
check(front.all(func(v): return v.rarity != "Common") and front.any(func(v): return v.card.id == "ff_phoenix" and v.rarity == "Rare"), "aufgewertete Karten stehen vorn in der Sammlung, der Phönix als Rare dabei")
|
||||
scr.search_box.text = "Phönix"
|
||||
scr.search_box.text_changed.emit("Phönix")
|
||||
await frames(1)
|
||||
check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
|
||||
var dust: int = p.dust
|
||||
await click(scr.b_dismantle)
|
||||
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["selten"], "wieder zerlegt")
|
||||
check(p.level("ff_phoenix") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
|
||||
await shot("20_sammlung")
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
@@ -487,22 +503,21 @@ func test_kampf_steuerung() -> void:
|
||||
scr.ai_delay = 0.0
|
||||
await wait_for_human(scr)
|
||||
var g: Game = scr.game
|
||||
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")]
|
||||
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
|
||||
g.human.hand = [Cards.by_id("f_wolf").copy()]
|
||||
scr.refresh()
|
||||
check(scr.hand_views.size() == 1 and scr.deck.enabled and scr.pick_views.is_empty(), "eine Handkarte, Stapel bereit, keine Wahl offen")
|
||||
# Ziehen: 1 Mana weg, drei zur Wahl, eine landet auf der Hand
|
||||
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
|
||||
# Ziehen: 1 Mana weg, eine Karte landet auf der Hand
|
||||
var mana: int = g.human.energy
|
||||
await click(scr.deck)
|
||||
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")
|
||||
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
|
||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == 2 and scr.hand_views.size() == 2, "gezogen: eine Karte auf der Hand")
|
||||
check(g.human.hand[1].cost <= g.human.max_energy, "nur bezahlbare Karten")
|
||||
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
|
||||
var board_before: int = g.human.board.size()
|
||||
mana = g.human.energy
|
||||
await key(KEY_1)
|
||||
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")
|
||||
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
|
||||
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
|
||||
check(scr.b_undo.visible, "Rückgängig angeboten")
|
||||
await click(scr.b_undo)
|
||||
@@ -513,21 +528,16 @@ func test_kampf_steuerung() -> void:
|
||||
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
||||
# Ziehen per Taste
|
||||
mana = g.human.energy
|
||||
var hand_before: int = g.human.hand.size()
|
||||
await key(KEY_Z)
|
||||
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")
|
||||
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == hand_before + 1, "Z zieht")
|
||||
await shot("22_kampf_steuerung")
|
||||
# Gamepad: A auf dem Stapel zieht, Steuerkreuz wählt die zweite Karte, A nimmt sie
|
||||
# Gamepad: A auf dem Stapel zieht
|
||||
await pad(JOY_BUTTON_DPAD_LEFT)
|
||||
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()
|
||||
hand_before = g.human.hand.size()
|
||||
await pad(JOY_BUTTON_A)
|
||||
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")
|
||||
check(g.human.hand.size() == hand_before + 1, "Gamepad A zieht")
|
||||
scr.ai_delay = 0.75
|
||||
await key(KEY_SPACE)
|
||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||
@@ -562,7 +572,6 @@ func test_kampf_grenzen() -> void:
|
||||
while g.human.hand.size() < Game.MAX_HAND and guard < 30:
|
||||
guard += 1
|
||||
await click(scr.deck)
|
||||
await pick_all(scr)
|
||||
check(g.human.hand.size() == Game.MAX_HAND, "Hand auf %d gefüllt" % Game.MAX_HAND)
|
||||
check(not scr.deck.enabled, "bei voller Hand lässt sich nichts ziehen")
|
||||
await click(scr.deck)
|
||||
@@ -573,20 +582,17 @@ func test_kampf_grenzen() -> void:
|
||||
guard += 1
|
||||
if g.human.hand.is_empty():
|
||||
await click(scr.deck)
|
||||
await pick_all(scr)
|
||||
await key(KEY_1)
|
||||
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)
|
||||
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
|
||||
await shot("25_kampf_voll")
|
||||
# Verschmelzen macht wieder Platz
|
||||
# Verschmelzen per Ziehen macht wieder Platz
|
||||
var before: int = g.human.board.size()
|
||||
await click(scr.b_fuse)
|
||||
await click(scr.own_views[0])
|
||||
await click(scr.own_views[1])
|
||||
scr.hold_time = 0.3
|
||||
await drag_fuse(scr, scr.own_views[0], scr.own_views[1])
|
||||
check(g.human.board.size() == before - 1, "Verschmelzen macht einen Platz frei")
|
||||
# Ohne Mana kein Ziehen
|
||||
g.human.energy = 0
|
||||
@@ -596,12 +602,13 @@ func test_kampf_grenzen() -> void:
|
||||
await give_up()
|
||||
|
||||
|
||||
## Elementregel, verdeckt legen, Kartenmenü und Verschmelzen im Kampf.
|
||||
## Elementboni, verdeckt legen, Kartenmenü und Verschmelzen (per Ziehen und per Menü) im Kampf.
|
||||
func test_kampf_elemente_und_verschmelzen() -> void:
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampf gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
scr.hold_time = 0.3
|
||||
await wait_for_human(scr)
|
||||
var g: Game = scr.game
|
||||
g.human.board.clear()
|
||||
@@ -609,7 +616,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
||||
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
|
||||
cr.attacks_left = 1
|
||||
cr.ready = true
|
||||
cr.hidden = hidden
|
||||
p.board.append(cr)
|
||||
return cr
|
||||
@@ -620,56 +627,70 @@ func test_kampf_elemente_und_verschmelzen() -> void:
|
||||
var secret: Game.Creature = put.call(scr.opponent(), "f_kobold", true)
|
||||
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
|
||||
g.human.energy = 20
|
||||
g.human.looks = {"w_reiter": 3}
|
||||
g.apply_auras()
|
||||
scr.refresh()
|
||||
await frames(2)
|
||||
check(scr.element_view.text() == "Feuer 2 · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen: " + scr.element_view.text())
|
||||
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")
|
||||
# verdeckt legen über das Kartenmenü (Rechtsklick auf die Handkarte)
|
||||
var menu_text := "Verdeckt legen ◆ %d" % Cards.by_id("f_kobold").cost
|
||||
check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1 · Erde 0 · Luft 1", "Zähler ohne das verdeckte Wesen, Feuerpaar: " + scr.element_view.text())
|
||||
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret and scr.hand_views[0].bonus == 1, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken; der Kobold würde den Stapel voll machen")
|
||||
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen und Eigenschaften, ohne Verschmelzen
|
||||
await hover(scr.hand_views[0])
|
||||
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“")
|
||||
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ 1", scr.menu_layer) != null and find_button("Spott ◆ 2", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
|
||||
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
|
||||
await shot("23_kartenmenue")
|
||||
await click(find_button(menu_text, scr.menu_layer))
|
||||
await right_click()
|
||||
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
|
||||
# Mittlere Maustaste legt die Handkarte verdeckt
|
||||
var mana: int = g.human.energy
|
||||
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
|
||||
var mine_hidden: Game.Creature = g.human.board[3]
|
||||
check(mine_hidden.hidden and 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)
|
||||
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2"), "mittlere Maustaste legt verdeckt, zählt nicht")
|
||||
# Rechtsklick auf ein eigenes Wesen: Eigenschaften kaufen
|
||||
await right_click_at(scr.own_views[0].get_global_rect().get_center())
|
||||
check(scr.menu_layer.visible and find_button("Doppelschlag ◆ 4", scr.menu_layer) != null and find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "Wesen-Menü mit Eigenschaften, ohne Verschmelzen")
|
||||
var wolf: Game.Creature = g.human.board[0]
|
||||
await click(find_button("Spott ◆ %d" % Cards.KEYWORD_COST["Spott"], scr.menu_layer))
|
||||
check(wolf.has("Spott") and "Spott" in wolf.card.keywords and g.human.energy == mana - 1 - Cards.KEYWORD_COST["Spott"], "Spott gekauft")
|
||||
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")
|
||||
check(mine_hidden.hidden and scr.msg_label.text == "Feuerkobold kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
|
||||
mine_hidden.ready = true
|
||||
await click(scr.own_views[3])
|
||||
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (+1/+1)"), "aufgedeckt: drei Feuer, +1/+1: " + scr.element_view.text())
|
||||
check(wolf.power() == 4 and falke.power() == 3 and mine_hidden.can_attack(), "Feuerwesen +1, kein Malus für Luft, aufgedeckt greift sofort an")
|
||||
check(scr.hand_views[0].bonus == 0, "die Luftkarte auf der Hand zeigt keinen Bonus")
|
||||
await shot("23b_drei_feuer")
|
||||
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
|
||||
await click(scr.own_views[0])
|
||||
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
|
||||
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
|
||||
await click(scr.tiles[secret.uid])
|
||||
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
|
||||
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
|
||||
await right_click()
|
||||
# Verschmelzen
|
||||
await click(scr.b_fuse)
|
||||
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
||||
await click(scr.own_views[1])
|
||||
check(scr.fuse_first != null, "erstes Wesen gewählt")
|
||||
await shot("24_verschmelzen")
|
||||
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen und kurz halten
|
||||
mana = g.human.energy
|
||||
await click(scr.own_views[2])
|
||||
check(g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
|
||||
check(not scr.fusing, "Modus beendet")
|
||||
var before: int = g.human.board.size()
|
||||
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
|
||||
check(g.human.board.size() == before - 1 and g.human.board.any(func(c): return c.name == "Dampf") and g.human.energy == mana - 1, "zu Dampf verschmolzen, 1 Mana bezahlt")
|
||||
var dampf: Game.Creature = g.human.board.filter(func(c): return c.name == "Dampf")[0]
|
||||
check(scr.tiles[dampf.uid].rarity() == "Ultra Rare" and dampf.card.look == 3, "die Fusion trägt die höhere Stufe (Wellenreiter war Ultra Rare)")
|
||||
await shot("24_verschmelzen")
|
||||
# Verschmelzen auf der Hand per Ziehen
|
||||
g.human.hand = [Cards.by_id("f_kobold").copy(), Cards.by_id("f_kobold").copy(), Cards.by_id("l_wind").copy()]
|
||||
scr.refresh()
|
||||
await frames(1)
|
||||
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
|
||||
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Kobolde auf der Hand zu Inferno 3/2")
|
||||
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
|
||||
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
|
||||
check(find_button("Verdeckt legen ◆ 2", scr.menu_layer) != null, "Gamepad-Menü: Verdeckt legen")
|
||||
await click(find_button("Verschmelzen mit … ◆ 2", scr.menu_layer))
|
||||
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
|
||||
await click(scr.hand_views[1])
|
||||
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Windgeist zu Blitz")
|
||||
await give_up()
|
||||
|
||||
|
||||
@@ -701,7 +722,6 @@ func test_netzwerk() -> void:
|
||||
var b = screen()
|
||||
check(b is BattleScreen and b.is_online() and not b.is_client(), "Netzwerkkampf als Host")
|
||||
b.ai_delay = 0.0
|
||||
await pick_all(b)
|
||||
var rounds := 0
|
||||
while b.game != null and b.game.winner == "" and rounds < 2:
|
||||
if b.game.current() == b.perspective:
|
||||
@@ -807,7 +827,6 @@ func test_host_wechsel() -> void:
|
||||
await frames(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")
|
||||
await pick_all(b)
|
||||
main.net.rejoin_seconds = 3.0
|
||||
_bot_quit(true)
|
||||
w = 0
|
||||
@@ -881,7 +900,6 @@ func test_kampagne() -> void:
|
||||
await click(find_button("Kämpfen"))
|
||||
var battle = screen()
|
||||
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.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
||||
await shot("16_kampagne_kampf")
|
||||
@@ -897,7 +915,6 @@ func test_kampagne() -> void:
|
||||
await shot("27_kampagne_kapitel2")
|
||||
await click(find_button("Kämpfen"))
|
||||
var b2 = screen()
|
||||
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")
|
||||
await give_up()
|
||||
@@ -951,16 +968,17 @@ func test_kampf_abbrechen() -> void:
|
||||
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
|
||||
g.human.energy = 10
|
||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
||||
wolf.ready = true
|
||||
g.human.board.append(wolf)
|
||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
||||
g.human.looks["w_reiter"] = Cards.RARITIES.find("episch")
|
||||
g.human.looks["w_reiter"] = Cards.RARITIES.find("Super Rare")
|
||||
scr.refresh()
|
||||
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 == "Super Rare", "Handkarte trägt die eigene Stufe")
|
||||
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
|
||||
await hover(reiter_view)
|
||||
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 == "Super Rare", "Vorschau ebenfalls")
|
||||
await shot("21_kampf_vorschau")
|
||||
if not scr.own_views.is_empty():
|
||||
await click(scr.own_views[0])
|
||||
|
||||
@@ -65,13 +65,12 @@ Große Schrift,Large text
|
||||
Abbrechen,Cancel
|
||||
"Ja, zurücksetzen","Yes, reset"
|
||||
Flink,Swift
|
||||
Schild,Shield
|
||||
Doppelschlag,Double Strike
|
||||
gewöhnlich,common
|
||||
selten,rare
|
||||
episch,epic
|
||||
legendär,legendary
|
||||
mythisch,mythic
|
||||
Common,Common
|
||||
Rare,Rare
|
||||
Super Rare,Super Rare
|
||||
Ultra Rare,Ultra Rare
|
||||
Ultimate Rare,Ultimate Rare
|
||||
Feuer,Fire
|
||||
Wasser,Water
|
||||
Erde,Earth
|
||||
@@ -84,9 +83,6 @@ Zuschauer,Spectator
|
||||
Starten,Start
|
||||
Sammlung,Collection
|
||||
Verschmelzen,Fuse
|
||||
Zwei eigene Wesen wählen,Pick two of your creatures
|
||||
Zweites Wesen wählen,Pick the second creature
|
||||
Zwei verschiedene Wesen wählen,Pick two different creatures
|
||||
Nur eigene Wesen,Only your own creatures
|
||||
Aufgewertet: %d von %d Karten · Kampagne: %d/%d,Upgraded: %d of %d cards · Campaign: %d/%d
|
||||
%s ist jetzt %s,%s is now %s
|
||||
@@ -99,7 +95,7 @@ Keine Karte gewählt,No card selected
|
||||
Zerlegen (+%d Staub),Dismantle (+%d dust)
|
||||
Nicht genug Staub (%d nötig),Not enough dust (%d needed)
|
||||
Schon auf der höchsten Stufe,Already at the top tier
|
||||
Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
||||
Common-Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
||||
Aufgewertet,Upgraded
|
||||
+%d Staub,+%d dust
|
||||
Dein Zug,Your turn
|
||||
@@ -109,19 +105,13 @@ Profil wirklich zurücksetzen?,Really reset your profile?
|
||||
%d Leben,%d life
|
||||
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
|
||||
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
|
||||
@@ -133,7 +123,6 @@ 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
|
||||
Blockt den ersten Schaden,Blocks the first damage
|
||||
Greift zweimal je Zug,Attacks twice per turn
|
||||
Verdeckt,Face-down
|
||||
Inferno,Inferno
|
||||
@@ -149,6 +138,19 @@ 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
|
||||
Spott,Taunt
|
||||
Regeneration,Regeneration
|
||||
Muss zuerst angegriffen werden,Must be attacked first
|
||||
Greift sofort an,Attacks right away
|
||||
Heilt sich am Zugende ganz,Heals fully at the end of the turn
|
||||
Wesen greifen sofort an,Creatures attack right away
|
||||
Verschmelzen mit … ◆ %d,Fuse with … ◆ %d
|
||||
Ziel wählen,Pick the partner
|
||||
Erst die Wesen mit Spott,Taunting creatures first
|
||||
%s kann erst im nächsten Zug aufgedeckt werden,%s can be revealed next turn
|
||||
%s greift erst im nächsten Zug an,%s attacks next turn
|
||||
greift erst im nächsten Zug an,attacks next turn
|
||||
%s durch das Element,%s from its element
|
||||
Zwei verschiedene Karten wählen,Pick two different cards
|
||||
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user