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-09-28 19:44:30 +02:00
parent dfcd3cb713
commit f0a8f18de8
37 changed files with 1588 additions and 880 deletions

View File

@@ -1,6 +1,6 @@
# Dinger # Dinger
Kartenspiel in 2D (Godot 4.7): du und 1 bis 5 KI-Magier, jeder gegen jeden, alle mit denselben Regeln. Kartenspiel in 2D (Godot 4.7): ein Duell gegen einen KI-Magier, beide mit denselben Regeln.
## Starten ## Starten
@@ -24,20 +24,20 @@ Der Spielstand liegt unter `user://spielstand.json`
- Kein Deck und keine Starthand: jeder hat einen eigenen Laden mit fünf Angeboten. Kaufen kostet Mana, Ausspielen nichts, neu würfeln kostet 1 Mana - Kein Deck und keine Starthand: jeder hat einen eigenen Laden mit fünf Angeboten. Kaufen kostet Mana, Ausspielen nichts, neu würfeln kostet 1 Mana
- Mana wächst um 1 je Zug (bis 10) und bleibt liegen, wenn du es nicht ausgibst — sparen ist eine echte Entscheidung - Mana wächst um 1 je Zug (bis 10) und bleibt liegen, wenn du es nicht ausgibst — sparen ist eine echte Entscheidung
- Der Laden zeigt nur Karten, die höchstens dein Einkommen plus eins kosten: teure Wesen tauchen erst später auf - Der Laden zeigt nur Karten, die höchstens dein Einkommen plus eins kosten: teure Wesen tauchen erst später auf
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik: sie ändert weder Werte noch Text - Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Holo-Karten: selten farbiger Rahmen, Schein und Edelstein, episch ein wandernder Lichtstreifen, legendär Funkeln, mythisch schillernde Farben; auch auf dem Feld für alle sichtbar, Werte und Text bleiben gleich
- Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück - Booster (100 Münzen): fünf gewürfelte Stufen, höhere werten die Karte auf, der Rest wird zu Staub. Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
- Resonanz: gleiche Elemente auf dem Feld verstärken sich (+1, +2, +3, +3) - Resonanz: gleiche Elemente auf dem Feld verstärken sich (+1, +2, +3, +3); der Bonus steht als „+n“ am Angriff, verbundene Wesen sind unter der Reihe verlinkt
- Feldkombos: ein neues Wesen neben einem Wesen mit anderem Element löst einen Effekt aus - Feldkombos: ein neues Wesen neben einem Wesen mit anderem Element löst einen Effekt aus
- Karteneffekte lösen beim Ausspielen aus (Schaden, Einfrieren, Heilen, Zurückwerfen, Verstärken, Schild, Mana); Kartenvorteil ist hier Manavorteil - Karteneffekte lösen beim Ausspielen aus (Schaden, Einfrieren, Heilen, Zurückwerfen, Verstärken, Schild, Mana); Kartenvorteil ist hier Manavorteil
- Elementvorteil im Kreis (Wasser > Feuer > Luft > Erde > Wasser): 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, auch für Karteneffekte gegen Wesen - Elementvorteil im Kreis (Wasser > Feuer > Luft > Erde > Wasser): 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, auch für Karteneffekte gegen Wesen
- Gemeinsames Feld: vier gespielte Karten eines Elements kippen es (Vulkan, Flut, Fels, Wirbel), Wesen dieses Elements bekommen +1 Angriff - Gemeinsames Feld: vier gespielte Karten eines Elements kippen es (Vulkan, Flut, Fels, Wirbel), Wesen dieses Elements bekommen +1 Angriff; die Feldzeile zeigt je Element die Ladung als vier Punkte
- Vorschau: Hand- und Ladenkarten zeigen, welchen Bonus und welche Feldkombo das Ausspielen jetzt brächte; beim Zeigen ist der Kombo-Partner markiert, beim Angreifen steht der Elementvorteil (+1/−1) am Ziel
- Verschmelzen: zwei eigene Wesen werden eins (Elemente und Schlüsselwörter beider, Werte Durchschnitt plus 1, Kosten der Durchschnitt der Karten); drei oder mehr Elemente ergeben eine Chimäre - Verschmelzen: zwei eigene Wesen werden eins (Elemente und Schlüsselwörter beider, Werte Durchschnitt plus 1, Kosten der Durchschnitt der Karten); drei oder mehr Elemente ergeben eine Chimäre
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner; die Hand fasst 8 Karten, das Feld 5 Wesen - Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 8 Karten, das Feld 5 Wesen
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach - Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread - KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1 - Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1
- Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei) - Netzwerkspiel im lokalen Netz: Host führt das Spiel, Mitspieler sehen nur ihre Hand, Zuschauer keine. Fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung, die anderen verbinden neu (Platz bleibt 60 s frei)
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne) werden laufend gespeichert und lassen sich im Menü fortsetzen
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`) - Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
- Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`) - Oberfläche und Kartentexte wahlweise auf Englisch (`translations/ui.csv`, `name_en` in `data/karten.json`)
- Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen - Gamepad in Menü, Sammlung, Booster und Kampf; große Schrift und Animationen in den Einstellungen
@@ -56,7 +56,7 @@ DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fe
DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests DINGER_ONLY=test_sammlung godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse) DINGER_NETZ=1 godot --path . tests/ui_test.tscn # zusätzlich die langsamen Netzwerktests (Bot-Prozesse)
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Ladenangebot, Musik godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Ladenangebot, Musik
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
``` ```
## Export ## Export
@@ -79,4 +79,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
- `scripts/campaign.gd` – Kampagnenstufen - `scripts/campaign.gd` – Kampagnenstufen
- `scenes/main.tscn` – Startszene - `scenes/main.tscn` – Startszene
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden - `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
- `scripts/ui/` – Bildschirme; `card_view.gd` und `creature_view.gd` zeichnen Karten und Wesen, `field_view.gd` die Feldzeile, `battle_layout.gd` die Aufteilung des Kampfes
- `translations/ui.csv` – englische Oberflächentexte - `translations/ui.csv` – englische Oberflächentexte
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs

View File

@@ -5,15 +5,14 @@
## Grundidee ## Grundidee
- Kartenspiel wie zum Beispiel Magic - Kartenspiel wie zum Beispiel Magic
- PC-Spiel - PC-Spiel
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln - Duell: Spieler gegen einen KI-Gegner (oder einen Mitspieler im Netz), beide mit gleichen Regeln
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl - Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
- Kampagne gegen benannte Magier als Fortschritt - Kampagne gegen benannte Magier als Fortschritt
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer - Im lokalen Netz zu zweit spielbar, weitere als Zuschauer
- Ein unterbrochener Kampf lässt sich später fortsetzen
## Sammeln ## Sammeln
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat - 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 (auch im Netzwerkspiel sichtbar) - Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik wie bei Holo-Karten: je höher die Stufe, desto mehr Glanz (Rahmen, Schein, Lichtstreifen, Funkeln, Schillern), auch im Netzwerkspiel sichtbar
- Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub - Booster öffnen: höhere Stufen werten Karten auf, der Rest wird zu Staub
- Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück - Staub wertet gezielt auf, eine Stufe abgeben bringt Staub zurück
- Kein Deckbau und kein Nachziehstapel: fünf Angebote im Laden, Kaufen kostet Mana, Ausspielen nichts - Kein Deckbau und kein Nachziehstapel: fünf Angebote im Laden, Kaufen kostet Mana, Ausspielen nichts
@@ -23,8 +22,8 @@
## Elemente ## Elemente
- Jede Karte gehört zu genau einem Element - Jede Karte gehört zu genau einem Element
- Kombos entstehen erst im Spiel: beim Verschmelzen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) und als Feldkombo - Kombos entstehen erst im Spiel: beim Verschmelzen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan) und als Feldkombo
- Elemente wirken auf dem Feld zusammen: gleiche verstärken sich (Resonanz), verschiedene lösen Feldkombos aus - Elemente wirken auf dem Feld zusammen: gleiche verstärken sich (Resonanz), verschiedene lösen Feldkombos aus; beides ist vor dem Ausspielen auf der Karte zu sehen
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (1 Schaden mehr im Vorteil, 1 weniger im Nachteil; Mehrfachelemente nehmen den für sie besseren Wert) - Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (1 Schaden mehr im Vorteil, 1 weniger im Nachteil; Mehrfachelemente nehmen den für sie besseren Wert)
- Das Feld gehört allen am Tisch: vier gespielte Karten eines Elements kippen es, Wesen dieses Elements bekommen +1 Angriff - Das Feld gehört beiden: vier gespielte Karten eines Elements kippen es, Wesen dieses Elements bekommen +1 Angriff
- Zwei eigene Wesen lassen sich für Energie verschmelzen: das neue erbt Elemente und Fähigkeiten beider, seine Werte sind der Durchschnitt plus 1 - Zwei eigene Wesen lassen sich für Energie verschmelzen: das neue erbt Elemente und Fähigkeiten beider, seine Werte sind der Durchschnitt plus 1
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus - Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus

92
design.md Normal file
View File

@@ -0,0 +1,92 @@
# Design von Dinger
Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entwurf.
## Woran wir uns orientieren
- **Hearthstone** – Kosten und Werte springen aus dem Kartenrahmen (Kristall oben links, Angriff
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.
- **Teamfight Tactics** – der Laden ist eine Reihe von fünf großen Karten unten, Neuwürfeln steht
mit seinem Preis direkt daneben, die Bank (bei uns die Hand) liegt als schmale Reihe darüber.
Der Laden zeigt an, was zum eigenen Brett passt (bei uns: „+n“ und die Feldkombo).
- **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,
Lesefluss von links (Vergangenheit) über die Mitte (Spiel) nach rechts (Zukunft), die eigene
Seite größer als die der Gegner, Zahlen groß, Text klein, Symbole statt Wörter, und Rückmeldung
für jede Aktion (Bewegung, Ton, kurzes Zittern).
## Karte
- Kostenkristall oben links, Bild (Elementsymbol) oben, Namensband auf 36 % Höhe, darunter
Schlüsselwörter als Symbole (Mauer = Wächter, Schild, Doppelpfeil = Flink, gekreuzte Klingen =
Doppelschlag), dann der Effekttext ohne Einleitung, unten Angriff (gelb) und Leben (rot) als
Kreise, dazwischen der Edelstein der Stufe.
- Große Karten (Vorschau, Sammlung, Booster) schreiben das Wort neben das Symbol und die
Überschrift „Beim Ausspielen“ über den Effekt. Kleine Karten zeigen nur Symbole.
- Handkacheln (96×62) sind flach: Kosten, Name, Werte, Symbole oder das Kombo-Schild. Details
kommen aus der schwebenden Vorschau.
- Seltenheit: gewöhnlich schlicht; selten Rahmen, Schein, Edelstein, stehender Lichtstreifen;
episch wandernder Lichtstreifen; legendär Funkeln; mythisch schillernde Farben.
- Der Kartentext in Daten und Sammlungssuche bleibt vollständig (`Card.text()`), die Karte
zeichnet nur `effect_text()`.
## Kampfbildschirm (1280×800)
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
- **Laden** als fünf große Karten, links daneben „Neu würfeln“ mit Preis, rechts „Zug beenden“.
- **Hand** als flache Kacheln (die Bank), für andere verdeckt.
- **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.
- **Feldband** mit vier Ladepunkten je Element.
- **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 Ladenkarten tragen „+n“ (Bonus auf dem Brett) und das Kombo-Schild; beim Zeigen ist
der Partner auf dem Brett markiert; beim Angreifen steht der Elementkonter (+1/−1) am Ziel.
- Wesen mit gleichem Element sind unter der Reihe verbunden (Resonanz).
## Bewegung und Ton
- Zugbeginn: Band „Dein Zug“ quer über die Mitte, Laden wird ausgeteilt, Ton.
- Kaufen: die Karte fliegt aus dem Laden auf die Hand, Münzton.
- Ausspielen: die Kachel fliegt von der Hand auf ihren Platz und federt ein.
- Angriff: Ausfallschritt, roter Rahmen am Ziel, kurzes Zittern des Bretts, Schadenszahl.
- Feldwechsel: Band in Elementfarbe, das Brett färbt sich.
- 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.
- Alles hängt an der Einstellung „Animationen“.
## Menüs
- Hauptmenü: Titel und drei Spielarten links, darunter Booster, Sammlung, Einstellungen,
Beenden; rechts drei Karten der Sammlung (höchste Stufen zuerst) als Schaufenster; Münzen und
Staub oben rechts.
- Unterbildschirme: Titel links oben, „Zurück“ rechts oben, Inhalt darunter.
- Kampagne als Pfad mit zehn Stationen je Kapitel, die nächste pulsiert; darunter die gewählte
Stufe mit Gegner, Leben, Belohnung und Merkmalen. Die Bosse werden über Leben und Merkmale
stärker, nicht über Begleiter.
- Sammlung: sechs Spalten, aufgewertete Karten zuerst, rechts die große Vorschau.
## Offen
- Bilder statt Elementsymbolen, wenn es einmal Kartenkunst gibt.
- Hero-Porträts für Kampagnenbosse.
- Ein Tisch mit mehreren Spielern ließe sich als TFT-Rotation obendrauf setzen: jede Runde ein
anderer Gegner, das Brett bleibt.
## Quellen
- https://hearthstone.wiki.gg/wiki/Battlefield
- https://hearthstone.fandom.com/wiki/Card
- https://moirao.me/home/tft-ui
- https://gdkeys.com/the-card-games-ui-design-of-fairtravel-battle/
- https://medium.com/@acbassettone/5-ux-ui-lessons-from-designing-a-card-game
- https://hackread.com/the-juice-factor-designing-game-feel/

View File

@@ -39,8 +39,8 @@ static func hand_value(p: Game.Player) -> float:
## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv, ## Stellung aus Sicht von p: eigenes Leben, Feld, Hand und übriges Mana zählen positiv,
## die der Gegner negativ, der stärkste Gegner etwas mehr (jeder gegen jeden: den Führenden ## die des Gegners negativ. Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung,
## bremsen). Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung, tödliche besonders. ## tödliche besonders.
static func evaluate(g: Game, p: Game.Player) -> float: static func evaluate(g: Game, p: Game.Player) -> float:
if not p.alive(): if not p.alive():
return -10000.0 return -10000.0

View File

@@ -1,47 +1,47 @@
class_name Campaign class_name Campaign
## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Gegnern am Tisch. ## Kampagne: zehn benannte Magier in drei Kapiteln, jede Stufe mit mehr Leben und Merkmalen.
const STAGES := [ const STAGES := [
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"}, {"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "life": 30, "reward": 100, "perk": "feuer_angriff"},
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "extra": 0, "reward": 100, "perk": "leben_35"}, {"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "life": 30, "reward": 100, "perk": "leben_35"},
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "extra": 1, "reward": 150, "perk": "waechter_leben"}, {"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "life": 31, "reward": 150, "perk": "waechter_leben"},
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "extra": 1, "reward": 150, "perk": "flink_angriff"}, {"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "life": 31, "reward": 150, "perk": "flink_angriff"},
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "extra": 2, "reward": 200, "perk": "wesen_leben"}, {"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "life": 32, "reward": 200, "perk": "wesen_leben"},
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "extra": 2, "reward": 200, "perk": "extra_mana"}, {"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 200, "perk": "extra_mana"},
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "extra": 3, "reward": 250, "perk": "resonanz_doppelt"}, {"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "life": 33, "reward": 250, "perk": "resonanz_doppelt"},
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "extra": 3, "reward": 250, "perk": "schild_wesen"}, {"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "life": 33, "reward": 250, "perk": "schild_wesen"},
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 300, "perk": "alle_flink"}, {"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 300, "perk": "alle_flink"},
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 500, "perk": "start_energie"}, {"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 500, "perk": "start_energie"},
] ]
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Gefolge zurück. ## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Leben zurück.
const STAGES2 := [ const STAGES2 := [
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "extra": 1, "reward": 300, "perks": ["feuer_angriff", "start_energie"]}, {"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "life": 30, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "extra": 1, "reward": 300, "perks": ["leben_35", "extra_mana"]}, {"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "life": 30, "reward": 300, "perks": ["leben_35", "extra_mana"]},
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "extra": 2, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]}, {"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "life": 31, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "extra": 2, "reward": 350, "perks": ["flink_angriff", "alle_flink"]}, {"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "life": 31, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "extra": 3, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]}, {"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "life": 32, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "extra": 3, "reward": 400, "perks": ["extra_mana", "flink_angriff"]}, {"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "extra": 4, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]}, {"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "life": 33, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]},
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "extra": 4, "reward": 450, "perks": ["schild_wesen", "leben_35"]}, {"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "life": 33, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 500, "perks": ["alle_flink", "start_energie"]}, {"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 500, "perks": ["alle_flink", "start_energie"]},
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]}, {"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "life": 34, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]},
] ]
## Kapitel 3: drei Merkmale und volle Tische. ## Kapitel 3: drei Merkmale und volle Tische.
const STAGES3 := [ const STAGES3 := [
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "extra": 2, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]}, {"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "life": 32, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "extra": 2, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]}, {"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "life": 32, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "extra": 3, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]}, {"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "life": 33, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "extra": 3, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]}, {"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "life": 33, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]},
{"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "extra": 4, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "waechter_leben"]}, {"name": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "life": 34, "reward": 800, "perks": ["wesen_leben", "feuer_angriff", "waechter_leben"]},
{"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "extra": 4, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]}, {"name": "Nyx", "title": "Orkanmutter", "elements": ["Wasser", "Luft"], "life": 34, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]},
{"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "extra": 4, "reward": 900, "perks": ["resonanz_doppelt", "wesen_leben", "start_energie"]}, {"name": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "life": 35, "reward": 900, "perks": ["resonanz_doppelt", "wesen_leben", "start_energie"]},
{"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "extra": 4, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]}, {"name": "Thalassa", "title": "Herz der Tiefe", "elements": ["Wasser"], "life": 35, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
{"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]}, {"name": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "life": 36, "reward": 1000, "perks": ["alle_flink", "start_energie", "wesen_leben"]},
{"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "extra": 4, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]}, {"name": "Archimagier", "title": "Ewiger Meister", "elements": ["Erde", "Luft"], "life": 36, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]},
] ]
const CHAPTERS := [STAGES, STAGES2, STAGES3] const CHAPTERS := [STAGES, STAGES2, STAGES3]
@@ -63,14 +63,9 @@ static func stage(i: int) -> Dictionary:
return CHAPTERS[chapter(i) - 1][i % STAGES.size()] return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
## Gegner einer Stufe: der Boss handelt nur mit seinen Elementen, Begleiter mit allen. ## Boss einer Stufe: handelt nur mit seinen Elementen, mit dem Leben und den Merkmalen der Stufe.
static func opponents(stage_index: int) -> Array: static func opponent(stage_index: int) -> Game.Player:
var st := stage(stage_index) var st := stage(stage_index)
var life: int = 35 if "leben_35" in st.perks else Game.START_LIFE var boss := Game.Player.new(st.name, false, int(st.life), st.title, st.elements)
var boss := Game.Player.new(st.name, false, life, st.title, st.elements)
boss.perks = Array(st.perks) boss.perks = Array(st.perks)
var out := [boss] return boss
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
for k in extra:
out.append(DeckGen.random_opponent(k + 1))
return out

View File

@@ -26,7 +26,7 @@ const COMBOS := {
const FIELD_COMBO_TEXT := { const FIELD_COMBO_TEXT := {
"Dampf": "Friere das stärkste gegnerische Wesen ein", "Dampf": "Friere das stärkste gegnerische Wesen ein",
"Lava": "1 Schaden an das stärkste gegnerische Wesen", "Lava": "1 Schaden an das stärkste gegnerische Wesen",
"Blitz": "2 Schaden an den schwächsten Gegner", "Blitz": "2 Schaden an den Gegner",
"Schlamm": "Heile dich um 3", "Schlamm": "Heile dich um 3",
"Sturm": "Erhalte 1 Mana", "Sturm": "Erhalte 1 Mana",
"Sand": "Das neue Wesen erhält +1/+1", "Sand": "Das neue Wesen erhält +1/+1",
@@ -165,9 +165,9 @@ class Effect extends RefCounted:
"all_enemy_creatures": "all_enemy_creatures":
return tr("%s Schaden an alle gegnerischen Wesen") % n return tr("%s Schaden an alle gegnerischen Wesen") % n
"all_enemy_heroes": "all_enemy_heroes":
return tr("%s Schaden an alle Gegner") % n return tr("%s Schaden an den Gegner") % n
"all_enemies": "all_enemies":
return tr("%s Schaden an alle Gegner und ihre Wesen") % n return tr("%s Schaden an den Gegner und seine Wesen") % n
"heal": "heal":
return tr("Heile dich um %s") % n return tr("Heile dich um %s") % n
"mana": "mana":
@@ -250,6 +250,15 @@ class Card extends RefCounted:
return fx.target return fx.target
return "" return ""
## Nur die Effekte (die Karte zeigt Schlüsselwörter als Symbole).
func effect_text() -> String:
if effects.is_empty():
return ""
var fx: Array[String] = []
for f in effects:
fx.append(f.text())
return ", ".join(fx) + "."
func text() -> String: func text() -> String:
var parts: Array[String] = [] var parts: Array[String] = []
if not keywords.is_empty(): if not keywords.is_empty():

View File

@@ -31,5 +31,5 @@ static func shop_cards(r: RandomNumberGenerator, income: int, elements: Array =
## Gegner mit eigenem Laden über alle Elemente. ## Gegner mit eigenem Laden über alle Elemente.
static func random_opponent(index: int, life: int = Game.START_LIFE) -> Game.Player: static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
return Game.Player.new("Gegner %d" % index, false, life, "Magier") return Game.Player.new("Gegner", false, life)

View File

@@ -9,7 +9,6 @@ const SHOP_SIZE := 5 # so viele Karten liegen im Laden
const REROLL_COST := 1 const REROLL_COST := 1
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben. # Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
const START_LIFE := 30 const START_LIFE := 30
const MAX_OPPONENTS := 5
# Merkmale der Kampagnen-Bosse (Player.perks). # Merkmale der Kampagnen-Bosse (Player.perks).
const PERKS := { const PERKS := {
@@ -18,7 +17,7 @@ const PERKS := {
"waechter_leben": "Wächter kommen mit +2 Leben", "waechter_leben": "Wächter kommen mit +2 Leben",
"wesen_leben": "Wesen kommen mit +1 Leben", "wesen_leben": "Wesen kommen mit +1 Leben",
"extra_mana": "Bekommt jeden Zug 1 Mana mehr", "extra_mana": "Bekommt jeden Zug 1 Mana mehr",
"leben_35": "Beginnt mit 35 Leben", "leben_35": "Beginnt mit 5 Leben mehr",
"schild_wesen": "Wesen kommen mit Schild", "schild_wesen": "Wesen kommen mit Schild",
"alle_flink": "Alle Wesen sind flink", "alle_flink": "Alle Wesen sind flink",
"start_energie": "Beginnt mit 3 Mana", "start_energie": "Beginnt mit 3 Mana",
@@ -151,8 +150,8 @@ class Player extends RefCounted:
var human: Player # der (erste) menschliche Spieler var human: Player # der (erste) menschliche Spieler
var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere) var humans: Array = [] # alle menschlichen Spieler (im Netz zwei)
var opponents: Array = [] # alle außer dem ersten Menschen var opponent: Player # der andere am Tisch
var players: Array = [] # in Zugreihenfolge var players: Array = [] # in Zugreihenfolge
var winner_player: Player = null var winner_player: Player = null
var rng: RandomNumberGenerator var rng: RandomNumberGenerator
@@ -167,10 +166,10 @@ var field_element := "" # gemeinsames Feld aller am Tisch, "" = neutral
var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel
func _init(h: Player, opps: Array, r: RandomNumberGenerator = null) -> void: func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
human = h human = h
opponents = opps opponent = o
players = [h] + opps players = [h, o]
humans = players.filter(func(p): return p.is_human) humans = players.filter(func(p): return p.is_human)
if r == null: if r == null:
r = make_rng() r = make_rng()
@@ -199,11 +198,15 @@ func ref_of(target) -> Dictionary:
return {"kind": "player", "index": players.find(target)} return {"kind": "player", "index": players.find(target)}
## Jeder gegen jeden: alle anderen lebenden Spieler sind Gegner. ## Der Gegner von p, solange er lebt (als Liste, damit Effekte einheitlich bleiben).
func enemies_of(p: Player) -> Array: func enemies_of(p: Player) -> Array:
return players.filter(func(o): return o != p and o.alive()) return players.filter(func(o): return o != p and o.alive())
func enemy_of(p: Player) -> Player:
return opponent if p == human else human
func find_creature(uid: int) -> Creature: func find_creature(uid: int) -> Creature:
for p in players: for p in players:
for c in p.board: for c in p.board:
@@ -218,7 +221,7 @@ func clone() -> Game:
for p in players: for p in players:
copies.append(p.clone()) copies.append(p.clone())
var idx := players.find(human) var idx := players.find(human)
var g := Game.new(copies[idx], copies.slice(0, idx) + copies.slice(idx + 1), RandomNumberGenerator.new()) var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
g.players = copies # gleiche Zugreihenfolge wie das Original g.players = copies # gleiche Zugreihenfolge wie das Original
g.turn = turn g.turn = turn
g.current_idx = current_idx g.current_idx = current_idx
@@ -483,6 +486,26 @@ static func resonance_for(partners: int) -> int:
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)] return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
## Vorschau fürs Ausspielen: Angriffsbonus aus Resonanz und Feld sowie die Feldkombo,
## die die Karte mit dem ersten eigenen Wesen aus anderem Element auslösen würde.
func forecast(p: Player, card: Cards.Card) -> Dictionary:
var partners := 0
var combo := ""
var partner: Creature = null
for o in p.board:
if o.card.elements.any(func(e): return e in card.elements):
partners += 1
if combo == "":
for e1 in card.elements:
for e2 in o.card.elements:
if e1 != e2 and combo == "":
combo = Cards.combo_name([e1, e2])
partner = o
var resonance := resonance_for(partners) * (2 if "resonanz_doppelt" in p.perks else 1)
var field := 1 if field_element != "" and field_element in card.elements else 0
return {"resonance": resonance, "field": field, "combo": combo, "partner": partner}
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for). ## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
func update_resonance() -> void: func update_resonance() -> void:
for p in players: for p in players:
@@ -642,20 +665,19 @@ func cleanup() -> void:
update_resonance() update_resonance()
## Ende: nur noch ein Spieler übrig, oder kein Mensch mehr am Tisch. ## Ende: einer von beiden hat kein Leben mehr.
func check_end() -> void: func check_end() -> void:
if winner != "": if winner != "":
return return
var alive := players.filter(func(p): return p.alive()) var alive := players.filter(func(p): return p.alive())
var humans_alive := alive.filter(func(p): return p.is_human) if alive.size() != 1:
if humans_alive.is_empty(): return
winner = "opponents" winner_player = alive[0]
winner_player = alive[0] if alive.size() == 1 else null winner = "human" if winner_player.is_human else "opponents"
say("Du wurdest besiegt." if humans.size() == 1 else "Alle Spieler sind besiegt, die KI gewinnt.") if humans.size() == 1:
elif alive.size() == 1: say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
winner = "human" else:
winner_player = alive[0] say("%s gewinnt!" % winner_player.name)
say("Alle Gegner sind besiegt!" if humans.size() == 1 else "%s gewinnt!" % winner_player.name)
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------ # --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
@@ -678,28 +700,23 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
arr[j] = tmp arr[j] = tmp
## rules: {"human_perks": [...], "opponent_life": int, "looks": {...}} ## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
static func new_battle(n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game: static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
if r == null: if r == null:
r = make_rng() r = make_rng()
var life: int = int(rules.get("opponent_life", START_LIFE)) return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), r, false, rules)
var opps := []
for i in n_opp:
opps.append(DeckGen.random_opponent(i + 1, life))
return new_battle_with(opps, r, false, rules)
static func new_battle_with(opps: Array, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game: static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
if r == null: if r == null:
r = make_rng() r = make_rng()
var human := Player.new("Spieler", true, START_LIFE) var human := Player.new("Spieler", true, START_LIFE)
human.perks = Array(rules.get("human_perks", [])) human.perks = Array(rules.get("human_perks", []))
human.looks = rules.get("looks", {}) human.looks = rules.get("looks", {})
var g := Game.new(human, opps, r) var g := Game.new(human, opp, r)
for p in g.players: for p in g.players:
if "leben_35" in p.perks and p.life == START_LIFE: if "leben_35" in p.perks:
p.life = 35 p.life += 5
p.max_life = 35 p.max_life += 5
g.start(fixed_order) g.start(fixed_order)
return g return g

View File

@@ -48,7 +48,7 @@ func _ready() -> void:
func host(port: int = PORT) -> Error: func host(port: int = PORT) -> Error:
peer = ENetMultiplayerPeer.new() peer = ENetMultiplayerPeer.new()
var err := peer.create_server(port, Game.MAX_OPPONENTS) var err := peer.create_server(port, 8) # ein Mitspieler, der Rest Zuschauer
if err != OK: if err != OK:
status = "Kann nicht hosten (Port %d belegt?)" % port status = "Kann nicht hosten (Port %d belegt?)" % port
return err return err
@@ -307,10 +307,11 @@ func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
lobby_changed.emit() lobby_changed.emit()
## Host: Spiel mit allen Verbundenen und n_ai KI-Gegnern starten. ## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI.
func start_battle(n_ai: int) -> void: func start_battle() -> void:
var ids: Array = names.keys() var ids: Array = names.keys()
ids.sort() ids.sort()
ids = ids.slice(0, 2)
var r := Game.make_rng() var r := Game.make_rng()
var players := [] var players := []
seats = {} seats = {}
@@ -321,9 +322,9 @@ func start_battle(n_ai: int) -> void:
if Cards.has_id(cid): if Cards.has_id(cid):
p.looks[cid] = int(pick.looks[cid]) p.looks[cid] = int(pick.looks[cid])
players.append(p) players.append(p)
for i in n_ai: if players.size() < 2:
players.append(DeckGen.random_opponent(i + 1)) players.append(DeckGen.random_opponent())
game = Game.new(players[0], players.slice(1), r) game = Game.new(players[0], players[1], r)
game.start() game.start()
for id in ids: for id in ids:
seats[id] = game.players.find(players[ids.find(id)]) seats[id] = game.players.find(players[ids.find(id)])

View File

@@ -80,7 +80,7 @@ static func from_snapshot(d: Dictionary) -> Game:
p.board.append(c) p.board.append(c)
list.append(p) list.append(p)
var hi := int(d.human) var hi := int(d.human)
var g := Game.new(list[hi], list.slice(0, hi) + list.slice(hi + 1), RandomNumberGenerator.new()) var g := Game.new(list[hi], list[1 - hi], RandomNumberGenerator.new())
g.players = list g.players = list
g.turn = int(d.turn) g.turn = int(d.turn)
g.current_idx = int(d.current_idx) g.current_idx = int(d.current_idx)

View File

@@ -10,7 +10,7 @@ const BOOSTER_COST := 100
const BOOSTER_SIZE := 5 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 := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
const START_COINS := 150 const START_COINS := 150
const REWARD_WIN := {1: 100, 2: 150, 3: 200, 4: 250, 5: 300} const REWARD_WIN := 100
const REWARD_LOSS := 50 const REWARD_LOSS := 50
# Schwierigkeit: Leben der Gegner, KI und Münzen. # Schwierigkeit: Leben der Gegner, KI und Münzen.
const DIFFICULTIES := { const DIFFICULTIES := {
@@ -190,10 +190,10 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück. ## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei. ## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
func record_battle(won: bool, n_opp: int, stage: int = -1, reward_factor: float = 1.0) -> int: func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
var reward := REWARD_LOSS var reward := REWARD_LOSS
if won: if won:
reward = int(round(REWARD_WIN[n_opp] * reward_factor)) reward = int(round(REWARD_WIN * reward_factor))
if stage >= 0 and stage == campaign_stage: if stage >= 0 and stage == campaign_stage:
campaign_stage += 1 campaign_stage += 1
reward = Campaign.stage(stage).reward reward = Campaign.stage(stage).reward

View File

@@ -4,6 +4,8 @@ extends RefCounted
var screen # BattleScreen var screen # BattleScreen
var enabled := true var enabled := true
var banners: Array = [] # wartende Ansagen [Text, Farbe]
var banner_running := false
func _init(s) -> void: func _init(s) -> void:
@@ -25,6 +27,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
var atk: CreatureView = screen.tiles.get(ev.attacker.uid) var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
lunge(atk, center_of(ev.target, centers)) lunge(atk, center_of(ev.target, centers))
flash_target(ev.target) flash_target(ev.target)
shake()
delay = 0.12 delay = 0.12
"damage": "damage":
sfx("damage") sfx("damage")
@@ -43,7 +46,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid}) var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
if where.x < 0: if where.x < 0:
where = screen.target_center({"kind": "player", "index": ev.player}) where = screen.target_center({"kind": "player", "index": ev.player})
popup("Feldkombo %s!" % ev.name, Style.GOLD, where + Vector2(0, -30), 0.1) popup("%s!" % tr(ev.name), Style.GOLD, where + Vector2(0, -30), 0.1)
"play": "play":
sfx("play") sfx("play")
"fuse": "fuse":
@@ -51,7 +54,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0) popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
"field": "field":
sfx("combo") sfx("combo")
popup("Feld: %s" % Cards.FIELD_NAMES[ev.element], Cards.ELEMENT_COLORS[ev.element], Vector2(640, 196), 0.0) banner("%s: %s" % [tr(Cards.FIELD_NAMES[ev.element]), tr(Cards.FIELD_TEXT[ev.element])], Cards.ELEMENT_COLORS[ev.element])
"buy": "buy":
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
"reroll": "reroll":
@@ -165,3 +168,108 @@ func move_to(tile: CreatureView, pos: Vector2) -> void:
screen.create_tween().tween_property(tile, "position", pos, 0.2) screen.create_tween().tween_property(tile, "position", pos, 0.2)
else: else:
tile.position = pos tile.position = pos
## Ein Wesen fliegt von der Handkachel auf seinen Platz.
func fly_in(tile: CreatureView, from: Rect2) -> void:
if not enabled:
return
tile.busy = true
tile.position = from.position
tile.scale = from.size / tile.size
var tw := tile.create_tween().set_parallel(true)
tw.tween_property(tile, "position", tile.slot, 0.22).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.tween_property(tile, "scale", Vector2.ONE, 0.22).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tw.chain().tween_callback(func():
tile.busy = false
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:
return
var layer: Control = screen.tile_layer
var tw: Tween = screen.create_tween()
for i in 3:
tw.tween_property(layer, "position", Vector2(3 if i % 2 == 0 else -3, 0), 0.03)
tw.tween_property(layer, "position", Vector2.ZERO, 0.03)
## Ansage quer über die Mitte: Zugbeginn, Feldwechsel. Mehrere folgen nacheinander.
func banner(text: String, color: Color) -> void:
if not enabled:
return
banners.append([text, color])
if not banner_running:
_next_banner()
func _next_banner() -> void:
if banners.is_empty() or not is_instance_valid(screen) or not screen.is_inside_tree():
banner_running = false
return
banner_running = true
var next: Array = banners.pop_front()
var text: String = next[0]
var color: Color = next[1]
var delay := 0.0
var band := ColorRect.new()
band.color = Color(0, 0, 0, 0.55)
band.position = Vector2(0, 300)
band.size = Vector2(1280, 90)
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
band.scale.y = 0.0
band.pivot_offset = Vector2(640, 45)
screen.fx_layer.add_child(band)
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 40)
l.add_theme_font_override("font", Style.FONT_BOLD)
l.add_theme_color_override("font_color", color)
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
l.add_theme_constant_override("outline_size", 8)
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
screen.fx_layer.add_child(l)
l.reset_size()
l.position = Vector2(640, 345) - l.size / 2
l.pivot_offset = l.size / 2
l.scale = Vector2(0.6, 0.6)
l.modulate.a = 0.0
var tw: Tween = screen.create_tween().set_parallel(true)
tw.tween_property(band, "scale:y", 1.0, 0.15).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.tween_property(l, "scale", Vector2.ONE, 0.25).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tw.tween_property(l, "modulate:a", 1.0, 0.12).set_delay(delay)
tw.tween_property(l, "modulate:a", 0.0, 0.3).set_delay(delay + 0.8)
tw.tween_property(band, "scale:y", 0.0, 0.2).set_delay(delay + 0.9)
tw.chain().tween_callback(func():
l.queue_free()
band.queue_free()
_next_banner())

View File

@@ -1,42 +1,71 @@
class_name BattleLayout class_name BattleLayout
## Feste Bildschirmaufteilung des Kampfes (1280x800). ## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Laden mit Neuwürfeln
## und Zug beenden, Hand, eigenes Brett (mit eigener Leiste links), Feldband, oben der Gegner
## mit seinem Brett.
const SIZES := {1: [110, 110, 10], 2: [96, 110, 8], 3: [78, 100, 4], 4: [54, 72, 4], 5: [44, 62, 3]} const B_GIVEUP := Rect2(10, 10, 96, 32)
const HAND_W := 130 const B_LOG := Rect2(112, 10, 96, 32)
const HAND_H := 185 const OPP_BOX := Rect2(330, 10, 620, 56)
const HAND_GAP := 8 const FIELD := Rect2(240, 226, 800, 32)
const BAR := Rect2(335, 415, 610, 68) const MSG := Rect2(240, 262, 800, 22)
const OWN_X := (1280 - 5 * 110 - 4 * 10) / 2 const OWN_PANEL := Rect2(240, 288, 800, 160)
const STATUS := Rect2(20, 288, 210, 160)
const B_FUSE := Rect2(1050, 298, 210, 40)
static func compact(n_others: int) -> bool: const B_UNDO := Rect2(1050, 346, 210, 40)
return n_others >= 4 const OWN_X := 345
const HAND_W := 96
const HAND_H := 62
static func hero_box_rect(n_others: int, i: int) -> Rect2: const HAND_GAP := 4
var col_w := 1280 / n_others const HAND_Y := 458
var margin := 6 if compact(n_others) else 10 const SHOP_Y := 532
return Rect2(i * col_w + margin, 10, col_w - 2 * margin, 64) const SHOP_W := 150
const SHOP_H := 205
const B_REROLL := Rect2(20, 606, 200, 52)
const B_END := Rect2(1060, 600, 200, 60)
const LOG_PANEL := Rect2(240, 60, 800, 560)
const PREVIEW_SIZE := Vector2(180, 252)
const CENTER_X := 240
const CENTER_W := 800
static func own_slot(j: int) -> Rect2: static func own_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 250, 110, 140) return Rect2(OWN_X + j * 120, 298, 110, 140)
static func other_slot(n_others: int, i: int, j: int) -> Rect2: ## Brett des Gegners unter seinem Kasten.
var col_w := 1280 / n_others static func opp_slot(j: int) -> Rect2:
var sz: Array = SIZES[n_others] return Rect2(OWN_X + j * 120, 76, 110, 140)
var bx: int = i * col_w + (col_w - 5 * sz[0] - 4 * sz[2]) / 2
return Rect2(bx + j * (sz[0] + sz[2]), 84, sz[0], sz[1])
## Laden: fünf Angebote über der Hand. ## Laden: fünf große Angebote unten, wie in TFT.
static func shop_rect(i: int) -> Rect2: static func shop_rect(i: int) -> Rect2:
return Rect2(340 + i * 121, 487, 115, 112) return Rect2(245 + i * (SHOP_W + 10), SHOP_Y, SHOP_W, SHOP_H)
## Hand: flache Kacheln zwischen Brett und Laden (die Bank).
static func hand_rect(count: int, i: int) -> Rect2: static func hand_rect(count: int, i: int) -> Rect2:
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
var x := (1280 - total) / 2 var x := CENTER_X + (CENTER_W - total) / 2
return Rect2(x + i * (HAND_W + HAND_GAP), 800 - HAND_H - 8, HAND_W, HAND_H) 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
## 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:
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x)
return Rect2(Vector2(x, anchor.position.y - 12 - PREVIEW_SIZE.y), PREVIEW_SIZE)
var x := anchor.end.x + 12
if x + PREVIEW_SIZE.x > 1270:
x = anchor.position.x - 12 - PREVIEW_SIZE.x
var y := clampf(anchor.get_center().y - PREVIEW_SIZE.y / 2, 10, 800 - PREVIEW_SIZE.y - 10)
return Rect2(Vector2(x, y), PREVIEW_SIZE)
## Zusatzzeile (Resonanz, Feld, Feldkombo) neben der Vorschau, rechts davon oder links.
static func info_rect(preview: Rect2) -> Rect2:
var w := 230.0
var x := preview.end.x + 8
if x + w > 1275:
x = preview.position.x - 8 - w
return Rect2(x, preview.position.y + preview.size.y / 2 - 40, w, 80)

View File

@@ -4,7 +4,6 @@ extends Control
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer). ## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
var app var app
var n_opp: int
var stage := -1 # Kampagnenstufe oder -1 var stage := -1 # Kampagnenstufe oder -1
var net_play := false # Netzwerkspiel (auch als Zuschauer) var net_play := false # Netzwerkspiel (auch als Zuschauer)
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1 var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
@@ -24,8 +23,6 @@ var ai_copy: Game # Spielkopie, auf der die KI rechnet
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt) var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
var reward := -1 var reward := -1
var ended := false var ended := false
var zoom_player = null
var preview_timer := 0.0
var pad: BattleGamepad var pad: BattleGamepad
var gp_zone: String: var gp_zone: String:
get: get:
@@ -36,7 +33,6 @@ var gp_index: int:
var skip_ai := false # Gegnerzüge im Schnelldurchlauf var skip_ai := false # Gegnerzüge im Schnelldurchlauf
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI) var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
var saved_game: Game = null # fortgesetzter Kampf (siehe resume)
var undo_log_len := 0 var undo_log_len := 0
var undo_t := 0.0 var undo_t := 0.0
@@ -54,11 +50,12 @@ var hero_layer: Control
var tile_layer: Control var tile_layer: Control
var hand_layer: Control var hand_layer: Control
var shop_layer: Control var shop_layer: Control
var zoom_layer: Control var log_overlay: Control
var b_log: Button
var status_panel: Panel
var fx_layer: Control var fx_layer: Control
var log_box: RichTextLabel var log_box: RichTextLabel
var preview_holder: Control var preview_holder: Control
var bar_panel: Panel
var name_label: Label var name_label: Label
var life_label: Label var life_label: Label
var energy_label: Label var energy_label: Label
@@ -69,38 +66,35 @@ var b_end: Button
var b_fuse: Button var b_fuse: Button
var b_reroll: Button var b_reroll: Button
var b_giveup: Button var b_giveup: Button
var field_label: Label var field_view: FieldView
var link_view: Links # Resonanzlinien unter den Feldreihen
var info_label: Label # Vorschau: Resonanz, Feld, Feldkombo
var board_panel: Panel # eigenes Brett, färbt sich mit dem Feld
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
var banner_turn := -1 # Zug, für den die Zugansage schon lief
var deal_shop := false # nächster Aufbau teilt den Laden animiert aus
var overlay: Control var overlay: Control
var result_label: Label var result_label: Label
var reward_label: Label var reward_label: Label
var notice_label: Label var notice_label: Label
var confirm: Control var confirm: Control
var b_save_leave: Button
var cursor_layer: Control var cursor_layer: Control
var wait_label: Label var wait_label: Label
var b_skip: Button var b_skip: Button
var b_undo: Button var b_undo: Button
func _init(n: int = 2, campaign_stage: int = -1) -> void: func _init(campaign_stage: int = -1) -> void:
n_opp = n
stage = campaign_stage stage = campaign_stage
static func online(seat: int) -> BattleScreen: static func online(seat: int) -> BattleScreen:
var b := BattleScreen.new(0, -1) var b := BattleScreen.new(-1)
b.net_play = true b.net_play = true
b.online_seat = seat b.online_seat = seat
return b return b
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
static func resume(data: Dictionary) -> BattleScreen:
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
s.saved_game = Game.from_snapshot(data.game)
return s
func is_online() -> bool: func is_online() -> bool:
return net_play return net_play
@@ -137,13 +131,11 @@ func _ready() -> void:
net.snapshot_received.connect(_on_snapshot) net.snapshot_received.connect(_on_snapshot)
wait_label.visible = true wait_label.visible = true
return # der erste Zustand kommt vom Host return # der erste Zustand kommt vom Host
elif saved_game != null:
game = saved_game
elif stage >= 0: elif stage >= 0:
var r := Game.make_rng() var r := Game.make_rng()
game = Game.new_battle_with(Campaign.opponents(stage), r, false, own_rules()) game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
else: else:
game = Game.new_battle(n_opp, Game.make_rng(), own_rules()) game = Game.new_battle(Game.make_rng(), own_rules())
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile() smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
_begin() _begin()
@@ -160,7 +152,6 @@ func own_rules() -> Dictionary:
func _begin() -> void: func _begin() -> void:
n_opp = game.players.size() - 1
if is_online(): if is_online():
perspective = game.players[maxi(online_seat, 0)] perspective = game.players[maxi(online_seat, 0)]
else: else:
@@ -170,67 +161,94 @@ func _begin() -> void:
if is_online() and net.is_host: if is_online() and net.is_host:
net.broadcast() net.broadcast()
game.events.clear() game.events.clear()
b_save_leave.visible = resumable()
save_battle()
refresh() refresh()
announce_turn()
func _build_static() -> void: func _build_static() -> void:
Style.panel(self, Rect2(10, 205, 320, 290)) var L := BattleLayout
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20)) # oben links: Aufgeben und Verlauf, klein
log_box = RichTextLabel.new() b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
log_box.position = Vector2(18, 230) b_giveup.add_theme_font_size_override("font_size", 13)
log_box.size = Vector2(304, 258) b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
log_box.bbcode_enabled = true b_log.add_theme_font_size_override("font_size", 13)
log_box.scroll_following = true # Mitte: Feldband, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
log_box.add_theme_font_size_override("normal_font_size", 13) Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE field_view = FieldView.new(L.FIELD)
add_child(log_box) add_child(field_view)
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true) msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
Style.panel(self, Rect2(950, 205, 320, 380)) board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10) var slots := Control.new()
bar_panel = Style.panel(self, BattleLayout.BAR) slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
name_label = Style.label(self, "", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true) slots.draw.connect(func():
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true) for j in Game.MAX_BOARD:
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true) Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 24), "right") Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24)) add_child(slots)
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true) status_panel = Style.panel(self, L.STATUS)
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c")) var sx: float = L.STATUS.position.x
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true) var sy: float = L.STATUS.position.y
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true)
name_label.clip_text = true
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true)
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
# unten: Laden mit Neuwürfeln links und Zug beenden rechts
b_reroll = Style.button(self, "", L.B_REROLL, do_reroll, Color("285a5a"))
b_reroll.add_theme_font_size_override("font_size", 15)
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
b_end.add_theme_font_size_override("font_size", 20)
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
b_skip.add_theme_font_size_override("font_size", 14) b_skip.add_theme_font_size_override("font_size", 14)
b_skip.visible = false b_skip.visible = false
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
b_undo.visible = false
field_label = Style.label(self, "", 15, Style.MUTED, Rect2(340, 211, 600, 24), "center")
b_fuse = Style.button(self, "Verschmelzen", Rect2(10, 503, 150, 40), toggle_fuse, Color("3c2858"))
b_fuse.visible = false
b_reroll = Style.button(self, "", Rect2(170, 503, 150, 40), do_reroll, Color("285a5a"))
b_reroll.add_theme_font_size_override("font_size", 14)
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center") wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
wait_label.visible = false wait_label.visible = false
hero_layer = _layer() hero_layer = _layer()
link_view = Links.new()
add_child(link_view)
tile_layer = _layer() tile_layer = _layer()
hand_layer = _layer() hand_layer = _layer()
shop_layer = _layer() shop_layer = _layer()
preview_holder = _layer() preview_holder = _layer()
zoom_layer = _layer() info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_label.visible = false
fx_layer = _layer() fx_layer = _layer()
cursor_layer = _layer() cursor_layer = _layer()
# Verlauf als Überlagerung
log_overlay = Style.overlay(self)
log_overlay.get_child(0).gui_input.connect(func(ev):
if ev is InputEventMouseButton and ev.pressed:
log_overlay.visible = false)
Style.panel(log_overlay, L.LOG_PANEL, Style.PANEL, 10, Style.MUTED, 2)
Style.label(log_overlay, "Verlauf", 20, Style.GOLD, Rect2(L.LOG_PANEL.position + Vector2(20, 12), Vector2(300, 30)), "left", true)
log_box = RichTextLabel.new()
log_box.position = L.LOG_PANEL.position + Vector2(20, 50)
log_box.size = L.LOG_PANEL.size - Vector2(40, 110)
log_box.bbcode_enabled = true
log_box.scroll_following = true
log_box.add_theme_font_size_override("normal_font_size", 14)
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
log_overlay.add_child(log_box)
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
# Spielende # Spielende
overlay = _overlay() overlay = _overlay()
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true) result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center") reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center") notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c")) Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
# Aufgeben bestätigen confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
confirm = Style.overlay(self)
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true) func toggle_log() -> void:
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828")) log_overlay.visible = not log_overlay.visible
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
b_save_leave.add_theme_font_size_override("font_size", 14)
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
func _layer() -> Control: func _layer() -> Control:
@@ -244,54 +262,27 @@ func _overlay() -> Control:
return Style.overlay(self) return Style.overlay(self)
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
func resumable() -> bool:
return counts_for_profile()
func save_battle() -> void:
if not resumable() or game == null or game.winner != "":
return
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "game": game.snapshot(-1)}
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(d))
f.close()
func save_and_leave() -> void:
save_battle()
app.show_screen(MenuScreen.new())
func leave() -> void: func leave() -> void:
if is_online(): if is_online():
net.leave() net.leave()
if resumable() and game != null and game.winner == "":
app.clear_battle_save() # aufgegeben
app.show_screen(MenuScreen.new()) app.show_screen(MenuScreen.new())
# --- Layout-Helfer ------------------------------------------------------------------ # --- Layout-Helfer ------------------------------------------------------------------
## Der Gegner aus Sicht der eigenen Perspektive (Zuschauer: der zweite Spieler).
func opponent() -> Game.Player:
return game.enemy_of(perspective)
func others() -> Array: func others() -> Array:
var out := [] return [opponent()]
var n := game.players.size()
var start := game.players.find(perspective)
for i in range(1, n):
out.append(game.players[(start + i) % n])
return out
func compact() -> bool:
return BattleLayout.compact(others().size())
func slot_rect(p: Game.Player, j: int) -> Rect2: func slot_rect(p: Game.Player, j: int) -> Rect2:
if p == perspective: if p == perspective:
return BattleLayout.own_slot(j) return BattleLayout.own_slot(j)
var os := others() return BattleLayout.opp_slot(j)
return BattleLayout.other_slot(os.size(), os.find(p), j)
func ref_key(ref: Dictionary) -> String: func ref_key(ref: Dictionary) -> String:
@@ -306,47 +297,37 @@ func target_center(ref: Dictionary) -> Vector2:
return Vector2(-1, -1) return Vector2(-1, -1)
var p: Game.Player = game.players[ref.index] var p: Game.Player = game.players[ref.index]
if p == perspective: if p == perspective:
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2 return BattleLayout.STATUS.get_center()
if hero_panels.has(p): if hero_panels.has(p):
var panel: Panel = hero_panels[p] var panel: Panel = hero_panels[p]
return panel.position + panel.size / 2 return panel.position + panel.size / 2
return Vector2(-1, -1) return Vector2(-1, -1)
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
func build_hero_boxes() -> void: func build_hero_boxes() -> void:
Style.clear(hero_layer) Style.clear(hero_layer)
hero_panels = {} hero_panels = {}
hero_labels = {} hero_labels = {}
var os := others() var o := opponent()
var c := compact() var box := BattleLayout.OPP_BOX
for i in os.size(): var panel := Style.panel(hero_layer, box)
var o: Game.Player = os[i] panel.gui_input.connect(func(ev):
var box := BattleLayout.hero_box_rect(os.size(), i) if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
var panel := Style.panel(hero_layer, box) on_click("hero", o))
panel.gui_input.connect(func(ev): if not o.perks.is_empty():
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
on_click("hero", o)) hero_panels[o] = panel
if not o.perks.is_empty(): var labels := {}
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k])) var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
hero_panels[o] = panel if not o.perks.is_empty():
var labels := {} title += " ★"
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "left", true)
if not o.perks.is_empty(): labels.name.clip_text = true
title += " ★" labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT, labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true)
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (58 if c else 20), 26), "left", true) labels["stats"] = Style.label(hero_layer, "", 14, Style.MUTED, Rect2(box.position.x + 80, box.position.y + 34, box.size.x - 88, 20), "right")
labels.name.clip_text = true hero_labels[o] = labels
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
Rect2(box.position.x + 90, box.position.y + 38, box.size.x - 100, 20), "right")
hero_labels[o] = labels
if c:
var b := Style.button(hero_layer, "Lupe", Rect2(box.end.x - 44, box.position.y + 5, 38, 22), func():
zoom_player = o
refresh())
b.add_theme_font_size_override("font_size", 11)
# --- Ablauf und Eingabe ------------------------------------------------------------------ # --- Ablauf und Eingabe ------------------------------------------------------------------
@@ -358,7 +339,7 @@ func my_turn() -> bool:
func any_overlay() -> bool: func any_overlay() -> bool:
return confirm.visible or overlay.visible return confirm.visible or overlay.visible or log_overlay.visible
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@@ -376,7 +357,9 @@ func _input(event: InputEvent) -> void:
KEY_ESCAPE: KEY_ESCAPE:
if confirm.visible: if confirm.visible:
confirm.visible = false confirm.visible = false
elif selected != null or pending != null or fusing or zoom_player != null: elif log_overlay.visible:
log_overlay.visible = false
elif selected != null or pending != null or fusing:
cancel_all() cancel_all()
elif not any_overlay(): elif not any_overlay():
confirm.visible = true confirm.visible = true
@@ -402,7 +385,6 @@ func cancel_all() -> void:
if confirm.visible: if confirm.visible:
confirm.visible = false confirm.visible = false
cancel() cancel()
zoom_player = null
refresh() refresh()
@@ -412,10 +394,6 @@ func _process(delta: float) -> void:
msg_t = maxf(0.0, msg_t - delta) msg_t = maxf(0.0, msg_t - delta)
if msg_t <= 0.0 and msg_label.text != "": if msg_t <= 0.0 and msg_label.text != "":
msg_label.text = "" msg_label.text = ""
if preview_timer > 0.0:
preview_timer -= delta
if preview_timer <= 0.0:
show_preview(null, false)
if undo_t > 0.0: if undo_t > 0.0:
undo_t -= delta undo_t -= delta
if undo_t <= 0.0: if undo_t <= 0.0:
@@ -477,10 +455,9 @@ func _finish() -> void:
var won := game.winner == "human" var won := game.winner == "human"
if counts_for_profile(): if counts_for_profile():
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0 var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, n_opp, stage, factor) reward = app.profile.record_battle(won, stage, factor)
app.save() app.save()
app.sfx.play("win" if won else "lose") app.sfx.play("win" if won else "lose")
app.clear_battle_save()
else: else:
var wp := game.winner_player var wp := game.winner_player
var mine: bool = wp != null and wp == perspective var mine: bool = wp != null and wp == perspective
@@ -506,10 +483,13 @@ func play(card: Cards.Card, target) -> void:
return return
var before: Game = game.clone() if not is_online() else null var before: Game = game.clone() if not is_online() else null
var log_len := game.log.size() var log_len := game.log.size()
var from := _hand_rect_of(card)
var err := game.play_card(perspective, card, target) var err := game.play_card(perspective, card, target)
if err != "": if err != "":
say(tr(err)) say(tr(err))
return return
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
arrivals[perspective.board[-1].uid] = from
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos) # Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
if before != null and game.events.size() == 1 and game.events[0].type == "play": if before != null and game.events.size() == 1 and game.events[0].type == "play":
undo_state = before undo_state = before
@@ -534,9 +514,15 @@ func do_buy(slot: int) -> void:
if is_client(): if is_client():
net.send_action({"type": "buy", "slot": slot}) net.send_action({"type": "buy", "slot": slot})
return return
var from := BattleLayout.shop_rect(slot)
var offer = perspective.shop[slot] if slot < perspective.shop.size() else null
var err := game.buy(perspective, slot) var err := game.buy(perspective, slot)
if err != "": if err != "":
say(tr(err)) say(tr(err))
elif offer != null:
fx.sfx("buy")
fx.fly_card(offer, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
Cards.RARITIES[int(perspective.looks.get(offer.id, 0))])
## Würfelt den Laden neu (kostet Mana). ## Würfelt den Laden neu (kostet Mana).
@@ -551,6 +537,8 @@ func do_reroll() -> void:
var err := game.reroll(perspective) var err := game.reroll(perspective)
if err != "": if err != "":
say(tr(err)) say(tr(err))
else:
deal_shop = true
after_action() after_action()
@@ -596,7 +584,6 @@ func end_turn() -> void:
return return
cancel() cancel()
clear_undo() clear_undo()
zoom_player = null
if is_client(): if is_client():
net.send_action({"type": "end"}) net.send_action({"type": "end"})
return return
@@ -629,7 +616,6 @@ func on_click(kind: String, obj) -> void:
play(card, obj) play(card, obj)
else: else:
say(tr("Abgebrochen")) say(tr("Abgebrochen"))
zoom_player = null
after_action() after_action()
return return
if selected != null: if selected != null:
@@ -643,7 +629,6 @@ func on_click(kind: String, obj) -> void:
var err := g.attack(cr, obj) var err := g.attack(cr, obj)
if err != "": if err != "":
say(tr(err)) say(tr(err))
zoom_player = null
after_action() after_action()
return return
match kind: match kind:
@@ -677,17 +662,121 @@ func on_tile_click(cr: Game.Creature) -> void:
on_click("enemy", cr) on_click("enemy", cr)
func show_preview(card, entered: bool, owner_looks: Dictionary = {}) -> void: ## Zugansage, wenn der eigene Zug beginnt.
func announce_turn() -> void:
if game == null or not my_turn() or banner_turn == game.turn:
return
banner_turn = game.turn
fx.banner(tr("Dein Zug"), Style.GOLD)
fx.sfx("turn")
deal_shop = true
refresh()
func _hand_rect_of(card: Cards.Card) -> Rect2:
for v in hand_views:
if v.card == card:
return Rect2(v.position, v.size)
return Rect2()
## Vorschau schwebt neben der gezeigten Karte (anchor), darunter die Zusatzzeile (info).
func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: String = "", anchor: Rect2 = Rect2()) -> void:
Style.clear(preview_holder) Style.clear(preview_holder)
info_label.visible = entered and info != ""
info_label.text = info if entered else ""
if entered and card != null: if entered and card != null:
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))]) var r := BattleLayout.preview_rect(anchor)
v.position = Vector2(1005, 215) var v := CardView.new(card, r.size, false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
v.position = r.position
v.mouse_filter = Control.MOUSE_FILTER_IGNORE v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v) preview_holder.add_child(v)
var ir := BattleLayout.info_rect(r)
info_label.position = ir.position
info_label.size = ir.size
func show_hand_preview(card: Cards.Card, entered: bool) -> void: ## Eigene Hand- und Ladenkarte: zeigt, was das Ausspielen jetzt brächte, und markiert den Kombo-Partner.
show_preview(card, entered, perspective.looks) func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
var f := game.forecast(perspective, card)
var lines: Array[String] = []
var parts: Array[String] = []
if f.resonance > 0:
parts.append(tr("Resonanz +%d") % f.resonance)
if f.field > 0:
parts.append(tr("Feld +%d") % f.field)
if not parts.is_empty():
lines.append(" · ".join(parts))
if f.combo != "":
lines.append(tr("Feldkombo %s mit %s: %s") % [tr(f.combo), f.partner.card.title(), tr(Cards.FIELD_COMBO_TEXT[f.combo])])
for t in tiles.values():
t.mark = ""
t.queue_redraw()
if entered and f.partner != null and tiles.has(f.partner.uid):
tiles[f.partner.uid].mark = f.combo
tiles[f.partner.uid].queue_redraw()
show_preview(card, entered, perspective.looks, "\n".join(lines), anchor)
## Hand- und Ladenkarte tragen ihren Bonus und ihre Feldkombo als Vorschau.
func apply_forecast(v: CardView) -> void:
var f := game.forecast(perspective, v.card)
v.bonus = f.resonance + f.field
v.tag = f.combo
## Was den Angriff eines Wesens gerade hebt.
func creature_info(cr: Game.Creature) -> String:
var parts: Array[String] = []
if cr.resonance > 0:
parts.append(tr("Resonanz +%d") % cr.resonance)
if cr.field_bonus > 0:
parts.append(tr("Feld +%d") % cr.field_bonus)
var perk := cr.power() - cr.attack - cr.resonance - cr.field_bonus
if perk > 0:
parts.append(tr("Merkmal +%d") % perk)
return " · ".join(parts)
## Elementvorteil gegen ein mögliches Ziel: des gewählten Angreifers oder eines Schadenszaubers auf der Hand.
func matchup_for(cr: Game.Creature, valid: Array) -> int:
if selected != null and game.can_attack(selected, cr)[0]:
return Game.matchup_bonus(selected, cr)
if pending != null and cr in valid and pending.effects.any(func(fx): return fx.kind == "damage" and fx.target in Cards.CHOSEN_TARGETS):
return Cards.matchup(pending.elements, cr.card.elements)
return 0
## Resonanzgruppen je Reihe: Wesen mit gleichem Element werden unter der Reihe verbunden.
func resonance_groups() -> Array:
var out := []
for p in game.players:
var k := 0
for e in Cards.ELEMENTS:
var xs: Array[float] = []
for j in p.board.size():
if e in p.board[j].card.elements:
xs.append(slot_rect(p, j).get_center().x)
if xs.size() < 2:
continue
out.append({"color": Style.lighter(Cards.ELEMENT_COLORS[e], 0.25), "y": slot_rect(p, 0).end.y + 4 + k * 6, "xs": xs})
k += 1
return out
class Links extends Control:
var groups: Array = []
func _init() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_IGNORE
func _draw() -> void:
for gr in groups:
var y: float = gr.y
draw_line(Vector2(gr.xs[0], y), Vector2(gr.xs[-1], y), gr.color, 3.0)
for x in gr.xs:
draw_circle(Vector2(x, y), 4.0, gr.color)
# --- Netzwerk ------------------------------------------------------------------------------- # --- Netzwerk -------------------------------------------------------------------------------
@@ -744,7 +833,7 @@ func after_action() -> void:
centers[ref_key(ev[key])] = target_center(ev[key]) centers[ref_key(ev[key])] = target_center(ev[key])
refresh() refresh()
fx.play_events(evs, centers) fx.play_events(evs, centers)
save_battle() announce_turn()
# --- Ansicht angleichen -------------------------------------------------------------------- # --- Ansicht angleichen --------------------------------------------------------------------
@@ -781,8 +870,7 @@ func refresh() -> void:
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED) labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
if o.alive(): if o.alive():
labels.life.text = "♥ %d" % maxi(o.life, 0) labels.life.text = "♥ %d" % maxi(o.life, 0)
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \ labels.stats.text = "Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy]
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy])
else: else:
labels.life.text = "besiegt" labels.life.text = "besiegt"
labels.life.add_theme_color_override("font_color", Style.MUTED) labels.life.add_theme_color_override("font_color", Style.MUTED)
@@ -798,10 +886,11 @@ func refresh() -> void:
if tiles.has(cr.uid): if tiles.has(cr.uid):
tile = tiles[cr.uid] tile = tiles[cr.uid]
tile.creature = cr tile.creature = cr
if tile.size != rect.size: if tile.size != rect.size: # erst die Mindestgröße, sonst bleibt die Kachel groß
tile.size = rect.size
tile.custom_minimum_size = rect.size tile.custom_minimum_size = rect.size
tile.size = rect.size
tile.pivot_offset = rect.size / 2 tile.pivot_offset = rect.size / 2
tile.set_process(Style.rarity_animated(tile.rarity()))
else: else:
tile = CreatureView.new(cr, rect.size) tile = CreatureView.new(cr, rect.size)
tile.position = rect.position tile.position = rect.position
@@ -809,13 +898,17 @@ func refresh() -> void:
tiles[cr.uid] = tile tiles[cr.uid] = tile
tile_layer.add_child(tile) tile_layer.add_child(tile)
var looks: Dictionary = p.looks var looks: Dictionary = p.looks
tile.hover.connect(func(card, entered): show_preview(card, entered, looks)) tile.hover.connect(func(card, entered): show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
var uid := cr.uid var uid := cr.uid
tile.clicked.connect(func(_v): tile.clicked.connect(func(_v):
var c := game.find_creature(uid) var c := game.find_creature(uid)
if c != null: if c != null:
on_tile_click(c)) on_tile_click(c))
fx.pop_in(tile) if arrivals.has(cr.uid):
fx.fly_in(tile, arrivals[cr.uid])
arrivals.erase(cr.uid)
else:
fx.pop_in(tile)
tile.slot = rect.position tile.slot = rect.position
if not tile.busy and tile.position != rect.position: if not tile.busy and tile.position != rect.position:
fx.move_to(tile, rect.position) fx.move_to(tile, rect.position)
@@ -835,6 +928,8 @@ func refresh() -> void:
w = 3 w = 3
tile.border = b tile.border = b
tile.width = w tile.width = w
tile.mark = ""
tile.matchup = matchup_for(cr, valid)
tile.queue_redraw() tile.queue_redraw()
if p == h: if p == h:
own_views.append(tile) own_views.append(tile)
@@ -845,6 +940,8 @@ func refresh() -> void:
var t: CreatureView = tiles[uid] var t: CreatureView = tiles[uid]
tiles.erase(uid) tiles.erase(uid)
fx.fade_out(t) fx.fade_out(t)
link_view.groups = resonance_groups()
link_view.queue_redraw()
Style.clear(shop_layer) Style.clear(shop_layer)
if not spectating(): if not spectating():
@@ -856,17 +953,22 @@ func refresh() -> void:
continue continue
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))]) var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
sv.position = sr.position sv.position = sr.position
sv.lift = mine
apply_forecast(sv)
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
sv.hover.connect(show_hand_preview) sv.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(sv.position, sv.size)))
var slot := i var slot := i
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
sv.clicked.connect(func(_view): on_click("shop", slot)) sv.clicked.connect(func(_view): on_click("shop", slot))
shop_layer.add_child(sv) shop_layer.add_child(sv)
shop_views.append(sv) shop_views.append(sv)
if deal_shop:
fx.deal(sv, i)
deal_shop = false
Style.clear(hand_layer) Style.clear(hand_layer)
if spectating(): if spectating():
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center") Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
else: else:
for i in h.hand.size(): for i in h.hand.size():
var card: Cards.Card = h.hand[i] var card: Cards.Card = h.hand[i]
@@ -874,12 +976,14 @@ func refresh() -> void:
var r := BattleLayout.hand_rect(h.hand.size(), i) 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, Cards.RARITIES[int(h.looks.get(card.id, 0))])
v.position = r.position v.position = r.position
v.lift = mine
apply_forecast(v)
if card == pending: if card == pending:
v.border = Style.YELLOW v.border = Style.YELLOW
elif playable: elif playable:
v.border = Style.GREEN v.border = Style.GREEN
v.dim = not playable v.dim = not playable
v.hover.connect(show_hand_preview) 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.clicked.connect(func(_view): on_click("hand", card))
hand_layer.add_child(v) hand_layer.add_child(v)
hand_views.append(v) hand_views.append(v)
@@ -887,7 +991,11 @@ func refresh() -> void:
var bar_border = Style.BLUE if mine else null var bar_border = Style.BLUE if mine else null
if int(flash_until.get(g.players.find(h), 0)) > now: if int(flash_until.get(g.players.find(h), 0)) > now:
bar_border = Style.RED bar_border = Style.RED
bar_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0)) status_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
var board_col := Style.darker(Style.PANEL, 0.8)
if g.field_element != "":
board_col = board_col.lerp(Cards.ELEMENT_COLORS[g.field_element], 0.22) # das Feld färbt das Brett
board_panel.add_theme_stylebox_override("panel", Style.box(board_col, 10))
name_label.text = h.name name_label.text = h.name
life_label.text = "♥ %d" % maxi(h.life, 0) life_label.text = "♥ %d" % maxi(h.life, 0)
energy_label.text = "◆ %d" % h.energy energy_label.text = "◆ %d" % h.energy
@@ -902,18 +1010,10 @@ func refresh() -> void:
b_undo.visible = undo_state != null and mine b_undo.visible = undo_state != null and mine
b_fuse.visible = mine and not spectating() and h.board.size() >= 2 b_fuse.visible = mine and not spectating() and h.board.size() >= 2
b_reroll.visible = mine and not spectating() b_reroll.visible = mine and not spectating()
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST b_reroll.text = tr("Neu würfeln ◆ %d") % Game.REROLL_COST
b_reroll.disabled = h.energy < Game.REROLL_COST b_reroll.disabled = h.energy < Game.REROLL_COST
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen") b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
if g.field_element == "": field_view.update_from(g)
var parts: Array[String] = []
for e in Cards.ELEMENTS:
parts.append("%s %d" % [tr(e), int(g.field_charge.get(e, 0))])
field_label.text = tr("Feld: neutral") + " · " + " ".join(parts)
field_label.add_theme_color_override("font_color", Style.MUTED)
else:
field_label.text = "%s: %s" % [tr(Cards.FIELD_NAMES[g.field_element]), tr(Cards.FIELD_TEXT[g.field_element])]
field_label.add_theme_color_override("font_color", Cards.ELEMENT_COLORS[g.field_element])
var lines: Array[String] = [] var lines: Array[String] = []
var start := maxi(0, g.log.size() - 60) var start := maxi(0, g.log.size() - 60)
for i in range(start, g.log.size()): for i in range(start, g.log.size()):
@@ -929,10 +1029,6 @@ func refresh() -> void:
log_box.text = "\n".join(lines) log_box.text = "\n".join(lines)
pad.draw_cursor() pad.draw_cursor()
Style.clear(zoom_layer)
if zoom_player != null and zoom_player.alive():
build_zoom(valid)
if g.winner != "": if g.winner != "":
var wp := g.winner_player var wp := g.winner_player
if counts_for_profile(): if counts_for_profile():
@@ -951,30 +1047,3 @@ func refresh() -> void:
overlay.visible = true overlay.visible = true
confirm.visible = false confirm.visible = false
func build_zoom(valid: Array) -> void:
var o: Game.Player = zoom_player
var g := game
var box := Rect2(190, 190, 900, 300)
var back := Style.panel(zoom_layer, box, Style.PANEL, 10, Style.GOLD, 2)
back.mouse_filter = Control.MOUSE_FILTER_STOP
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d ◆ %d (+%d)" % [o.name, o.title, o.life, o.hand.size(), o.energy, o.max_energy],
18, Style.TEXT, Rect2(box.position.x + 20, box.position.y + 12, 800, 30), "left", true)
for j in o.board.size():
var cr: Game.Creature = o.board[j]
var b = null
if cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
b = Style.ORANGE
var v := CreatureView.new(cr, Vector2(110, 140), b, 3 if b != null else 2)
v.position = Vector2(box.position.x + 20 + j * 122, box.position.y + 56)
v.hover.connect(show_preview)
v.clicked.connect(func(_x): on_tile_click(cr))
zoom_layer.add_child(v)
if o.board.is_empty():
Style.label(zoom_layer, "Keine Wesen auf dem Feld", 16, Style.MUTED, Rect2(box.position.x + 20, box.position.y + 110, 600, 30))
var hero_ok: bool = (o in valid) or (selected != null and g.can_attack(selected, o)[0])
var bh := Style.button(zoom_layer, "Held als Ziel", Rect2(box.position.x + 20, box.end.y - 60, 180, 44), func(): on_click("hero", o), Color("5a3c28"))
bh.disabled = not hero_ok
Style.button(zoom_layer, "Schließen", Rect2(box.end.x - 180, box.end.y - 60, 160, 44), func():
zoom_player = null
refresh())

View File

@@ -12,6 +12,8 @@ var msg_label: Label
var b_buy: Button var b_buy: Button
var b_all: Button var b_all: Button
var slots: Control var slots: Control
var fx_layer: Control
var fresh := {} # gerade aufgedeckt: wird umgedreht
class CardBack extends Control: class CardBack extends Control:
@@ -47,15 +49,18 @@ func slot_rect(i: int) -> Rect2:
func _ready() -> void: func _ready() -> void:
Style.label(self, "Booster", 36, Style.GOLD, Rect2(0, 20, 1280, 50), "center", true) Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center") coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
fx_layer = Control.new()
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(fx_layer)
slots = Control.new() slots = Control.new()
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(slots) add_child(slots)
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 1280, 22), "center") msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center")
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(290, 720, 330, 48), buy, Color("5a4628")) b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628"))
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all) b_buy.add_theme_font_size_override("font_size", 20)
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new())) b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
refresh() refresh()
@@ -81,16 +86,44 @@ func buy() -> void:
func reveal(i: int) -> void: func reveal(i: int) -> void:
revealed[i] = true revealed[i] = true
app.sfx.play("click") fresh = {i: 0.0}
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click")
refresh() refresh()
func reveal_all() -> void: func reveal_all() -> void:
fresh = {}
var k := 0
for i in Profile.BOOSTER_SIZE: for i in Profile.BOOSTER_SIZE:
if not revealed.has(i):
fresh[i] = k * 0.12
k += 1
revealed[i] = true revealed[i] = true
app.sfx.play("play")
refresh() refresh()
## Umdrehen: die Karte wächst aus der Mitte; seltene Stufen blitzen in ihrer Farbe auf.
func flip(v: CardView, delay: float, rarity: String) -> void:
if not Style.animations:
return
v.scale.x = 0.0
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":
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
burst.modulate.a = 0.0
var bt := create_tween()
bt.tween_interval(delay + 0.1)
bt.tween_property(burst, "modulate:a", 1.0, 0.05)
bt.parallel().tween_property(burst, "scale", Vector2(1.25, 1.25), 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
bt.tween_property(burst, "modulate:a", 0.0, 0.3)
bt.tween_callback(burst.queue_free)
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
coins_label.text = "Münzen: %d" % p.coins coins_label.text = "Münzen: %d" % p.coins
@@ -107,6 +140,8 @@ func refresh() -> void:
var v := CardView.new(c, r.size, false, res.rarity) var v := CardView.new(c, r.size, false, res.rarity)
v.position = r.position v.position = r.position
slots.add_child(v) slots.add_child(v)
if fresh.has(i):
flip(v, fresh[i], res.rarity)
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true) Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
if res.upgraded: if res.upgraded:
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true) Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
@@ -117,3 +152,4 @@ func refresh() -> void:
back.position = r.position back.position = r.position
back.clicked.connect(reveal.bind(i)) back.clicked.connect(reveal.bind(i))
slots.add_child(back) slots.add_child(back)
fresh = {}

View File

@@ -1,32 +1,37 @@
class_name CampaignScreen class_name CampaignScreen
extends Control extends Control
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail.
var app var app
var chapter := 1 var chapter := 1
var selected := 0 # gewählte Stufe (Index über alle Kapitel)
var perk_box: OptionButton var perk_box: OptionButton
var node_buttons: Array = []
func _ready() -> void: func _ready() -> void:
var p: Profile = app.profile var p: Profile = app.profile
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size()) chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
selected = mini(p.campaign_stage, Campaign.total() - 1)
build() build()
func build() -> void: func build() -> void:
Style.clear(self) Style.clear(self)
node_buttons = []
var p: Profile = app.profile var p: Profile = app.profile
var col := Style.column(self, 1000, 16, 5) Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true) var n := Campaign.STAGES.size()
var tabs := Style.row(col, 10) var x := 40
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
for c in Campaign.CHAPTERS.size(): for c in Campaign.CHAPTERS.size():
var b := Style.cbutton(tabs, "Kapitel %d" % (c + 1), Vector2(140, 36), set_chapter.bind(c + 1), Color("285a3c") if chapter == c + 1 else null) var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 150, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
b.disabled = p.campaign_stage < c * Campaign.STAGES.size() b.disabled = p.campaign_stage < c * n
x += 160
if p.perk_unlocked(): if p.perk_unlocked():
var pr := Style.row(tabs, 8) Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right")
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
perk_box = OptionButton.new() perk_box = OptionButton.new()
perk_box.custom_minimum_size = Vector2(300, 36) perk_box.position = Vector2(900, 88)
perk_box.size = Vector2(340, 38)
perk_box.add_item("keins") perk_box.add_item("keins")
var keys: Array = Game.PERKS.keys() var keys: Array = Game.PERKS.keys()
for k in keys: for k in keys:
@@ -35,54 +40,90 @@ func build() -> void:
perk_box.item_selected.connect(func(i: int): perk_box.item_selected.connect(func(i: int):
p.perk = "" if i == 0 else keys[i - 1] p.perk = "" if i == 0 else keys[i - 1]
app.save()) app.save())
pr.add_child(perk_box) add_child(perk_box)
var offset := (chapter - 1) * Campaign.STAGES.size() # Pfad mit Stationen
for k in Campaign.STAGES.size(): var offset := (chapter - 1) * n
var y := 190.0
var step := (1280 - 200) / float(n - 1)
var centers: Array[Vector2] = []
for k in n:
centers.append(Vector2(100 + k * step, y))
var path := Control.new()
path.mouse_filter = Control.MOUSE_FILTER_IGNORE
path.draw.connect(func():
for k in n - 1:
var done: bool = offset + k + 1 <= p.campaign_stage
path.draw_line(centers[k], centers[k + 1], Style.GREEN if done else Style.PANEL_LIGHT, 4.0))
add_child(path)
for k in n:
var i := offset + k var i := offset + k
var st := Campaign.stage(i)
var done := i < p.campaign_stage var done := i < p.campaign_stage
var next := i == p.campaign_stage var next := i == p.campaign_stage
var panel := PanelContainer.new() var b := Button.new()
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6, b.text = str(i + 1)
Style.GOLD if next else null, 2 if next else 0)) b.position = centers[k] - Vector2(24, 24)
col.add_child(panel) b.size = Vector2(48, 48)
var pad := MarginContainer.new() # Innenabstand, damit Name und Knopf nicht am Rahmen kleben var base: Color = Style.PRIMARY if done else (Color("5a4628") if next else Style.PANEL)
for side in ["left", "right"]: var edge: Color = Style.GOLD if i == selected else (Style.GREEN if done else Style.MUTED)
pad.add_theme_constant_override("margin_" + side, 14) b.add_theme_stylebox_override("normal", Style.box(base, 24, edge, 3 if i == selected else 2))
panel.add_child(pad) b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 24, edge, 3))
var r := Style.row(pad, 10) b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 24, edge, 3))
r.custom_minimum_size = Vector2(0, 50) b.add_theme_stylebox_override("disabled", Style.box(Style.darker(Style.PANEL, 0.8), 24, Style.PANEL_LIGHT, 2))
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420) b.add_theme_stylebox_override("focus", Style.box(Color(0, 0, 0, 0), 24, Style.GOLD, 3))
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts b.disabled = i > p.campaign_stage
var dots := Control.new() b.pressed.connect(func():
dots.custom_minimum_size = Vector2(60, 0) selected = i
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE build())
var elems: Array = st.elements Style.juice(b)
dots.draw.connect(func(): add_child(b)
for m in elems.size(): node_buttons.append(b)
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]])) if next and Style.animations:
r.add_child(dots) b.pivot_offset = b.size / 2
if done: var tw := b.create_tween().set_loops()
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i)) tw.tween_property(b, "scale", Vector2(1.1, 1.1), 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
elif next: tw.tween_property(b, "scale", Vector2.ONE, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c")) # Gewählte Stufe
else: var st := Campaign.stage(selected)
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass) var done := selected < p.campaign_stage
b.disabled = true var next := selected == p.campaign_stage
Style.panel(self, Rect2(240, 270, 800, 300))
Style.label(self, "%s · %s" % [st.name, st.title], 28, Style.TEXT, Rect2(270, 290, 740, 40), "center", true)
var dots := Control.new()
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
var elems: Array = st.elements
dots.draw.connect(func():
var w := elems.size() * 26 - 6
for m in elems.size():
dots.draw_circle(Vector2(640 - w / 2.0 + 10 + m * 26, 348), 10, Cards.ELEMENT_COLORS[elems[m]]))
add_child(dots)
Style.label(self, "%s · %s" % [tr("%d Leben") % int(st.life), tr("%d Münzen") % st.reward], 16, Style.MUTED, Rect2(270, 370, 740, 24), "center")
var perks: Array = st.perks
var py := 402
for k in perks:
Style.label(self, "★ " + tr(Game.PERKS[k]), 15, Style.GOLD, Rect2(270, py, 740, 22), "center")
py += 24
if done:
Style.button(self, "Wiederholen", Rect2(540, 500, 200, 50), _start.bind(selected))
elif next:
var b := Style.button(self, "Kämpfen", Rect2(540, 500, 200, 50), _start.bind(selected), Style.PRIMARY)
b.add_theme_font_size_override("font_size", 20)
else:
var b := Style.button(self, "Gesperrt", Rect2(540, 500, 200, 50), func(): pass)
b.disabled = true
if p.campaign_stage >= Campaign.total(): if p.campaign_stage >= Campaign.total():
Style.clabel(col, "Alle Kapitel abgeschlossen", 16, Style.GREEN, "center", true) Style.label(self, "Alle Kapitel abgeschlossen", 16, Style.GREEN, Rect2(0, 600, 1280, 24), "center", true)
var br := Style.row(col)
br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
func set_chapter(c: int) -> void: func set_chapter(c: int) -> void:
chapter = c chapter = c
var p: Profile = app.profile
var first := (c - 1) * Campaign.STAGES.size()
selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first
build() build()
func _start(stage: int) -> void: func _start(stage: int) -> void:
app.show_screen(BattleScreen.new(0, stage)) app.show_screen(BattleScreen.new(stage))
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:

View File

@@ -1,13 +1,13 @@
class_name CardGrid class_name CardGrid
extends ScrollContainer extends ScrollContainer
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte. ## Scrollbares Kartenraster.
signal card_clicked(card: Cards.Card) signal card_clicked(card: Cards.Card)
const COLS := 7 const COLS := 6
const CW := 100 const CW := 112
const CH := 140 const CH := 157
const GAP := 12 const GAP := 14
var grid: GridContainer var grid: GridContainer
@@ -24,19 +24,16 @@ func _init(rect: Rect2) -> void:
add_child(grid) add_child(grid)
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool; rarity_fn(card) -> String (Optik) ## cards: Karten; rarity_fn(card) -> String: die Stufe, die die Karte zeigt (Optik)
func fill(cards: Array, label_fn: Callable, dim_fn: Callable, rarity_fn: Callable = Callable()) -> void: func fill(cards: Array, rarity_fn: Callable) -> void:
Style.clear(grid) Style.clear(grid)
for c in cards: for c in cards:
var cell := Control.new() var cell := Control.new()
cell.custom_minimum_size = Vector2(CW, CH + 16) cell.custom_minimum_size = Vector2(CW, CH)
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c) if rarity_fn.is_valid() else "gewöhnlich") var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c))
v.dim = dim_fn.call(c)
v.clicked.connect(func(_view): card_clicked.emit(c)) v.clicked.connect(func(_view): card_clicked.emit(c))
cell.add_child(v) cell.add_child(v)
var text: String = label_fn.call(c)
Style.label(cell, text, 12, Style.MUTED if v.dim else Style.TEXT, Rect2(0, CH + 1, CW, 16), "center")
grid.add_child(cell) grid.add_child(cell)

View File

@@ -1,25 +1,37 @@
class_name CardView class_name CardView
extends Control extends Control
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau). ## 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.
signal clicked(view: CardView) signal clicked(view: CardView)
signal hover(card: Cards.Card, entered: bool) signal hover(card: Cards.Card, entered: bool)
var card: Cards.Card var card: Cards.Card
var border = null # Color oder null (dann Seltenheitsfarbe) var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
var rarity := "gewöhnlich" # nur Optik: Rahmenfarbe der eigenen Stufe var rarity := "gewöhnlich" # nur Optik
var dim := false var dim := false
var bonus := 0 # Vorschau im Kampf: Angriff, den die Karte auf dem Feld dazubekäme
var tag := "" # Vorschau im Kampf: Feldkombo, die das Ausspielen auslöst
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick. ## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
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 = "gewöhnlich") -> void:
card = c card = c
rarity = rar rarity = rar
set_process(Style.rarity_animated(rarity)) # bewegter Glanz
custom_minimum_size = sz custom_minimum_size = sz
size = sz size = sz
pivot_offset = sz / 2
mouse_filter = Control.MOUSE_FILTER_STOP mouse_filter = Control.MOUSE_FILTER_STOP
mouse_entered.connect(func(): hover.emit(card, true)) mouse_entered.connect(func():
mouse_exited.connect(func(): hover.emit(card, false)) hover.emit(card, true)
_lift(true))
mouse_exited.connect(func():
hover.emit(card, false)
_lift(false))
if focusable: if focusable:
focus_mode = Control.FOCUS_ALL focus_mode = Control.FOCUS_ALL
focus_entered.connect(func(): focus_entered.connect(func():
@@ -30,6 +42,17 @@ func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "g
queue_redraw()) queue_redraw())
func _lift(up: bool) -> void:
if not lift or not Style.animations:
return
z_index = 1 if up else 0
create_tween().tween_property(self, "scale", Vector2(1.07, 1.07) if up else Vector2.ONE, 0.1).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
func _process(_delta: float) -> void:
queue_redraw()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event() accept_event()
@@ -41,47 +64,135 @@ func _gui_input(event: InputEvent) -> void:
func _draw() -> void: func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
var big := size.x >= 180 var seed := hash(card.id)
var mid := size.x >= 115 var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
var f_name := 18 if big else (14 if mid else 12) if rarity != "gewöhnlich":
var f_text := 14 if big else (11 if mid else 10) Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
var f_stat := 24 if big else (18 if mid else 15)
Style.fill_elements(self, rect, card.elements, 8) Style.fill_elements(self, rect, card.elements, 8)
var bcol: Color = border if border != null else Cards.RARITY_COLORS[rarity] if size.y < 90:
Style.border(self, rect, bcol, 3 if big else 2, 8) _draw_compact(rect, frame, seed)
# Kosten else:
var r := 16 if big else 12 _draw_full(rect, frame, seed)
var cc := Vector2(r + 4, r + 4)
draw_circle(cc, r, Color("14141c"))
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
# Name und Kombo
var name_left := 2 * r + 10
var nm := Style.fit(card.title(), true, f_name, size.x - name_left - 6)
Style.text(self, nm, f_name, Style.TEXT, Vector2((name_left + size.x - 6) / 2.0, 4), "midtop", true)
if mid:
var sub: String = tr(card.combo()) if card.is_combo() else ""
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
var tsize := Style.fs(f_text)
var text_w: float = size.x - 20
var needed: float = Style.FONT.get_multiline_string_size(card.text(), HORIZONTAL_ALIGNMENT_LEFT, text_w, tsize).y + 8 if card.text() != "" else size.y * 0.42
var box_bottom: float = size.y - (28 if big else 22)
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) # unter Name und Kombo
var box_top: float = maxf(maxf(size.y * 0.30, band_top), box_bottom - maxf(needed, size.y * 0.42))
var box := Rect2(6, box_top, size.x - 12, box_bottom - box_top)
# Bild
var band_bottom: float = box_top - 4
if band_bottom - band_top > 14:
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
var max_lines := maxi(1, int((box.size.y - 6) / Style.FONT.get_height(tsize))) # große Schrift: lieber kürzen als überlaufen
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
# Werte
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
if dim: if dim:
draw_rect(rect, Color(0, 0, 0, 0.47)) draw_rect(rect, Color(0, 0, 0, 0.47))
if has_focus(): if has_focus():
Style.border(self, rect, Style.GOLD, 4, 8) Style.border(self, rect, Style.GOLD, 4, 8)
## Volle Karte: Laden, Sammlung, Booster, Vorschau.
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
var big := size.x >= 180
var mid := size.x >= 140
var f_name := 17 if big else (14 if mid else 11)
var f_text := 13 if big else (11 if mid else 10)
var f_stat := 20 if big else (17 if mid else 14)
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
var icon := 11.0 if big else (9.0 if mid else 7.0)
# Bild: das Elementsymbol füllt den oberen Teil
var band_top: float = size.y * 0.36
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)
# 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)
var y := band.end.y + 4
# Kombo eines verschmolzenen Wesens
if card.is_combo():
Style.text(self, tr(card.combo()), f_text, Color("e6e6e6"), Vector2(size.x / 2, y), "midtop")
y += Style.fs(f_text) + 4
# Schlüsselwörter als Symbole, auf großen Karten mit Wort
var kws: Array[String] = []
for k in Cards.KEYWORDS:
if k in card.keywords:
kws.append(k)
if not kws.is_empty():
var with_words := big
var widths: Array[float] = []
var total := 0.0
for k in kws:
var w: float = icon * 2 + 6 + (Style.FONT.get_string_size(tr(k), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(f_text)).x + 8 if with_words else 0)
widths.append(w)
total += w + 6
if with_words and total > size.x - 12:
with_words = false
total = kws.size() * (icon * 2 + 12)
var x := (size.x - total + 6) / 2
for i in kws.size():
var k := kws[i]
Style.keyword_icon(self, k, Vector2(x + icon, y + icon + 2), icon)
if with_words:
Style.text(self, tr(k), f_text, Style.TEXT, Vector2(x + icon * 2 + 6, y + icon + 2), "midleft")
x += widths[i] + 6
else:
x += icon * 2 + 12
y += icon * 2 + 8
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
var box := Rect2()
var fx := card.effect_text()
var box_bottom: float = size.y - r_stat * 2 - 4
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
var tsize := Style.fs(f_text)
var heading := big
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8 + (tsize + 2 if heading else 0)
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
var ty := box.position.y + 4
if heading:
Style.text(self, tr("Beim Ausspielen"), f_text - 2, Style.MUTED, Vector2(box.position.x + 4, ty), "topleft")
ty += tsize + 2
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, ty + Style.FONT.get_ascent(tsize)),
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
Style.border(self, rect, frame, 3 if big else 2, 8)
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
if tag != "":
_draw_tag(Vector2(size.x - 6, 6), f_text - 1)
Style.rarity_sparkle(self, rect, rarity, seed, box)
## Flache Handkachel: Kosten, Name, Symbole, Werte.
func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
Style.rarity_sheen(self, rect, rarity, seed)
Style.border(self, rect, frame, 2, 8)
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
var name_w: float = size.x - 30 - (12 if rarity != "gewöhnlich" 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)
if tag != "":
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(9)).x + 12
_draw_tag(Vector2(size.x / 2 + w / 2, size.y - 21), 9)
else:
var kws: Array[String] = []
for k in Cards.KEYWORDS:
if k in card.keywords:
kws.append(k)
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
for k in kws:
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
x += 18
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
Style.rarity_sparkle(self, rect, rarity, seed)
## Goldenes Schild mit der Feldkombo, rechts oben.
func _draw_tag(top_right: Vector2, font: int) -> void:
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(font)).x + 12
var h := Style.fs(font) + 6
var r := Rect2(top_right - Vector2(w, 0), Vector2(w, h))
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), r)
Style.text(self, tr(tag), font, Style.GOLD, r.get_center(), "center", true)

View File

@@ -18,33 +18,32 @@ var b_upgrade: Button
func _ready() -> void: func _ready() -> void:
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true) Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind("")) Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
for i in Cards.ELEMENTS.size(): for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i] var e: String = Cards.ELEMENTS[i]
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8)) var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
fb.add_theme_font_size_override("font_size", 14) fb.add_theme_font_size_override("font_size", 14)
search_box = LineEdit.new() search_box = LineEdit.new()
search_box.position = Vector2(424, 55) search_box.position = Vector2(444, 86)
search_box.size = Vector2(200, 34) search_box.size = Vector2(220, 34)
search_box.placeholder_text = "Suche" search_box.placeholder_text = "Suche"
search_box.text_changed.connect(func(t: String): search_box.text_changed.connect(func(t: String):
search = t search = t
refresh()) refresh())
add_child(search_box) add_child(search_box)
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690)) grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
grid.card_clicked.connect(select) grid.card_clicked.connect(select)
add_child(grid) add_child(grid)
Style.panel(self, Rect2(845, 50, 420, 680)) Style.panel(self, Rect2(820, 86, 420, 696))
dust_label = Style.label(self, "", 22, Style.BLUE, Rect2(860, 58, 390, 30), "left", true) dust_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
preview_holder = Control.new() preview_holder = Control.new()
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(preview_holder) add_child(preview_holder)
info_label = Style.label(self, "", 14, Style.MUTED, Rect2(860, 396, 390, 66), "center") info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
b_upgrade = Style.button(self, "", Rect2(860, 480, 390, 44), upgrade, Color("285a3c")) b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
b_dismantle = Style.button(self, "", Rect2(860, 534, 390, 44), dismantle, Color("5a3c28")) b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 600, 390, 48), "center") msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
refresh() refresh()
@@ -58,6 +57,7 @@ func set_filter(e: String) -> void:
refresh() refresh()
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
func cards() -> Array: func cards() -> Array:
var out: Array = Cards.all().duplicate() var out: Array = Cards.all().duplicate()
if filter != "": if filter != "":
@@ -65,6 +65,11 @@ func cards() -> Array:
if search != "": if search != "":
var q := search.to_lower() var q := search.to_lower()
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower()) out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
var p: Profile = app.profile
var order := {}
for i in out.size():
order[out[i].id] = i
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
return out return out
@@ -109,7 +114,7 @@ func dismantle() -> void:
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
dust_label.text = "Staub: %d" % p.dust dust_label.text = "Staub: %d" % p.dust
grid.fill(cards(), func(c): return tr(p.rarity(c.id)), func(_c): return false, func(c): return p.rarity(c.id)) grid.fill(cards(), func(c): return p.rarity(c.id))
Style.clear(preview_holder) Style.clear(preview_holder)
if selected == null: if selected == null:
info_label.text = "Keine Karte gewählt" info_label.text = "Keine Karte gewählt"
@@ -118,8 +123,8 @@ func refresh() -> void:
b_upgrade.disabled = true b_upgrade.disabled = true
b_dismantle.disabled = true b_dismantle.disabled = true
return return
var v := CardView.new(selected, Vector2(200, 280), false, p.rarity(selected.id)) var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
v.position = Vector2(955, 100) v.position = Vector2(920, 152)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v) preview_holder.add_child(v)
var lvl := p.level(selected.id) var lvl := p.level(selected.id)

View File

@@ -11,6 +11,8 @@ var width := 2
var flash := false # kurz rot: wurde gerade angegriffen var flash := false # kurz rot: wurde gerade angegriffen
var slot: Vector2 # Zielposition im Raster var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren) var busy := false # wird gerade animiert (kein Neupositionieren)
var mark := "" # Feldkombo, die dieses Wesen als Partner auslösen würde
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void: func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
@@ -20,58 +22,93 @@ func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
custom_minimum_size = sz custom_minimum_size = sz
size = sz size = sz
pivot_offset = sz / 2 pivot_offset = sz / 2
set_process(Style.rarity_animated(rarity())) # bewegter Glanz
mouse_filter = Control.MOUSE_FILTER_STOP mouse_filter = Control.MOUSE_FILTER_STOP
mouse_entered.connect(func(): hover.emit(creature.card, true)) mouse_entered.connect(func(): hover.emit(creature.card, true))
mouse_exited.connect(func(): hover.emit(creature.card, false)) mouse_exited.connect(func(): hover.emit(creature.card, false))
func _process(_delta: float) -> void:
queue_redraw()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event() accept_event()
clicked.emit(self) clicked.emit(self)
func rarity() -> String:
var owner = creature.owner()
return Cards.RARITIES[int(owner.looks.get(creature.card.id, 0)) if owner != null else 0]
func _draw() -> void: func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size) var rect := Rect2(Vector2.ZERO, size)
var small := size.x < 100 var small := size.x < 100
var mini := size.x < 60 var mini := size.x < 60
var rar := rarity()
var seed := hash(creature.card.id) + creature.uid
if rar != "gewöhnlich":
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 4 if mini else 6)
Style.fill_elements(self, rect, creature.card.elements, 6) Style.fill_elements(self, rect, creature.card.elements, 6)
var owner = creature.owner() if not mini:
var lvl: int = int(owner.looks.get(creature.card.id, 0)) if owner != null else 0 Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
var bcol: Color = border if border != null else Cards.RARITY_COLORS[Cards.RARITIES[lvl]] Style.rarity_sheen(self, rect, rar, seed)
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
if flash: if flash:
bcol = Style.RED bcol = Style.RED
Style.border(self, rect, bcol, 4 if flash else width, 6) Style.border(self, rect, bcol, 4 if flash else width, 6)
if mark != "":
Style.border(self, rect.grow(3), Style.GOLD, 3, 8)
if mini: if mini:
_draw_mini() _draw_mini()
Style.rarity_sparkle(self, rect, rar, seed)
return return
var fs := 11 if small else 13 var fs := 11 if small else 13
var nm := Style.fit(creature.card.title(), true, fs, size.x - 6) var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true) var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
var badges: Array[String] = [] draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
if creature.taunt(): 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)
badges.append("Wächter") # Schlüsselwörter als Symbole
if creature.shield: var icon := 7.0 if small else 9.0
badges.append("Schild") var kws: Array[String] = []
if creature.attacks_per_turn() == 2: for k in Cards.KEYWORDS:
badges.append("x2") if k in creature.card.keywords:
if creature.frozen: kws.append(k)
badges.append("Eis") if creature.shield and not ("Schild" in kws):
if creature.resonance > 0: kws.append("Schild")
badges.append("Resonanz") var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
if creature.field_bonus > 0: var y := Style.fs(fs) + 14 + icon
badges.append("Feld") for k in kws:
var y := 18.0 if small else 22.0 Style.keyword_icon(self, k, Vector2(x + icon, y), icon)
for b in badges: x += icon * 2 + 6
Style.text(self, b, 10 if small else 12, Color("ebebf5"), Vector2(size.x / 2, y), "midtop") _draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
y += 12 if small else 15 Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
var f := 16 if small else 20 if mark != "":
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW var w := Style.FONT_BOLD.get_string_size(tr(mark), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(11)).x + 14
Style.text(self, str(creature.power()), f, atk_col, Vector2(6, size.y - 4), "bottomleft", true) draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), Rect2(size.x / 2 - w / 2, -9, w, 18))
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8") Style.text(self, tr(mark), 11, Style.GOLD, Vector2(size.x / 2, 0), "center", true)
Style.text(self, str(creature.health), f, hp_col, Vector2(size.x - 6, size.y - 4), "bottomright", true)
if creature.frozen: if creature.frozen:
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35)) draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
Style.text_outlined(self, "❄", 22 if small else 28, Color("dff2ff"), size / 2, "center", 3)
Style.rarity_sparkle(self, rect, rar, seed)
## Angriff (grün mit „+n“, wenn Resonanz, Feld oder Merkmale ihn heben), Leben, Elementvorteil.
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.AMBER
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
if extra > 0:
Style.text_outlined(self, "+%d" % extra, f_small, Style.GREEN, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
if matchup != 0:
var c := Vector2(size.x - 12, 12)
draw_circle(c, 11, Color("14141c"))
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe. ## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
@@ -84,15 +121,14 @@ func _draw_mini() -> void:
if creature.attacks_per_turn() == 2: if creature.attacks_per_turn() == 2:
letters.append("2") letters.append("2")
if creature.frozen: if creature.frozen:
letters.append("E") letters.append("❄")
if creature.resonance > 0:
letters.append("R")
if creature.field_bonus > 0:
letters.append("F")
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true) Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW var extra := creature.power() - creature.attack
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true) Style.text_outlined(self, str(creature.power()), 13, Style.GREEN if extra > 0 else Style.YELLOW, Vector2(9, size.y - 9), "center", 3)
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8") var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
Style.text(self, str(creature.health), 13, hp_col, Vector2(size.x - 4, size.y - 2), "bottomright", true) Style.text_outlined(self, str(creature.health), 13, hp_col, Vector2(size.x - 9, size.y - 9), "center", 3)
if matchup != 0:
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), 10,
Style.GREEN if matchup > 0 else Style.RED, Vector2(size.x - 3, 2), "topright", true)
if creature.frozen: if creature.frozen:
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35)) draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))

53
scripts/ui/field_view.gd Normal file
View File

@@ -0,0 +1,53 @@
class_name FieldView
extends Control
## Das gemeinsame Feld: je Element vier Ladepunkte, bei vollen Punkten kippt das Feld.
## Das aktive Feld steht farbig mit seinem Bonus da.
var element := ""
var charge := {}
func _init(rect: Rect2) -> void:
position = rect.position
size = rect.size
mouse_filter = Control.MOUSE_FILTER_IGNORE
func update_from(g: Game) -> void:
element = g.field_element
charge = g.field_charge.duplicate()
queue_redraw()
## Der Inhalt als Text (Tests, Protokoll).
func text() -> String:
var parts: Array[String] = []
for e in Cards.ELEMENTS:
if e == element:
parts.append("%s: +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")])
else:
parts.append("%s %d/%d" % [tr(e), int(charge.get(e, 0)), Cards.FIELD_CHARGE])
return " · ".join(parts)
func _draw() -> void:
var w := size.x / Cards.ELEMENTS.size()
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var col: Color = Cards.ELEMENT_COLORS[e]
var x0 := i * w
var cy := size.y / 2
if e == element:
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
Style.text(self, "%s +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")], 13, Style.lighter(col, 0.4), Vector2(x0 + w / 2, cy), "center", true)
continue
Style.text(self, tr(e), 13, Style.MUTED, Vector2(x0 + 12, cy), "midleft")
var n := int(charge.get(e, 0))
var px := x0 + 12 + Style.FONT.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 14
for k in Cards.FIELD_CHARGE:
var c := Vector2(px + k * 13, cy)
if k < n:
draw_circle(c, 4.5, col)
else:
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)

View File

@@ -0,0 +1 @@
uid://i4rdq5rmcyuk

View File

@@ -11,8 +11,6 @@ var b_host: Button
var b_join: Button var b_join: Button
var b_watch: Button var b_watch: Button
var b_start: Button var b_start: Button
var ai_buttons: Array = []
var n_ai := 0
var addr_label: Label var addr_label: Label
@@ -20,9 +18,8 @@ func _ready() -> void:
net = app.net net = app.net
net.my_name = "Spieler" net.my_name = "Spieler"
net.my_pick = {"looks": app.profile.looks()} net.my_pick = {"looks": app.profile.looks()}
var col := Style.column(self, 760, 40, 10) Style.header(self, "Netzwerkspiel", _back)
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true) var col := Style.column(self, 760, 110, 10)
Style.spacer(col, 10)
var hr := Style.row(col, 12) var hr := Style.row(col, 12)
hr.alignment = BoxContainer.ALIGNMENT_CENTER hr.alignment = BoxContainer.ALIGNMENT_CENTER
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c")) b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
@@ -38,17 +35,10 @@ func _ready() -> void:
Style.spacer(col, 6) Style.spacer(col, 6)
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true) Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
list_label = Style.clabel(col, "", 16, Style.TEXT) list_label = Style.clabel(col, "", 16, Style.TEXT)
Style.spacer(col, 6)
Style.clabel(col, "KI-Gegner", 16, Style.MUTED)
var ar := Style.row(col, 10)
ar.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(0, 5):
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 40), func(): _set_ai(n)))
Style.spacer(col, 10) Style.spacer(col, 10)
var br := Style.row(col, 20) var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER br.alignment = BoxContainer.ALIGNMENT_CENTER
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c")) b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
Style.cbutton(br, "Zurück", Vector2(160, 48), _back)
net.lobby_changed.connect(refresh) net.lobby_changed.connect(refresh)
net.started.connect(_started) net.started.connect(_started)
net.ended.connect(func(_reason): refresh()) net.ended.connect(func(_reason): refresh())
@@ -79,13 +69,8 @@ func _watch() -> void:
refresh() refresh()
func _set_ai(n: int) -> void:
n_ai = n
refresh()
func _start() -> void: func _start() -> void:
net.start_battle(n_ai) net.start_battle()
func _started(seat: int) -> void: func _started(seat: int) -> void:
@@ -112,9 +97,8 @@ func refresh() -> void:
b_watch.disabled = net.connected b_watch.disabled = net.connected
ip_box.editable = not net.connected ip_box.editable = not net.connected
var humans := ids.size() var humans := ids.size()
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1) b_start.disabled = not net.is_host
for i in ai_buttons.size(): b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
ai_buttons[i].disabled = not net.is_host or i == n_ai or humans + i > Game.MAX_OPPONENTS + 1
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else "" addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""

View File

@@ -32,13 +32,13 @@ func apply_settings() -> void:
sfx.set_music_volume(profile.settings.music) sfx.set_music_volume(profile.settings.music)
set_language(profile.settings.language) set_language(profile.settings.language)
Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0 Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0
Style.animations = bool(profile.settings.get("animations", true))
theme = Style.make_theme() theme = Style.make_theme()
## Neues Profil: Sammlung und Fortschritt werden verworfen. ## Neues Profil: Sammlung und Fortschritt werden verworfen.
func reset_profile() -> void: func reset_profile() -> void:
profile = Profile.starter() profile = Profile.starter()
clear_battle_save()
save() save()
apply_settings() apply_settings()
@@ -54,6 +54,9 @@ func show_screen(s: Control) -> void:
s.app = self s.app = self
s.set_anchors_preset(Control.PRESET_FULL_RECT) s.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(s) add_child(s)
if Style.animations:
s.modulate.a = 0.0
create_tween().tween_property(s, "modulate:a", 1.0, 0.15)
if pad_mode: if pad_mode:
focus_first_button.call_deferred() focus_first_button.call_deferred()
@@ -107,16 +110,6 @@ func save() -> void:
profile.save(save_path) profile.save(save_path)
## Datei für einen unterbrochenen Kampf (neben dem Spielstand).
func battle_save_path() -> String:
return save_path.get_basename() + "_kampf.json"
func clear_battle_save() -> void:
if FileAccess.file_exists(battle_save_path()):
DirAccess.remove_absolute(ProjectSettings.globalize_path(battle_save_path()))
func _exit_tree() -> void: func _exit_tree() -> void:
if net != null and net.is_inside_tree(): if net != null and net.is_inside_tree():
net.leave() net.leave()

View File

@@ -1,53 +1,55 @@
class_name MenuScreen class_name MenuScreen
extends Control extends Control
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster.
var app var app
var b_resume: Button
var coins_label: Label var coins_label: Label
var coll_label: Label var coll_label: Label
var notice_label: Label var notice_label: Label
func _ready() -> void: func _ready() -> void:
var col := Style.column(self, 640, 40, 5) var p: Profile = app.profile
Style.clabel(col, "Dinger", 64, Style.GOLD, "center", true) Style.label(self, "Dinger", 76, Style.GOLD, Rect2(80, 64, 600, 90), "left", true)
Style.clabel(col, "Ein Kartenspiel: jeder gegen jeden", 22, Style.MUTED) Style.label(self, "Ein Kartenspiel: Duell um das Feld", 22, Style.MUTED, Rect2(84, 156, 600, 30), "left")
Style.spacer(col, 8) coins_label = Style.chip(self, "", Rect2(930, 20, 310, 38))
coins_label = Style.clabel(col, "", 20, Style.GOLD) coll_label = Style.label(self, "", 15, Style.MUTED, Rect2(700, 64, 540, 24), "right")
coll_label = Style.clabel(col, "", 16, Style.MUTED) var y := 230
Style.spacer(col, 8) for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
var grid := GridContainer.new() ["Kampagne", func(): app.show_screen(CampaignScreen.new())],
grid.columns = 2 ["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
grid.add_theme_constant_override("h_separation", 12) var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
grid.add_theme_constant_override("v_separation", 8) b.add_theme_font_size_override("font_size", 24)
var center := CenterContainer.new() y += 74
center.add_child(grid) y += 18
col.add_child(center) var x := 80
var sz := Vector2(270, 44) for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
if FileAccess.file_exists(app.battle_save_path()): ["Sammlung", func(): app.show_screen(CollectionScreen.new())],
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628")) ["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c")) ["Beenden", func(): get_tree().quit()]]:
Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c")) Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c")) x += 176
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new())) notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
Style.cbutton(grid, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
Style.spacer(col, 10)
notice_label = Style.clabel(col, "", 16, Style.GREEN)
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
_showcase(p)
refresh() refresh()
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen. ## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
func _resume() -> void: func _showcase(p: Profile) -> void:
var json := JSON.new() var cards: Array = Cards.all().duplicate()
var text := FileAccess.get_file_as_string(app.battle_save_path()) cards.sort_custom(func(a, b):
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"): if p.level(a.id) != p.level(b.id):
app.clear_battle_save() return p.level(a.id) > p.level(b.id)
app.show_screen(MenuScreen.new()) return hash(a.id) % 97 < hash(b.id) % 97)
return var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
app.show_screen(BattleScreen.resume(json.data)) for i in 3:
var c: Cards.Card = cards[i]
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
v.position = spots[i][0]
v.rotation = spots[i][1]
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(v)
func refresh() -> void: func refresh() -> void:

View File

@@ -1,79 +1,54 @@
class_name OpponentSelectScreen class_name OpponentSelectScreen
extends Control extends Control
## Einzelspieler: Schwierigkeit und Gegnerzahl wählen. Die Karten kommen im Kampf ## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
## aus dem Laden (siehe DeckGen.shop_cards). ## aus dem Laden (siehe DeckGen.shop_cards).
var app var app
var diff_buttons := {} var diff_buttons := {}
var count_slider: HSlider
var count_label: Label
var info_label: Label var info_label: Label
func _ready() -> void: func _ready() -> void:
var col := Style.column(self, 1000, 50, 12) Style.header(self, "Einzelspieler", func(): app.show_screen(MenuScreen.new()))
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true) var col := Style.column(self, 1000, 170, 12)
Style.spacer(col, 8)
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true) Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
var dr := Style.row(col, 10) var dr := Style.row(col, 10)
dr.alignment = BoxContainer.ALIGNMENT_CENTER dr.alignment = BoxContainer.ALIGNMENT_CENTER
for key in Profile.DIFFICULTIES: for key in Profile.DIFFICULTIES:
var k: String = key var k: String = key
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k)) diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
Style.spacer(col, 20)
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
var sr := Style.row(col, 12)
sr.alignment = BoxContainer.ALIGNMENT_CENTER
Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
count_slider = HSlider.new()
count_slider.min_value = 1
count_slider.max_value = Game.MAX_OPPONENTS
count_slider.step = 1
count_slider.value = 1
count_slider.tick_count = Game.MAX_OPPONENTS
count_slider.ticks_on_borders = true
count_slider.custom_minimum_size = Vector2(420, 32)
count_slider.value_changed.connect(func(_v): refresh())
sr.add_child(count_slider)
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
info_label = Style.clabel(col, "", 15, Style.MUTED) info_label = Style.clabel(col, "", 15, Style.MUTED)
Style.spacer(col, 24) Style.spacer(col, 24)
var br := Style.row(col, 16) var br := Style.row(col, 16)
br.alignment = BoxContainer.ALIGNMENT_CENTER br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c")) var b := Style.cbutton(br, "Starten", Vector2(260, 56), start, Style.PRIMARY)
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new())) b.add_theme_font_size_override("font_size", 22)
refresh() refresh()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())
func set_difficulty(key: String) -> void: func set_difficulty(key: String) -> void:
app.profile.settings.difficulty = key app.profile.settings.difficulty = key
app.save() app.save()
refresh() refresh()
func opponents() -> int:
return int(count_slider.value)
func start() -> void: func start() -> void:
app.show_screen(BattleScreen.new(opponents())) app.show_screen(BattleScreen.new())
func refresh() -> void: func refresh() -> void:
var p: Profile = app.profile var p: Profile = app.profile
for key in diff_buttons: for key in diff_buttons:
_mark(diff_buttons[key], key == p.settings.difficulty) _mark(diff_buttons[key], key == p.settings.difficulty)
var n := opponents() var d := p.difficulty()
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))]
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void: func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
var base: Color = col if active else Style.PANEL_LIGHT var base: Color = col if active else Style.PANEL_LIGHT
b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2)) b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2))
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())

View File

@@ -16,9 +16,8 @@ var speed_label: Label
func _ready() -> void: func _ready() -> void:
var s: Dictionary = app.profile.settings var s: Dictionary = app.profile.settings
var col := Style.column(self, 700, 50, 24) Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
Style.clabel(col, "Einstellungen", 36, Style.GOLD, "center", true) var col := Style.column(self, 700, 110, 24)
Style.spacer(col, 30)
var grid := GridContainer.new() var grid := GridContainer.new()
grid.columns = 3 grid.columns = 3
grid.add_theme_constant_override("h_separation", 24) grid.add_theme_constant_override("h_separation", 24)
@@ -80,7 +79,6 @@ func _ready() -> void:
var r := Style.row(col, 16) var r := Style.row(col, 16)
r.alignment = BoxContainer.ALIGNMENT_CENTER r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828")) Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.", confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.",
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED) "Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
_labels() _labels()
@@ -135,6 +133,7 @@ func _on_speed(v: float) -> void:
func _on_anim(on: bool) -> void: func _on_anim(on: bool) -> void:
app.profile.settings.animations = on app.profile.settings.animations = on
Style.animations = on
app.save() app.save()

View File

@@ -18,6 +18,8 @@ func _ready() -> void:
streams["damage"] = _tone(140, 0.12, "square") streams["damage"] = _tone(140, 0.12, "square")
streams["heal"] = _tone(523, 0.22, "sine", 784) streams["heal"] = _tone(523, 0.22, "sine", 784)
streams["combo"] = _tone(660, 0.2, "sine", 1100) streams["combo"] = _tone(660, 0.2, "sine", 1100)
streams["buy"] = _tone(1200, 0.08, "sine", 1700)
streams["turn"] = _tone(520, 0.3, "sine", 1040)
streams["win"] = _tone(400, 0.6, "sine", 1200) streams["win"] = _tone(400, 0.6, "sine", 1200)
streams["lose"] = _tone(320, 0.7, "square", 90) streams["lose"] = _tone(320, 0.7, "square", 90)
for i in 4: for i in 4:

View File

@@ -4,6 +4,7 @@ class_name Style
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf") const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf") const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
static var font_scale := 1.0 # Einstellung „Große Schrift“ static var font_scale := 1.0 # Einstellung „Große Schrift“
static var animations := true # Einstellung „Animationen“: der Glanz seltener Karten bewegt sich
const BG := Color("1a1c24") const BG := Color("1a1c24")
const PANEL := Color("282b36") const PANEL := Color("282b36")
@@ -17,6 +18,11 @@ const BLUE := Color("6eaaf0")
const YELLOW := Color("ffe65a") const YELLOW := Color("ffe65a")
const ORANGE := Color("ff963c") const ORANGE := Color("ff963c")
const PURPLE := Color("8c78dc") const PURPLE := Color("8c78dc")
const AMBER := Color("f0b429") # Angriff
const CRIMSON := Color("d64545") # Leben
const MANA := Color("3a78d2") # Kostenkristall
const PRIMARY := Color("285a3c") # Hauptaktion
const DANGER := Color("5a2828")
## Schriftgröße mit der Einstellung „Große Schrift“. ## Schriftgröße mit der Einstellung „Große Schrift“.
@@ -80,7 +86,11 @@ static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8,
var p := Panel.new() var p := Panel.new()
p.position = rect.position p.position = rect.position
p.size = rect.size p.size = rect.size
p.add_theme_stylebox_override("panel", box(bg, radius, border_col, border_w)) var sb := box(bg, radius, border_col, border_w)
sb.shadow_color = Color(0, 0, 0, 0.35)
sb.shadow_size = 6
sb.shadow_offset = Vector2(0, 3)
p.add_theme_stylebox_override("panel", sb)
p.mouse_filter = Control.MOUSE_FILTER_IGNORE p.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(p) parent.add_child(p)
return p return p
@@ -115,10 +125,35 @@ static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2)) b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2)) b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2)) b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
juice(b)
parent.add_child(b) parent.add_child(b)
return b return b
## Knöpfe federn beim Drücken kurz ein.
static func juice(b: Button) -> void:
b.button_down.connect(func():
if animations:
b.pivot_offset = b.size / 2
b.create_tween().tween_property(b, "scale", Vector2(0.96, 0.96), 0.05))
b.button_up.connect(func():
if animations:
b.pivot_offset = b.size / 2
b.create_tween().tween_property(b, "scale", Vector2.ONE, 0.12).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT))
## Kopfzeile eines Unterbildschirms: Titel links, Zurück rechts oben.
static func header(parent: Node, title: String, on_back: Callable) -> Button:
label(parent, title, 34, GOLD, Rect2(40, 22, 700, 46), "left", true)
return button(parent, "Zurück", Rect2(1120, 26, 120, 40), on_back)
## Kleine Kachel mit Text, etwa für Münzen und Staub.
static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label:
panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1)
return label(parent, text, 17, color, rect, "center", true)
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme). ## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer: static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
var v := VBoxContainer.new() var v := VBoxContainer.new()
@@ -230,6 +265,33 @@ static func text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vec
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color) ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
## Text mit dunklem Rand: bleibt auf jeder Farbe lesbar (Werte, Kosten).
static func text_outlined(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "center", outline: int = 4) -> void:
var size := fs(size_in)
var dim := FONT_BOLD.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
var x := pos.x
var y := pos.y
match anchor:
"center":
x -= dim.x / 2
y -= dim.y / 2
"midtop":
x -= dim.x / 2
"midleft":
y -= dim.y / 2
var at := Vector2(x, y + FONT_BOLD.get_ascent(size))
ci.draw_string_outline(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, outline, Color(0.05, 0.05, 0.08, 0.95))
ci.draw_string(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
## Schriftgröße, mit der ein Text in eine Breite passt (bis zur Mindestgröße).
static func fit_size(s: String, bold: bool, size: int, max_w: float, min_size: int = 9) -> int:
var font: Font = FONT_BOLD if bold else FONT
while size > min_size and font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(size)).x > max_w:
size -= 1
return size
static func fit(s: String, bold: bool, size: int, max_w: float) -> String: static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
var font: Font = FONT_BOLD if bold else FONT var font: Font = FONT_BOLD if bold else FONT
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3: while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
@@ -246,6 +308,88 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
ci.draw_style_box(sb, rect) ci.draw_style_box(sb, rect)
# --- Seltenheit als Aussehen ------------------------------------------------------
## Ab episch 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.
static func rarity_color(rar: String, seed: int = 0) -> Color:
if rar == "mythisch" 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.
static func rarity_frame(rar: String, seed: int = 0) -> Color:
return Color("3a3e4c") if rar == "gewöhnlich" 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:
var lvl := Cards.RARITIES.find(rar)
if lvl < 1:
return
var phase := (seed % 100) / 100.0
if lvl >= 2 and animations:
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
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]]:
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):
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1]))
## Funkeln ab legendär: 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:
return
var t := Time.get_ticks_msec() / 1000.0
var n := 5 if lvl == 3 else 9
for i in n:
var h := absi(hash(Vector2i(seed, i)))
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
if tw < 0.15 or avoid.has_point(p):
continue
var s := 2.0 + 4.0 * tw
var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
ci.draw_line(p - Vector2(s, 0), p + Vector2(s, 0), col, 1.4)
ci.draw_line(p - Vector2(0, s), p + Vector2(0, s), col, 1.4)
ci.draw_circle(p, 1.3, col)
## Weicher Schein um eine Karte höherer Stufe; vor dem Hintergrund zeichnen.
static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spread: int = 8) -> void:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0, 0, 0, 0)
sb.set_corner_radius_all(radius)
sb.shadow_color = Color(color, 0.6)
sb.shadow_size = spread
ci.draw_style_box(sb, rect)
## Edelstein in der Farbe der Stufe (ab selten).
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
if rar == "gewöhnlich":
return
var col := rarity_color(rar, seed)
var diamond := func(h: float, k: Color) -> void:
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.72, 0), c + Vector2(0, h), c + Vector2(-h * 0.72, 0)]), k)
diamond.call(s + 2, Color("14141c"))
diamond.call(s, col)
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen. ## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void: static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
if elements.size() == 1: if elements.size() == 1:
@@ -265,12 +409,63 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y))) ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
# --- Kartenteile ------------------------------------------------------
## Kostenkristall: sechseckig, blau, Zahl in der Mitte.
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void:
var pts := PackedVector2Array()
var outer := PackedVector2Array()
for i in 6:
var a := -PI / 2 + i * PI / 3
pts.append(c + Vector2(cos(a), sin(a)) * r)
outer.append(c + Vector2(cos(a), sin(a)) * (r + 2))
ci.draw_colored_polygon(outer, Color("14141c"))
ci.draw_colored_polygon(pts, MANA)
var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)])
ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22))
text_outlined(ci, str(cost), font, Color.WHITE, c, "center", 3)
## Wertkreis (Angriff gelb, Leben rot) mit Zahl.
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int) -> void:
ci.draw_circle(c, r + 2, Color("14141c"))
ci.draw_circle(c, r, color)
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
## Schlüsselwort als Symbol auf dunklem Kreis: Wächter Mauer, Schild Schild, Flink Doppelpfeil,
## 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)
"Flink":
for i in 2:
var x := c.x - r * 0.55 + i * r * 0.55
ci.draw_polyline(PackedVector2Array([Vector2(x, c.y - r * 0.6), Vector2(x + r * 0.5, c.y), Vector2(x, c.y + r * 0.6)]), col, w)
"Doppelschlag":
ci.draw_line(c + Vector2(-r * 0.65, r * 0.65), c + Vector2(r * 0.6, -r * 0.6), col, w)
ci.draw_line(c + Vector2(r * 0.65, r * 0.65), c + Vector2(-r * 0.6, -r * 0.6), col, w)
ci.draw_line(c + Vector2(-r * 0.7, r * 0.35), c + Vector2(-r * 0.35, r * 0.7), col, w)
ci.draw_line(c + Vector2(r * 0.7, r * 0.35), c + Vector2(r * 0.35, r * 0.7), col, w)
# --- Kartensymbole (prozedural, je Element) -------------------------- # --- Kartensymbole (prozedural, je Element) --------------------------
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void: static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95) -> void:
var variant := absi(hash(card.id)) % 3 var variant := absi(hash(card.id)) % 3
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38) var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
col.a = 0.95 col.a = alpha
var elems: Array = card.elements var elems: Array = card.elements
if elems.size() == 1: if elems.size() == 1:
_element_glyph(ci, elems[0], center, size, variant, col) _element_glyph(ci, elems[0], center, size, variant, col)
@@ -278,7 +473,7 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring # Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col) _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) var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
col2.a = 0.95 col2.a = alpha
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2) _element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
if elems.size() >= 3: if elems.size() >= 3:
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2) ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
@@ -321,7 +516,7 @@ static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
ci.draw_colored_polygon(pts, col) ci.draw_colored_polygon(pts, col)
var inner := PackedVector2Array([ var inner := PackedVector2Array([
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)]) c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75)) ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75 * col.a))
## Dezenter Hintergrund: Farbverlauf und schräge Linien. ## Dezenter Hintergrund: Farbverlauf und schräge Linien.

View File

@@ -1,5 +1,5 @@
extends Node extends Node
## Fensterprobe: Kosten von refresh() und längste Bildzeit während KI-Zügen (5 Gegner). ## Fensterprobe: Kosten von refresh() und längste Bildzeit während KI-Zügen.
var main: Node var main: Node
var max_dt := 0.0 var max_dt := 0.0
@@ -14,7 +14,7 @@ func _ready() -> void:
add_child(main) add_child(main)
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
var scr: BattleScreen = BattleScreen.new(5) var scr: BattleScreen = BattleScreen.new()
main.show_screen(scr) main.show_screen(scr)
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
@@ -23,7 +23,7 @@ func _ready() -> void:
var t0 := Time.get_ticks_usec() var t0 := Time.get_ticks_usec()
for i in 30: for i in 30:
scr.refresh() scr.refresh()
print("refresh() mit 5 Gegnern und 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30)) print("refresh() mit 8 Handkarten: %.2f ms" % ((Time.get_ticks_usec() - t0) / 1000.0 / 30))
scr.ai_delay = 0.2 scr.ai_delay = 0.2
measuring = true measuring = true
var start := Time.get_ticks_msec() var start := Time.get_ticks_msec()

View File

@@ -38,7 +38,7 @@ func _ready() -> void:
net.host() net.host()
net.lobby_changed.connect(func(): net.lobby_changed.connect(func():
if net.game == null and net.names.size() >= 2: if net.game == null and net.names.size() >= 2:
net.start_battle(0)) net.start_battle())
net.action_applied.connect(func(): net.action_applied.connect(func():
net.broadcast() net.broadcast()
_act_host.call_deferred()) _act_host.call_deferred())

View File

@@ -15,12 +15,9 @@ func _initialize() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 3 r.seed = 3
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50)) print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
print("Vorschau 1..5 Gegner: %.1f ms" % ms(func(): print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
for n in range(1, 6): for n in [1]:
for i in n: var g := Game.new_battle(r)
DeckGen.random_opponent(i + 1)))
for n in [1, 3, 5]:
var g := Game.new_battle(n, r)
# ein volles Brett für alle, damit die Entscheidung teuer wird # ein volles Brett für alle, damit die Entscheidung teuer wird
var uid := 5000 var uid := 5000
for p in g.players: for p in g.players:
@@ -41,7 +38,7 @@ func _initialize() -> void:
var t_budget := ms(func(): AI.choose_action(g, h)) var t_budget := ms(func(): AI.choose_action(g, h))
var cands := AI.candidate_actions(g, h).size() var cands := AI.candidate_actions(g, h).size()
print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget]) print("KI-Entscheidung, %d Gegner, %d Kandidaten: ohne Budget %.0f ms, mit Budget %.0f ms" % [n, cands, t_full, t_budget])
print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(5, r).clone(), 20)) print("Spielkopie: %.3f ms" % ms(func(): Game.new_battle(r).clone(), 20))
print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music())) print("Musik erzeugen (läuft im Hintergrund-Thread): %.0f ms" % ms(func(): Sfx.new()._music()))
print("Snapshot 6 Spieler: %.2f ms" % ms(func(): Game.new_battle(5, r).snapshot(0), 10)) print("Snapshot: %.2f ms" % ms(func(): Game.new_battle(r).snapshot(0), 10))
quit() quit()

View File

@@ -1,6 +1,6 @@
extends SceneTree extends SceneTree
## Balance-Simulation, die KI spielt auch den Menschen. Bei gleichen Regeln ## Balance-Simulation, die KI spielt auch den Menschen. Bei gleichen Regeln
## ist etwa 1/(Gegner+1) Siege zu erwarten: ## sind etwa 50 % Siege zu erwarten:
## godot --headless --path . -s tests/simulate.gd ## godot --headless --path . -s tests/simulate.gd
@@ -9,25 +9,37 @@ func _initialize() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 0 r.seed = 0
var t0 := Time.get_ticks_msec() var t0 := Time.get_ticks_msec()
print("Alle gierig (Erwartung etwa 1/(Gegner+1)):") print("Beide gierig (Erwartung etwa 50 %):")
for n in range(1, Game.MAX_OPPONENTS + 1): _run(games, r, true, true)
_run(n, games, r, true, true)
print("Du mit Vorausschau, Gegner gierig:") print("Du mit Vorausschau, Gegner gierig:")
for n in [1, 2, 3]: _run(games / 2, r, false, true)
_run(n, games / 2, r, false, true) print("Beide mit Vorausschau:")
print("Alle mit Vorausschau:") _run(games / 2, r, false, false)
_run(1, games / 2, r, false, false) print("Kampagne (du mit Vorausschau), je Stufe:")
_run(2, games / 4, r, false, false) for stage in [0, 4, 9, 14, 19, 24, 29]:
_campaign(stage, games / 4, r)
print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0)) print("Dauer %.1f s" % ((Time.get_ticks_msec() - t0) / 1000.0))
quit() quit()
func _run(n_opp: int, games: int, r: RandomNumberGenerator, func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> void:
human_greedy: bool, opp_greedy: bool) -> void: var wins := 0
for i in games:
var g := Game.new_battle_with(Campaign.opponent(stage), r)
while g.winner == "":
AI.take_turn(g, g.current(), false)
assert(g.turn < 300, "Spiel endet nicht")
if g.winner == "human":
wins += 1
var st := Campaign.stage(stage)
print("Stufe %2d %-12s %2d Leben, %d Merkmale: Siege %3d%%" % [stage + 1, st.name, int(st.life), st.perks.size(), wins * 100 / games])
func _run(games: int, r: RandomNumberGenerator, human_greedy: bool, opp_greedy: bool) -> void:
var wins := 0 var wins := 0
var turns := 0 var turns := 0
for i in games: for i in games:
var g := Game.new_battle(n_opp, r) var g := Game.new_battle(r)
while g.winner == "": while g.winner == "":
var p := g.current() var p := g.current()
AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy) AI.take_turn(g, p, human_greedy if p.is_human else opp_greedy)
@@ -35,4 +47,4 @@ func _run(n_opp: int, games: int, r: RandomNumberGenerator,
if g.winner == "human": if g.winner == "human":
wins += 1 wins += 1
turns += g.turn turns += g.turn
print("gegen %d: Siege %3d%% Ø Züge %.1f" % [n_opp, wins * 100 / games, float(turns) / games]) print("Siege %3d%% Ø Züge %.1f" % [wins * 100 / games, float(turns) / games])

View File

@@ -18,25 +18,23 @@ func C(id: String) -> Cards.Card:
return Cards.by_id(id) return Cards.by_id(id)
func setup(human_hand: Array = [], opp_hand: Array = [], n_opp: int = 1) -> Array: ## Duell mit vollem Mana; der Gegner hat 15 Leben. Liefert [Spiel, Spieler, Gegner].
func setup(human_hand: Array = [], opp_hand: Array = []) -> Array:
var human := Game.Player.new("Spieler", true, 30) var human := Game.Player.new("Spieler", true, 30)
var opps := [] var o := Game.Player.new("Gegner", false, 15)
for i in n_opp:
opps.append(Game.Player.new("Gegner %d" % (i + 1), false, 15))
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 0 r.seed = 0
var g := Game.new(human, opps, r) var g := Game.new(human, o, r)
g.turn = 1 g.turn = 1
human.max_energy = 10 human.max_energy = 10
human.energy = 10 human.energy = 10
for cid in human_hand: for cid in human_hand:
human.hand.append(C(cid)) human.hand.append(C(cid))
for o in opps: o.max_energy = 10
o.max_energy = 10 o.energy = 10
o.energy = 10 for cid in opp_hand:
for cid in opp_hand: o.hand.append(C(cid))
o.hand.append(C(cid)) return [g, human, o]
return [g, human, opps]
var next_uid := 9000 var next_uid := 9000
@@ -60,7 +58,6 @@ func _initialize() -> void:
test_wesen_spielen() test_wesen_spielen()
test_flink() test_flink()
test_waechter() test_waechter()
test_waechter_nur_eigener_besitzer()
test_kampf_beidseitig() test_kampf_beidseitig()
test_schild() test_schild()
test_doppelschlag() test_doppelschlag()
@@ -76,15 +73,13 @@ func _initialize() -> void:
test_mana_effekt() test_mana_effekt()
test_gleiche_regeln() test_gleiche_regeln()
test_zugreihenfolge() test_zugreihenfolge()
test_tote_gegner()
test_jeder_gegen_jeden()
test_gegnerzahl()
test_sieg_niederlage() test_sieg_niederlage()
test_booster() test_booster()
test_spielstand() test_spielstand()
test_resonanz() test_resonanz()
test_elementvorteil() test_elementvorteil()
test_feld() test_feld()
test_vorschau()
test_verschmelzen() test_verschmelzen()
test_feldkombos() test_feldkombos()
test_schild_effekt_und_sonne() test_schild_effekt_und_sonne()
@@ -141,7 +136,7 @@ func test_flink() -> void:
var s := setup(["l_falke"]) var s := setup(["l_falke"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
g.play_card(h, C("l_falke")) g.play_card(h, C("l_falke"))
check(g.attack(h.board[0], o) == "", "Flink greift sofort an") check(g.attack(h.board[0], o) == "", "Flink greift sofort an")
check(o.life == 12, "Falke 3 Schaden") check(o.life == 12, "Falke 3 Schaden")
@@ -151,7 +146,7 @@ func test_waechter() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_wolf") var a := put(h, "f_wolf")
var w := put(o, "e_wall") var w := put(o, "e_wall")
check(g.attack(a, o) != "", "Wächter blockt Angriff auf Held") check(g.attack(a, o) != "", "Wächter blockt Angriff auf Held")
@@ -159,20 +154,11 @@ func test_waechter() -> void:
check(w.health == 1 and a.health == 2, "Schaden verrechnet") check(w.health == 1 and a.health == 2, "Schaden verrechnet")
func test_waechter_nur_eigener_besitzer() -> void:
var s := setup([], [], 2)
var g: Game = s[0]
var h: Game.Player = s[1]
var a := put(h, "f_wolf")
put(s[2][0], "e_wall")
check(g.attack(a, s[2][1]) == "" and s[2][1].life == 12, "Wächter schützt nur Besitzer")
func test_kampf_beidseitig() -> void: func test_kampf_beidseitig() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_asche") var a := put(h, "f_asche")
var d := put(o, "w_reiter") var d := put(o, "w_reiter")
g.attack(a, d) g.attack(a, d)
@@ -184,7 +170,7 @@ func test_schild() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var sh := put(o, "ee_bastion") var sh := put(o, "ee_bastion")
var a := put(h, "f_asche") var a := put(h, "f_asche")
g.attack(a, sh) g.attack(a, sh)
@@ -198,7 +184,7 @@ func test_doppelschlag() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "l_schlange") var a := put(h, "l_schlange")
g.attack(a, o) g.attack(a, o)
g.attack(a, o) g.attack(a, o)
@@ -210,7 +196,7 @@ func test_einfrieren() -> void:
var s := setup([], ["w_frost"]) var s := setup([], ["w_frost"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_wolf") var a := put(h, "f_wolf")
g.end_turn() g.end_turn()
check(g.play_card(o, C("w_frost"), a) == "", "Frostmagier spielen") check(g.play_card(o, C("w_frost"), a) == "", "Frostmagier spielen")
@@ -226,30 +212,29 @@ func test_effekt_ziel() -> void:
var s := setup(["w_frost", "f_ritter"]) var s := setup(["w_frost", "f_ritter"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
check(g.play_card(h, C("w_frost")) == "" and h.board.size() == 1, "ohne Ziel kommt das Wesen trotzdem, der Effekt verpufft") check(g.play_card(h, C("w_frost")) == "" and h.board.size() == 1, "ohne Ziel kommt das Wesen trotzdem, der Effekt verpufft")
g.play_card(h, C("f_ritter"), o) g.play_card(h, C("f_ritter"), o)
check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden") check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden")
func test_flaechenschaden() -> void: func test_flaechenschaden() -> void:
var s := setup(["ff_phoenix"], [], 2) var s := setup(["ff_phoenix"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o1: Game.Player = s[2][0] var o: Game.Player = s[2]
var o2: Game.Player = s[2][1] put(o, "f_kobold")
put(o1, "f_kobold") put(o, "e_golem")
put(o2, "e_golem")
g.play_card(h, C("ff_phoenix")) g.play_card(h, C("ff_phoenix"))
check(o1.life == 11 and o2.life == 11, "Helden 4 Schaden") check(o.life == 11, "Held 4 Schaden")
check(o1.board.is_empty() and o2.board[0].health == 1, "Wesen getroffen") check(o.board.size() == 1 and o.board[0].health == 1, "Wesen getroffen, Kobold tot")
func test_zurueck_auf_hand() -> void: func test_zurueck_auf_hand() -> void:
var s := setup(["wl_nebel"]) var s := setup(["wl_nebel"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var d := put(o, "e_troll") var d := put(o, "e_troll")
g.play_card(h, C("wl_nebel"), d) g.play_card(h, C("wl_nebel"), d)
check(o.board.is_empty() and C("e_troll") in o.hand, "zurück auf Hand") check(o.board.is_empty() and C("e_troll") in o.hand, "zurück auf Hand")
@@ -285,11 +270,10 @@ func test_limits() -> void:
## Grenzfälle, die schon einmal falsch waren. ## Grenzfälle, die schon einmal falsch waren.
func test_grenzfaelle() -> void: func test_grenzfaelle() -> void:
var s := setup(["f_wolf"], [], 2) var s := setup(["f_wolf"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o1: Game.Player = s[2][0] var o1: Game.Player = s[2]
var o2: Game.Player = s[2][1]
# Nur Karten von der eigenen Hand # Nur Karten von der eigenen Hand
check(not g.can_play(h, C("e_troll"))[0] and g.play_card(h, C("e_troll")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen") check(not g.can_play(h, C("e_troll"))[0] and g.play_card(h, C("e_troll")) == "Nicht auf der Hand", "fremde Karte lässt sich nicht spielen")
check(g.play_card(h, C("f_wolf")) == "" and h.board.size() == 1, "eigene Handkarte geht") check(g.play_card(h, C("f_wolf")) == "" and h.board.size() == 1, "eigene Handkarte geht")
@@ -298,7 +282,7 @@ func test_grenzfaelle() -> void:
o1.life = 0 o1.life = 0
g.cleanup() g.cleanup()
check(o1.board.is_empty() and not o1.alive() and g.log.any(func(l): return l.find("verschwindet mit") >= 0), "besiegter Spieler räumt sein Feld") check(o1.board.is_empty() and not o1.alive() and g.log.any(func(l): return l.find("verschwindet mit") >= 0), "besiegter Spieler räumt sein Feld")
check(not (o1 in g.enemies_of(h)) and o2 in g.enemies_of(h), "Besiegte sind keine Ziele mehr") check(g.enemies_of(h).is_empty(), "Besiegte sind keine Ziele mehr")
check(dead != null, "Wesen war vorher da") check(dead != null, "Wesen war vorher da")
# Verschmolzene Karte auf der Hand übersteht den Spielstand # Verschmolzene Karte auf der Hand übersteht den Spielstand
var a := put(h, "f_kobold") var a := put(h, "f_kobold")
@@ -314,10 +298,10 @@ func test_grenzfaelle() -> void:
## Zweite Runde Grenzfälle: Konter ohne Angriff, Chimäre, Ereignisse mit verschmolzener Karte, ## Zweite Runde Grenzfälle: Konter ohne Angriff, Chimäre, Ereignisse mit verschmolzener Karte,
## ausgeschiedener Netzwerkspieler mitten im eigenen Zug. ## ausgeschiedener Netzwerkspieler mitten im eigenen Zug.
func test_grenzfaelle_2() -> void: func test_grenzfaelle_2() -> void:
var s := setup([], [], 2) var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o1: Game.Player = s[2][0] var o1: Game.Player = s[2]
# Ein Wesen ohne Angriff schlägt nicht zurück, auch nicht mit Elementvorteil # Ein Wesen ohne Angriff schlägt nicht zurück, auch nicht mit Elementvorteil
var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser
var reiter := put(h, "w_reiter") # Wasser 3/3 var reiter := put(h, "w_reiter") # Wasser 3/3
@@ -347,10 +331,10 @@ func test_grenzfaelle_2() -> void:
net.game = g net.game = g
net.rejoin_seconds = 0.0 net.rejoin_seconds = 0.0
net.waiting = {"Spieler": {"seat": g.players.find(h), "since": 0.0}} net.waiting = {"Spieler": {"seat": g.players.find(h), "since": 0.0}}
o1.is_human = true # noch ein Mensch am Tisch, sonst wäre die Partie schlicht verloren o1.is_human = true # ein Duell zweier Menschen
check(g.current() == h, "Spieler ist dran") check(g.current() == h, "Spieler ist dran")
net.tick() net.tick()
check(not h.alive() and h.board.is_empty() and g.current() != h, "Ausgeschiedener räumt das Feld und der Zug geht weiter") check(not h.alive() and h.board.is_empty() and g.winner != "" and g.winner_player == o1, "Ausgeschiedener räumt das Feld, der andere gewinnt")
net.free() net.free()
@@ -421,7 +405,7 @@ func test_mana_effekt() -> void:
func test_gleiche_regeln() -> void: func test_gleiche_regeln() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 0 r.seed = 0
var g := Game.new_battle(3, r) var g := Game.new_battle(r)
var first: Game.Player = g.players[0] var first: Game.Player = g.players[0]
check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Mana, kein Bonus") check(g.current() == first and first.max_energy == 1 and first.energy == 1, "Startspieler: 1 Mana, kein Bonus")
check(first.shop.size() == Game.SHOP_SIZE and first.hand.is_empty(), "jeder beginnt mit einem Laden und leerer Hand") check(first.shop.size() == Game.SHOP_SIZE and first.hand.is_empty(), "jeder beginnt mit einem Laden und leerer Hand")
@@ -432,9 +416,9 @@ func test_gleiche_regeln() -> void:
g.end_turn() g.end_turn()
var second: Game.Player = g.players[1] var second: Game.Player = g.players[1]
check(g.current() == second and second.max_energy == 1 and second.energy == 2, "Nachzieher: 1 Mana plus Ausgleich") check(g.current() == second and second.max_energy == 1 and second.energy == 2, "Nachzieher: 1 Mana plus Ausgleich")
for i in 4: for i in 2:
g.end_turn() g.end_turn()
check(g.current() == second and second.max_energy == 2, "Ausgleich nur einmal") check(g.current() == second and second.max_energy == 2 and second.energy == 4, "Ausgleich nur einmal: 2 übrig plus 2 Einkommen")
func test_zugreihenfolge() -> void: func test_zugreihenfolge() -> void:
@@ -442,9 +426,9 @@ func test_zugreihenfolge() -> void:
for seed in 12: for seed in 12:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = seed r.seed = seed
var g := Game.new_battle(2, r) var g := Game.new_battle(r)
starters[g.players[0].name] = true starters[g.players[0].name] = true
check(g.players.size() == 3 and g.human in g.players and g.opponents.size() == 2, "alle am Tisch") check(g.players.size() == 2 and g.human in g.players and g.opponent in g.players, "beide am Tisch")
for i in g.players.size(): for i in g.players.size():
check(g.players[i].order == i, "Reihenfolge nummeriert") check(g.players[i].order == i, "Reihenfolge nummeriert")
check(g.log[0].begins_with("Zugreihenfolge:"), "Reihenfolge im Verlauf") check(g.log[0].begins_with("Zugreihenfolge:"), "Reihenfolge im Verlauf")
@@ -453,58 +437,15 @@ func test_zugreihenfolge() -> void:
check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size()) check(starters.size() > 1, "Startspieler wechselt (%d verschiedene)" % starters.size())
func test_tote_gegner() -> void:
var s := setup([], [], 2)
var g: Game = s[0]
s[2][0].life = 0
g.end_turn()
check(g.current() == s[2][1], "toter Gegner übersprungen")
g.end_turn()
check(g.current() == s[1] and g.turn == 2, "zurück beim Spieler, Zug 2")
func test_jeder_gegen_jeden() -> void:
var s := setup([], [], 2)
var g: Game = s[0]
var h: Game.Player = s[1]
var o1: Game.Player = s[2][0]
var o2: Game.Player = s[2][1]
check(g.enemies_of(o1) == [h, o2], "Gegner bekämpft Spieler und anderen Gegner")
check(g.enemies_of(h) == [o1, o2], "Spieler bekämpft alle Gegner")
g.end_turn()
o1.hand = []
var a := put(o1, "f_wolf")
check(g.can_attack(a, o2)[0], "Gegner darf Gegner angreifen")
check(g.attack(a, o2) == "" and o2.life == 12, "Gegner trifft Gegner")
check(AI.choose_action(g, o1).type == "buy", "KI kauft aus ihrem Laden")
o1.shop = []
check(AI.choose_action(g, o1).type == "end", "ohne Angebot und ohne Angriff endet der Zug")
o2.life = 2
var b := put(o1, "f_kobold")
check(AI.choose_action(g, o1).target == o2, "KI nutzt Lethal gegen anderen Gegner")
func test_gegnerzahl() -> void:
var r := RandomNumberGenerator.new()
r.seed = 2
for n in range(1, Game.MAX_OPPONENTS + 1):
var g := Game.new_battle(n, r)
check(g.opponents.size() == n, "%d Gegner" % n)
check(g.players.all(func(o): return o.life == Game.START_LIFE), "gleiches Leben bei %d" % n)
check(g.players[0].max_energy == 1 and g.current() == g.players[0], "Startspieler bei %d" % n)
check(g.human.shop.size() == Game.SHOP_SIZE, "Laden bei %d" % n)
check(Profile.REWARD_WIN.has(n), "Belohnung bei %d" % n)
func test_sieg_niederlage() -> void: func test_sieg_niederlage() -> void:
var s := setup([], [], 2) var s := setup()
var g: Game = s[0] var g: Game = s[0]
g.damage(s[2][0], 99) g.damage(s[2], 14)
g.check_end() g.check_end()
check(g.winner == "", "noch offen") check(g.winner == "", "noch offen")
g.damage(s[2][1], 99) g.damage(s[2], 1)
g.check_end() g.check_end()
check(g.winner == "human", "Sieg") check(g.winner == "human" and g.winner_player == s[1], "Sieg")
var s2 := setup() var s2 := setup()
var g2: Game = s2[0] var g2: Game = s2[0]
g2.damage(s2[1], 30) g2.damage(s2[1], 30)
@@ -562,7 +503,7 @@ func test_ki_lethal() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
o.life = 3 o.life = 3
var a := put(h, "f_asche") var a := put(h, "f_asche")
put(o, "w_reiter") put(o, "w_reiter")
@@ -573,23 +514,20 @@ func test_ki_lethal() -> void:
func test_ki_spiele_enden() -> void: func test_ki_spiele_enden() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 7 r.seed = 7
for n in [1, 3, 5]: for i in 12:
for i in 6: var g := Game.new_battle(r)
var pool: Array = Cards.ELEMENTS.duplicate() var guard := 0
Game.shuffle(pool, r) while g.winner == "" and guard < 300:
var g := Game.new_battle(n, r) AI.take_turn(g, g.current(), i % 2 == 0)
var guard := 0 guard += 1
while g.winner == "" and guard < 300: check(g.winner != "", "Spiel endet (Partie %d)" % i)
AI.take_turn(g, g.current(), i % 2 == 0)
guard += 1
check(g.winner != "", "Spiel endet (%d Gegner)" % n)
func test_resonanz() -> void: func test_resonanz() -> void:
var s := setup(["f_wolf", "f_kobold", "e_wall"]) var s := setup(["f_wolf", "f_kobold", "e_wall"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
g.play_card(h, C("f_wolf")) g.play_card(h, C("f_wolf"))
check(h.board[0].power() == 3, "allein keine Resonanz") check(h.board[0].power() == 3, "allein keine Resonanz")
g.play_card(h, C("f_kobold")) g.play_card(h, C("f_kobold"))
@@ -621,7 +559,7 @@ func test_elementvorteil() -> void:
var s := setup(["f_ritter"]) var s := setup(["f_ritter"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var mine := put(h, "w_reiter") # Wasser 3/3 var mine := put(h, "w_reiter") # Wasser 3/3
var his := put(o, "f_wolf") # Feuer 3/2 var his := put(o, "f_wolf") # Feuer 3/2
g.attack(mine, his) g.attack(mine, his)
@@ -633,7 +571,7 @@ func test_elementvorteil() -> void:
s = setup(["f_ritter"]) s = setup(["f_ritter"])
g = s[0] g = s[0]
h = s[1] h = s[1]
o = s[2][0] o = s[2]
var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft
g.play_card(h, C("f_ritter"), air) g.play_card(h, C("f_ritter"), air)
check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr") check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr")
@@ -644,7 +582,7 @@ func test_feld() -> void:
var s := setup(["f_ritter"]) var s := setup(["f_ritter"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
check(g.field_element == "" and int(g.field_charge["Feuer"]) == 0, "das Feld beginnt neutral") check(g.field_element == "" and int(g.field_charge["Feuer"]) == 0, "das Feld beginnt neutral")
var wolf := put(h, "f_wolf") var wolf := put(h, "f_wolf")
for i in Cards.FIELD_CHARGE - 1: for i in Cards.FIELD_CHARGE - 1:
@@ -669,11 +607,34 @@ func test_feld() -> void:
## Verschmelzen: zwei eigene Wesen werden eins, Werte Durchschnitt plus 1. ## Verschmelzen: zwei eigene Wesen werden eins, Werte Durchschnitt plus 1.
## Vorschau fürs Ausspielen: Resonanz, Feldbonus und Feldkombo mit Partner.
func test_vorschau() -> void:
var s := setup(["f_kobold", "e_wall"])
var g: Game = s[0]
var h: Game.Player = s[1]
var f := g.forecast(h, C("f_kobold"))
check(f.resonance == 0 and f.field == 0 and f.combo == "" and f.partner == null, "leeres Feld: keine Vorschau")
var wolf := put(h, "f_wolf")
put(h, "f_asche")
var reiter := put(h, "w_reiter")
f = g.forecast(h, C("f_kobold"))
check(f.resonance == 2 and f.combo == "Dampf" and f.partner == reiter, "zwei Feuerpartner geben +2, das Wasserwesen die Kombo Dampf")
f = g.forecast(h, C("e_wall"))
check(f.resonance == 0 and f.combo == "Lava" and f.partner == wolf, "Erde: kein Partner, Kombo Lava mit dem ersten Feuerwesen")
g.field_element = "Feuer"
check(g.forecast(h, C("f_kobold")).field == 1 and g.forecast(h, C("e_wall")).field == 0, "der Feldbonus zählt nur fürs eigene Element")
h.perks.append("resonanz_doppelt")
check(g.forecast(h, C("f_kobold")).resonance == 4, "doppelte Resonanz verdoppelt die Vorschau")
var real := g.play_card(h, C("f_kobold"))
var kobold: Game.Creature = h.board[-1]
check(real == "" and kobold.power() == 2 + 4 + 1, "das Ausspielen bringt genau den vorhergesagten Angriff")
func test_verschmelzen() -> void: func test_verschmelzen() -> void:
var s := setup() var s := setup()
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_wolf") # Feuer 3/2, Kosten 2 var a := put(h, "f_wolf") # Feuer 3/2, Kosten 2
var b := put(h, "w_reiter") # Wasser 3/3, Kosten 2 var b := put(h, "w_reiter") # Wasser 3/3, Kosten 2
check(Cards.fusion_cost(a.card, b.card) == 2 and Cards.fusion_cost(C("f_kobold"), C("f_asche")) == 2, "Kosten: Durchschnitt, aufgerundet") check(Cards.fusion_cost(a.card, b.card) == 2 and Cards.fusion_cost(C("f_kobold"), C("f_asche")) == 2, "Kosten: Durchschnitt, aufgerundet")
@@ -719,7 +680,7 @@ func test_feldkombos() -> void:
var s := setup(["w_reiter"]) var s := setup(["w_reiter"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
put(h, "f_wolf") put(h, "f_wolf")
var weak := put(o, "f_kobold") var weak := put(o, "f_kobold")
var strong := put(o, "e_troll") var strong := put(o, "e_troll")
@@ -730,20 +691,19 @@ func test_feldkombos() -> void:
s = setup(["e_golem"]) s = setup(["e_golem"])
g = s[0] g = s[0]
h = s[1] h = s[1]
o = s[2][0] o = s[2]
put(h, "f_wolf") put(h, "f_wolf")
var k := put(o, "f_kobold") var k := put(o, "f_kobold")
var big := put(o, "e_troll") var big := put(o, "e_troll")
g.play_card(h, C("e_golem")) g.play_card(h, C("e_golem"))
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen") check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
# Blitz: 2 Schaden an den schwächsten Gegner # Blitz: 2 Schaden an den Gegner
s = setup(["l_wind"], [], 2) s = setup(["l_wind"])
g = s[0] g = s[0]
h = s[1] h = s[1]
put(h, "f_wolf") put(h, "f_wolf")
s[2][1].life = 5
g.play_card(h, C("l_wind")) g.play_card(h, C("l_wind"))
check(s[2][1].life == 3 and s[2][0].life == 15, "Blitz trifft den schwächsten Gegner") check(s[2].life == 13, "Blitz trifft den Gegner")
# Schlamm heilt, Sturm gibt Mana, Sand gibt +1/+1 # Schlamm heilt, Sturm gibt Mana, Sand gibt +1/+1
s = setup(["e_wall", "l_wind", "w_reiter"]) s = setup(["e_wall", "l_wind", "w_reiter"])
g = s[0] g = s[0]
@@ -773,7 +733,7 @@ func test_feldkombos() -> void:
s = setup(["fe_golem"]) s = setup(["fe_golem"])
g = s[0] g = s[0]
h = s[1] h = s[1]
o = s[2][0] o = s[2]
put(h, "w_reiter") put(h, "w_reiter")
var t := put(o, "e_troll") var t := put(o, "e_troll")
g.play_card(h, C("fe_golem")) g.play_card(h, C("fe_golem"))
@@ -784,7 +744,7 @@ func test_schild_effekt_und_sonne() -> void:
var s := setup(["el_duenen", "fff_sonne"]) var s := setup(["el_duenen", "fff_sonne"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_wolf") var a := put(h, "f_wolf")
g.play_card(h, C("el_duenen"), a) g.play_card(h, C("el_duenen"), a)
check(a.shield, "Dünenläufer gibt Schild") check(a.shield, "Dünenläufer gibt Schild")
@@ -792,7 +752,7 @@ func test_schild_effekt_und_sonne() -> void:
s = setup(["fff_sonne"]) s = setup(["fff_sonne"])
g = s[0] g = s[0]
h = s[1] h = s[1]
o = s[2][0] o = s[2]
check(C("fff_sonne").elements == ["Feuer"] and C("fff_sonne").cost == 9, "Kern der Sonne ist ein teures Feuerwesen") check(C("fff_sonne").elements == ["Feuer"] and C("fff_sonne").cost == 9, "Kern der Sonne ist ein teures Feuerwesen")
var troll := put(o, "e_troll") var troll := put(o, "e_troll")
g.play_card(h, C("fff_sonne")) g.play_card(h, C("fff_sonne"))
@@ -803,7 +763,7 @@ func test_kopie() -> void:
var s := setup(["f_wolf"]) var s := setup(["f_wolf"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
var a := put(h, "f_asche") var a := put(h, "f_asche")
var g2 := g.clone() var g2 := g.clone()
var a2 := g2.find_creature(a.uid) var a2 := g2.find_creature(a.uid)
@@ -823,7 +783,7 @@ func test_ki_besser_als_gierig() -> void:
for i in games: for i in games:
var pool: Array = Cards.ELEMENTS.duplicate() var pool: Array = Cards.ELEMENTS.duplicate()
Game.shuffle(pool, r) Game.shuffle(pool, r)
var g := Game.new_battle(1, r) var g := Game.new_battle(r)
# abwechselnd beginnt die schlaue oder die gierige KI # abwechselnd beginnt die schlaue oder die gierige KI
var smart_is_human := i % 2 == 0 var smart_is_human := i % 2 == 0
var guard := 0 var guard := 0
@@ -855,22 +815,22 @@ func test_kampagne() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 4 r.seed = 4
check(Campaign.STAGES.size() == 10, "zehn Stufen") check(Campaign.STAGES.size() == 10, "zehn Stufen")
var opps := Campaign.opponents(4) var boss := Campaign.opponent(4)
check(opps.size() == 3 and opps[0].name == "Vulkar" and opps[0].title == "Herr der Lava", "Stufe 5: Vulkar mit 2 Begleitern") check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 32, "Stufe 5: Vulkar mit 32 Leben")
check(opps[0].elements == ["Feuer", "Erde"] and opps[0].shop.all(func(c): return c.elements.all(func(e): return e in ["Feuer", "Erde"])), "Boss handelt nur mit seinen Elementen") check(boss.elements == ["Feuer", "Erde"] and boss.shop.all(func(c): return c.elements.all(func(e): return e in ["Feuer", "Erde"])), "Boss handelt nur mit seinen Elementen")
var g := Game.new_battle_with(opps, r) var g := Game.new_battle_with(boss, r)
check(g.opponents.size() == 3 and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet") check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
var p := Profile.starter() var p := Profile.starter()
var reward := p.record_battle(true, 1, 0) var reward := p.record_battle(true, 0)
check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen") check(p.campaign_stage == 1 and reward == Campaign.STAGES[0].reward, "Stufe 1 abgeschlossen")
reward = p.record_battle(true, 1, 0) reward = p.record_battle(true, 0)
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN[1], "Wiederholung gibt normale Belohnung") check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung")
reward = p.record_battle(false, 2, 1) reward = p.record_battle(false, 1)
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei") check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
p.campaign_stage = 9 p.campaign_stage = 9
p.record_battle(true, 5, 9) p.record_battle(true, 9)
check(p.campaign_stage == 10, "Kampagne abgeschlossen") check(p.campaign_stage == 10, "Kampagne abgeschlossen")
var sim := Game.new_battle_with(Campaign.opponents(0), r) var sim := Game.new_battle_with(Campaign.opponent(0), r)
var guard := 0 var guard := 0
while sim.winner == "" and guard < 300: while sim.winner == "" and guard < 300:
AI.take_turn(sim, sim.current(), true) AI.take_turn(sim, sim.current(), true)
@@ -916,7 +876,7 @@ func test_merkmale() -> void:
var s := setup(["f_wolf", "l_falke", "e_wall"]) var s := setup(["f_wolf", "l_falke", "e_wall"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
h.perks = ["feuer_angriff", "flink_angriff", "waechter_leben", "schild_wesen", "alle_flink", "resonanz_doppelt"] h.perks = ["feuer_angriff", "flink_angriff", "waechter_leben", "schild_wesen", "alle_flink", "resonanz_doppelt"]
g.play_card(h, C("f_wolf")) g.play_card(h, C("f_wolf"))
var wolf: Game.Creature = h.board[0] var wolf: Game.Creature = h.board[0]
@@ -932,15 +892,15 @@ func test_merkmale() -> void:
check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk) check(Campaign.STAGES.any(func(st): return st.perk == perk), "Merkmal vergeben: " + perk)
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 2 r.seed = 2
var mara: Game.Player = Campaign.opponents(1)[0] var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1), r).opponent
check(mara.life == 35 and mara.perks == ["leben_35"], "Mara hat 35 Leben") check(mara.life == 35 and mara.max_life == 35 and mara.perks == ["leben_35"], "Mara hat mit ihrem Merkmal 35 Leben")
var g3 := Game.new_battle_with(Campaign.opponents(9), r, true) var g3 := Game.new_battle_with(Campaign.opponent(9), r, true)
var archi: Game.Player = g3.opponents[0] var archi: Game.Player = g3.opponent
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt") check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
g3.end_turn() g3.end_turn()
check(g3.current() == archi and archi.energy == 3 + 1, "Archimagier beginnt mit 3 Mana (plus Ausgleich)") check(g3.current() == archi and archi.energy == 3 + 1, "Archimagier beginnt mit 3 Mana (plus Ausgleich)")
var nyx: Game.Player = Campaign.opponents(5)[0] var nyx: Game.Player = Campaign.opponent(5)
var g4 := Game.new_battle_with([nyx], r, true) var g4 := Game.new_battle_with(nyx, r, true)
g4.end_turn() g4.end_turn()
# Einkommen 1, dazu 1 Mana für ihr Merkmal und 1 als Ausgleich für den späteren Start # Einkommen 1, dazu 1 Mana für ihr Merkmal und 1 als Ausgleich für den späteren Start
check(nyx.max_energy == 1 and nyx.energy == 3, "Nyx bekommt 1 Mana mehr als ihr Einkommen") check(nyx.max_energy == 1 and nyx.energy == 3, "Nyx bekommt 1 Mana mehr als ihr Einkommen")
@@ -952,7 +912,7 @@ func test_ki_zwei_schritte() -> void:
var s := setup(["el_koenig"]) var s := setup(["el_koenig"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
h.turns = 2 h.turns = 2
o.life = 5 o.life = 5
put(h, "f_wolf", false) put(h, "f_wolf", false)
@@ -966,10 +926,10 @@ func test_ki_zwei_schritte() -> void:
func test_snapshot() -> void: func test_snapshot() -> void:
var s := setup(["f_wolf"], ["w_frost"], 2) var s := setup(["f_wolf"], ["w_frost"])
var g: Game = s[0] var g: Game = s[0]
var h: Game.Player = s[1] var h: Game.Player = s[1]
var o: Game.Player = s[2][0] var o: Game.Player = s[2]
g.play_card(h, C("f_wolf")) g.play_card(h, C("f_wolf"))
put(o, "e_troll") put(o, "e_troll")
var hi := g.players.find(h) var hi := g.players.find(h)
@@ -979,7 +939,7 @@ func test_snapshot() -> void:
var snap := g.snapshot(hi) var snap := g.snapshot(hi)
var json: Dictionary = JSON.parse_string(JSON.stringify(snap)) var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
var g2 := Game.from_snapshot(json) var g2 := Game.from_snapshot(json)
check(g2.players.size() == 3 and g2.turn == g.turn and g2.current_idx == g.current_idx, "Grunddaten") check(g2.players.size() == 2 and g2.turn == g.turn and g2.current_idx == g.current_idx, "Grunddaten")
check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar") check(g2.players[hi].hand.is_empty(), "eigene Hand sichtbar")
check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_frost"), "fremde Hand nur als Anzahl") check(g2.players[oi].hand.size() == 1 and g2.players[oi].hand[0] != C("w_frost"), "fremde Hand nur als Anzahl")
check(g2.players[oi].shop.is_empty() and g2.players[hi].shop.size() == Game.SHOP_SIZE, "eigener Laden sichtbar, fremder nicht") check(g2.players[oi].shop.is_empty() and g2.players[hi].shop.size() == Game.SHOP_SIZE, "eigener Laden sichtbar, fremder nicht")
@@ -1002,17 +962,17 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1)) check(st.has("perks") and st.perks.all(func(k): return Game.PERKS.has(k)), "Stufe %d hat gültige Merkmale" % (i + 1))
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 8 r.seed = 8
var opps := Campaign.opponents(19) var archi2 := Campaign.opponent(19)
check(opps.size() == 5 and opps[0].name == "Archimagier" and opps[0].perks.size() == 3, "letzte Stufe: Archimagier mit drei Merkmalen, voller Tisch") check(archi2.name == "Archimagier" and archi2.perks.size() == 3 and archi2.life == 34, "letzte Stufe von Kapitel 2: Archimagier mit drei Merkmalen und 34 Leben")
var p := Profile.starter() var p := Profile.starter()
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1") check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
p.campaign_stage = 10 p.campaign_stage = 10
check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet") check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet")
p.perk = "feuer_angriff" p.perk = "feuer_angriff"
var g := Game.new_battle(1, r, {"human_perks": [p.perk]}) var g := Game.new_battle(r, {"human_perks": [p.perk]})
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf") check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Spieler: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
p.campaign_stage = 19 p.campaign_stage = 19
p.record_battle(true, 5, 19) p.record_battle(true, 19)
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen") check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict()))) var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen") check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
@@ -1021,7 +981,7 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
func test_ki_zeitbudget() -> void: func test_ki_zeitbudget() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 12 r.seed = 12
var g := Game.new_battle(5, r) var g := Game.new_battle(r)
var h: Game.Player = g.players[0] var h: Game.Player = g.players[0]
h.energy = 10 h.energy = 10
h.max_energy = 10 h.max_energy = 10
@@ -1058,7 +1018,7 @@ func test_englische_karten() -> void:
func test_kampf_speichern() -> void: func test_kampf_speichern() -> void:
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 7 r.seed = 7
var g := Game.new_battle(2, r) var g := Game.new_battle(r)
g.start() g.start()
AI.take_turn(g, g.current()) AI.take_turn(g, g.current())
var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1))) var d: Dictionary = JSON.parse_string(JSON.stringify(g.snapshot(-1)))
@@ -1075,7 +1035,7 @@ func test_kampf_speichern() -> void:
while g2.winner == "" and guard < 300: while g2.winner == "" and guard < 300:
AI.take_turn(g2, g2.current()) AI.take_turn(g2, g2.current())
guard += 1 guard += 1
check(g2.winner != "", "fortgesetzte Partie ist bis zum Ende spielbar") check(g2.winner != "", "der volle Spielstand ist bis zum Ende spielbar")
var s2: Dictionary = g.snapshot(-2) var s2: Dictionary = g.snapshot(-2)
check(s2.players.all(func(p): return not (p.hand is Array) and p.shop.is_empty()), "Zuschauer-Sicht ohne Hände und Läden") check(s2.players.all(func(p): return not (p.hand is Array) and p.shop.is_empty()), "Zuschauer-Sicht ohne Hände und Läden")

View File

@@ -26,6 +26,7 @@ func frames(n: int = 2) -> void:
func shot(name: String) -> void: func shot(name: String) -> void:
if shots == "": if shots == "":
return return
await frames(10) # Einblenden und Austeilen abwarten
await RenderingServer.frame_post_draw await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png")) get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
@@ -142,18 +143,15 @@ func _ready() -> void:
for suffix in ["", ".bak", ".corrupt"]: for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(SAVE + suffix): if FileAccess.file_exists(SAVE + suffix):
DirAccess.remove_absolute(abs + suffix) DirAccess.remove_absolute(abs + suffix)
if FileAccess.file_exists(SAVE.get_basename() + "_kampf.json"):
DirAccess.remove_absolute(abs.get_basename() + "_kampf.json")
main = load("res://scenes/main.tscn").instantiate() main = load("res://scenes/main.tscn").instantiate()
main.save_path = SAVE main.save_path = SAVE
add_child(main) add_child(main)
await frames(3) await frames(3)
var fast := OS.get_environment("DINGER_FAST") != "" var fast := OS.get_environment("DINGER_FAST") != ""
var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen, var suite := [test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen, test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen]
test_kampf_fortsetzen]
if not fast: if not fast:
suite += [test_kampf.bind(2, 3), test_kampf_abbrechen] suite += [test_kampf.bind(3), test_kampf_abbrechen]
# Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten. # Die Netzwerktests starten Bot-Prozesse und warten auf Verbindungen; das dauert Minuten.
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY). # Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
if OS.get_environment("DINGER_NETZ") != "": if OS.get_environment("DINGER_NETZ") != "":
@@ -254,7 +252,7 @@ func human_turn(scr) -> void:
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected) "' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " pending=", scr.pending, " selected=", scr.selected)
check(scr.selected == cr, "Wesen gewählt") check(scr.selected == cr, "Wesen gewählt")
var targets := [] var targets := []
for o in g.opponents: for o in [g.opponent]:
if o.alive() and g.can_attack(cr, o)[0]: if o.alive() and g.can_attack(cr, o)[0]:
targets.append(o) targets.append(o)
for c in o.board: for c in o.board:
@@ -267,11 +265,9 @@ func human_turn(scr) -> void:
await click(scr.b_end) await click(scr.b_end)
func start_battle(n_opp: int) -> void: func start_battle() -> void:
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
check(screen() is OpponentSelectScreen, "Gegnerauswahl") check(screen() is OpponentSelectScreen, "Gegnerauswahl")
screen().count_slider.value = n_opp
await frames(1)
await click(find_button("Starten")) await click(find_button("Starten"))
@@ -279,51 +275,33 @@ func test_gegnerauswahl() -> void:
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
var sel = screen() var sel = screen()
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet") check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
check(find_button("Neu würfeln") == null and find_button("1 Gegner") == null and find_button("Starten") != null, "kein Würfeln und keine Gegnerknöpfe, nur Starten") check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
check(find_button("Feuer") == null and find_button("Luft") == null, "keine Elementwahl mehr")
check(sel.count_slider.min_value == 1 and sel.count_slider.max_value == Game.MAX_OPPONENTS and sel.count_slider.step == 1, "Gegnerzahl als Schieberegler von 1 bis %d" % Game.MAX_OPPONENTS)
check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
var rect: Rect2 = sel.count_slider.get_global_rect()
await click_at(Vector2(rect.end.x - 4, rect.get_center().y)) # ganz rechts anklicken
check(sel.opponents() == Game.MAX_OPPONENTS and sel.count_label.text == "%d Gegner" % Game.MAX_OPPONENTS, "Mausklick am rechten Rand stellt %d Gegner ein" % Game.MAX_OPPONENTS)
sel.count_slider.grab_focus()
await key(KEY_LEFT)
check(sel.opponents() == Game.MAX_OPPONENTS - 1, "Pfeiltaste links nimmt einen Gegner weg")
sel.count_slider.value = 4
await frames(1)
check(sel.opponents() == 4 and sel.count_label.text == "4 Gegner" and "5 am Tisch" in sel.info_label.text
and str(Profile.REWARD_WIN[4]) in sel.info_label.text, "Regler auf 4: fünf am Tisch mit Belohnung")
await click(find_button("Schwer")) await click(find_button("Schwer"))
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert") check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
check(str(Profile.DIFFICULTIES.schwer.life) in sel.info_label.text and str(int(Profile.REWARD_WIN * 1.5)) in sel.info_label.text, "Leben des Gegners und Belohnung stehen dabei")
await shot("07_gegnerauswahl") await shot("07_gegnerauswahl")
sel.count_slider.value = 1
await frames(1)
await click(find_button("Starten")) await click(find_button("Starten"))
var hard = screen() var hard = screen()
check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet") check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
var opp: Game.Player = hard.game.opponents[0] var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life) check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(), check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
"Start mit vollem Laden und leerer Hand") "Start mit vollem Laden und leerer Hand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
await shot("11_kampf")
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
await click(find_button("Leicht")) await click(find_button("Leicht"))
await click(find_button("Starten")) await click(find_button("Starten"))
var easy = screen() var easy = screen()
opp = easy.game.opponents[0] opp = easy.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life) check(opp.max_life == Profile.DIFFICULTIES.leicht.life and not easy.smart_ai, "Leicht: Gegner mit %d Leben, einfache KI" % Profile.DIFFICULTIES.leicht.life)
check(easy.game.players[0].max_energy == 1 and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
await give_up() await give_up()
await click(find_button("Einzelspieler")) await click(find_button("Einzelspieler"))
await click(find_button("Normal")) await click(find_button("Normal"))
await click(find_button("Zurück")) await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü") check(screen() is MenuScreen, "zurück im Menü")
for n in [1, 5]:
await start_battle(n)
var scr = screen()
check(scr is BattleScreen and scr.game.opponents.size() == n, "Kampf gegen %d gestartet" % n)
check(scr.game.players[0].max_energy == 1 and scr.game.opponents[0].life == Game.START_LIFE, "gleiche Regeln gegen %d" % n)
await shot("11_kampf_%d" % n)
await give_up()
## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben. ## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben.
@@ -348,32 +326,6 @@ func give_up() -> void:
check(screen() is MenuScreen, "Aufgeben führt ins Menü") check(screen() is MenuScreen, "Aufgeben führt ins Menü")
## Kampf speichern, im Menü fortsetzen, Aufgeben löscht die Datei.
func test_kampf_fortsetzen() -> void:
await start_battle(1)
var scr = screen()
scr.ai_delay = 0.0
await wait_for_human(scr)
var kampf: String = main.battle_save_path()
check(FileAccess.file_exists(kampf), "Kampf wird laufend gespeichert")
await click(scr.b_end)
await wait_for_human(scr)
var turn: int = scr.game.turn
var life: int = scr.game.human.life
var hand: int = scr.game.human.hand.size()
await click(find_button("Aufgeben"))
await click(find_button("Speichern und zurück"))
check(screen() is MenuScreen and find_button("Kampf fortsetzen") != null, "Speichern führt ins Menü mit Fortsetzen-Knopf")
await click(find_button("Kampf fortsetzen"))
var scr2 = screen()
check(scr2 is BattleScreen and scr2.game.turn == turn and scr2.game.human.life == life and scr2.game.human.hand.size() == hand
and scr2.game.current() == scr2.game.human, "Kampf fortgesetzt mit gleichem Zug, Leben und Hand")
scr2.ai_delay = 0.0
await shot("30_kampf_fortgesetzt")
await give_up()
check(not FileAccess.file_exists(kampf) and find_button("Kampf fortsetzen") == null, "Aufgeben löscht den gespeicherten Kampf")
func test_einstellungen() -> void: func test_einstellungen() -> void:
await click(find_button("Einstellungen")) await click(find_button("Einstellungen"))
var scr = screen() var scr = screen()
@@ -414,6 +366,14 @@ func test_sammlung() -> void:
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") == "gewöhnlich", "Karte gewählt, noch gewöhnlich")
await click(scr.b_upgrade) 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["selten"], "auf selten 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")
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") check(Profile.load_or_new(SAVE).level("ff_phoenix") == 1, "Sammlung gespeichert")
var dust: int = p.dust var dust: int = p.dust
await click(scr.b_dismantle) await click(scr.b_dismantle)
@@ -491,7 +451,7 @@ func test_sprache() -> void:
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc. ## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
func test_kampf_steuerung() -> void: func test_kampf_steuerung() -> void:
await start_battle(1) await start_battle()
var scr = screen() var scr = screen()
check(scr is BattleScreen, "Kampf gestartet") check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0 scr.ai_delay = 0.0
@@ -551,7 +511,7 @@ func test_kampf_steuerung() -> void:
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana. ## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
func test_kampf_grenzen() -> void: func test_kampf_grenzen() -> void:
await start_battle(1) await start_battle()
var scr = screen() var scr = screen()
scr.ai_delay = 0.0 scr.ai_delay = 0.0
await wait_for_human(scr) await wait_for_human(scr)
@@ -603,26 +563,45 @@ func test_kampf_grenzen() -> void:
## Feldanzeige und Verschmelzen im Kampf. ## Feldanzeige und Verschmelzen im Kampf.
func test_kampf_feld_und_verschmelzen() -> void: func test_kampf_feld_und_verschmelzen() -> void:
await start_battle(1) await start_battle()
var scr = screen() var scr = screen()
check(scr is BattleScreen, "Kampf gestartet") check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0 scr.ai_delay = 0.0
await wait_for_human(scr) await wait_for_human(scr)
var g: Game = scr.game var g: Game = scr.game
g.human.board.clear() g.human.board.clear()
for id in ["f_wolf", "w_reiter"]: var put := func(p: Game.Player, id: String) -> Game.Creature:
var cr := Game.Creature.new(Cards.by_id(id), g.human, g.next_uid) var cr := Game.Creature.new(Cards.by_id(id), p, g.next_uid)
g.next_uid += 1 g.next_uid += 1
g.human.board.append(cr) cr.ready = true
cr.attacks_left = 1
p.board.append(cr)
return cr
put.call(g.human, "f_wolf")
put.call(g.human, "w_reiter")
var falke: Game.Creature = put.call(scr.others()[0], "l_falke")
g.human.hand = [Cards.by_id("f_kobold")]
g.human.energy = 10 g.human.energy = 10
g.field_charge["Wasser"] = 2
scr.refresh()
await frames(2)
check(scr.field_view.text() == "Feuer 0/4 · Wasser 2/4 · Erde 0/4 · Luft 0/4", "neutrales Feld zeigt die Ladung: " + scr.field_view.text())
for i in Cards.FIELD_CHARGE: for i in Cards.FIELD_CHARGE:
g.charge_field(Cards.by_id("f_kobold")) g.charge_field(Cards.by_id("f_kobold"))
scr.refresh() scr.refresh()
await frames(2) await frames(2)
check(scr.field_label.text.begins_with("Vulkan"), "Feldanzeige nennt das Feld: " + scr.field_label.text) check(scr.field_view.text().begins_with("Vulkan: +1 Angriff"), "Feldanzeige nennt das Feld: " + scr.field_view.text())
check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff") check(g.human.board[0].field_bonus == 1 and g.human.board[0].power() == 4, "das Feuerwesen bekommt +1 Angriff")
check(scr.hand_views[0].bonus == 2 and scr.hand_views[0].tag == "Dampf", "die Handkarte zeigt +2 (Resonanz und Feld) und die Kombo Dampf")
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten") check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
await shot("23_feld") await shot("23_feld")
await hover(scr.hand_views[0])
check(scr.tiles[g.human.board[1].uid].mark == "Dampf" and "Dampf" in scr.info_label.text, "beim Zeigen auf die Handkarte ist der Kombo-Partner markiert")
await click(scr.own_views[0])
check(scr.selected == g.human.board[0] and scr.tiles[falke.uid].matchup == 1, "Angreifer gewählt: das Luftwesen zeigt +1 für Feuer")
await shot("23b_angriffsziel")
await right_click() # Auswahl wieder aufheben
check(scr.selected == null and scr.tiles[falke.uid].matchup == 0, "Auswahl aufgehoben, Vorteil verschwindet")
await click(scr.b_fuse) await click(scr.b_fuse)
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt") check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
await click(scr.own_views[0]) await click(scr.own_views[0])
@@ -834,13 +813,15 @@ func test_kampagne() -> void:
var scr = screen() var scr = screen()
check(scr is CampaignScreen, "Kampagne geöffnet") check(scr is CampaignScreen, "Kampagne geöffnet")
await shot("15_kampagne") await shot("15_kampagne")
check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen") check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES.size() - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen")
await click(scr.node_buttons[3])
check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen")
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt") check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
await click(find_button("Kämpfen")) await click(find_button("Kämpfen"))
var battle = screen() var battle = screen()
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet") check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
check(battle.game.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner") check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
check(battle.game.opponents[0].perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv") check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
await shot("16_kampagne_kampf") await shot("16_kampagne_kampf")
await give_up() await give_up()
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl # Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
@@ -854,7 +835,7 @@ func test_kampagne() -> void:
await shot("27_kampagne_kapitel2") await shot("27_kampagne_kapitel2")
await click(find_button("Kämpfen")) await click(find_button("Kämpfen"))
var b2 = screen() var b2 = screen()
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponents[0].perks.size() == 2, "Kapitel-2-Boss mit zwei Merkmalen") check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponent.perks.size() == 2 and b2.game.opponent.life == 30, "Kapitel-2-Boss mit zwei Merkmalen und 30 Leben")
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf") check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
await give_up() await give_up()
main.profile.campaign_stage = 0 main.profile.campaign_stage = 0
@@ -862,13 +843,13 @@ func test_kampagne() -> void:
main.save() main.save()
func test_kampf(n_opp: int, shot_turn: int) -> void: func test_kampf(shot_turn: int) -> void:
await start_battle(n_opp) await start_battle()
var scr = screen() var scr = screen()
check(scr is BattleScreen, "Kampfbildschirm") check(scr is BattleScreen, "Kampfbildschirm")
scr.ai_delay = 0.0 scr.ai_delay = 0.0
var g: Game = scr.game var g: Game = scr.game
check(g.opponents.size() == n_opp and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost") check(g.players.size() == 2 and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
await wait_for_human(scr) await wait_for_human(scr)
check(g.current() == g.human, "Spieler ist dran") check(g.current() == g.human, "Spieler ist dran")
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar") check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
@@ -898,12 +879,12 @@ func test_kampf(n_opp: int, shot_turn: int) -> void:
func test_kampf_abbrechen() -> void: func test_kampf_abbrechen() -> void:
await start_battle(5) await start_battle()
var scr = screen() var scr = screen()
scr.ai_delay = 0.0 scr.ai_delay = 0.0
var g: Game = scr.game var g: Game = scr.game
await wait_for_human(scr) await wait_for_human(scr)
check(g.opponents.size() == 5 and g.human.max_energy == 1, "5 Gegner") check(g.opponent != null and g.human.max_energy == 1, "Duell läuft")
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit # Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
g.human.energy = 10 g.human.energy = 10
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000) var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
@@ -913,7 +894,7 @@ func test_kampf_abbrechen() -> void:
scr.refresh() scr.refresh()
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0] var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe") check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
await hover(scr.log_box) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird await hover(scr.field_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
await hover(reiter_view) await hover(reiter_view)
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView) var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls") check(previews.size() == 1 and previews[0].rarity == "episch", "Vorschau ebenfalls")
@@ -930,11 +911,10 @@ func test_kampf_abbrechen() -> void:
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(), print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control()) " dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover") check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
await click(find_button("Lupe")) await click(scr.b_log)
check(scr.zoom_player == scr.others()[0], "Lupe öffnet die erste Spalte") check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
await shot("12_lupe") await key(KEY_ESCAPE)
await click(find_button("Schließen")) check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
check(scr.zoom_player == null, "Lupe geschlossen")
# Runden laufen lassen, damit die Gegner ihre Züge machen # Runden laufen lassen, damit die Gegner ihre Züge machen
for round in 4: for round in 4:
await click(scr.b_end) await click(scr.b_end)
@@ -946,4 +926,4 @@ func test_kampf_abbrechen() -> void:
and g.log.any(func(l): return " greift " in l) and g.log.any(func(l): return " greift " in l)
check(ai_attacked, "Gegner spielen Karten und greifen an") check(ai_attacked, "Gegner spielen Karten und greifen an")
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen") check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
await shot("10_kampf_fuenf") await shot("10_kampf_runden")

View File

@@ -1,6 +1,6 @@
keys,en keys,en
Dinger,Dinger Dinger,Dinger
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all Ein Kartenspiel: Duell um das Feld,A card game: duel for the field
Einzelspieler,Single player Einzelspieler,Single player
Kampagne,Campaign Kampagne,Campaign
Netzwerkspiel,Network game Netzwerkspiel,Network game
@@ -62,18 +62,14 @@ Normal,Normal
Schwer,Hard Schwer,Hard
Profil zurücksetzen,Reset profile Profil zurücksetzen,Reset profile
Große Schrift,Large text Große Schrift,Large text
Duell,Duel
%d am Tisch,%d at the table
%d Münzen,%d coins %d Münzen,%d coins
Abbrechen,Cancel Abbrechen,Cancel
"Ja, zurücksetzen","Yes, reset" "Ja, zurücksetzen","Yes, reset"
"Speichern und zurück","Save and go back"
"Kampf fortsetzen","Resume battle"
%s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature %s Schaden an ein gegnerisches Wesen,%s damage to an enemy creature
%s Schaden an ein Ziel,%s damage to a target %s Schaden an ein Ziel,%s damage to a target
%s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures %s Schaden an alle gegnerischen Wesen,%s damage to all enemy creatures
%s Schaden an alle Gegner,%s damage to all opponents %s Schaden an den Gegner,%s damage to the opponent
%s Schaden an alle Gegner und ihre Wesen,%s damage to all opponents and their creatures %s Schaden an den Gegner und seine Wesen,%s damage to the opponent and their creatures
Heile dich um %s,Heal yourself for %s Heile dich um %s,Heal yourself for %s
Erhalte 1 Mana,Gain 1 mana Erhalte 1 Mana,Gain 1 mana
Erhalte %s Mana,Gain %s mana Erhalte %s Mana,Gain %s mana
@@ -117,14 +113,10 @@ Zuschauen,Watch
Zuschauer,Spectator Zuschauer,Spectator
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden" "Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
Starten,Start Starten,Start
1 Gegner,1 opponent
%d Gegner,%d opponents
KI-Gegner,AI opponents
Sammlung,Collection Sammlung,Collection
Verschmelzen,Fuse Verschmelzen,Fuse
Zwei eigene Wesen wählen,Pick two of your creatures Zwei eigene Wesen wählen,Pick two of your creatures
Zweites Wesen wählen,Pick the second creature Zweites Wesen wählen,Pick the second creature
Feld: neutral,Field: neutral
Zwei verschiedene Wesen wählen,Pick two different creatures Zwei verschiedene Wesen wählen,Pick two different creatures
Nur eigene Wesen,Only your own creatures Nur eigene Wesen,Only your own creatures
Vulkan,Volcano Vulkan,Volcano
@@ -150,10 +142,26 @@ Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
Aufgewertet,Upgraded Aufgewertet,Upgraded
+%d Staub,+%d dust +%d Staub,+%d dust
+%d je Zug,+%d per turn +%d je Zug,+%d per turn
Neu würfeln (%d),Reroll (%d) Neu würfeln ◆ %d,Reroll ◆ %d
Dein Zug,Your turn
Beim Ausspielen,On play
Nicht genug Mana,Not enough mana Nicht genug Mana,Not enough mana
Kein Angebot,No offer Kein Angebot,No offer
Hand voll,Hand full Hand voll,Hand full
Profil wirklich zurücksetzen?,Really reset your profile? Profil wirklich zurücksetzen?,Really reset your profile?
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost." "Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
Chimäre,Chimera Chimäre,Chimera
Angriff,attack
Eis,Ice
Resonanz +%d,Resonance +%d
Feld +%d,Field +%d
Merkmal +%d,Perk +%d
Feldkombo %s mit %s: %s,Field combo %s with %s: %s
1 Schaden an das stärkste gegnerische Wesen,1 damage to the strongest enemy creature
2 Schaden an den Gegner,2 damage to the opponent
Heile dich um 3,Heal yourself for 3
Das neue Wesen erhält +1/+1,The new creature gets +1/+1
%d Leben,%d life
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
Beginnt mit 5 Leben mehr,Starts with 5 extra life
1 keys en
2 Dinger Dinger
3 Ein Kartenspiel: jeder gegen jeden Ein Kartenspiel: Duell um das Feld A card game: free-for-all A card game: duel for the field
4 Einzelspieler Single player
5 Kampagne Campaign
6 Netzwerkspiel Network game
62 Schwer Hard
63 Profil zurücksetzen Reset profile
64 Große Schrift Large text
Duell Duel
%d am Tisch %d at the table
65 %d Münzen %d coins
66 Abbrechen Cancel
67 Ja, zurücksetzen Yes, reset
Speichern und zurück Save and go back
Kampf fortsetzen Resume battle
68 %s Schaden an ein gegnerisches Wesen %s damage to an enemy creature
69 %s Schaden an ein Ziel %s damage to a target
70 %s Schaden an alle gegnerischen Wesen %s damage to all enemy creatures
71 %s Schaden an alle Gegner %s Schaden an den Gegner %s damage to all opponents %s damage to the opponent
72 %s Schaden an alle Gegner und ihre Wesen %s Schaden an den Gegner und seine Wesen %s damage to all opponents and their creatures %s damage to the opponent and their creatures
73 Heile dich um %s Heal yourself for %s
74 Erhalte 1 Mana Gain 1 mana
75 Erhalte %s Mana Gain %s mana
113 Zuschauer Spectator
114 Zuschauer: Hände sind verdeckt Spectator: hands are hidden
115 Starten Start
1 Gegner 1 opponent
%d Gegner %d opponents
KI-Gegner AI opponents
116 Sammlung Collection
117 Verschmelzen Fuse
118 Zwei eigene Wesen wählen Pick two of your creatures
119 Zweites Wesen wählen Pick the second creature
Feld: neutral Field: neutral
120 Zwei verschiedene Wesen wählen Pick two different creatures
121 Nur eigene Wesen Only your own creatures
122 Vulkan Volcano
142 Aufgewertet Upgraded
143 +%d Staub +%d dust
144 +%d je Zug +%d per turn
145 Neu würfeln (%d) Neu würfeln ◆ %d Reroll (%d) Reroll ◆ %d
146 Dein Zug Your turn
147 Beim Ausspielen On play
148 Nicht genug Mana Not enough mana
149 Kein Angebot No offer
150 Hand voll Hand full
151 Profil wirklich zurücksetzen? Really reset your profile?
152 Sammlung, Münzen und Fortschritt gehen verloren. Collection, coins and progress are lost.
153 Chimäre Chimera
154 Angriff attack
155 Eis Ice
156 Resonanz +%d Resonance +%d
157 Feld +%d Field +%d
158 Merkmal +%d Perk +%d
159 Feldkombo %s mit %s: %s Field combo %s with %s: %s
160 1 Schaden an das stärkste gegnerische Wesen 1 damage to the strongest enemy creature
161 2 Schaden an den Gegner 2 damage to the opponent
162 Heile dich um 3 Heal yourself for 3
163 Das neue Wesen erhält +1/+1 The new creature gets +1/+1
164 %d Leben %d life
165 Gegner mit %d Leben · %d Münzen bei Sieg Opponent with %d life · %d coins for a win
166 Start gegen die KI Start against the AI
167 Beginnt mit 5 Leben mehr Starts with 5 extra life

Binary file not shown.