update
This commit is contained in:
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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`)
|
||||
- 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
|
||||
@@ -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_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 --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf
|
||||
```
|
||||
|
||||
## Export
|
||||
@@ -79,4 +79,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
||||
- `scripts/campaign.gd` – Kampagnenstufen
|
||||
- `scenes/main.tscn` – Startszene
|
||||
- `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
|
||||
- `design.md` – Gestaltung: Vorbilder, Karte, Kampfbildschirm, Bewegung, Menüs
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
## Grundidee
|
||||
- Kartenspiel wie zum Beispiel Magic
|
||||
- PC-Spiel
|
||||
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
||||
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
||||
- Duell: Spieler gegen einen KI-Gegner (oder einen Mitspieler im Netz), beide mit gleichen Regeln
|
||||
- Einzelspieler-Knopf führt zur Wahl der Schwierigkeit
|
||||
- Kampagne gegen benannte Magier als Fortschritt
|
||||
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
|
||||
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
||||
- Im lokalen Netz zu zweit spielbar, weitere als Zuschauer
|
||||
|
||||
## Sammeln
|
||||
- Jede Karte gehört jedem von Anfang an: wer zum ersten Mal spielt, kann so gut sein wie wer tausend Partien hat
|
||||
- Gesammelt wird die Seltenheitsstufe einer Karte, und die ist reine Optik (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
|
||||
- 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
|
||||
@@ -23,8 +22,8 @@
|
||||
## Elemente
|
||||
- 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
|
||||
- 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)
|
||||
- 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
|
||||
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus
|
||||
|
||||
92
design.md
Normal file
92
design.md
Normal 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/
|
||||
@@ -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,
|
||||
## die der Gegner negativ, der stärkste Gegner etwas mehr (jeder gegen jeden: den Führenden
|
||||
## bremsen). Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung, tödliche besonders.
|
||||
## die des Gegners negativ. Gegnerische Angriffskraft ohne eigenen Wächter ist eine Bedrohung,
|
||||
## tödliche besonders.
|
||||
static func evaluate(g: Game, p: Game.Player) -> float:
|
||||
if not p.alive():
|
||||
return -10000.0
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
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 := [
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "extra": 0, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "extra": 1, "reward": 150, "perk": "waechter_leben"},
|
||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "extra": 1, "reward": 150, "perk": "flink_angriff"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "extra": 2, "reward": 200, "perk": "wesen_leben"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "extra": 2, "reward": 200, "perk": "extra_mana"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "extra": 3, "reward": 250, "perk": "resonanz_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "extra": 3, "reward": 250, "perk": "schild_wesen"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 300, "perk": "alle_flink"},
|
||||
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 500, "perk": "start_energie"},
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "life": 30, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "life": 30, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "life": 31, "reward": 150, "perk": "waechter_leben"},
|
||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "life": 31, "reward": 150, "perk": "flink_angriff"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "life": 32, "reward": 200, "perk": "wesen_leben"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 200, "perk": "extra_mana"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "life": 33, "reward": 250, "perk": "resonanz_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "life": 33, "reward": 250, "perk": "schild_wesen"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "life": 34, "reward": 300, "perk": "alle_flink"},
|
||||
{"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 := [
|
||||
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "extra": 1, "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": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "extra": 2, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "extra": 2, "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": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "extra": 3, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "extra": 4, "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": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "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": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "life": 30, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "life": 30, "reward": 300, "perks": ["leben_35", "extra_mana"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "life": 31, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "life": 31, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
||||
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "life": 32, "reward": 400, "perks": ["wesen_leben", "feuer_angriff"]},
|
||||
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "life": 32, "reward": 400, "perks": ["extra_mana", "flink_angriff"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "life": 33, "reward": 450, "perks": ["resonanz_doppelt", "wesen_leben"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "life": 33, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||
{"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"], "life": 34, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_mana"]},
|
||||
]
|
||||
|
||||
|
||||
## Kapitel 3: drei Merkmale und volle Tische.
|
||||
const STAGES3 := [
|
||||
{"name": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "extra": 2, "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": "Borin", "title": "Berg selbst", "elements": ["Erde"], "extra": 3, "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": "Vulkar", "title": "Magmakaiser", "elements": ["Feuer", "Erde"], "extra": 4, "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": "Ignis", "title": "Sonnenpriester", "elements": ["Feuer"], "extra": 4, "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": "Zephyra", "title": "Gewitterkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "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": "Fenn", "title": "Aschefürst", "elements": ["Feuer"], "life": 32, "reward": 600, "perks": ["feuer_angriff", "start_energie", "wesen_leben"]},
|
||||
{"name": "Mara", "title": "Sturmflut", "elements": ["Wasser"], "life": 32, "reward": 600, "perks": ["leben_35", "extra_mana", "schild_wesen"]},
|
||||
{"name": "Borin", "title": "Berg selbst", "elements": ["Erde"], "life": 33, "reward": 700, "perks": ["waechter_leben", "schild_wesen", "leben_35"]},
|
||||
{"name": "Liska", "title": "Windbraut", "elements": ["Luft"], "life": 33, "reward": 700, "perks": ["flink_angriff", "alle_flink", "extra_mana"]},
|
||||
{"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"], "life": 34, "reward": 800, "perks": ["extra_mana", "flink_angriff", "resonanz_doppelt"]},
|
||||
{"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"], "life": 35, "reward": 900, "perks": ["schild_wesen", "leben_35", "resonanz_doppelt"]},
|
||||
{"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"], "life": 36, "reward": 1200, "perks": ["start_energie", "resonanz_doppelt", "extra_mana", "leben_35"]},
|
||||
]
|
||||
|
||||
const CHAPTERS := [STAGES, STAGES2, STAGES3]
|
||||
@@ -63,14 +63,9 @@ static func stage(i: int) -> Dictionary:
|
||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||
|
||||
|
||||
## Gegner einer Stufe: der Boss handelt nur mit seinen Elementen, Begleiter mit allen.
|
||||
static func opponents(stage_index: int) -> Array:
|
||||
## Boss einer Stufe: handelt nur mit seinen Elementen, mit dem Leben und den Merkmalen der Stufe.
|
||||
static func opponent(stage_index: int) -> Game.Player:
|
||||
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, life, st.title, st.elements)
|
||||
var boss := Game.Player.new(st.name, false, int(st.life), st.title, st.elements)
|
||||
boss.perks = Array(st.perks)
|
||||
var out := [boss]
|
||||
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
||||
for k in extra:
|
||||
out.append(DeckGen.random_opponent(k + 1))
|
||||
return out
|
||||
return boss
|
||||
|
||||
@@ -26,7 +26,7 @@ const COMBOS := {
|
||||
const FIELD_COMBO_TEXT := {
|
||||
"Dampf": "Friere das stärkste gegnerische Wesen ein",
|
||||
"Lava": "1 Schaden an das stärkste gegnerische Wesen",
|
||||
"Blitz": "2 Schaden an den schwächsten Gegner",
|
||||
"Blitz": "2 Schaden an den Gegner",
|
||||
"Schlamm": "Heile dich um 3",
|
||||
"Sturm": "Erhalte 1 Mana",
|
||||
"Sand": "Das neue Wesen erhält +1/+1",
|
||||
@@ -165,9 +165,9 @@ class Effect extends RefCounted:
|
||||
"all_enemy_creatures":
|
||||
return tr("%s Schaden an alle gegnerischen Wesen") % n
|
||||
"all_enemy_heroes":
|
||||
return tr("%s Schaden an alle Gegner") % n
|
||||
return tr("%s Schaden an den Gegner") % n
|
||||
"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":
|
||||
return tr("Heile dich um %s") % n
|
||||
"mana":
|
||||
@@ -250,6 +250,15 @@ class Card extends RefCounted:
|
||||
return fx.target
|
||||
return ""
|
||||
|
||||
## Nur die Effekte (die Karte zeigt Schlüsselwörter als Symbole).
|
||||
func effect_text() -> String:
|
||||
if effects.is_empty():
|
||||
return ""
|
||||
var fx: Array[String] = []
|
||||
for f in effects:
|
||||
fx.append(f.text())
|
||||
return ", ".join(fx) + "."
|
||||
|
||||
func text() -> String:
|
||||
var parts: Array[String] = []
|
||||
if not keywords.is_empty():
|
||||
|
||||
@@ -31,5 +31,5 @@ static func shop_cards(r: RandomNumberGenerator, income: int, elements: Array =
|
||||
|
||||
|
||||
## Gegner mit eigenem Laden über alle Elemente.
|
||||
static func random_opponent(index: int, life: int = Game.START_LIFE) -> Game.Player:
|
||||
return Game.Player.new("Gegner %d" % index, false, life, "Magier")
|
||||
static func random_opponent(life: int = Game.START_LIFE) -> Game.Player:
|
||||
return Game.Player.new("Gegner", false, life)
|
||||
|
||||
@@ -9,7 +9,6 @@ const SHOP_SIZE := 5 # so viele Karten liegen im Laden
|
||||
const REROLL_COST := 1
|
||||
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
|
||||
const START_LIFE := 30
|
||||
const MAX_OPPONENTS := 5
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
const PERKS := {
|
||||
@@ -18,7 +17,7 @@ const PERKS := {
|
||||
"waechter_leben": "Wächter kommen mit +2 Leben",
|
||||
"wesen_leben": "Wesen kommen mit +1 Leben",
|
||||
"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",
|
||||
"alle_flink": "Alle Wesen sind flink",
|
||||
"start_energie": "Beginnt mit 3 Mana",
|
||||
@@ -151,8 +150,8 @@ class Player extends RefCounted:
|
||||
|
||||
|
||||
var human: Player # der (erste) menschliche Spieler
|
||||
var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere)
|
||||
var opponents: Array = [] # alle außer dem ersten Menschen
|
||||
var humans: Array = [] # alle menschlichen Spieler (im Netz zwei)
|
||||
var opponent: Player # der andere am Tisch
|
||||
var players: Array = [] # in Zugreihenfolge
|
||||
var winner_player: Player = null
|
||||
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
|
||||
|
||||
|
||||
func _init(h: Player, opps: Array, r: RandomNumberGenerator = null) -> void:
|
||||
func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
||||
human = h
|
||||
opponents = opps
|
||||
players = [h] + opps
|
||||
opponent = o
|
||||
players = [h, o]
|
||||
humans = players.filter(func(p): return p.is_human)
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
@@ -199,11 +198,15 @@ func ref_of(target) -> Dictionary:
|
||||
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:
|
||||
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:
|
||||
for p in players:
|
||||
for c in p.board:
|
||||
@@ -218,7 +221,7 @@ func clone() -> Game:
|
||||
for p in players:
|
||||
copies.append(p.clone())
|
||||
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.turn = turn
|
||||
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)]
|
||||
|
||||
|
||||
## 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).
|
||||
func update_resonance() -> void:
|
||||
for p in players:
|
||||
@@ -642,20 +665,19 @@ func cleanup() -> void:
|
||||
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:
|
||||
if winner != "":
|
||||
return
|
||||
var alive := players.filter(func(p): return p.alive())
|
||||
var humans_alive := alive.filter(func(p): return p.is_human)
|
||||
if humans_alive.is_empty():
|
||||
winner = "opponents"
|
||||
winner_player = alive[0] if alive.size() == 1 else null
|
||||
say("Du wurdest besiegt." if humans.size() == 1 else "Alle Spieler sind besiegt, die KI gewinnt.")
|
||||
elif alive.size() == 1:
|
||||
winner = "human"
|
||||
winner_player = alive[0]
|
||||
say("Alle Gegner sind besiegt!" if humans.size() == 1 else "%s gewinnt!" % winner_player.name)
|
||||
if alive.size() != 1:
|
||||
return
|
||||
winner_player = alive[0]
|
||||
winner = "human" if winner_player.is_human else "opponents"
|
||||
if humans.size() == 1:
|
||||
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||||
else:
|
||||
say("%s gewinnt!" % winner_player.name)
|
||||
|
||||
|
||||
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
||||
@@ -678,28 +700,23 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
arr[j] = tmp
|
||||
|
||||
|
||||
## rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
||||
static func new_battle(n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
|
||||
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
||||
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
var life: int = int(rules.get("opponent_life", START_LIFE))
|
||||
var opps := []
|
||||
for i in n_opp:
|
||||
opps.append(DeckGen.random_opponent(i + 1, life))
|
||||
return new_battle_with(opps, r, false, rules)
|
||||
return new_battle_with(DeckGen.random_opponent(int(rules.get("opponent_life", START_LIFE))), 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:
|
||||
r = make_rng()
|
||||
var human := Player.new("Spieler", true, START_LIFE)
|
||||
human.perks = Array(rules.get("human_perks", []))
|
||||
human.looks = rules.get("looks", {})
|
||||
var g := Game.new(human, opps, r)
|
||||
var g := Game.new(human, opp, r)
|
||||
for p in g.players:
|
||||
if "leben_35" in p.perks and p.life == START_LIFE:
|
||||
p.life = 35
|
||||
p.max_life = 35
|
||||
if "leben_35" in p.perks:
|
||||
p.life += 5
|
||||
p.max_life += 5
|
||||
g.start(fixed_order)
|
||||
return g
|
||||
|
||||
@@ -48,7 +48,7 @@ func _ready() -> void:
|
||||
|
||||
func host(port: int = PORT) -> Error:
|
||||
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:
|
||||
status = "Kann nicht hosten (Port %d belegt?)" % port
|
||||
return err
|
||||
@@ -307,10 +307,11 @@ func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
## Host: Spiel mit allen Verbundenen und n_ai KI-Gegnern starten.
|
||||
func start_battle(n_ai: int) -> void:
|
||||
## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI.
|
||||
func start_battle() -> void:
|
||||
var ids: Array = names.keys()
|
||||
ids.sort()
|
||||
ids = ids.slice(0, 2)
|
||||
var r := Game.make_rng()
|
||||
var players := []
|
||||
seats = {}
|
||||
@@ -321,9 +322,9 @@ func start_battle(n_ai: int) -> void:
|
||||
if Cards.has_id(cid):
|
||||
p.looks[cid] = int(pick.looks[cid])
|
||||
players.append(p)
|
||||
for i in n_ai:
|
||||
players.append(DeckGen.random_opponent(i + 1))
|
||||
game = Game.new(players[0], players.slice(1), r)
|
||||
if players.size() < 2:
|
||||
players.append(DeckGen.random_opponent())
|
||||
game = Game.new(players[0], players[1], r)
|
||||
game.start()
|
||||
for id in ids:
|
||||
seats[id] = game.players.find(players[ids.find(id)])
|
||||
|
||||
@@ -80,7 +80,7 @@ static func from_snapshot(d: Dictionary) -> Game:
|
||||
p.board.append(c)
|
||||
list.append(p)
|
||||
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.turn = int(d.turn)
|
||||
g.current_idx = int(d.current_idx)
|
||||
|
||||
@@ -10,7 +10,7 @@ const BOOSTER_COST := 100
|
||||
const BOOSTER_SIZE := 5
|
||||
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
||||
const START_COINS := 150
|
||||
const REWARD_WIN := {1: 100, 2: 150, 3: 200, 4: 250, 5: 300}
|
||||
const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
# Schwierigkeit: Leben der Gegner, KI und Münzen.
|
||||
const DIFFICULTIES := {
|
||||
@@ -190,10 +190,10 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
|
||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
func record_battle(won: bool, 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
|
||||
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:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
|
||||
@@ -4,6 +4,8 @@ extends RefCounted
|
||||
|
||||
var screen # BattleScreen
|
||||
var enabled := true
|
||||
var banners: Array = [] # wartende Ansagen [Text, Farbe]
|
||||
var banner_running := false
|
||||
|
||||
|
||||
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)
|
||||
lunge(atk, center_of(ev.target, centers))
|
||||
flash_target(ev.target)
|
||||
shake()
|
||||
delay = 0.12
|
||||
"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})
|
||||
if where.x < 0:
|
||||
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":
|
||||
sfx("play")
|
||||
"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)
|
||||
"field":
|
||||
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":
|
||||
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
||||
"reroll":
|
||||
@@ -165,3 +168,108 @@ func move_to(tile: CreatureView, pos: Vector2) -> void:
|
||||
screen.create_tween().tween_property(tile, "position", pos, 0.2)
|
||||
else:
|
||||
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())
|
||||
|
||||
@@ -1,42 +1,71 @@
|
||||
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 HAND_W := 130
|
||||
const HAND_H := 185
|
||||
const HAND_GAP := 8
|
||||
const BAR := Rect2(335, 415, 610, 68)
|
||||
const OWN_X := (1280 - 5 * 110 - 4 * 10) / 2
|
||||
|
||||
|
||||
static func compact(n_others: int) -> bool:
|
||||
return n_others >= 4
|
||||
|
||||
|
||||
static func hero_box_rect(n_others: int, i: int) -> Rect2:
|
||||
var col_w := 1280 / n_others
|
||||
var margin := 6 if compact(n_others) else 10
|
||||
return Rect2(i * col_w + margin, 10, col_w - 2 * margin, 64)
|
||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||
const B_LOG := Rect2(112, 10, 96, 32)
|
||||
const OPP_BOX := Rect2(330, 10, 620, 56)
|
||||
const FIELD := Rect2(240, 226, 800, 32)
|
||||
const MSG := Rect2(240, 262, 800, 22)
|
||||
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
||||
const STATUS := Rect2(20, 288, 210, 160)
|
||||
const B_FUSE := Rect2(1050, 298, 210, 40)
|
||||
const B_UNDO := Rect2(1050, 346, 210, 40)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 96
|
||||
const HAND_H := 62
|
||||
const HAND_GAP := 4
|
||||
const HAND_Y := 458
|
||||
const SHOP_Y := 532
|
||||
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:
|
||||
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:
|
||||
var col_w := 1280 / n_others
|
||||
var sz: Array = SIZES[n_others]
|
||||
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])
|
||||
## Brett des Gegners unter seinem Kasten.
|
||||
static func opp_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||
|
||||
|
||||
## Laden: fünf Angebote über der Hand.
|
||||
## Laden: fünf große Angebote unten, wie in TFT.
|
||||
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:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
var x := (1280 - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), 800 - HAND_H - 8, HAND_W, HAND_H)
|
||||
var x := CENTER_X + (CENTER_W - total) / 2
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,6 @@ extends Control
|
||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
|
||||
var app
|
||||
var n_opp: int
|
||||
var stage := -1 # Kampagnenstufe oder -1
|
||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||
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 reward := -1
|
||||
var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
var pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
get:
|
||||
@@ -36,7 +33,6 @@ var gp_index: int:
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
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_t := 0.0
|
||||
|
||||
@@ -54,11 +50,12 @@ var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_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 log_box: RichTextLabel
|
||||
var preview_holder: Control
|
||||
var bar_panel: Panel
|
||||
var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
@@ -69,38 +66,35 @@ var b_end: Button
|
||||
var b_fuse: Button
|
||||
var b_reroll: 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 result_label: Label
|
||||
var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var b_save_leave: Button
|
||||
var cursor_layer: Control
|
||||
var wait_label: Label
|
||||
var b_skip: Button
|
||||
var b_undo: Button
|
||||
|
||||
|
||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||
n_opp = n
|
||||
func _init(campaign_stage: int = -1) -> void:
|
||||
stage = campaign_stage
|
||||
|
||||
|
||||
static func online(seat: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
var b := BattleScreen.new(-1)
|
||||
b.net_play = true
|
||||
b.online_seat = seat
|
||||
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:
|
||||
return net_play
|
||||
|
||||
@@ -137,13 +131,11 @@ func _ready() -> void:
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
wait_label.visible = true
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif saved_game != null:
|
||||
game = saved_game
|
||||
elif stage >= 0:
|
||||
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:
|
||||
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()
|
||||
_begin()
|
||||
|
||||
@@ -160,7 +152,6 @@ func own_rules() -> Dictionary:
|
||||
|
||||
|
||||
func _begin() -> void:
|
||||
n_opp = game.players.size() - 1
|
||||
if is_online():
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
else:
|
||||
@@ -170,67 +161,94 @@ func _begin() -> void:
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
game.events.clear()
|
||||
b_save_leave.visible = resumable()
|
||||
save_battle()
|
||||
refresh()
|
||||
announce_turn()
|
||||
|
||||
|
||||
func _build_static() -> void:
|
||||
Style.panel(self, Rect2(10, 205, 320, 290))
|
||||
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20))
|
||||
log_box = RichTextLabel.new()
|
||||
log_box.position = Vector2(18, 230)
|
||||
log_box.size = Vector2(304, 258)
|
||||
log_box.bbcode_enabled = true
|
||||
log_box.scroll_following = true
|
||||
log_box.add_theme_font_size_override("normal_font_size", 13)
|
||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(log_box)
|
||||
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
||||
Style.panel(self, Rect2(950, 205, 320, 380))
|
||||
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10)
|
||||
bar_panel = Style.panel(self, BattleLayout.BAR)
|
||||
name_label = Style.label(self, "", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
||||
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true)
|
||||
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 24), "right")
|
||||
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
||||
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
||||
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
||||
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
|
||||
var L := BattleLayout
|
||||
# oben links: Aufgeben und Verlauf, klein
|
||||
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
|
||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||
b_log.add_theme_font_size_override("font_size", 13)
|
||||
# Mitte: Feldband, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||
field_view = FieldView.new(L.FIELD)
|
||||
add_child(field_view)
|
||||
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
||||
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
||||
var slots := Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
slots.draw.connect(func():
|
||||
for j in Game.MAX_BOARD:
|
||||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
|
||||
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||||
add_child(slots)
|
||||
status_panel = Style.panel(self, L.STATUS)
|
||||
var sx: float = L.STATUS.position.x
|
||||
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.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.visible = false
|
||||
hero_layer = _layer()
|
||||
link_view = Links.new()
|
||||
add_child(link_view)
|
||||
tile_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
shop_layer = _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()
|
||||
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
|
||||
overlay = _overlay()
|
||||
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")
|
||||
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"))
|
||||
# Aufgeben bestätigen
|
||||
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)
|
||||
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
|
||||
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)
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
|
||||
|
||||
func toggle_log() -> void:
|
||||
log_overlay.visible = not log_overlay.visible
|
||||
|
||||
|
||||
func _layer() -> Control:
|
||||
@@ -244,54 +262,27 @@ func _overlay() -> Control:
|
||||
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:
|
||||
if is_online():
|
||||
net.leave()
|
||||
if resumable() and game != null and game.winner == "":
|
||||
app.clear_battle_save() # aufgegeben
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
# --- 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:
|
||||
var out := []
|
||||
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())
|
||||
return [opponent()]
|
||||
|
||||
|
||||
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
||||
if p == perspective:
|
||||
return BattleLayout.own_slot(j)
|
||||
var os := others()
|
||||
return BattleLayout.other_slot(os.size(), os.find(p), j)
|
||||
return BattleLayout.opp_slot(j)
|
||||
|
||||
|
||||
func ref_key(ref: Dictionary) -> String:
|
||||
@@ -306,47 +297,37 @@ func target_center(ref: Dictionary) -> Vector2:
|
||||
return Vector2(-1, -1)
|
||||
var p: Game.Player = game.players[ref.index]
|
||||
if p == perspective:
|
||||
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
|
||||
return BattleLayout.STATUS.get_center()
|
||||
if hero_panels.has(p):
|
||||
var panel: Panel = hero_panels[p]
|
||||
return panel.position + panel.size / 2
|
||||
return Vector2(-1, -1)
|
||||
|
||||
|
||||
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
|
||||
func build_hero_boxes() -> void:
|
||||
Style.clear(hero_layer)
|
||||
hero_panels = {}
|
||||
hero_labels = {}
|
||||
var os := others()
|
||||
var c := compact()
|
||||
for i in os.size():
|
||||
var o: Game.Player = os[i]
|
||||
var box := BattleLayout.hero_box_rect(os.size(), i)
|
||||
var panel := Style.panel(hero_layer, box)
|
||||
panel.gui_input.connect(func(ev):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
on_click("hero", o))
|
||||
if not o.perks.is_empty():
|
||||
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
||||
hero_panels[o] = panel
|
||||
var labels := {}
|
||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||
if not o.perks.is_empty():
|
||||
title += " ★"
|
||||
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (58 if c else 20), 26), "left", true)
|
||||
labels.name.clip_text = true
|
||||
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)
|
||||
var o := opponent()
|
||||
var box := BattleLayout.OPP_BOX
|
||||
var panel := Style.panel(hero_layer, box)
|
||||
panel.gui_input.connect(func(ev):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
on_click("hero", o))
|
||||
if not o.perks.is_empty():
|
||||
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
||||
hero_panels[o] = panel
|
||||
var labels := {}
|
||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||
if not o.perks.is_empty():
|
||||
title += " ★"
|
||||
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)
|
||||
labels.name.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "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")
|
||||
hero_labels[o] = labels
|
||||
|
||||
|
||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||
@@ -358,7 +339,7 @@ func my_turn() -> 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:
|
||||
@@ -376,7 +357,9 @@ func _input(event: InputEvent) -> void:
|
||||
KEY_ESCAPE:
|
||||
if confirm.visible:
|
||||
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()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
@@ -402,7 +385,6 @@ func cancel_all() -> void:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
cancel()
|
||||
zoom_player = null
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -412,10 +394,6 @@ func _process(delta: float) -> void:
|
||||
msg_t = maxf(0.0, msg_t - delta)
|
||||
if msg_t <= 0.0 and 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:
|
||||
undo_t -= delta
|
||||
if undo_t <= 0.0:
|
||||
@@ -477,10 +455,9 @@ func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if counts_for_profile():
|
||||
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.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
else:
|
||||
var wp := game.winner_player
|
||||
var mine: bool = wp != null and wp == perspective
|
||||
@@ -506,10 +483,13 @@ func play(card: Cards.Card, target) -> void:
|
||||
return
|
||||
var before: Game = game.clone() if not is_online() else null
|
||||
var log_len := game.log.size()
|
||||
var from := _hand_rect_of(card)
|
||||
var err := game.play_card(perspective, card, target)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
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)
|
||||
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||
undo_state = before
|
||||
@@ -534,9 +514,15 @@ func do_buy(slot: int) -> void:
|
||||
if is_client():
|
||||
net.send_action({"type": "buy", "slot": slot})
|
||||
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)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
elif offer != null:
|
||||
fx.sfx("buy")
|
||||
fx.fly_card(offer, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
|
||||
Cards.RARITIES[int(perspective.looks.get(offer.id, 0))])
|
||||
|
||||
|
||||
## Würfelt den Laden neu (kostet Mana).
|
||||
@@ -551,6 +537,8 @@ func do_reroll() -> void:
|
||||
var err := game.reroll(perspective)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
else:
|
||||
deal_shop = true
|
||||
after_action()
|
||||
|
||||
|
||||
@@ -596,7 +584,6 @@ func end_turn() -> void:
|
||||
return
|
||||
cancel()
|
||||
clear_undo()
|
||||
zoom_player = null
|
||||
if is_client():
|
||||
net.send_action({"type": "end"})
|
||||
return
|
||||
@@ -629,7 +616,6 @@ func on_click(kind: String, obj) -> void:
|
||||
play(card, obj)
|
||||
else:
|
||||
say(tr("Abgebrochen"))
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
if selected != null:
|
||||
@@ -643,7 +629,6 @@ func on_click(kind: String, obj) -> void:
|
||||
var err := g.attack(cr, obj)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
match kind:
|
||||
@@ -677,17 +662,121 @@ func on_tile_click(cr: Game.Creature) -> void:
|
||||
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)
|
||||
info_label.visible = entered and info != ""
|
||||
info_label.text = info if entered else ""
|
||||
if entered and card != null:
|
||||
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||
v.position = Vector2(1005, 215)
|
||||
var r := BattleLayout.preview_rect(anchor)
|
||||
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
|
||||
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:
|
||||
show_preview(card, entered, perspective.looks)
|
||||
## Eigene Hand- und Ladenkarte: zeigt, was das Ausspielen jetzt brächte, und markiert den Kombo-Partner.
|
||||
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 -------------------------------------------------------------------------------
|
||||
@@ -744,7 +833,7 @@ func after_action() -> void:
|
||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||
refresh()
|
||||
fx.play_events(evs, centers)
|
||||
save_battle()
|
||||
announce_turn()
|
||||
|
||||
|
||||
# --- 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)
|
||||
if o.alive():
|
||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \
|
||||
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy])
|
||||
labels.stats.text = "Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy]
|
||||
else:
|
||||
labels.life.text = "besiegt"
|
||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||
@@ -798,10 +886,11 @@ func refresh() -> void:
|
||||
if tiles.has(cr.uid):
|
||||
tile = tiles[cr.uid]
|
||||
tile.creature = cr
|
||||
if tile.size != rect.size:
|
||||
tile.size = rect.size
|
||||
if tile.size != rect.size: # erst die Mindestgröße, sonst bleibt die Kachel groß
|
||||
tile.custom_minimum_size = rect.size
|
||||
tile.size = rect.size
|
||||
tile.pivot_offset = rect.size / 2
|
||||
tile.set_process(Style.rarity_animated(tile.rarity()))
|
||||
else:
|
||||
tile = CreatureView.new(cr, rect.size)
|
||||
tile.position = rect.position
|
||||
@@ -809,13 +898,17 @@ func refresh() -> void:
|
||||
tiles[cr.uid] = tile
|
||||
tile_layer.add_child(tile)
|
||||
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
|
||||
tile.clicked.connect(func(_v):
|
||||
var c := game.find_creature(uid)
|
||||
if c != null:
|
||||
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
|
||||
if not tile.busy and tile.position != rect.position:
|
||||
fx.move_to(tile, rect.position)
|
||||
@@ -835,6 +928,8 @@ func refresh() -> void:
|
||||
w = 3
|
||||
tile.border = b
|
||||
tile.width = w
|
||||
tile.mark = ""
|
||||
tile.matchup = matchup_for(cr, valid)
|
||||
tile.queue_redraw()
|
||||
if p == h:
|
||||
own_views.append(tile)
|
||||
@@ -845,6 +940,8 @@ func refresh() -> void:
|
||||
var t: CreatureView = tiles[uid]
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
link_view.groups = resonance_groups()
|
||||
link_view.queue_redraw()
|
||||
|
||||
Style.clear(shop_layer)
|
||||
if not spectating():
|
||||
@@ -856,17 +953,22 @@ func refresh() -> void:
|
||||
continue
|
||||
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
||||
sv.position = sr.position
|
||||
sv.lift = mine
|
||||
apply_forecast(sv)
|
||||
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
||||
sv.hover.connect(show_hand_preview)
|
||||
sv.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(sv.position, sv.size)))
|
||||
var slot := i
|
||||
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
||||
sv.clicked.connect(func(_view): on_click("shop", slot))
|
||||
shop_layer.add_child(sv)
|
||||
shop_views.append(sv)
|
||||
if deal_shop:
|
||||
fx.deal(sv, i)
|
||||
deal_shop = false
|
||||
|
||||
Style.clear(hand_layer)
|
||||
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:
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
@@ -874,12 +976,14 @@ func refresh() -> void:
|
||||
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))])
|
||||
v.position = r.position
|
||||
v.lift = mine
|
||||
apply_forecast(v)
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
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))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
@@ -887,7 +991,11 @@ func refresh() -> void:
|
||||
var bar_border = Style.BLUE if mine else null
|
||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||
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
|
||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||
energy_label.text = "◆ %d" % h.energy
|
||||
@@ -902,18 +1010,10 @@ func refresh() -> void:
|
||||
b_undo.visible = undo_state != null and mine
|
||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||
b_reroll.visible = mine and not spectating()
|
||||
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST
|
||||
b_reroll.text = tr("Neu würfeln ◆ %d") % Game.REROLL_COST
|
||||
b_reroll.disabled = h.energy < Game.REROLL_COST
|
||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||
if g.field_element == "":
|
||||
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])
|
||||
field_view.update_from(g)
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
@@ -929,10 +1029,6 @@ func refresh() -> void:
|
||||
log_box.text = "\n".join(lines)
|
||||
pad.draw_cursor()
|
||||
|
||||
Style.clear(zoom_layer)
|
||||
if zoom_player != null and zoom_player.alive():
|
||||
build_zoom(valid)
|
||||
|
||||
if g.winner != "":
|
||||
var wp := g.winner_player
|
||||
if counts_for_profile():
|
||||
@@ -951,30 +1047,3 @@ func refresh() -> void:
|
||||
overlay.visible = true
|
||||
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())
|
||||
|
||||
@@ -12,6 +12,8 @@ var msg_label: Label
|
||||
var b_buy: Button
|
||||
var b_all: Button
|
||||
var slots: Control
|
||||
var fx_layer: Control
|
||||
var fresh := {} # gerade aufgedeckt: wird umgedreht
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
@@ -47,15 +49,18 @@ func slot_rect(i: int) -> Rect2:
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Booster", 36, Style.GOLD, Rect2(0, 20, 1280, 50), "center", true)
|
||||
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
||||
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
|
||||
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.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 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_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all)
|
||||
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
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(370, 690, 330, 52), buy, Color("5a4628"))
|
||||
b_buy.add_theme_font_size_override("font_size", 20)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -81,16 +86,44 @@ func buy() -> void:
|
||||
|
||||
func reveal(i: int) -> void:
|
||||
revealed[i] = true
|
||||
app.sfx.play("click")
|
||||
fresh = {i: 0.0}
|
||||
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click")
|
||||
refresh()
|
||||
|
||||
|
||||
func reveal_all() -> void:
|
||||
fresh = {}
|
||||
var k := 0
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
if not revealed.has(i):
|
||||
fresh[i] = k * 0.12
|
||||
k += 1
|
||||
revealed[i] = true
|
||||
app.sfx.play("play")
|
||||
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:
|
||||
var p: Profile = app.profile
|
||||
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)
|
||||
v.position = r.position
|
||||
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)
|
||||
if res.upgraded:
|
||||
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.clicked.connect(reveal.bind(i))
|
||||
slots.add_child(back)
|
||||
fresh = {}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
class_name CampaignScreen
|
||||
extends Control
|
||||
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail.
|
||||
|
||||
var app
|
||||
var chapter := 1
|
||||
var selected := 0 # gewählte Stufe (Index über alle Kapitel)
|
||||
var perk_box: OptionButton
|
||||
var node_buttons: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||
selected = mini(p.campaign_stage, Campaign.total() - 1)
|
||||
build()
|
||||
|
||||
|
||||
func build() -> void:
|
||||
Style.clear(self)
|
||||
node_buttons = []
|
||||
var p: Profile = app.profile
|
||||
var col := Style.column(self, 1000, 16, 5)
|
||||
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
||||
var tabs := Style.row(col, 10)
|
||||
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
|
||||
var n := Campaign.STAGES.size()
|
||||
var x := 40
|
||||
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)
|
||||
b.disabled = p.campaign_stage < c * Campaign.STAGES.size()
|
||||
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 * n
|
||||
x += 160
|
||||
if p.perk_unlocked():
|
||||
var pr := Style.row(tabs, 8)
|
||||
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
||||
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right")
|
||||
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")
|
||||
var keys: Array = Game.PERKS.keys()
|
||||
for k in keys:
|
||||
@@ -35,54 +40,90 @@ func build() -> void:
|
||||
perk_box.item_selected.connect(func(i: int):
|
||||
p.perk = "" if i == 0 else keys[i - 1]
|
||||
app.save())
|
||||
pr.add_child(perk_box)
|
||||
var offset := (chapter - 1) * Campaign.STAGES.size()
|
||||
for k in Campaign.STAGES.size():
|
||||
add_child(perk_box)
|
||||
# Pfad mit Stationen
|
||||
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 st := Campaign.stage(i)
|
||||
var done := i < p.campaign_stage
|
||||
var next := i == p.campaign_stage
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
||||
Style.GOLD if next else null, 2 if next else 0))
|
||||
col.add_child(panel)
|
||||
var pad := MarginContainer.new() # Innenabstand, damit Name und Knopf nicht am Rahmen kleben
|
||||
for side in ["left", "right"]:
|
||||
pad.add_theme_constant_override("margin_" + side, 14)
|
||||
panel.add_child(pad)
|
||||
var r := Style.row(pad, 10)
|
||||
r.custom_minimum_size = Vector2(0, 50)
|
||||
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
|
||||
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
|
||||
var dots := Control.new()
|
||||
dots.custom_minimum_size = Vector2(60, 0)
|
||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var elems: Array = st.elements
|
||||
dots.draw.connect(func():
|
||||
for m in elems.size():
|
||||
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
||||
r.add_child(dots)
|
||||
if done:
|
||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||
elif next:
|
||||
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
||||
else:
|
||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||
b.disabled = true
|
||||
var b := Button.new()
|
||||
b.text = str(i + 1)
|
||||
b.position = centers[k] - Vector2(24, 24)
|
||||
b.size = Vector2(48, 48)
|
||||
var base: Color = Style.PRIMARY if done else (Color("5a4628") if next else Style.PANEL)
|
||||
var edge: Color = Style.GOLD if i == selected else (Style.GREEN if done else Style.MUTED)
|
||||
b.add_theme_stylebox_override("normal", Style.box(base, 24, edge, 3 if i == selected else 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 24, edge, 3))
|
||||
b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 24, edge, 3))
|
||||
b.add_theme_stylebox_override("disabled", Style.box(Style.darker(Style.PANEL, 0.8), 24, Style.PANEL_LIGHT, 2))
|
||||
b.add_theme_stylebox_override("focus", Style.box(Color(0, 0, 0, 0), 24, Style.GOLD, 3))
|
||||
b.disabled = i > p.campaign_stage
|
||||
b.pressed.connect(func():
|
||||
selected = i
|
||||
build())
|
||||
Style.juice(b)
|
||||
add_child(b)
|
||||
node_buttons.append(b)
|
||||
if next and Style.animations:
|
||||
b.pivot_offset = b.size / 2
|
||||
var tw := b.create_tween().set_loops()
|
||||
tw.tween_property(b, "scale", Vector2(1.1, 1.1), 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(b, "scale", Vector2.ONE, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
# Gewählte Stufe
|
||||
var st := Campaign.stage(selected)
|
||||
var done := selected < p.campaign_stage
|
||||
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():
|
||||
Style.clabel(col, "Alle Kapitel abgeschlossen", 16, Style.GREEN, "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()))
|
||||
Style.label(self, "Alle Kapitel abgeschlossen", 16, Style.GREEN, Rect2(0, 600, 1280, 24), "center", true)
|
||||
|
||||
|
||||
func set_chapter(c: int) -> void:
|
||||
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()
|
||||
|
||||
|
||||
func _start(stage: int) -> void:
|
||||
app.show_screen(BattleScreen.new(0, stage))
|
||||
app.show_screen(BattleScreen.new(stage))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
class_name CardGrid
|
||||
extends ScrollContainer
|
||||
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte.
|
||||
## Scrollbares Kartenraster.
|
||||
|
||||
signal card_clicked(card: Cards.Card)
|
||||
|
||||
const COLS := 7
|
||||
const CW := 100
|
||||
const CH := 140
|
||||
const GAP := 12
|
||||
const COLS := 6
|
||||
const CW := 112
|
||||
const CH := 157
|
||||
const GAP := 14
|
||||
|
||||
var grid: GridContainer
|
||||
|
||||
@@ -24,19 +24,16 @@ func _init(rect: Rect2) -> void:
|
||||
add_child(grid)
|
||||
|
||||
|
||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool; rarity_fn(card) -> String (Optik)
|
||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable, rarity_fn: Callable = Callable()) -> void:
|
||||
## cards: Karten; rarity_fn(card) -> String: die Stufe, die die Karte zeigt (Optik)
|
||||
func fill(cards: Array, rarity_fn: Callable) -> void:
|
||||
Style.clear(grid)
|
||||
for c in cards:
|
||||
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
|
||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c) if rarity_fn.is_valid() else "gewöhnlich")
|
||||
v.dim = dim_fn.call(c)
|
||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c))
|
||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
class_name CardView
|
||||
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 hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var card: Cards.Card
|
||||
var border = null # Color oder null (dann Seltenheitsfarbe)
|
||||
var rarity := "gewöhnlich" # nur Optik: Rahmenfarbe der eigenen Stufe
|
||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||
var rarity := "gewöhnlich" # nur Optik
|
||||
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.
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
||||
card = c
|
||||
rarity = rar
|
||||
set_process(Style.rarity_animated(rarity)) # bewegter Glanz
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
pivot_offset = sz / 2
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(card, true))
|
||||
mouse_exited.connect(func(): hover.emit(card, false))
|
||||
mouse_entered.connect(func():
|
||||
hover.emit(card, true)
|
||||
_lift(true))
|
||||
mouse_exited.connect(func():
|
||||
hover.emit(card, false)
|
||||
_lift(false))
|
||||
if focusable:
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
focus_entered.connect(func():
|
||||
@@ -30,6 +42,17 @@ func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "g
|
||||
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:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
@@ -41,47 +64,135 @@ func _gui_input(event: InputEvent) -> void:
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 115
|
||||
var f_name := 18 if big else (14 if mid else 12)
|
||||
var f_text := 14 if big else (11 if mid else 10)
|
||||
var f_stat := 24 if big else (18 if mid else 15)
|
||||
var seed := hash(card.id)
|
||||
var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
|
||||
if rarity != "gewöhnlich":
|
||||
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[rarity]
|
||||
Style.border(self, rect, bcol, 3 if big else 2, 8)
|
||||
# Kosten
|
||||
var r := 16 if big else 12
|
||||
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 size.y < 90:
|
||||
_draw_compact(rect, frame, seed)
|
||||
else:
|
||||
_draw_full(rect, frame, seed)
|
||||
if dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
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)
|
||||
|
||||
@@ -18,33 +18,32 @@ var b_upgrade: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var 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)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(424, 55)
|
||||
search_box.size = Vector2(200, 34)
|
||||
search_box.position = Vector2(444, 86)
|
||||
search_box.size = Vector2(220, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
refresh())
|
||||
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)
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(845, 50, 420, 680))
|
||||
dust_label = Style.label(self, "", 22, Style.BLUE, Rect2(860, 58, 390, 30), "left", true)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
dust_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
preview_holder = Control.new()
|
||||
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(preview_holder)
|
||||
info_label = Style.label(self, "", 14, Style.MUTED, Rect2(860, 396, 390, 66), "center")
|
||||
b_upgrade = Style.button(self, "", Rect2(860, 480, 390, 44), upgrade, Color("285a3c"))
|
||||
b_dismantle = Style.button(self, "", Rect2(860, 534, 390, 44), dismantle, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 600, 390, 48), "center")
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
||||
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
||||
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -58,6 +57,7 @@ func set_filter(e: String) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
if filter != "":
|
||||
@@ -65,6 +65,11 @@ func cards() -> Array:
|
||||
if search != "":
|
||||
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())
|
||||
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
|
||||
|
||||
|
||||
@@ -109,7 +114,7 @@ func dismantle() -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
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)
|
||||
if selected == null:
|
||||
info_label.text = "Keine Karte gewählt"
|
||||
@@ -118,8 +123,8 @@ func refresh() -> void:
|
||||
b_upgrade.disabled = true
|
||||
b_dismantle.disabled = true
|
||||
return
|
||||
var v := CardView.new(selected, Vector2(200, 280), false, p.rarity(selected.id))
|
||||
v.position = Vector2(955, 100)
|
||||
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
|
||||
v.position = Vector2(920, 152)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var lvl := p.level(selected.id)
|
||||
|
||||
@@ -11,6 +11,8 @@ var width := 2
|
||||
var flash := false # kurz rot: wurde gerade angegriffen
|
||||
var slot: Vector2 # Zielposition im Raster
|
||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||
var 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:
|
||||
@@ -20,58 +22,93 @@ func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
pivot_offset = sz / 2
|
||||
set_process(Style.rarity_animated(rarity())) # bewegter Glanz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(creature.card, true))
|
||||
mouse_exited.connect(func(): hover.emit(creature.card, false))
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
|
||||
|
||||
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:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var small := size.x < 100
|
||||
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)
|
||||
var owner = creature.owner()
|
||||
var lvl: int = int(owner.looks.get(creature.card.id, 0)) if owner != null else 0
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[Cards.RARITIES[lvl]]
|
||||
if not mini:
|
||||
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
|
||||
Style.rarity_sheen(self, rect, rar, seed)
|
||||
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
if mark != "":
|
||||
Style.border(self, rect.grow(3), Style.GOLD, 3, 8)
|
||||
if mini:
|
||||
_draw_mini()
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
return
|
||||
var fs := 11 if small else 13
|
||||
var nm := Style.fit(creature.card.title(), true, fs, size.x - 6)
|
||||
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
||||
var badges: Array[String] = []
|
||||
if creature.taunt():
|
||||
badges.append("Wächter")
|
||||
if creature.shield:
|
||||
badges.append("Schild")
|
||||
if creature.attacks_per_turn() == 2:
|
||||
badges.append("x2")
|
||||
if creature.frozen:
|
||||
badges.append("Eis")
|
||||
if creature.resonance > 0:
|
||||
badges.append("Resonanz")
|
||||
if creature.field_bonus > 0:
|
||||
badges.append("Feld")
|
||||
var y := 18.0 if small else 22.0
|
||||
for b in badges:
|
||||
Style.text(self, b, 10 if small else 12, Color("ebebf5"), Vector2(size.x / 2, y), "midtop")
|
||||
y += 12 if small else 15
|
||||
var f := 16 if small else 20
|
||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), f, atk_col, Vector2(6, size.y - 4), "bottomleft", true)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(self, str(creature.health), f, hp_col, Vector2(size.x - 6, size.y - 4), "bottomright", true)
|
||||
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
|
||||
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
||||
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
|
||||
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||
# Schlüsselwörter als Symbole
|
||||
var icon := 7.0 if small else 9.0
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in creature.card.keywords:
|
||||
kws.append(k)
|
||||
if creature.shield and not ("Schild" in kws):
|
||||
kws.append("Schild")
|
||||
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
|
||||
var y := Style.fs(fs) + 14 + icon
|
||||
for k in kws:
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon)
|
||||
x += icon * 2 + 6
|
||||
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
|
||||
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||
if mark != "":
|
||||
var w := Style.FONT_BOLD.get_string_size(tr(mark), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(11)).x + 14
|
||||
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), Rect2(size.x / 2 - w / 2, -9, w, 18))
|
||||
Style.text(self, tr(mark), 11, Style.GOLD, Vector2(size.x / 2, 0), "center", true)
|
||||
if creature.frozen:
|
||||
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
||||
Style.text_outlined(self, "❄", 22 if small else 28, Color("dff2ff"), size / 2, "center", 3)
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
|
||||
|
||||
## 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.
|
||||
@@ -84,15 +121,14 @@ func _draw_mini() -> void:
|
||||
if creature.attacks_per_turn() == 2:
|
||||
letters.append("2")
|
||||
if creature.frozen:
|
||||
letters.append("E")
|
||||
if creature.resonance > 0:
|
||||
letters.append("R")
|
||||
if creature.field_bonus > 0:
|
||||
letters.append("F")
|
||||
letters.append("❄")
|
||||
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
|
||||
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true)
|
||||
var extra := creature.power() - creature.attack
|
||||
Style.text_outlined(self, str(creature.power()), 13, Style.GREEN if extra > 0 else Style.YELLOW, Vector2(9, size.y - 9), "center", 3)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(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:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
||||
|
||||
53
scripts/ui/field_view.gd
Normal file
53
scripts/ui/field_view.gd
Normal 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)
|
||||
1
scripts/ui/field_view.gd.uid
Normal file
1
scripts/ui/field_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://i4rdq5rmcyuk
|
||||
@@ -11,8 +11,6 @@ var b_host: Button
|
||||
var b_join: Button
|
||||
var b_watch: Button
|
||||
var b_start: Button
|
||||
var ai_buttons: Array = []
|
||||
var n_ai := 0
|
||||
var addr_label: Label
|
||||
|
||||
|
||||
@@ -20,9 +18,8 @@ func _ready() -> void:
|
||||
net = app.net
|
||||
net.my_name = "Spieler"
|
||||
net.my_pick = {"looks": app.profile.looks()}
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 10)
|
||||
Style.header(self, "Netzwerkspiel", _back)
|
||||
var col := Style.column(self, 760, 110, 10)
|
||||
var hr := Style.row(col, 12)
|
||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
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.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||
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)
|
||||
var br := Style.row(col, 20)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
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.started.connect(_started)
|
||||
net.ended.connect(func(_reason): refresh())
|
||||
@@ -79,13 +69,8 @@ func _watch() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func _set_ai(n: int) -> void:
|
||||
n_ai = n
|
||||
refresh()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
net.start_battle(n_ai)
|
||||
net.start_battle()
|
||||
|
||||
|
||||
func _started(seat: int) -> void:
|
||||
@@ -112,9 +97,8 @@ func refresh() -> void:
|
||||
b_watch.disabled = net.connected
|
||||
ip_box.editable = not net.connected
|
||||
var humans := ids.size()
|
||||
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
||||
for i in ai_buttons.size():
|
||||
ai_buttons[i].disabled = not net.is_host or i == n_ai or humans + i > Game.MAX_OPPONENTS + 1
|
||||
b_start.disabled = not net.is_host
|
||||
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
|
||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
||||
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ func apply_settings() -> void:
|
||||
sfx.set_music_volume(profile.settings.music)
|
||||
set_language(profile.settings.language)
|
||||
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()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
clear_battle_save()
|
||||
save()
|
||||
apply_settings()
|
||||
|
||||
@@ -54,6 +54,9 @@ func show_screen(s: Control) -> void:
|
||||
s.app = self
|
||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
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:
|
||||
focus_first_button.call_deferred()
|
||||
|
||||
@@ -107,16 +110,6 @@ func save() -> void:
|
||||
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:
|
||||
if net != null and net.is_inside_tree():
|
||||
net.leave()
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
class_name MenuScreen
|
||||
extends Control
|
||||
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster.
|
||||
|
||||
var app
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
var notice_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 640, 40, 5)
|
||||
Style.clabel(col, "Dinger", 64, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Ein Kartenspiel: jeder gegen jeden", 22, Style.MUTED)
|
||||
Style.spacer(col, 8)
|
||||
coins_label = Style.clabel(col, "", 20, Style.GOLD)
|
||||
coll_label = Style.clabel(col, "", 16, Style.MUTED)
|
||||
Style.spacer(col, 8)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 2
|
||||
grid.add_theme_constant_override("h_separation", 12)
|
||||
grid.add_theme_constant_override("v_separation", 8)
|
||||
var center := CenterContainer.new()
|
||||
center.add_child(grid)
|
||||
col.add_child(center)
|
||||
var sz := Vector2(270, 44)
|
||||
if FileAccess.file_exists(app.battle_save_path()):
|
||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||
Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
||||
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)
|
||||
var p: Profile = app.profile
|
||||
Style.label(self, "Dinger", 76, Style.GOLD, Rect2(80, 64, 600, 90), "left", true)
|
||||
Style.label(self, "Ein Kartenspiel: Duell um das Feld", 22, Style.MUTED, Rect2(84, 156, 600, 30), "left")
|
||||
coins_label = Style.chip(self, "", Rect2(930, 20, 310, 38))
|
||||
coll_label = Style.label(self, "", 15, Style.MUTED, Rect2(700, 64, 540, 24), "right")
|
||||
var y := 230
|
||||
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
||||
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
||||
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
|
||||
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 24)
|
||||
y += 74
|
||||
y += 18
|
||||
var x := 80
|
||||
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
|
||||
["Sammlung", func(): app.show_screen(CollectionScreen.new())],
|
||||
["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
|
||||
["Beenden", func(): get_tree().quit()]]:
|
||||
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||
x += 176
|
||||
notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
|
||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
_showcase(p)
|
||||
refresh()
|
||||
|
||||
|
||||
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen.
|
||||
func _resume() -> void:
|
||||
var json := JSON.new()
|
||||
var text := FileAccess.get_file_as_string(app.battle_save_path())
|
||||
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"):
|
||||
app.clear_battle_save()
|
||||
app.show_screen(MenuScreen.new())
|
||||
return
|
||||
app.show_screen(BattleScreen.resume(json.data))
|
||||
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
|
||||
func _showcase(p: Profile) -> void:
|
||||
var cards: Array = Cards.all().duplicate()
|
||||
cards.sort_custom(func(a, b):
|
||||
if p.level(a.id) != p.level(b.id):
|
||||
return p.level(a.id) > p.level(b.id)
|
||||
return hash(a.id) % 97 < hash(b.id) % 97)
|
||||
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
||||
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:
|
||||
|
||||
@@ -1,79 +1,54 @@
|
||||
class_name OpponentSelectScreen
|
||||
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).
|
||||
|
||||
var app
|
||||
var diff_buttons := {}
|
||||
var count_slider: HSlider
|
||||
var count_label: Label
|
||||
var info_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 1000, 50, 12)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 8)
|
||||
Style.header(self, "Einzelspieler", func(): app.show_screen(MenuScreen.new()))
|
||||
var col := Style.column(self, 1000, 170, 12)
|
||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in Profile.DIFFICULTIES:
|
||||
var k: String = key
|
||||
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)
|
||||
Style.spacer(col, 24)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
|
||||
var b := Style.cbutton(br, "Starten", Vector2(260, 56), start, Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 22)
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_difficulty(key: String) -> void:
|
||||
app.profile.settings.difficulty = key
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
func opponents() -> int:
|
||||
return int(count_slider.value)
|
||||
|
||||
|
||||
func start() -> void:
|
||||
app.show_screen(BattleScreen.new(opponents()))
|
||||
app.show_screen(BattleScreen.new())
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
for key in diff_buttons:
|
||||
_mark(diff_buttons[key], key == p.settings.difficulty)
|
||||
var n := opponents()
|
||||
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
||||
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
var d := p.difficulty()
|
||||
info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))]
|
||||
|
||||
|
||||
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
||||
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("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())
|
||||
|
||||
@@ -16,9 +16,8 @@ var speed_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
var col := Style.column(self, 700, 50, 24)
|
||||
Style.clabel(col, "Einstellungen", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 30)
|
||||
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
|
||||
var col := Style.column(self, 700, 110, 24)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 24)
|
||||
@@ -80,7 +79,6 @@ func _ready() -> void:
|
||||
var r := Style.row(col, 16)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
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.",
|
||||
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
|
||||
_labels()
|
||||
@@ -135,6 +133,7 @@ func _on_speed(v: float) -> void:
|
||||
|
||||
func _on_anim(on: bool) -> void:
|
||||
app.profile.settings.animations = on
|
||||
Style.animations = on
|
||||
app.save()
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ func _ready() -> void:
|
||||
streams["damage"] = _tone(140, 0.12, "square")
|
||||
streams["heal"] = _tone(523, 0.22, "sine", 784)
|
||||
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["lose"] = _tone(320, 0.7, "square", 90)
|
||||
for i in 4:
|
||||
|
||||
@@ -4,6 +4,7 @@ class_name Style
|
||||
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
||||
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
||||
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 PANEL := Color("282b36")
|
||||
@@ -17,6 +18,11 @@ const BLUE := Color("6eaaf0")
|
||||
const YELLOW := Color("ffe65a")
|
||||
const ORANGE := Color("ff963c")
|
||||
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“.
|
||||
@@ -80,7 +86,11 @@ static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8,
|
||||
var p := Panel.new()
|
||||
p.position = rect.position
|
||||
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
|
||||
parent.add_child(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("hover", box(lighter(color), 8, MUTED, 2))
|
||||
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
|
||||
juice(b)
|
||||
parent.add_child(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).
|
||||
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
|
||||
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)
|
||||
|
||||
|
||||
## 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:
|
||||
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:
|
||||
@@ -246,6 +308,88 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
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.
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
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)))
|
||||
|
||||
|
||||
# --- 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) --------------------------
|
||||
|
||||
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 col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||
col.a = 0.95
|
||||
col.a = alpha
|
||||
var elems: Array = card.elements
|
||||
if elems.size() == 1:
|
||||
_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
|
||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||
col2.a = 0.95
|
||||
col2.a = alpha
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
@@ -321,7 +516,7 @@ static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
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)])
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 max_dt := 0.0
|
||||
@@ -14,7 +14,7 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
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)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
@@ -23,7 +23,7 @@ func _ready() -> void:
|
||||
var t0 := Time.get_ticks_usec()
|
||||
for i in 30:
|
||||
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
|
||||
measuring = true
|
||||
var start := Time.get_ticks_msec()
|
||||
|
||||
@@ -38,7 +38,7 @@ func _ready() -> void:
|
||||
net.host()
|
||||
net.lobby_changed.connect(func():
|
||||
if net.game == null and net.names.size() >= 2:
|
||||
net.start_battle(0))
|
||||
net.start_battle())
|
||||
net.action_applied.connect(func():
|
||||
net.broadcast()
|
||||
_act_host.call_deferred())
|
||||
|
||||
@@ -15,12 +15,9 @@ func _initialize() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 3
|
||||
print("Ladenangebot: %.2f ms" % ms(func(): DeckGen.shop_cards(r, 5), 50))
|
||||
print("Vorschau 1..5 Gegner: %.1f ms" % ms(func():
|
||||
for n in range(1, 6):
|
||||
for i in n:
|
||||
DeckGen.random_opponent(i + 1)))
|
||||
for n in [1, 3, 5]:
|
||||
var g := Game.new_battle(n, r)
|
||||
print("Gegner erzeugen: %.2f ms" % ms(func(): DeckGen.random_opponent(), 20))
|
||||
for n in [1]:
|
||||
var g := Game.new_battle(r)
|
||||
# ein volles Brett für alle, damit die Entscheidung teuer wird
|
||||
var uid := 5000
|
||||
for p in g.players:
|
||||
@@ -41,7 +38,7 @@ func _initialize() -> void:
|
||||
var t_budget := ms(func(): AI.choose_action(g, h))
|
||||
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("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("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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends SceneTree
|
||||
## 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
|
||||
|
||||
|
||||
@@ -9,25 +9,37 @@ func _initialize() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 0
|
||||
var t0 := Time.get_ticks_msec()
|
||||
print("Alle gierig (Erwartung etwa 1/(Gegner+1)):")
|
||||
for n in range(1, Game.MAX_OPPONENTS + 1):
|
||||
_run(n, games, r, true, true)
|
||||
print("Beide gierig (Erwartung etwa 50 %):")
|
||||
_run(games, r, true, true)
|
||||
print("Du mit Vorausschau, Gegner gierig:")
|
||||
for n in [1, 2, 3]:
|
||||
_run(n, games / 2, r, false, true)
|
||||
print("Alle mit Vorausschau:")
|
||||
_run(1, games / 2, r, false, false)
|
||||
_run(2, games / 4, r, false, false)
|
||||
_run(games / 2, r, false, true)
|
||||
print("Beide mit Vorausschau:")
|
||||
_run(games / 2, r, false, false)
|
||||
print("Kampagne (du mit Vorausschau), je Stufe:")
|
||||
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))
|
||||
quit()
|
||||
|
||||
|
||||
func _run(n_opp: int, games: int, r: RandomNumberGenerator,
|
||||
human_greedy: bool, opp_greedy: bool) -> void:
|
||||
func _campaign(stage: int, games: int, r: RandomNumberGenerator) -> 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 turns := 0
|
||||
for i in games:
|
||||
var g := Game.new_battle(n_opp, r)
|
||||
var g := Game.new_battle(r)
|
||||
while g.winner == "":
|
||||
var p := g.current()
|
||||
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":
|
||||
wins += 1
|
||||
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])
|
||||
|
||||
@@ -18,25 +18,23 @@ func C(id: String) -> Cards.Card:
|
||||
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 opps := []
|
||||
for i in n_opp:
|
||||
opps.append(Game.Player.new("Gegner %d" % (i + 1), false, 15))
|
||||
var o := Game.Player.new("Gegner", false, 15)
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 0
|
||||
var g := Game.new(human, opps, r)
|
||||
var g := Game.new(human, o, r)
|
||||
g.turn = 1
|
||||
human.max_energy = 10
|
||||
human.energy = 10
|
||||
for cid in human_hand:
|
||||
human.hand.append(C(cid))
|
||||
for o in opps:
|
||||
o.max_energy = 10
|
||||
o.energy = 10
|
||||
for cid in opp_hand:
|
||||
o.hand.append(C(cid))
|
||||
return [g, human, opps]
|
||||
o.max_energy = 10
|
||||
o.energy = 10
|
||||
for cid in opp_hand:
|
||||
o.hand.append(C(cid))
|
||||
return [g, human, o]
|
||||
|
||||
|
||||
var next_uid := 9000
|
||||
@@ -60,7 +58,6 @@ func _initialize() -> void:
|
||||
test_wesen_spielen()
|
||||
test_flink()
|
||||
test_waechter()
|
||||
test_waechter_nur_eigener_besitzer()
|
||||
test_kampf_beidseitig()
|
||||
test_schild()
|
||||
test_doppelschlag()
|
||||
@@ -76,15 +73,13 @@ func _initialize() -> void:
|
||||
test_mana_effekt()
|
||||
test_gleiche_regeln()
|
||||
test_zugreihenfolge()
|
||||
test_tote_gegner()
|
||||
test_jeder_gegen_jeden()
|
||||
test_gegnerzahl()
|
||||
test_sieg_niederlage()
|
||||
test_booster()
|
||||
test_spielstand()
|
||||
test_resonanz()
|
||||
test_elementvorteil()
|
||||
test_feld()
|
||||
test_vorschau()
|
||||
test_verschmelzen()
|
||||
test_feldkombos()
|
||||
test_schild_effekt_und_sonne()
|
||||
@@ -141,7 +136,7 @@ func test_flink() -> void:
|
||||
var s := setup(["l_falke"])
|
||||
var g: Game = s[0]
|
||||
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"))
|
||||
check(g.attack(h.board[0], o) == "", "Flink greift sofort an")
|
||||
check(o.life == 12, "Falke 3 Schaden")
|
||||
@@ -151,7 +146,7 @@ func test_waechter() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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 w := put(o, "e_wall")
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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 d := put(o, "w_reiter")
|
||||
g.attack(a, d)
|
||||
@@ -184,7 +170,7 @@ func test_schild() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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 a := put(h, "f_asche")
|
||||
g.attack(a, sh)
|
||||
@@ -198,7 +184,7 @@ func test_doppelschlag() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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")
|
||||
g.attack(a, o)
|
||||
g.attack(a, o)
|
||||
@@ -210,7 +196,7 @@ func test_einfrieren() -> void:
|
||||
var s := setup([], ["w_frost"])
|
||||
var g: Game = s[0]
|
||||
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")
|
||||
g.end_turn()
|
||||
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 g: Game = s[0]
|
||||
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")
|
||||
g.play_card(h, C("f_ritter"), o)
|
||||
check(o.life == 14 and h.board.size() == 2, "Ritter macht 1 Schaden an den Helden")
|
||||
|
||||
|
||||
func test_flaechenschaden() -> void:
|
||||
var s := setup(["ff_phoenix"], [], 2)
|
||||
var s := setup(["ff_phoenix"])
|
||||
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]
|
||||
put(o1, "f_kobold")
|
||||
put(o2, "e_golem")
|
||||
var o: Game.Player = s[2]
|
||||
put(o, "f_kobold")
|
||||
put(o, "e_golem")
|
||||
g.play_card(h, C("ff_phoenix"))
|
||||
check(o1.life == 11 and o2.life == 11, "Helden 4 Schaden")
|
||||
check(o1.board.is_empty() and o2.board[0].health == 1, "Wesen getroffen")
|
||||
check(o.life == 11, "Held 4 Schaden")
|
||||
check(o.board.size() == 1 and o.board[0].health == 1, "Wesen getroffen, Kobold tot")
|
||||
|
||||
|
||||
func test_zurueck_auf_hand() -> void:
|
||||
var s := setup(["wl_nebel"])
|
||||
var g: Game = s[0]
|
||||
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")
|
||||
g.play_card(h, C("wl_nebel"), d)
|
||||
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.
|
||||
func test_grenzfaelle() -> void:
|
||||
var s := setup(["f_wolf"], [], 2)
|
||||
var s := setup(["f_wolf"])
|
||||
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]
|
||||
var o1: Game.Player = s[2]
|
||||
# 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(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
|
||||
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(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")
|
||||
# Verschmolzene Karte auf der Hand übersteht den Spielstand
|
||||
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,
|
||||
## ausgeschiedener Netzwerkspieler mitten im eigenen Zug.
|
||||
func test_grenzfaelle_2() -> void:
|
||||
var s := setup([], [], 2)
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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
|
||||
var wall := put(o1, "e_wall") # Erde 0/4, Erde schlägt Wasser
|
||||
var reiter := put(h, "w_reiter") # Wasser 3/3
|
||||
@@ -347,10 +331,10 @@ func test_grenzfaelle_2() -> void:
|
||||
net.game = g
|
||||
net.rejoin_seconds = 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")
|
||||
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()
|
||||
|
||||
|
||||
@@ -421,7 +405,7 @@ func test_mana_effekt() -> void:
|
||||
func test_gleiche_regeln() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 0
|
||||
var g := Game.new_battle(3, r)
|
||||
var g := Game.new_battle(r)
|
||||
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(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()
|
||||
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")
|
||||
for i in 4:
|
||||
for i in 2:
|
||||
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:
|
||||
@@ -442,9 +426,9 @@ func test_zugreihenfolge() -> void:
|
||||
for seed in 12:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = seed
|
||||
var g := Game.new_battle(2, r)
|
||||
var g := Game.new_battle(r)
|
||||
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():
|
||||
check(g.players[i].order == i, "Reihenfolge nummeriert")
|
||||
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())
|
||||
|
||||
|
||||
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:
|
||||
var s := setup([], [], 2)
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
g.damage(s[2][0], 99)
|
||||
g.damage(s[2], 14)
|
||||
g.check_end()
|
||||
check(g.winner == "", "noch offen")
|
||||
g.damage(s[2][1], 99)
|
||||
g.damage(s[2], 1)
|
||||
g.check_end()
|
||||
check(g.winner == "human", "Sieg")
|
||||
check(g.winner == "human" and g.winner_player == s[1], "Sieg")
|
||||
var s2 := setup()
|
||||
var g2: Game = s2[0]
|
||||
g2.damage(s2[1], 30)
|
||||
@@ -562,7 +503,7 @@ func test_ki_lethal() -> void:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2][0]
|
||||
var o: Game.Player = s[2]
|
||||
o.life = 3
|
||||
var a := put(h, "f_asche")
|
||||
put(o, "w_reiter")
|
||||
@@ -573,23 +514,20 @@ func test_ki_lethal() -> void:
|
||||
func test_ki_spiele_enden() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 7
|
||||
for n in [1, 3, 5]:
|
||||
for i in 6:
|
||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
||||
Game.shuffle(pool, r)
|
||||
var g := Game.new_battle(n, r)
|
||||
var guard := 0
|
||||
while g.winner == "" and guard < 300:
|
||||
AI.take_turn(g, g.current(), i % 2 == 0)
|
||||
guard += 1
|
||||
check(g.winner != "", "Spiel endet (%d Gegner)" % n)
|
||||
for i in 12:
|
||||
var g := Game.new_battle(r)
|
||||
var guard := 0
|
||||
while g.winner == "" and guard < 300:
|
||||
AI.take_turn(g, g.current(), i % 2 == 0)
|
||||
guard += 1
|
||||
check(g.winner != "", "Spiel endet (Partie %d)" % i)
|
||||
|
||||
|
||||
func test_resonanz() -> void:
|
||||
var s := setup(["f_wolf", "f_kobold", "e_wall"])
|
||||
var g: Game = s[0]
|
||||
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"))
|
||||
check(h.board[0].power() == 3, "allein keine Resonanz")
|
||||
g.play_card(h, C("f_kobold"))
|
||||
@@ -621,7 +559,7 @@ func test_elementvorteil() -> void:
|
||||
var s := setup(["f_ritter"])
|
||||
var g: Game = s[0]
|
||||
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 his := put(o, "f_wolf") # Feuer 3/2
|
||||
g.attack(mine, his)
|
||||
@@ -633,7 +571,7 @@ func test_elementvorteil() -> void:
|
||||
s = setup(["f_ritter"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
o = s[2][0]
|
||||
o = s[2]
|
||||
var air := put(o, "l_laeufer") # Luft 3/4, Feuer schlägt Luft
|
||||
g.play_card(h, C("f_ritter"), air)
|
||||
check(air.health == 2, "Feuereffekt gegen Luft: 1 Schaden mehr")
|
||||
@@ -644,7 +582,7 @@ func test_feld() -> void:
|
||||
var s := setup(["f_ritter"])
|
||||
var g: Game = s[0]
|
||||
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")
|
||||
var wolf := put(h, "f_wolf")
|
||||
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.
|
||||
## 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:
|
||||
var s := setup()
|
||||
var g: Game = s[0]
|
||||
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 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")
|
||||
@@ -719,7 +680,7 @@ func test_feldkombos() -> void:
|
||||
var s := setup(["w_reiter"])
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2][0]
|
||||
var o: Game.Player = s[2]
|
||||
put(h, "f_wolf")
|
||||
var weak := put(o, "f_kobold")
|
||||
var strong := put(o, "e_troll")
|
||||
@@ -730,20 +691,19 @@ func test_feldkombos() -> void:
|
||||
s = setup(["e_golem"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
o = s[2][0]
|
||||
o = s[2]
|
||||
put(h, "f_wolf")
|
||||
var k := put(o, "f_kobold")
|
||||
var big := put(o, "e_troll")
|
||||
g.play_card(h, C("e_golem"))
|
||||
check(k in o.board and big.health == 5, "Lava trifft nur das stärkste Wesen")
|
||||
# Blitz: 2 Schaden an den schwächsten Gegner
|
||||
s = setup(["l_wind"], [], 2)
|
||||
# Blitz: 2 Schaden an den Gegner
|
||||
s = setup(["l_wind"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
put(h, "f_wolf")
|
||||
s[2][1].life = 5
|
||||
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
|
||||
s = setup(["e_wall", "l_wind", "w_reiter"])
|
||||
g = s[0]
|
||||
@@ -773,7 +733,7 @@ func test_feldkombos() -> void:
|
||||
s = setup(["fe_golem"])
|
||||
g = s[0]
|
||||
h = s[1]
|
||||
o = s[2][0]
|
||||
o = s[2]
|
||||
put(h, "w_reiter")
|
||||
var t := put(o, "e_troll")
|
||||
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 g: Game = s[0]
|
||||
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")
|
||||
g.play_card(h, C("el_duenen"), a)
|
||||
check(a.shield, "Dünenläufer gibt Schild")
|
||||
@@ -792,7 +752,7 @@ func test_schild_effekt_und_sonne() -> void:
|
||||
s = setup(["fff_sonne"])
|
||||
g = s[0]
|
||||
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")
|
||||
var troll := put(o, "e_troll")
|
||||
g.play_card(h, C("fff_sonne"))
|
||||
@@ -803,7 +763,7 @@ func test_kopie() -> void:
|
||||
var s := setup(["f_wolf"])
|
||||
var g: Game = s[0]
|
||||
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 g2 := g.clone()
|
||||
var a2 := g2.find_creature(a.uid)
|
||||
@@ -823,7 +783,7 @@ func test_ki_besser_als_gierig() -> void:
|
||||
for i in games:
|
||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
||||
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
|
||||
var smart_is_human := i % 2 == 0
|
||||
var guard := 0
|
||||
@@ -855,22 +815,22 @@ func test_kampagne() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 4
|
||||
check(Campaign.STAGES.size() == 10, "zehn Stufen")
|
||||
var opps := Campaign.opponents(4)
|
||||
check(opps.size() == 3 and opps[0].name == "Vulkar" and opps[0].title == "Herr der Lava", "Stufe 5: Vulkar mit 2 Begleitern")
|
||||
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")
|
||||
var g := Game.new_battle_with(opps, r)
|
||||
check(g.opponents.size() == 3 and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
|
||||
var boss := Campaign.opponent(4)
|
||||
check(boss.name == "Vulkar" and boss.title == "Herr der Lava" and boss.life == 32, "Stufe 5: Vulkar mit 32 Leben")
|
||||
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(boss, r)
|
||||
check(g.opponent == boss and g.human.life == Game.START_LIFE, "Kampagnenkampf gestartet")
|
||||
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")
|
||||
reward = p.record_battle(true, 1, 0)
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN[1], "Wiederholung gibt normale Belohnung")
|
||||
reward = p.record_battle(false, 2, 1)
|
||||
reward = p.record_battle(true, 0)
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_WIN, "Wiederholung gibt normale Belohnung")
|
||||
reward = p.record_battle(false, 1)
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
||||
p.campaign_stage = 9
|
||||
p.record_battle(true, 5, 9)
|
||||
p.record_battle(true, 9)
|
||||
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
|
||||
while sim.winner == "" and guard < 300:
|
||||
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 g: Game = s[0]
|
||||
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"]
|
||||
g.play_card(h, C("f_wolf"))
|
||||
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)
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 2
|
||||
var mara: Game.Player = Campaign.opponents(1)[0]
|
||||
check(mara.life == 35 and mara.perks == ["leben_35"], "Mara hat 35 Leben")
|
||||
var g3 := Game.new_battle_with(Campaign.opponents(9), r, true)
|
||||
var archi: Game.Player = g3.opponents[0]
|
||||
var mara: Game.Player = Game.new_battle_with(Campaign.opponent(1), r).opponent
|
||||
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.opponent(9), r, true)
|
||||
var archi: Game.Player = g3.opponent
|
||||
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
|
||||
g3.end_turn()
|
||||
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 g4 := Game.new_battle_with([nyx], r, true)
|
||||
var nyx: Game.Player = Campaign.opponent(5)
|
||||
var g4 := Game.new_battle_with(nyx, r, true)
|
||||
g4.end_turn()
|
||||
# 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")
|
||||
@@ -952,7 +912,7 @@ func test_ki_zwei_schritte() -> void:
|
||||
var s := setup(["el_koenig"])
|
||||
var g: Game = s[0]
|
||||
var h: Game.Player = s[1]
|
||||
var o: Game.Player = s[2][0]
|
||||
var o: Game.Player = s[2]
|
||||
h.turns = 2
|
||||
o.life = 5
|
||||
put(h, "f_wolf", false)
|
||||
@@ -966,10 +926,10 @@ func test_ki_zwei_schritte() -> 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 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"))
|
||||
put(o, "e_troll")
|
||||
var hi := g.players.find(h)
|
||||
@@ -979,7 +939,7 @@ func test_snapshot() -> void:
|
||||
var snap := g.snapshot(hi)
|
||||
var json: Dictionary = JSON.parse_string(JSON.stringify(snap))
|
||||
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[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")
|
||||
@@ -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))
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 8
|
||||
var opps := Campaign.opponents(19)
|
||||
check(opps.size() == 5 and opps[0].name == "Archimagier" and opps[0].perks.size() == 3, "letzte Stufe: Archimagier mit drei Merkmalen, voller Tisch")
|
||||
var archi2 := Campaign.opponent(19)
|
||||
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()
|
||||
check(not p.perk_unlocked(), "Merkmalwahl erst nach Kapitel 1")
|
||||
p.campaign_stage = 10
|
||||
check(p.perk_unlocked(), "nach Kapitel 1 freigeschaltet")
|
||||
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")
|
||||
p.campaign_stage = 19
|
||||
p.record_battle(true, 5, 19)
|
||||
p.record_battle(true, 19)
|
||||
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
||||
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")
|
||||
@@ -1021,7 +981,7 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
|
||||
func test_ki_zeitbudget() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 12
|
||||
var g := Game.new_battle(5, r)
|
||||
var g := Game.new_battle(r)
|
||||
var h: Game.Player = g.players[0]
|
||||
h.energy = 10
|
||||
h.max_energy = 10
|
||||
@@ -1058,7 +1018,7 @@ func test_englische_karten() -> void:
|
||||
func test_kampf_speichern() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 7
|
||||
var g := Game.new_battle(2, r)
|
||||
var g := Game.new_battle(r)
|
||||
g.start()
|
||||
AI.take_turn(g, g.current())
|
||||
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:
|
||||
AI.take_turn(g2, g2.current())
|
||||
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)
|
||||
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")
|
||||
|
||||
|
||||
148
tests/ui_test.gd
148
tests/ui_test.gd
@@ -26,6 +26,7 @@ func frames(n: int = 2) -> void:
|
||||
func shot(name: String) -> void:
|
||||
if shots == "":
|
||||
return
|
||||
await frames(10) # Einblenden und Austeilen abwarten
|
||||
await RenderingServer.frame_post_draw
|
||||
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"]:
|
||||
if FileAccess.file_exists(SAVE + 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.save_path = SAVE
|
||||
add_child(main)
|
||||
await frames(3)
|
||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||
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_kampf_fortsetzen]
|
||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung, test_kampf_feld_und_verschmelzen, test_kampf_grenzen]
|
||||
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.
|
||||
# Sie laufen nur auf Wunsch: DINGER_NETZ=1 (oder gezielt über DINGER_ONLY).
|
||||
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)
|
||||
check(scr.selected == cr, "Wesen gewählt")
|
||||
var targets := []
|
||||
for o in g.opponents:
|
||||
for o in [g.opponent]:
|
||||
if o.alive() and g.can_attack(cr, o)[0]:
|
||||
targets.append(o)
|
||||
for c in o.board:
|
||||
@@ -267,11 +265,9 @@ func human_turn(scr) -> void:
|
||||
await click(scr.b_end)
|
||||
|
||||
|
||||
func start_battle(n_opp: int) -> void:
|
||||
func start_battle() -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||
screen().count_slider.value = n_opp
|
||||
await frames(1)
|
||||
await click(find_button("Starten"))
|
||||
|
||||
|
||||
@@ -279,51 +275,33 @@ func test_gegnerauswahl() -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
var sel = screen()
|
||||
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("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")
|
||||
check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
|
||||
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(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")
|
||||
sel.count_slider.value = 1
|
||||
await frames(1)
|
||||
await click(find_button("Starten"))
|
||||
var hard = screen()
|
||||
check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet")
|
||||
var opp: Game.Player = hard.game.opponents[0]
|
||||
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
|
||||
var opp: Game.Player = hard.game.opponent
|
||||
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
|
||||
check(hard.game.human.shop.size() == Game.SHOP_SIZE and hard.game.human.hand.is_empty(),
|
||||
"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 click(find_button("Einzelspieler"))
|
||||
await click(find_button("Leicht"))
|
||||
await click(find_button("Starten"))
|
||||
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(easy.game.players[0].max_energy == 1 and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
|
||||
await give_up()
|
||||
await click(find_button("Einzelspieler"))
|
||||
await click(find_button("Normal"))
|
||||
await click(find_button("Zurück"))
|
||||
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.
|
||||
@@ -348,32 +326,6 @@ func give_up() -> void:
|
||||
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:
|
||||
await click(find_button("Einstellungen"))
|
||||
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")
|
||||
await click(scr.b_upgrade)
|
||||
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")
|
||||
var dust: int = p.dust
|
||||
await click(scr.b_dismantle)
|
||||
@@ -491,7 +451,7 @@ func test_sprache() -> void:
|
||||
|
||||
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
|
||||
func test_kampf_steuerung() -> void:
|
||||
await start_battle(1)
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampf gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
@@ -551,7 +511,7 @@ func test_kampf_steuerung() -> void:
|
||||
|
||||
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
|
||||
func test_kampf_grenzen() -> void:
|
||||
await start_battle(1)
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
scr.ai_delay = 0.0
|
||||
await wait_for_human(scr)
|
||||
@@ -603,26 +563,45 @@ func test_kampf_grenzen() -> void:
|
||||
|
||||
## Feldanzeige und Verschmelzen im Kampf.
|
||||
func test_kampf_feld_und_verschmelzen() -> void:
|
||||
await start_battle(1)
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampf gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
await wait_for_human(scr)
|
||||
var g: Game = scr.game
|
||||
g.human.board.clear()
|
||||
for id in ["f_wolf", "w_reiter"]:
|
||||
var cr := Game.Creature.new(Cards.by_id(id), g.human, g.next_uid)
|
||||
var put := func(p: Game.Player, id: String) -> Game.Creature:
|
||||
var cr := Game.Creature.new(Cards.by_id(id), p, g.next_uid)
|
||||
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.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:
|
||||
g.charge_field(Cards.by_id("f_kobold"))
|
||||
scr.refresh()
|
||||
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(scr.hand_views[0].bonus == 2 and scr.hand_views[0].tag == "Dampf", "die Handkarte zeigt +2 (Resonanz und Feld) und die Kombo Dampf")
|
||||
check(scr.b_fuse.visible and scr.b_fuse.text == "Verschmelzen", "Verschmelzen wird angeboten")
|
||||
await shot("23_feld")
|
||||
await hover(scr.hand_views[0])
|
||||
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)
|
||||
check(scr.fusing and scr.b_fuse.text == "Abbrechen", "Verschmelzen ausgewählt")
|
||||
await click(scr.own_views[0])
|
||||
@@ -834,13 +813,15 @@ func test_kampagne() -> void:
|
||||
var scr = screen()
|
||||
check(scr is CampaignScreen, "Kampagne geöffnet")
|
||||
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")
|
||||
await click(find_button("Kämpfen"))
|
||||
var battle = screen()
|
||||
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.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.name == "Fenn", "Fenn ist der Gegner")
|
||||
check(battle.game.opponent.perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
||||
await shot("16_kampagne_kampf")
|
||||
await give_up()
|
||||
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
|
||||
@@ -854,7 +835,7 @@ func test_kampagne() -> void:
|
||||
await shot("27_kampagne_kapitel2")
|
||||
await click(find_button("Kämpfen"))
|
||||
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")
|
||||
await give_up()
|
||||
main.profile.campaign_stage = 0
|
||||
@@ -862,13 +843,13 @@ func test_kampagne() -> void:
|
||||
main.save()
|
||||
|
||||
|
||||
func test_kampf(n_opp: int, shot_turn: int) -> void:
|
||||
await start_battle(n_opp)
|
||||
func test_kampf(shot_turn: int) -> void:
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen, "Kampfbildschirm")
|
||||
scr.ai_delay = 0.0
|
||||
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)
|
||||
check(g.current() == g.human, "Spieler ist dran")
|
||||
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:
|
||||
await start_battle(5)
|
||||
await start_battle()
|
||||
var scr = screen()
|
||||
scr.ai_delay = 0.0
|
||||
var g: Game = scr.game
|
||||
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
|
||||
g.human.energy = 10
|
||||
var wolf := Game.Creature.new(Cards.by_id("f_wolf"), g.human, 9000)
|
||||
@@ -913,7 +894,7 @@ func test_kampf_abbrechen() -> void:
|
||||
scr.refresh()
|
||||
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_reiter")[0]
|
||||
check(reiter_view.rarity == "episch", "Handkarte trägt die eigene Stufe")
|
||||
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)
|
||||
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")
|
||||
@@ -930,11 +911,10 @@ func test_kampf_abbrechen() -> void:
|
||||
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())
|
||||
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
|
||||
await click(find_button("Lupe"))
|
||||
check(scr.zoom_player == scr.others()[0], "Lupe öffnet die erste Spalte")
|
||||
await shot("12_lupe")
|
||||
await click(find_button("Schließen"))
|
||||
check(scr.zoom_player == null, "Lupe geschlossen")
|
||||
await click(scr.b_log)
|
||||
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
|
||||
await key(KEY_ESCAPE)
|
||||
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
|
||||
# Runden laufen lassen, damit die Gegner ihre Züge machen
|
||||
for round in 4:
|
||||
await click(scr.b_end)
|
||||
@@ -946,4 +926,4 @@ func test_kampf_abbrechen() -> void:
|
||||
and g.log.any(func(l): return " greift " in l)
|
||||
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")
|
||||
await shot("10_kampf_fuenf")
|
||||
await shot("10_kampf_runden")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
keys,en
|
||||
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
|
||||
Kampagne,Campaign
|
||||
Netzwerkspiel,Network game
|
||||
@@ -62,18 +62,14 @@ Normal,Normal
|
||||
Schwer,Hard
|
||||
Profil zurücksetzen,Reset profile
|
||||
Große Schrift,Large text
|
||||
Duell,Duel
|
||||
%d am Tisch,%d at the table
|
||||
%d Münzen,%d coins
|
||||
Abbrechen,Cancel
|
||||
"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 Ziel,%s damage to a target
|
||||
%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 alle Gegner und ihre Wesen,%s damage to all opponents and their creatures
|
||||
%s Schaden an den Gegner,%s damage to the opponent
|
||||
%s Schaden an den Gegner und seine Wesen,%s damage to the opponent and their creatures
|
||||
Heile dich um %s,Heal yourself for %s
|
||||
Erhalte 1 Mana,Gain 1 mana
|
||||
Erhalte %s Mana,Gain %s mana
|
||||
@@ -117,14 +113,10 @@ Zuschauen,Watch
|
||||
Zuschauer,Spectator
|
||||
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
||||
Starten,Start
|
||||
1 Gegner,1 opponent
|
||||
%d Gegner,%d opponents
|
||||
KI-Gegner,AI opponents
|
||||
Sammlung,Collection
|
||||
Verschmelzen,Fuse
|
||||
Zwei eigene Wesen wählen,Pick two of your creatures
|
||||
Zweites Wesen wählen,Pick the second creature
|
||||
Feld: neutral,Field: neutral
|
||||
Zwei verschiedene Wesen wählen,Pick two different creatures
|
||||
Nur eigene Wesen,Only your own creatures
|
||||
Vulkan,Volcano
|
||||
@@ -150,10 +142,26 @@ Gewöhnliche Karten lassen sich nicht zerlegen,Common cards cannot be dismantled
|
||||
Aufgewertet,Upgraded
|
||||
+%d Staub,+%d dust
|
||||
+%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
|
||||
Kein Angebot,No offer
|
||||
Hand voll,Hand full
|
||||
Profil wirklich zurücksetzen?,Really reset your profile?
|
||||
"Sammlung, Münzen und Fortschritt gehen verloren.","Collection, coins and progress are lost."
|
||||
Chimäre,Chimera
|
||||
Angriff,attack
|
||||
Eis,Ice
|
||||
Resonanz +%d,Resonance +%d
|
||||
Feld +%d,Field +%d
|
||||
Merkmal +%d,Perk +%d
|
||||
Feldkombo %s mit %s: %s,Field combo %s with %s: %s
|
||||
1 Schaden an das stärkste gegnerische Wesen,1 damage to the strongest enemy creature
|
||||
2 Schaden an den Gegner,2 damage to the opponent
|
||||
Heile dich um 3,Heal yourself for 3
|
||||
Das neue Wesen erhält +1/+1,The new creature gets +1/+1
|
||||
%d Leben,%d life
|
||||
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
|
||||
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user