update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
team3
2026-10-03 01:17:27 +02:00
parent 24a03ed1ee
commit 5f9442497a
27 changed files with 1040 additions and 871 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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":

View File

@@ -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"]},
]

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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":

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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":

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 := {}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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]:

View File

@@ -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

View File

@@ -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")

View File

@@ -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])

View File

@@ -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
1 keys en
65 Abbrechen Cancel
66 Ja, zurücksetzen Yes, reset
67 Flink Swift
Schild Shield
68 Doppelschlag Double Strike
69 gewöhnlich Common common Common
70 selten Rare rare Rare
71 episch Super Rare epic Super Rare
72 legendär Ultra Rare legendary Ultra Rare
73 mythisch Ultimate Rare mythic Ultimate Rare
74 Feuer Fire
75 Wasser Water
76 Erde Earth
83 Starten Start
84 Sammlung Collection
85 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
86 Nur eigene Wesen Only your own creatures
87 Aufgewertet: %d von %d Karten · Kampagne: %d/%d Upgraded: %d of %d cards · Campaign: %d/%d
88 %s ist jetzt %s %s is now %s
95 Zerlegen (+%d Staub) Dismantle (+%d dust)
96 Nicht genug Staub (%d nötig) Not enough dust (%d needed)
97 Schon auf der höchsten Stufe Already at the top tier
98 Gewöhnliche Karten lassen sich nicht zerlegen Common-Karten lassen sich nicht zerlegen Common cards cannot be dismantled
99 Aufgewertet Upgraded
100 +%d Staub +%d dust
101 Dein Zug Your turn
105 %d Leben %d life
106 Gegner mit %d Leben · %d Münzen bei Sieg Opponent with %d life · %d coins for a win
107 Start gegen die KI Start against the AI
Wähle %d von %d Pick %d of %d
108 Ziehen Draw
Erst wählen Pick first
109 Deine Hand ist voll Your hand is full
Keine Wahl offen Nothing to pick
Keine solche Karte No such card
Vorbei Over
110 Verdeckt legen ◆ %d Set face-down ◆ %d
111 Aufdecken Reveal
112 verdeckt face-down
113 Aufgedeckt! Revealed!
114 Elementbonus +%d/+%d Element bonus +%d/+%d
Elementbonus %d/%d Element bonus %d/%d
115 Merkmal +%d Perk +%d
116 nächster Zug ◆ %d next turn ◆ %d
117 Erst die Wesen Creatures first
123 Hat im ersten Zug 2 Mana mehr Has 2 extra mana on the first turn
124 Elementbonus zählt doppelt Element bonus counts twice
125 Beginnt mit 5 Leben mehr Starts with 5 extra life
Blockt den ersten Schaden Blocks the first damage
126 Greift zweimal je Zug Attacks twice per turn
127 Verdeckt Face-down
128 Inferno Inferno
138 Feuerwesen haben +1 Angriff Fire creatures have +1 attack
139 Wesen kommen mit +1 Leben Creatures enter with +1 health
140 Bekommt jeden Zug 1 Mana mehr Gets 1 extra mana each turn
Wesen kommen mit Schild Creatures enter with a shield
141 Ziehen kostet kein Mana Drawing costs no mana
142 Eigenschaften kosten 1 weniger Traits cost 1 less
143 Spott Taunt
144 Regeneration Regeneration
145 Muss zuerst angegriffen werden Must be attacked first
146 Greift sofort an Attacks right away
147 Heilt sich am Zugende ganz Heals fully at the end of the turn
148 Wesen greifen sofort an Creatures attack right away
149 Verschmelzen mit … ◆ %d Fuse with … ◆ %d
150 Ziel wählen Pick the partner
151 Erst die Wesen mit Spott Taunting creatures first
152 %s kann erst im nächsten Zug aufgedeckt werden %s can be revealed next turn
153 %s greift erst im nächsten Zug an %s attacks next turn
154 greift erst im nächsten Zug an attacks next turn
155 %s durch das Element %s from its element
156 Zwei verschiedene Karten wählen Pick two different cards

Binary file not shown.