Compare commits
4 Commits
5c1084fd79
...
8944fdd017
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8944fdd017 | ||
|
|
89db101137 | ||
|
|
43073351b0 | ||
|
|
0f2dc22ef6 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
|||||||
sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1
|
sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libgl1 libegl1
|
||||||
- name: Import
|
- name: Import
|
||||||
run: ./godot --headless --path . --import || true
|
run: ./godot --headless --path . --import || true
|
||||||
- name: Fenster-Test (voll, mit Netzwerk und Hotseat)
|
- name: Fenster-Test (voll)
|
||||||
run: xvfb-run -a --server-args="-screen 0 1280x800x24" ./godot --path . tests/ui_test.tscn 2>&1 | tee fenster.log | grep -E "FEHLER|Prüfungen" || true
|
run: xvfb-run -a --server-args="-screen 0 1280x800x24" ./godot --path . tests/ui_test.tscn 2>&1 | tee fenster.log | grep -E "FEHLER|Prüfungen" || true
|
||||||
- name: Ergebnis
|
- name: Ergebnis
|
||||||
run: grep -q " 0 fehlgeschlagen" fenster.log
|
run: grep -q " 0 fehlgeschlagen" fenster.log
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -28,46 +28,39 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- 100 Karten in `data/karten.json`: 4 Elemente, 10 Zweier-Kombos (auch gleiche Elemente), 4 mythische Dreifach-Kombos, 4 Fallen
|
- 100 Karten in `data/karten.json`: 4 Elemente, 10 Zweier-Kombos (auch gleiche Elemente), 4 mythische Dreifach-Kombos, 4 Fallen
|
||||||
- Resonanz: gleiche Elemente auf dem Feld verstärken sich. Feldkombos: verschiedene Elemente lösen beim Ausspielen Effekte aus
|
- Resonanz: gleiche Elemente auf dem Feld verstärken sich. Feldkombos: verschiedene Elemente lösen beim Ausspielen Effekte aus
|
||||||
- Fallen liegen verdeckt und lösen im gegnerischen Zug aus (die KI kennt fremde Fallen nicht)
|
- Fallen liegen verdeckt und lösen im gegnerischen Zug aus (die KI kennt fremde Fallen nicht)
|
||||||
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern, Erfolge, Tagesaufgabe
|
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern je Kapitel, Tagesaufgabe
|
||||||
- Lokal zu mehreren: bis zu 4 Menschen am selben Rechner plus KI
|
- Booster, fünf Deck-Slots, Sammlung (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
|
||||||
- Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
|
|
||||||
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
|
||||||
- Anleitung (geführte erste Partie), Mulligan im ersten Zug, Tastatur (Leertaste, 1 bis 9, Esc, Enter)
|
|
||||||
- Kampagnen-Bosse mit Merkmalen, Kartenstatistik, zwölf Tagesaufgaben, Pity-Zähler für mythische Karten
|
- Kampagnen-Bosse mit Merkmalen, Kartenstatistik, zwölf Tagesaufgaben, Pity-Zähler für mythische Karten
|
||||||
- Netzwerkspiel im lokalen Netz (Host führt das Spiel, Mitspieler sehen nur ihre Hand)
|
- Netzwerkspiel im lokalen Netz (Host führt das Spiel, Mitspieler sehen nur ihre Hand)
|
||||||
- Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch
|
- Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch
|
||||||
- Geführte Anleitung mit Hervorhebungen, Rückgängig für Fehlklicks, Überspringen von Gegnerzügen
|
|
||||||
- Zwei Kampagnenkapitel (Bosse mit ein bis drei Merkmalen), wählbares eigenes Merkmal nach Kapitel 1
|
- Zwei Kampagnenkapitel (Bosse mit ein bis drei Merkmalen), wählbares eigenes Merkmal nach Kapitel 1
|
||||||
- Wöchentliche Herausforderung mit Regelvarianten, Bestenliste (Serie, schnellster Sieg), Statistik-Export als CSV
|
- Bestenliste (Serie, schnellster Sieg) im Hintergrund: sie steuert den Schwierigkeitstipp
|
||||||
- Zehn Fallenkarten, Netzwerk mit Wiederverbinden (Platz bleibt 60 s frei)
|
- Zehn Fallenkarten, Netzwerk mit Wiederverbinden (Platz bleibt 60 s frei)
|
||||||
- Deckstärke aus Seltenheit, Schlüsselwörtern und Effekten; Gegnerdecks werden auf die eigene Stärke gebaut
|
- Deckstärke aus Seltenheit, Schlüsselwörtern und Effekten; Gegnerdecks werden auf die eigene Stärke gebaut
|
||||||
- Schwierigkeit Leicht/Normal/Schwer: Zielstärke 0,9/1,0/1,2-fach, schwache oder starke Archetypen, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
- Schwierigkeit Leicht/Normal/Schwer: Zielstärke 0,9/1,0/1,2-fach, schwache oder starke Archetypen, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
|
||||||
- Gegnervorschau vor dem Kampf (Element und Stärke, neu würfelbar), Vorschlag zur Schwierigkeit nach drei Siegen oder Niederlagen
|
- Gegnervorschau vor dem Kampf (Element und Stärke, neu würfelbar), Vorschlag zur Schwierigkeit nach drei Siegen oder Niederlagen
|
||||||
- Deckstärke mit Kurvenabzug und Gewichtung durch die eigene Kartenstatistik; Booster schlagen passende Karten fürs Deck vor
|
- Deckstärke mit Kurvenabzug und Gewichtung durch die eigene Kartenstatistik; Booster schlagen passende Karten fürs Deck vor
|
||||||
- Resonanz gedeckelt (+1, +2, +3, +3), günstige Wasser-Antworten (Ebbe, Tiefenstoß, Strudel), Wasser-Wesen angehoben, Gischtläufer für Sturm
|
- Resonanz gedeckelt (+1, +2, +3, +3), günstige Wasser-Antworten (Ebbe, Tiefenstoß, Strudel), Wasser-Wesen angehoben, Gischtläufer für Sturm
|
||||||
- Anleitung Teil 2 (Mulligan, Fallen, Feldkombo Dampf), Gamepad-Steuerung, große Schrift, Profil zurücksetzen
|
|
||||||
- Gegnervorschau mit höchstens drei Neuwürfen, Schwierigkeitstipp nach Spielverlauf (deutliche Siege oder drei Niederlagen)
|
- Gegnervorschau mit höchstens drei Neuwürfen, Schwierigkeitstipp nach Spielverlauf (deutliche Siege oder drei Niederlagen)
|
||||||
- KI mit Zeitbudget je Entscheidung (150 ms), Warnung bei ungespeichertem Deck
|
- KI mit Zeitbudget je Entscheidung (150 ms), Warnung bei ungespeichertem Deck
|
||||||
- Lava trifft nur noch das stärkste gegnerische Wesen; Archetypen-Werte kommen aus `data/archetypen.json` (Messlauf schreibt sie)
|
- Lava trifft nur noch das stärkste gegnerische Wesen; Archetypen-Werte kommen aus `data/archetypen.json` (Messlauf schreibt sie)
|
||||||
- Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle
|
|
||||||
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
|
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
|
||||||
- KI-Bewertung mit Bedrohung (gegnerische Angriffskraft ohne eigenen Wächter, tödliche besonders) und Führenden-Bremse; im Duell 62:38 gegen die alte Bewertung
|
- KI-Bewertung mit Bedrohung (gegnerische Angriffskraft ohne eigenen Wächter, tödliche besonders) und Führenden-Bremse; im Duell 62:38 gegen die alte Bewertung
|
||||||
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
||||||
- Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
- Gamepad im Deckbau, in der Sammlung und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
||||||
- Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`)
|
- Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`)
|
||||||
- Deck-Codes zum Teilen (`DINGER1:id*anzahl,…`) mit Zwischenablage im Deckbau
|
- Deck-Codes zum Teilen (`DINGER1:id*anzahl,…`) mit Zwischenablage im Deckbau
|
||||||
- Kapitel 3 der Kampagne (drei Merkmale, volle Tische), acht neue Karten (je Elementpaar eine, zwei Fallen), Elementbooster für 120 Münzen
|
- Kapitel 3 der Kampagne (drei Merkmale, volle Tische), acht neue Karten (je Elementpaar eine, zwei Fallen), Elementbooster für 120 Münzen
|
||||||
- Netzwerk: fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung (er bekommt laufend den vollen Spielstand), die anderen verbinden neu; Zuschauer sehen die Partie ohne Hände
|
- Netzwerk: fällt der Host aus, übernimmt der Mitspieler mit der kleinsten Kennung (er bekommt laufend den vollen Spielstand), die anderen verbinden neu; Zuschauer sehen die Partie ohne Hände
|
||||||
- Balance in drei Messrunden (je 200 Partien pro Archetyp): Sand gibt +1/+1 statt Schild, Wasser und Feuer zäher, Lava-Karten leicht schwächer; Spanne jetzt 40–64 % statt 36–78 % (Feuer+Erde bleibt mit 64 % die Spitze)
|
- Balance in drei Messrunden (je 200 Partien pro Archetyp): Sand gibt +1/+1 statt Schild, Wasser und Feuer zäher, Lava-Karten leicht schwächer; Spanne jetzt 40–64 % statt 36–78 % (Feuer+Erde bleibt mit 64 % die Spitze)
|
||||||
- Große Schrift: Kartennamen neben dem Kostenkreis, Kartentext läuft nicht mehr über, breitere Knöpfe in Booster, Deckbau und Werkstatt
|
- Große Schrift: Kartennamen neben dem Kostenkreis, Kartentext läuft nicht mehr über, breitere Knöpfe in Booster, Deckbau und Sammlung
|
||||||
- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
|
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
|
||||||
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test und Archetypen-Messung nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
|
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test und Archetypen-Messung nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
|
||||||
`DINGER_FAST=1` lässt den Fenster-Test ohne Hotseat, Netzwerk und lange Kämpfe laufen (etwa ein Drittel der Zeit).
|
`DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit).
|
||||||
Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren (macht start.sh auch)
|
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren (macht start.sh auch)
|
||||||
@@ -76,7 +69,8 @@ godot --headless --path . -s tests/simulate.gd # Balance: KI spielt gegen K
|
|||||||
godot --headless --path . -s tests/matchup.gd # Startvorteil und Mono gegen Paar
|
godot --headless --path . -s tests/matchup.gd # Startvorteil und Mono gegen Paar
|
||||||
godot --headless --path . -s tests/strength_probe.gd # Stärke der Elementkombinationen messen (Werte in DeckGen.ARCHETYPE_STRENGTH)
|
godot --headless --path . -s tests/strength_probe.gd # Stärke der Elementkombinationen messen (Werte in DeckGen.ARCHETYPE_STRENGTH)
|
||||||
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
DINGER_SHOTS=/tmp/shots godot --path . tests/ui_test.tscn # Oberfläche mit Fenster und Screenshots
|
||||||
DINGER_ONLY=test_host_wechsel,test_zuschauer godot --path . tests/ui_test.tscn # nur einzelne Oberflächentests
|
DINGER_ONLY=test_deckbau 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)
|
||||||
N=200 godot --headless --path . -s tests/strength_probe.gd # Messlauf mit 200 statt 60 Partien je Archetyp
|
N=200 godot --headless --path . -s tests/strength_probe.gd # Messlauf mit 200 statt 60 Partien je Archetyp
|
||||||
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Deckerzeugung, Musik
|
godot --headless --path . -s tests/perf_probe.gd # Laufzeiten: KI-Entscheidung, Deckerzeugung, Musik
|
||||||
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
godot --path . tests/frame_probe.tscn # Bildzeiten im Kampf mit 5 Gegnern
|
||||||
@@ -94,14 +88,12 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
|||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
||||||
- `scripts/game.gd` – Spielregeln, Züge, Kampf, Fallen, Feldkombos, Resonanz, Deckregeln, Hotseat
|
- `scripts/game.gd` – Spielregeln, Züge, Kampf, Fallen, Feldkombos, Resonanz, Deckregeln
|
||||||
- `scripts/deckgen.gd` – Deckstärke, Gegnerdecks entlang der Energiekurve, Archetypen
|
- `scripts/deckgen.gd` – Deckstärke, Gegnerdecks entlang der Energiekurve, Archetypen
|
||||||
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
|
||||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||||
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Erfolge, Tagesaufgabe, Einstellungen
|
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Tagesaufgabe, Einstellungen
|
||||||
- `scripts/campaign.gd` – Kampagnenstufen
|
- `scripts/campaign.gd` – Kampagnenstufen
|
||||||
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton
|
|
||||||
- `scenes/main.tscn` – Startszene
|
- `scenes/main.tscn` – Startszene
|
||||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||||
- `scripts/challenge.gd` – wöchentliche Herausforderungen
|
|
||||||
- `translations/ui.csv` – englische Oberflächentexte
|
- `translations/ui.csv` – englische Oberflächentexte
|
||||||
|
|||||||
@@ -5,11 +5,10 @@
|
|||||||
- PC-Spiel
|
- PC-Spiel
|
||||||
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
||||||
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
||||||
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
- Kampagne gegen benannte Magier und Tagesaufgaben als Fortschritt
|
||||||
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich, auch als Zuschauer
|
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
|
||||||
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
||||||
- Geführte erste Partie als Einstieg
|
- Bestenliste als Langzeitziel
|
||||||
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel
|
|
||||||
|
|
||||||
## Sammeln
|
## Sammeln
|
||||||
- Booster öffnen
|
- Booster öffnen
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
class_name Challenge
|
|
||||||
## Wöchentliche Herausforderung: feste Regelvariante, einmal pro Woche belohnt.
|
|
||||||
|
|
||||||
const LIST := [
|
|
||||||
{"id": "eile", "name": "Eile", "text": "Alle beginnen mit 3 Energie und 20 Leben, drei Gegner.", "n_opp": 3,
|
|
||||||
"rules": {"everyone_perk": "start_energie", "start_life": 20}, "reward": 200},
|
|
||||||
{"id": "resonanz", "name": "Resonanzsturm", "text": "Resonanz zählt für alle doppelt, zwei Gegner.", "n_opp": 2,
|
|
||||||
"rules": {"everyone_perk": "resonanz_doppelt"}, "reward": 180},
|
|
||||||
{"id": "flink", "name": "Blitzkrieg", "text": "Alle Wesen sind flink, vier Gegner.", "n_opp": 4,
|
|
||||||
"rules": {"everyone_perk": "alle_flink"}, "reward": 250},
|
|
||||||
{"id": "schild", "name": "Panzerschlacht", "text": "Alle Wesen kommen mit Schild, Duell mit 40 Leben.", "n_opp": 1,
|
|
||||||
"rules": {"everyone_perk": "schild_wesen", "start_life": 40}, "reward": 150},
|
|
||||||
{"id": "karten", "name": "Kartenflut", "text": "Jeder zieht 2 Karten pro Zug, fünf Gegner am Tisch.", "n_opp": 5,
|
|
||||||
"rules": {"everyone_perk": "extra_karten"}, "reward": 300},
|
|
||||||
{"id": "mono", "name": "Reine Lehre", "text": "Nur Decks mit einem Element sind erlaubt, drei Gegner.", "n_opp": 3,
|
|
||||||
"rules": {"mono_only": true}, "reward": 220},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
static func week() -> int:
|
|
||||||
var t := Profile.today()
|
|
||||||
return int(Time.get_unix_time_from_datetime_string(t + "T00:00:00") / 604800)
|
|
||||||
|
|
||||||
|
|
||||||
static func current() -> Dictionary:
|
|
||||||
return LIST[week() % LIST.size()]
|
|
||||||
|
|
||||||
|
|
||||||
## Prüft, ob das Deck zur Herausforderung passt. Gibt "" oder eine Fehlermeldung zurück.
|
|
||||||
static func deck_allowed(ch: Dictionary, deck: Array) -> String:
|
|
||||||
if ch.rules.get("mono_only", false) and Game.deck_elements(deck).size() != 1:
|
|
||||||
return "Diese Woche sind nur Decks mit einem Element erlaubt"
|
|
||||||
return ""
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://n5h6q6kfoo34
|
|
||||||
@@ -156,7 +156,7 @@ class Player extends RefCounted:
|
|||||||
|
|
||||||
|
|
||||||
var human: Player # der (erste) menschliche Spieler
|
var human: Player # der (erste) menschliche Spieler
|
||||||
var humans: Array = [] # alle menschlichen Spieler (Hotseat: mehrere)
|
var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere)
|
||||||
var opponents: Array = [] # alle außer dem ersten Menschen
|
var opponents: Array = [] # alle außer dem ersten Menschen
|
||||||
var players: Array = [] # in Zugreihenfolge
|
var players: Array = [] # in Zugreihenfolge
|
||||||
var winner_player: Player = null
|
var winner_player: Player = null
|
||||||
@@ -746,19 +746,3 @@ static func new_battle_with(deck: Array, opps: Array, r: RandomNumberGenerator =
|
|||||||
p.max_life = 35
|
p.max_life = 35
|
||||||
g.start(fixed_order)
|
g.start(fixed_order)
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
## Hotseat: mehrere Menschen am selben Rechner, dazu KI-Gegner.
|
|
||||||
static func new_hotseat(human_decks: Array, n_ai: int, r: RandomNumberGenerator = null) -> Game:
|
|
||||||
assert(human_decks.size() >= 1 and human_decks.size() + n_ai <= MAX_OPPONENTS + 1, "höchstens 6 am Tisch")
|
|
||||||
if r == null:
|
|
||||||
r = make_rng()
|
|
||||||
var first := Player.new("Spieler 1", human_decks[0], true, START_LIFE)
|
|
||||||
var others := []
|
|
||||||
for i in range(1, human_decks.size()):
|
|
||||||
others.append(Player.new("Spieler %d" % (i + 1), human_decks[i], true, START_LIFE))
|
|
||||||
for i in n_ai:
|
|
||||||
others.append(random_opponent(r, i + 1))
|
|
||||||
var g := Game.new(first, others, r)
|
|
||||||
g.start()
|
|
||||||
return g
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class_name Profile
|
class_name Profile
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Statistik, Erfolge, Tagesaufgabe, Einstellungen.
|
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Kartenstatistik (für Deckstärke und Schwierigkeit), Tagesaufgabe, Einstellungen.
|
||||||
|
|
||||||
const SAVE_VERSION := 5
|
const SAVE_VERSION := 5
|
||||||
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
|
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
|
||||||
@@ -20,27 +20,10 @@ const DIFFICULTIES := {
|
|||||||
"normal": {"name": "Normal", "factor": 1.0, "reward": 1.0, "smart": true, "tier": ""},
|
"normal": {"name": "Normal", "factor": 1.0, "reward": 1.0, "smart": true, "tier": ""},
|
||||||
"schwer": {"name": "Schwer", "factor": 1.2, "reward": 1.5, "smart": true, "tier": "schwer"},
|
"schwer": {"name": "Schwer", "factor": 1.2, "reward": 1.5, "smart": true, "tier": "schwer"},
|
||||||
}
|
}
|
||||||
# Werkstatt: Zerlegen bringt Staub, Herstellen kostet Staub.
|
# Sammlung: Zerlegen bringt Staub, Herstellen kostet Staub.
|
||||||
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600}
|
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600}
|
||||||
const DUST_CRAFT := {"gewöhnlich": 40, "selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200}
|
const DUST_CRAFT := {"gewöhnlich": 40, "selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200}
|
||||||
|
|
||||||
const ACHIEVEMENTS := [
|
|
||||||
{"id": "erster_sieg", "name": "Erster Sieg", "text": "Gewinne einen Kampf.", "reward": 50},
|
|
||||||
{"id": "duellant", "name": "Duellant", "text": "Gewinne ein Duell.", "reward": 50},
|
|
||||||
{"id": "tischherr", "name": "Herr des Tisches", "text": "Gewinne gegen 3 oder mehr Gegner.", "reward": 100},
|
|
||||||
{"id": "chaosmeister", "name": "Chaosmeister", "text": "Gewinne gegen 5 Gegner.", "reward": 200},
|
|
||||||
{"id": "sammler", "name": "Sammler", "text": "Besitze 50 verschiedene Karten.", "reward": 100},
|
|
||||||
{"id": "vollstaendig", "name": "Vollständig", "text": "Besitze jede Karte.", "reward": 500},
|
|
||||||
{"id": "boosterfreund", "name": "Boosterfreund", "text": "Öffne 10 Booster.", "reward": 100},
|
|
||||||
{"id": "elementarist", "name": "Elementarist", "text": "Gewinne mit einem Deck aus nur einem Element.", "reward": 100},
|
|
||||||
{"id": "kombokoenig", "name": "Kombokönig", "text": "Gewinne mit mindestens 6 Kombokarten im Deck.", "reward": 100},
|
|
||||||
{"id": "kampagnenheld", "name": "Kampagnenheld", "text": "Schließe Kapitel 1 der Kampagne ab.", "reward": 300},
|
|
||||||
{"id": "elementmeister", "name": "Meister der Elemente", "text": "Schließe Kapitel 2 der Kampagne ab.", "reward": 500},
|
|
||||||
{"id": "archimagier", "name": "Archimagier", "text": "Schließe Kapitel 3 der Kampagne ab.", "reward": 800},
|
|
||||||
{"id": "serie", "name": "Siegesserie", "text": "Gewinne fünf Kämpfe in Folge.", "reward": 150},
|
|
||||||
{"id": "wochenheld", "name": "Wochenheld", "text": "Bestehe eine wöchentliche Herausforderung.", "reward": 100},
|
|
||||||
]
|
|
||||||
|
|
||||||
const DAILY_TASKS := [
|
const DAILY_TASKS := [
|
||||||
{"id": "sieg", "kind": "win", "text": "Gewinne einen Kampf", "goal": 1, "reward": 80},
|
{"id": "sieg", "kind": "win", "text": "Gewinne einen Kampf", "goal": 1, "reward": 80},
|
||||||
{"id": "kaempfe", "kind": "game", "text": "Bestreite zwei Kämpfe", "goal": 2, "reward": 60},
|
{"id": "kaempfe", "kind": "game", "text": "Bestreite zwei Kämpfe", "goal": 2, "reward": 60},
|
||||||
@@ -64,18 +47,14 @@ var collection := {} # id -> Anzahl
|
|||||||
var decks: Array = [] # DECK_SLOTS Einträge {"name": String, "cards": Array von ids}
|
var decks: Array = [] # DECK_SLOTS Einträge {"name": String, "cards": Array von ids}
|
||||||
var active_deck := 0
|
var active_deck := 0
|
||||||
var stats := {"games": 0, "wins": 0, "boosters": 0, "wins_by_opp": {}}
|
var stats := {"games": 0, "wins": 0, "boosters": 0, "wins_by_opp": {}}
|
||||||
var achievements: Array = []
|
|
||||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||||
var daily := {"date": "", "task": "", "progress": 0, "done": false}
|
var daily := {"date": "", "task": "", "progress": 0, "done": false}
|
||||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Erfolge, Tagesaufgabe)
|
var notices: Array[String] = [] # Meldungen für die Oberfläche (Tagesaufgabe, Fortschritt)
|
||||||
var card_stats := {} # id -> {"played", "games", "wins"}
|
var card_stats := {} # id -> {"played", "games", "wins"}
|
||||||
var booster_since_mythic := 0
|
var booster_since_mythic := 0
|
||||||
var tutorial_done := false
|
|
||||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||||
var tutorial2_done := false
|
|
||||||
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0, "margins": []}
|
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0, "margins": []}
|
||||||
var challenge_week := -1 # Woche der zuletzt bestandenen Herausforderung
|
|
||||||
|
|
||||||
## Das aktive Deck (Kartenliste des gewählten Slots).
|
## Das aktive Deck (Kartenliste des gewählten Slots).
|
||||||
var deck: Array:
|
var deck: Array:
|
||||||
@@ -153,8 +132,6 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
p.stats.boosters = int(st.get("boosters", 0))
|
p.stats.boosters = int(st.get("boosters", 0))
|
||||||
for k in st.get("wins_by_opp", {}):
|
for k in st.get("wins_by_opp", {}):
|
||||||
p.stats.wins_by_opp[str(k)] = int(st.wins_by_opp[k])
|
p.stats.wins_by_opp[str(k)] = int(st.wins_by_opp[k])
|
||||||
for a in d.get("achievements", []):
|
|
||||||
p.achievements.append(str(a))
|
|
||||||
p.campaign_stage = int(d.get("campaign_stage", 0))
|
p.campaign_stage = int(d.get("campaign_stage", 0))
|
||||||
var dl = d.get("daily", {})
|
var dl = d.get("daily", {})
|
||||||
p.daily = {"date": str(dl.get("date", "")), "task": str(dl.get("task", "")),
|
p.daily = {"date": str(dl.get("date", "")), "task": str(dl.get("task", "")),
|
||||||
@@ -172,7 +149,6 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
var st = d.card_stats[cid]
|
var st = d.card_stats[cid]
|
||||||
p.card_stats[cid] = {"played": int(st.get("played", 0)), "games": int(st.get("games", 0)), "wins": int(st.get("wins", 0))}
|
p.card_stats[cid] = {"played": int(st.get("played", 0)), "games": int(st.get("games", 0)), "wins": int(st.get("wins", 0))}
|
||||||
p.booster_since_mythic = int(d.get("booster_since_mythic", 0))
|
p.booster_since_mythic = int(d.get("booster_since_mythic", 0))
|
||||||
p.tutorial_done = bool(d.get("tutorial_done", false))
|
|
||||||
if version >= 5:
|
if version >= 5:
|
||||||
p.perk = str(d.get("perk", ""))
|
p.perk = str(d.get("perk", ""))
|
||||||
if not Game.PERKS.has(p.perk):
|
if not Game.PERKS.has(p.perk):
|
||||||
@@ -181,18 +157,16 @@ static func from_dict(d: Dictionary) -> Profile:
|
|||||||
p.records = {"streak": int(rc.get("streak", 0)), "best_streak": int(rc.get("best_streak", 0)),
|
p.records = {"streak": int(rc.get("streak", 0)), "best_streak": int(rc.get("best_streak", 0)),
|
||||||
"fastest_win": int(rc.get("fastest_win", 0)), "loss_streak": int(rc.get("loss_streak", 0)),
|
"fastest_win": int(rc.get("fastest_win", 0)), "loss_streak": int(rc.get("loss_streak", 0)),
|
||||||
"margins": Array(rc.get("margins", [])).map(func(m): return int(m))}
|
"margins": Array(rc.get("margins", [])).map(func(m): return int(m))}
|
||||||
p.challenge_week = int(d.get("challenge_week", -1))
|
|
||||||
p.tutorial2_done = bool(d.get("tutorial2_done", false))
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
func to_dict() -> Dictionary:
|
||||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
||||||
"decks": decks, "active_deck": active_deck,
|
"decks": decks, "active_deck": active_deck,
|
||||||
"stats": stats, "achievements": achievements, "campaign_stage": campaign_stage,
|
"stats": stats, "campaign_stage": campaign_stage,
|
||||||
"daily": daily, "settings": settings, "card_stats": card_stats,
|
"daily": daily, "settings": settings, "card_stats": card_stats,
|
||||||
"booster_since_mythic": booster_since_mythic, "tutorial_done": tutorial_done,
|
"booster_since_mythic": booster_since_mythic,
|
||||||
"perk": perk, "records": records, "challenge_week": challenge_week, "tutorial2_done": tutorial2_done}
|
"perk": perk, "records": records}
|
||||||
|
|
||||||
|
|
||||||
## Speichert und sichert den vorherigen Stand als .bak.
|
## Speichert und sichert den vorherigen Stand als .bak.
|
||||||
@@ -274,18 +248,6 @@ func autofill_deck(slot: int) -> String:
|
|||||||
return "" if ok[0] else ok[1]
|
return "" if ok[0] else ok[1]
|
||||||
|
|
||||||
|
|
||||||
## Bestenliste: Karten mit den meisten Siegen (mindestens eine Partie).
|
|
||||||
func top_cards(n: int) -> Array:
|
|
||||||
var rows := []
|
|
||||||
for cid in card_stats:
|
|
||||||
var st: Dictionary = card_stats[cid]
|
|
||||||
if st.games > 0:
|
|
||||||
rows.append({"card": Cards.by_id(cid), "games": st.games, "wins": st.wins, "played": st.played,
|
|
||||||
"rate": float(st.wins) / st.games})
|
|
||||||
rows.sort_custom(func(a, b): return a.wins > b.wins or (a.wins == b.wins and a.rate > b.rate))
|
|
||||||
return rows.slice(0, n)
|
|
||||||
|
|
||||||
|
|
||||||
func owned_cards() -> Array:
|
func owned_cards() -> Array:
|
||||||
return Cards.all().filter(func(c): return collection.get(c.id, 0) > 0)
|
return Cards.all().filter(func(c): return collection.get(c.id, 0) > 0)
|
||||||
|
|
||||||
@@ -298,7 +260,7 @@ func distinct_owned() -> int:
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
# --- Werkstatt --------------------------------------------------------------------
|
# --- Zerlegen und Herstellen ------------------------------------------------------
|
||||||
|
|
||||||
## Zerlegt n Kopien in Staub. Gibt "" zurück oder eine Fehlermeldung.
|
## Zerlegt n Kopien in Staub. Gibt "" zurück oder eine Fehlermeldung.
|
||||||
func dismantle(cid: String, n: int = 1) -> String:
|
func dismantle(cid: String, n: int = 1) -> String:
|
||||||
@@ -323,7 +285,6 @@ func craft(cid: String) -> String:
|
|||||||
dust -= cost
|
dust -= cost
|
||||||
collection[cid] = collection.get(cid, 0) + 1
|
collection[cid] = collection.get(cid, 0) + 1
|
||||||
daily_event("craft")
|
daily_event("craft")
|
||||||
check_achievements()
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -407,17 +368,16 @@ func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Arra
|
|||||||
collection[c.id] = collection.get(c.id, 0) + 1
|
collection[c.id] = collection.get(c.id, 0) + 1
|
||||||
stats.boosters += 1
|
stats.boosters += 1
|
||||||
daily_event("booster")
|
daily_event("booster")
|
||||||
check_achievements()
|
|
||||||
return cards
|
return cards
|
||||||
|
|
||||||
|
|
||||||
# --- Kampfergebnis, Erfolge, Tagesaufgabe -------------------------------------------
|
# --- Kampfergebnis, Tagesaufgabe -------------------------------------------
|
||||||
|
|
||||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||||
## turns: Dauer der Partie für die Bestenliste. challenge: Herausforderung dieser Woche.
|
## turns: Dauer der Partie für die Bestenliste.
|
||||||
func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, played_ids: Array = [],
|
func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, played_ids: Array = [],
|
||||||
turns: int = 0, challenge: Dictionary = {}, reward_factor: float = 1.0, remaining_life: int = -1) -> int:
|
turns: int = 0, reward_factor: float = 1.0, remaining_life: int = -1) -> int:
|
||||||
stats.games += 1
|
stats.games += 1
|
||||||
records.margins.append(maxi(remaining_life, 0) if won else 0)
|
records.margins.append(maxi(remaining_life, 0) if won else 0)
|
||||||
while records.margins.size() > 5:
|
while records.margins.size() > 5:
|
||||||
@@ -432,7 +392,7 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
|
|||||||
records.streak = 0
|
records.streak = 0
|
||||||
records.loss_streak += 1
|
records.loss_streak += 1
|
||||||
var tip := suggest_difficulty()
|
var tip := suggest_difficulty()
|
||||||
if tip != "" and stage < 0 and challenge.is_empty():
|
if tip != "" and stage < 0:
|
||||||
notices.append("Tipp: %s. Schwierigkeit %s?" % [
|
notices.append("Tipp: %s. Schwierigkeit %s?" % [
|
||||||
"%d Siege in Folge" % records.streak if won else "%d Niederlagen in Folge" % records.loss_streak, DIFFICULTIES[tip].name])
|
"%d Siege in Folge" % records.streak if won else "%d Niederlagen in Folge" % records.loss_streak, DIFFICULTIES[tip].name])
|
||||||
for cid in played_ids:
|
for cid in played_ids:
|
||||||
@@ -454,10 +414,6 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
|
|||||||
campaign_stage += 1
|
campaign_stage += 1
|
||||||
reward = Campaign.stage(stage).reward
|
reward = Campaign.stage(stage).reward
|
||||||
notices.append("Kampagne: %s besiegt!" % Campaign.stage(stage).name)
|
notices.append("Kampagne: %s besiegt!" % Campaign.stage(stage).name)
|
||||||
if not challenge.is_empty() and challenge_week != Challenge.week():
|
|
||||||
challenge_week = Challenge.week()
|
|
||||||
reward = challenge.reward
|
|
||||||
notices.append("Herausforderung bestanden: %s (+%d Münzen)" % [challenge.name, challenge.reward])
|
|
||||||
coins += reward
|
coins += reward
|
||||||
daily_event("game")
|
daily_event("game")
|
||||||
if won:
|
if won:
|
||||||
@@ -466,7 +422,6 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
|
|||||||
daily_event("win_vs3")
|
daily_event("win_vs3")
|
||||||
for e in Game.deck_elements(deck_used):
|
for e in Game.deck_elements(deck_used):
|
||||||
daily_event("win_element:" + e)
|
daily_event("win_element:" + e)
|
||||||
check_achievements({"won": won, "n_opp": n_opp, "deck": deck_used})
|
|
||||||
return reward
|
return reward
|
||||||
|
|
||||||
|
|
||||||
@@ -476,55 +431,6 @@ func _stat(cid: String) -> Dictionary:
|
|||||||
return card_stats[cid]
|
return card_stats[cid]
|
||||||
|
|
||||||
|
|
||||||
func check_achievements(ctx: Dictionary = {}) -> Array:
|
|
||||||
var unlocked := []
|
|
||||||
for a in ACHIEVEMENTS:
|
|
||||||
if a.id in achievements:
|
|
||||||
continue
|
|
||||||
if _achieved(a.id, ctx):
|
|
||||||
achievements.append(a.id)
|
|
||||||
coins += a.reward
|
|
||||||
unlocked.append(a)
|
|
||||||
notices.append("Erfolg: %s (+%d Münzen)" % [a.name, a.reward])
|
|
||||||
return unlocked
|
|
||||||
|
|
||||||
|
|
||||||
func _achieved(id: String, ctx: Dictionary) -> bool:
|
|
||||||
var won: bool = ctx.get("won", false)
|
|
||||||
var n: int = ctx.get("n_opp", 0)
|
|
||||||
var deck_used: Array = ctx.get("deck", [])
|
|
||||||
match id:
|
|
||||||
"erster_sieg":
|
|
||||||
return stats.wins >= 1
|
|
||||||
"duellant":
|
|
||||||
return won and n == 1
|
|
||||||
"tischherr":
|
|
||||||
return won and n >= 3
|
|
||||||
"chaosmeister":
|
|
||||||
return won and n == 5
|
|
||||||
"sammler":
|
|
||||||
return distinct_owned() >= 50
|
|
||||||
"vollstaendig":
|
|
||||||
return distinct_owned() >= Cards.all().size()
|
|
||||||
"boosterfreund":
|
|
||||||
return stats.boosters >= 10
|
|
||||||
"elementarist":
|
|
||||||
return won and Game.deck_elements(deck_used).size() == 1
|
|
||||||
"kombokoenig":
|
|
||||||
return won and deck_used.filter(func(c): return c.is_combo()).size() >= 6
|
|
||||||
"kampagnenheld":
|
|
||||||
return campaign_stage >= Campaign.STAGES.size()
|
|
||||||
"elementmeister":
|
|
||||||
return campaign_stage >= 2 * Campaign.STAGES.size()
|
|
||||||
"archimagier":
|
|
||||||
return campaign_stage >= Campaign.total()
|
|
||||||
"serie":
|
|
||||||
return records.best_streak >= 5
|
|
||||||
"wochenheld":
|
|
||||||
return challenge_week >= 0
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func difficulty() -> Dictionary:
|
func difficulty() -> Dictionary:
|
||||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||||
|
|
||||||
@@ -606,26 +512,6 @@ func perk_unlocked() -> bool:
|
|||||||
return campaign_stage >= Campaign.STAGES.size()
|
return campaign_stage >= Campaign.STAGES.size()
|
||||||
|
|
||||||
|
|
||||||
## Schreibt Statistik als CSV (Kartenwerte und Gesamtzahlen), z. B. für Spieltests.
|
|
||||||
func export_stats(path: String) -> String:
|
|
||||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
if f == null:
|
|
||||||
return "Datei nicht schreibbar"
|
|
||||||
f.store_line("karte;seltenheit;gespielt;partien;siege")
|
|
||||||
for cid in card_stats:
|
|
||||||
var st: Dictionary = card_stats[cid]
|
|
||||||
var c := Cards.by_id(cid)
|
|
||||||
f.store_line("%s;%s;%d;%d;%d" % [c.name, c.rarity, st.played, st.games, st.wins])
|
|
||||||
f.store_line("")
|
|
||||||
f.store_line("kaempfe;%d" % stats.games)
|
|
||||||
f.store_line("siege;%d" % stats.wins)
|
|
||||||
f.store_line("booster;%d" % stats.boosters)
|
|
||||||
f.store_line("beste_serie;%d" % records.best_streak)
|
|
||||||
f.store_line("schnellster_sieg;%d" % records.fastest_win)
|
|
||||||
f.close()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
static func today() -> String:
|
static func today() -> String:
|
||||||
return today_override if today_override != "" else Time.get_date_string_from_system()
|
return today_override if today_override != "" else Time.get_date_string_from_system()
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
class_name AchievementsScreen
|
|
||||||
extends Control
|
|
||||||
|
|
||||||
var app
|
|
||||||
var export_label: Label
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var p: Profile = app.profile
|
|
||||||
var col := Style.column(self, 900, 12, 4)
|
|
||||||
Style.clabel(col, "Erfolge", 32, Style.GOLD, "center", true)
|
|
||||||
var st: Dictionary = p.stats
|
|
||||||
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
|
|
||||||
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.total()], 16, Style.MUTED)
|
|
||||||
var task := p.daily_task()
|
|
||||||
var daily_text: String = ("Tagesaufgabe erledigt: %s ✓" % task.text) if p.daily.done else \
|
|
||||||
"Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
|
|
||||||
Style.clabel(col, daily_text, 16, Style.BLUE)
|
|
||||||
Style.clabel(col, "Serie: %d (beste %d) · Schnellster Sieg: %s" % [p.records.streak, p.records.best_streak,
|
|
||||||
("%d Züge" % p.records.fastest_win) if p.records.fastest_win > 0 else "–"], 15, Style.MUTED)
|
|
||||||
Style.spacer(col, 4)
|
|
||||||
for a in Profile.ACHIEVEMENTS:
|
|
||||||
var done: bool = a.id in p.achievements
|
|
||||||
var panel := PanelContainer.new()
|
|
||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
|
|
||||||
col.add_child(panel)
|
|
||||||
var r := Style.row(panel, 12)
|
|
||||||
r.custom_minimum_size = Vector2(0, 36)
|
|
||||||
Style.clabel(r, "✓" if done else "·", 22, Style.GREEN if done else Style.MUTED, "center", true, 40)
|
|
||||||
Style.clabel(r, a.name, 18, Style.TEXT if done else Style.MUTED, "left", true, 250)
|
|
||||||
Style.clabel(r, a.text, 15, Style.MUTED, "left", false, 380)
|
|
||||||
Style.clabel(r, "+%d" % a.reward, 16, Style.GOLD, "right", true, 90)
|
|
||||||
var top := p.top_cards(5)
|
|
||||||
if not top.is_empty():
|
|
||||||
Style.spacer(col, 4)
|
|
||||||
var parts: Array[String] = []
|
|
||||||
for row in top:
|
|
||||||
parts.append("%s %d/%d" % [row.card.name, row.wins, row.games])
|
|
||||||
Style.clabel(col, "Beste Karten (Siege/Partien): " + " · ".join(parts), 14, Style.MUTED)
|
|
||||||
export_label = Style.clabel(col, "", 13, Style.GREEN)
|
|
||||||
Style.spacer(col, 2)
|
|
||||||
var br := Style.row(col, 16)
|
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
Style.cbutton(br, "Statistik exportieren", Vector2(220, 40), export_stats)
|
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
|
|
||||||
|
|
||||||
func export_stats() -> void:
|
|
||||||
var path := "user://statistik.csv"
|
|
||||||
var err: String = app.profile.export_stats(path)
|
|
||||||
export_label.text = ("Gespeichert: " + ProjectSettings.globalize_path(path)) if err == "" else err
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cw4rtcukefdl8
|
|
||||||
@@ -64,9 +64,6 @@ func move(dir: int) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func confirm() -> void:
|
func confirm() -> void:
|
||||||
if screen.handover.visible:
|
|
||||||
screen.take_over()
|
|
||||||
return
|
|
||||||
if screen.mulligan.visible:
|
if screen.mulligan.visible:
|
||||||
screen.do_mulligan([])
|
screen.do_mulligan([])
|
||||||
return
|
return
|
||||||
@@ -120,6 +117,19 @@ func confirm() -> void:
|
|||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
|
|
||||||
|
## Pulsierender Rahmen als Zeiger.
|
||||||
|
class Highlight extends Control:
|
||||||
|
var t := 0.0
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
t += delta
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var a := 0.55 + 0.45 * sin(t * 6.0)
|
||||||
|
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
|
||||||
|
|
||||||
|
|
||||||
func draw_cursor() -> void:
|
func draw_cursor() -> void:
|
||||||
Style.clear(screen.cursor_layer)
|
Style.clear(screen.cursor_layer)
|
||||||
if not active or screen.game == null or not screen.my_turn():
|
if not active or screen.game == null or not screen.my_turn():
|
||||||
@@ -130,7 +140,7 @@ func draw_cursor() -> void:
|
|||||||
index = clampi(index, 0, items.size() - 1)
|
index = clampi(index, 0, items.size() - 1)
|
||||||
var item = items[index]
|
var item = items[index]
|
||||||
var r: Rect2 = Rect2(item.slot, item.size) if item is CreatureView else Rect2(item.position, item.size)
|
var r: Rect2 = Rect2(item.slot, item.size) if item is CreatureView else Rect2(item.position, item.size)
|
||||||
var hl := BattleScreen.Highlight.new()
|
var hl := Highlight.new()
|
||||||
hl.position = r.position - Vector2(6, 6)
|
hl.position = r.position - Vector2(6, 6)
|
||||||
hl.size = r.size + Vector2(12, 12)
|
hl.size = r.size + Vector2(12, 12)
|
||||||
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
class_name BattleScreen
|
class_name BattleScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||||
## Modi: Einzelspieler, Kampagne, Anleitung, Hotseat (Perspektivwechsel), Netzwerk (Host oder Mitspieler).
|
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var n_opp: int
|
var n_opp: int
|
||||||
var stage := -1 # Kampagnenstufe oder -1
|
var stage := -1 # Kampagnenstufe oder -1
|
||||||
var tutorial := false
|
|
||||||
var hotseat_decks: Array = []
|
|
||||||
var hotseat_ai := 0
|
|
||||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||||
var net: Net
|
var net: Net
|
||||||
@@ -28,11 +25,6 @@ var ended := false
|
|||||||
var zoom_player = null
|
var zoom_player = null
|
||||||
var preview_timer := 0.0
|
var preview_timer := 0.0
|
||||||
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||||
var guide: BattleTutorial
|
|
||||||
var tutorial_part := 1
|
|
||||||
var tutorial_step: int:
|
|
||||||
get:
|
|
||||||
return guide.step if guide != null else 0
|
|
||||||
var pad: BattleGamepad
|
var pad: BattleGamepad
|
||||||
var gp_zone: String:
|
var gp_zone: String:
|
||||||
get:
|
get:
|
||||||
@@ -40,8 +32,6 @@ var gp_zone: String:
|
|||||||
var gp_index: int:
|
var gp_index: int:
|
||||||
get:
|
get:
|
||||||
return pad.index
|
return pad.index
|
||||||
var challenge := {} # wöchentliche Herausforderung oder leer
|
|
||||||
var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner
|
|
||||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||||
@@ -83,8 +73,6 @@ var reward_label: Label
|
|||||||
var notice_label: Label
|
var notice_label: Label
|
||||||
var confirm: Control
|
var confirm: Control
|
||||||
var b_save_leave: Button
|
var b_save_leave: Button
|
||||||
var handover: Control
|
|
||||||
var handover_label: Label
|
|
||||||
var mulligan: BattleMulligan
|
var mulligan: BattleMulligan
|
||||||
var mulligan_box: Control:
|
var mulligan_box: Control:
|
||||||
get:
|
get:
|
||||||
@@ -102,26 +90,9 @@ var mulligan_selected: Array:
|
|||||||
get:
|
get:
|
||||||
return mulligan.selected
|
return mulligan.selected
|
||||||
var cursor_layer: Control
|
var cursor_layer: Control
|
||||||
var tutorial_panel: Panel
|
|
||||||
var tutorial_text: RichTextLabel
|
|
||||||
var b_tut_next: Button
|
|
||||||
var wait_label: Label
|
var wait_label: Label
|
||||||
var b_skip: Button
|
var b_skip: Button
|
||||||
var b_undo: Button
|
var b_undo: Button
|
||||||
var highlight_layer: Control
|
|
||||||
|
|
||||||
|
|
||||||
## Pulsierender Rahmen für die Anleitung.
|
|
||||||
class Highlight extends Control:
|
|
||||||
var t := 0.0
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
t += delta
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var a := 0.55 + 0.45 * sin(t * 6.0)
|
|
||||||
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
|
|
||||||
|
|
||||||
|
|
||||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||||
@@ -129,13 +100,6 @@ func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
|||||||
stage = campaign_stage
|
stage = campaign_stage
|
||||||
|
|
||||||
|
|
||||||
static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
|
|
||||||
var b := BattleScreen.new(0, -1)
|
|
||||||
b.hotseat_decks = decks
|
|
||||||
b.hotseat_ai = n_ai
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
static func online(seat: int) -> BattleScreen:
|
static func online(seat: int) -> BattleScreen:
|
||||||
var b := BattleScreen.new(0, -1)
|
var b := BattleScreen.new(0, -1)
|
||||||
b.net_play = true
|
b.net_play = true
|
||||||
@@ -143,38 +107,14 @@ static func online(seat: int) -> BattleScreen:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
static func guided(part: int = 1) -> BattleScreen:
|
|
||||||
var b := BattleScreen.new(1, -1)
|
|
||||||
b.tutorial = true
|
|
||||||
b.tutorial_part = part
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
static func against(opps: Array) -> BattleScreen:
|
|
||||||
var b := BattleScreen.new(opps.size(), -1)
|
|
||||||
b.preset_opponents = opps
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
||||||
static func resume(data: Dictionary) -> BattleScreen:
|
static func resume(data: Dictionary) -> BattleScreen:
|
||||||
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
|
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
|
||||||
s.challenge = data.get("challenge", {})
|
|
||||||
s.played_ids = Array(data.get("played_ids", []))
|
s.played_ids = Array(data.get("played_ids", []))
|
||||||
s.saved_game = Game.from_snapshot(data.game)
|
s.saved_game = Game.from_snapshot(data.game)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
static func weekly(ch: Dictionary) -> BattleScreen:
|
|
||||||
var b := BattleScreen.new(int(ch.n_opp), -1)
|
|
||||||
b.challenge = ch
|
|
||||||
return b
|
|
||||||
|
|
||||||
|
|
||||||
func is_hotseat() -> bool:
|
|
||||||
return game != null and game.humans.size() > 1 and not net_play
|
|
||||||
|
|
||||||
|
|
||||||
func is_online() -> bool:
|
func is_online() -> bool:
|
||||||
return net_play
|
return net_play
|
||||||
|
|
||||||
@@ -189,7 +129,7 @@ func is_client() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func counts_for_profile() -> bool:
|
func counts_for_profile() -> bool:
|
||||||
return not is_hotseat() and not is_online()
|
return not is_online()
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -213,24 +153,9 @@ func _ready() -> void:
|
|||||||
return # der erste Zustand kommt vom Host
|
return # der erste Zustand kommt vom Host
|
||||||
elif saved_game != null:
|
elif saved_game != null:
|
||||||
game = saved_game
|
game = saved_game
|
||||||
elif not hotseat_decks.is_empty():
|
|
||||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
|
||||||
elif tutorial:
|
|
||||||
guide = BattleTutorial.new(self, tutorial_part)
|
|
||||||
var r := Game.make_rng()
|
|
||||||
var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards()))
|
|
||||||
opps[0].perks = []
|
|
||||||
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
|
||||||
guide.inject_cards(game)
|
|
||||||
elif not challenge.is_empty():
|
|
||||||
var rules: Dictionary = challenge.rules.duplicate()
|
|
||||||
rules["target_rating"] = Game.deck_rating(app.profile.deck_cards())
|
|
||||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, rules)
|
|
||||||
elif stage >= 0:
|
elif stage >= 0:
|
||||||
var r := Game.make_rng()
|
var r := Game.make_rng()
|
||||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
|
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
|
||||||
elif not preset_opponents.is_empty():
|
|
||||||
game = Game.new_battle_with(app.profile.deck_cards(), preset_opponents, null, false, own_rules())
|
|
||||||
else:
|
else:
|
||||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
||||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||||
@@ -273,22 +198,6 @@ func _build_static() -> void:
|
|||||||
log_box.add_theme_font_size_override("normal_font_size", 13)
|
log_box.add_theme_font_size_override("normal_font_size", 13)
|
||||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(log_box)
|
add_child(log_box)
|
||||||
if tutorial:
|
|
||||||
tutorial_panel = Style.panel(self, Rect2(10, 205, 320, 150), Style.PANEL, 8, Style.GOLD, 2)
|
|
||||||
tutorial_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
||||||
Style.label(self, "Anleitung", 14, Style.GOLD, Rect2(20, 209, 200, 20), "left", true)
|
|
||||||
tutorial_text = RichTextLabel.new()
|
|
||||||
tutorial_text.position = Vector2(20, 230)
|
|
||||||
tutorial_text.size = Vector2(300, 90)
|
|
||||||
tutorial_text.bbcode_enabled = false
|
|
||||||
tutorial_text.scroll_active = false
|
|
||||||
tutorial_text.add_theme_font_size_override("normal_font_size", 13)
|
|
||||||
tutorial_text.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
add_child(tutorial_text)
|
|
||||||
b_tut_next = Style.button(self, "Weiter", Rect2(230, 318, 90, 30), tutorial_next)
|
|
||||||
b_tut_next.add_theme_font_size_override("font_size", 14)
|
|
||||||
log_box.position = Vector2(18, 365)
|
|
||||||
log_box.size = Vector2(304, 173)
|
|
||||||
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
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(950, 205, 320, 380))
|
||||||
preview_hint = Style.label(self, "Karte überfahren für Details", 14, Style.MUTED, Rect2(950, 345, 320, 24), "center")
|
preview_hint = Style.label(self, "Karte überfahren für Details", 14, Style.MUTED, Rect2(950, 345, 320, 24), "center")
|
||||||
@@ -313,7 +222,6 @@ func _build_static() -> void:
|
|||||||
trap_layer = _layer()
|
trap_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
highlight_layer = _layer()
|
|
||||||
zoom_layer = _layer()
|
zoom_layer = _layer()
|
||||||
fx_layer = _layer()
|
fx_layer = _layer()
|
||||||
cursor_layer = _layer()
|
cursor_layer = _layer()
|
||||||
@@ -336,12 +244,6 @@ func _build_static() -> void:
|
|||||||
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
|
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)
|
b_save_leave.add_theme_font_size_override("font_size", 14)
|
||||||
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
||||||
# Übergabe zwischen Menschen (Hotseat)
|
|
||||||
handover = _overlay()
|
|
||||||
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
|
||||||
handover_label = Style.label(handover, "", 30, Style.TEXT, Rect2(340, 310, 600, 44), "center", true)
|
|
||||||
Style.label(handover, "Maus weitergeben, dann weiter. Die Hand des Vorgängers ist verdeckt.", 16, Style.MUTED, Rect2(340, 362, 600, 24), "center")
|
|
||||||
Style.button(handover, "Weiter", Rect2(540, 420, 200, 50), take_over, Color("285a3c"))
|
|
||||||
|
|
||||||
|
|
||||||
func _layer() -> Control:
|
func _layer() -> Control:
|
||||||
@@ -355,15 +257,15 @@ func _overlay() -> Control:
|
|||||||
return Style.overlay(self)
|
return Style.overlay(self)
|
||||||
|
|
||||||
|
|
||||||
## Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) lassen sich unterbrechen und fortsetzen.
|
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
|
||||||
func resumable() -> bool:
|
func resumable() -> bool:
|
||||||
return counts_for_profile() and not tutorial
|
return counts_for_profile()
|
||||||
|
|
||||||
|
|
||||||
func save_battle() -> void:
|
func save_battle() -> void:
|
||||||
if not resumable() or game == null or game.winner != "":
|
if not resumable() or game == null or game.winner != "":
|
||||||
return
|
return
|
||||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "challenge": challenge, "played_ids": played_ids, "game": game.snapshot(-1)}
|
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "played_ids": played_ids, "game": game.snapshot(-1)}
|
||||||
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
||||||
if f != null:
|
if f != null:
|
||||||
f.store_string(JSON.stringify(d))
|
f.store_string(JSON.stringify(d))
|
||||||
@@ -380,12 +282,6 @@ func leave() -> void:
|
|||||||
net.leave()
|
net.leave()
|
||||||
if resumable() and game != null and game.winner == "":
|
if resumable() and game != null and game.winner == "":
|
||||||
app.clear_battle_save() # aufgegeben
|
app.clear_battle_save() # aufgegeben
|
||||||
if tutorial:
|
|
||||||
if tutorial_part == 1:
|
|
||||||
app.profile.tutorial_done = true
|
|
||||||
else:
|
|
||||||
app.profile.tutorial2_done = true
|
|
||||||
app.save()
|
|
||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|
||||||
|
|
||||||
@@ -475,7 +371,7 @@ func my_turn() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func any_overlay() -> bool:
|
func any_overlay() -> bool:
|
||||||
return confirm.visible or handover.visible or mulligan.visible or overlay.visible
|
return confirm.visible or mulligan.visible or overlay.visible
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
@@ -506,14 +402,10 @@ func _input(event: InputEvent) -> void:
|
|||||||
if b_undo.visible:
|
if b_undo.visible:
|
||||||
undo()
|
undo()
|
||||||
KEY_ENTER, KEY_KP_ENTER:
|
KEY_ENTER, KEY_KP_ENTER:
|
||||||
if handover.visible:
|
if mulligan.visible:
|
||||||
take_over()
|
|
||||||
elif mulligan.visible:
|
|
||||||
do_mulligan([])
|
do_mulligan([])
|
||||||
elif overlay.visible:
|
elif overlay.visible:
|
||||||
leave()
|
leave()
|
||||||
elif tutorial and b_tut_next.visible:
|
|
||||||
tutorial_next()
|
|
||||||
_:
|
_:
|
||||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
||||||
var idx: int = event.keycode - KEY_1
|
var idx: int = event.keycode - KEY_1
|
||||||
@@ -552,12 +444,7 @@ func _process(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
var cur := game.current()
|
var cur := game.current()
|
||||||
if cur.is_human:
|
if cur.is_human:
|
||||||
if cur != perspective and is_hotseat() and not handover.visible:
|
if cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible:
|
||||||
handover_label.text = "%s ist dran" % cur.name
|
|
||||||
handover.visible = true
|
|
||||||
hand_layer.visible = false
|
|
||||||
trap_layer.visible = false
|
|
||||||
elif cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible and not handover.visible:
|
|
||||||
show_mulligan()
|
show_mulligan()
|
||||||
return
|
return
|
||||||
if is_client():
|
if is_client():
|
||||||
@@ -606,13 +493,8 @@ func _exit_tree() -> void:
|
|||||||
func _finish() -> void:
|
func _finish() -> void:
|
||||||
var won := game.winner == "human"
|
var won := game.winner == "human"
|
||||||
if counts_for_profile():
|
if counts_for_profile():
|
||||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 and challenge.is_empty() and not tutorial else 1.0
|
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor, game.human.life)
|
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life)
|
||||||
if tutorial:
|
|
||||||
if tutorial_part == 1:
|
|
||||||
app.profile.tutorial_done = true
|
|
||||||
else:
|
|
||||||
app.profile.tutorial2_done = true
|
|
||||||
app.save()
|
app.save()
|
||||||
app.sfx.play("win" if won else "lose")
|
app.sfx.play("win" if won else "lose")
|
||||||
app.clear_battle_save()
|
app.clear_battle_save()
|
||||||
@@ -623,17 +505,6 @@ func _finish() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func take_over() -> void:
|
|
||||||
handover.visible = false
|
|
||||||
hand_layer.visible = true
|
|
||||||
trap_layer.visible = true
|
|
||||||
perspective = game.current()
|
|
||||||
cancel()
|
|
||||||
zoom_player = null
|
|
||||||
build_hero_boxes()
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func say(m: String) -> void:
|
func say(m: String) -> void:
|
||||||
msg_t = 2.5
|
msg_t = 2.5
|
||||||
msg_label.text = m
|
msg_label.text = m
|
||||||
@@ -908,30 +779,6 @@ func _note_events(evs: Array) -> void:
|
|||||||
app.profile.daily_event("combo")
|
app.profile.daily_event("combo")
|
||||||
|
|
||||||
|
|
||||||
# --- Anleitung ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func tutorial_next() -> void:
|
|
||||||
if guide != null and guide.next():
|
|
||||||
app.sfx.play("click")
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _tutorial_refresh() -> void:
|
|
||||||
guide.advance()
|
|
||||||
tutorial_text.text = guide.text()
|
|
||||||
b_tut_next.visible = not guide.finished()
|
|
||||||
|
|
||||||
|
|
||||||
func _tutorial_highlights(mine: bool) -> void:
|
|
||||||
Style.clear(highlight_layer)
|
|
||||||
for r in guide.highlight_rects(mine):
|
|
||||||
var hl := Highlight.new()
|
|
||||||
hl.position = r.position - Vector2(4, 4)
|
|
||||||
hl.size = r.size + Vector2(8, 8)
|
|
||||||
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
highlight_layer.add_child(hl)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
@@ -1084,9 +931,6 @@ func refresh() -> void:
|
|||||||
else:
|
else:
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
log_box.text = "\n".join(lines)
|
log_box.text = "\n".join(lines)
|
||||||
if tutorial:
|
|
||||||
_tutorial_refresh()
|
|
||||||
_tutorial_highlights(mine)
|
|
||||||
pad.draw_cursor()
|
pad.draw_cursor()
|
||||||
|
|
||||||
Style.clear(zoom_layer)
|
Style.clear(zoom_layer)
|
||||||
@@ -1110,7 +954,6 @@ func refresh() -> void:
|
|||||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
||||||
overlay.visible = true
|
overlay.visible = true
|
||||||
confirm.visible = false
|
confirm.visible = false
|
||||||
handover.visible = false
|
|
||||||
mulligan.visible = false
|
mulligan.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
class_name BattleTutorial
|
|
||||||
extends RefCounted
|
|
||||||
## Geführte Partien: Schritte, Bedingungen und Hervorhebungen (ausgelagert aus dem Kampfbildschirm).
|
|
||||||
|
|
||||||
const STEPS1 := [
|
|
||||||
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
|
||||||
"played_creature"],
|
|
||||||
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
|
|
||||||
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
|
|
||||||
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
|
|
||||||
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.", "combo"],
|
|
||||||
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
|
|
||||||
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
|
|
||||||
]
|
|
||||||
const STEPS2 := [
|
|
||||||
["Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.", "mulligan"],
|
|
||||||
["Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.", "trap_set"],
|
|
||||||
["Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.", "trap_fired"],
|
|
||||||
["Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.", "combo_dampf"],
|
|
||||||
["Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!", ""],
|
|
||||||
]
|
|
||||||
|
|
||||||
var screen # BattleScreen
|
|
||||||
var part := 1
|
|
||||||
var step := 0
|
|
||||||
|
|
||||||
|
|
||||||
func _init(s, p: int = 1) -> void:
|
|
||||||
screen = s
|
|
||||||
part = p
|
|
||||||
|
|
||||||
|
|
||||||
func steps() -> Array:
|
|
||||||
return STEPS1 if part == 1 else STEPS2
|
|
||||||
|
|
||||||
|
|
||||||
func stage_index() -> int:
|
|
||||||
return 0 if part == 1 else 1
|
|
||||||
|
|
||||||
|
|
||||||
## Teil 1: ein Wesen für 1 Energie auf der Hand, damit der erste Schritt immer möglich ist.
|
|
||||||
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
|
|
||||||
func inject_cards(g: Game) -> void:
|
|
||||||
if part == 1 and not g.human.hand.any(func(c): return c.is_creature() and c.cost == 1):
|
|
||||||
g.human.hand.append(Cards.by_id("f_kobold"))
|
|
||||||
if part == 2:
|
|
||||||
g.human.hand.append(Cards.by_id("w_falle"))
|
|
||||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
|
||||||
for i in 2:
|
|
||||||
g.opponents[0].hand.append(Cards.by_id("w_reiter"))
|
|
||||||
|
|
||||||
|
|
||||||
func condition(kind: String) -> bool:
|
|
||||||
var g: Game = screen.game
|
|
||||||
var h: Game.Player = g.human
|
|
||||||
match kind:
|
|
||||||
"played_creature":
|
|
||||||
return g.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
|
||||||
"turn_passed":
|
|
||||||
return h.turns >= 2
|
|
||||||
"attacked":
|
|
||||||
return g.log.any(func(l): return l.begins_with(h.board[0].name + " greift") if not h.board.is_empty() else false) \
|
|
||||||
or g.log.any(func(l): return l.ends_with("greift Fenn an."))
|
|
||||||
"resonance":
|
|
||||||
return h.board.any(func(c): return c.resonance > 0)
|
|
||||||
"combo":
|
|
||||||
return g.log.any(func(l): return l.begins_with("Feldkombo"))
|
|
||||||
"spell":
|
|
||||||
return g.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
|
||||||
"mulligan":
|
|
||||||
return h.mulligan_done
|
|
||||||
"trap_set":
|
|
||||||
return g.log.any(func(l): return l == "Du legt eine Falle.")
|
|
||||||
"trap_fired":
|
|
||||||
return g.log.any(func(l): return l.begins_with("Falle von Du"))
|
|
||||||
"combo_dampf":
|
|
||||||
return g.log.any(func(l): return l.begins_with("Feldkombo Dampf"))
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func advance() -> void:
|
|
||||||
while step < steps().size() - 1 and condition(steps()[step][1]):
|
|
||||||
step += 1
|
|
||||||
|
|
||||||
|
|
||||||
func next() -> bool:
|
|
||||||
if step < steps().size() - 1:
|
|
||||||
step += 1
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func text() -> String:
|
|
||||||
return "%d/%d %s" % [step + 1, steps().size(), screen.tr(steps()[step][0])]
|
|
||||||
|
|
||||||
|
|
||||||
func finished() -> bool:
|
|
||||||
return step >= steps().size() - 1
|
|
||||||
|
|
||||||
|
|
||||||
## Rechtecke, die der aktuelle Schritt hervorhebt.
|
|
||||||
func highlight_rects(mine: bool) -> Array[Rect2]:
|
|
||||||
var rects: Array[Rect2] = []
|
|
||||||
if not mine or screen.any_overlay():
|
|
||||||
return rects
|
|
||||||
var g: Game = screen.game
|
|
||||||
var h: Game.Player = screen.perspective
|
|
||||||
match steps()[step][1]:
|
|
||||||
"played_creature":
|
|
||||||
for v in screen.hand_views:
|
|
||||||
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
"turn_passed", "trap_fired":
|
|
||||||
rects.append(screen.b_end.get_rect())
|
|
||||||
"attacked":
|
|
||||||
for v in screen.own_views:
|
|
||||||
if v.creature.can_attack():
|
|
||||||
rects.append(Rect2(v.slot, v.size))
|
|
||||||
if screen.own_views.any(func(v): return v.creature.can_attack()):
|
|
||||||
for o in screen.others():
|
|
||||||
if o.alive() and screen.hero_panels.has(o):
|
|
||||||
rects.append(screen.hero_panels[o].get_rect())
|
|
||||||
"resonance":
|
|
||||||
for v in screen.hand_views:
|
|
||||||
var c: Cards.Card = v.card
|
|
||||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
"combo":
|
|
||||||
for v in screen.hand_views:
|
|
||||||
var c: Cards.Card = v.card
|
|
||||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
"spell":
|
|
||||||
for v in screen.hand_views:
|
|
||||||
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
"trap_set":
|
|
||||||
for v in screen.hand_views:
|
|
||||||
if v.card.is_trap() and g.can_play(h, v.card)[0]:
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
"combo_dampf":
|
|
||||||
var has_fire: bool = h.board.any(func(b): return "Feuer" in b.card.elements)
|
|
||||||
for v in screen.hand_views:
|
|
||||||
var c: Cards.Card = v.card
|
|
||||||
if not c.is_creature() or not g.can_play(h, c)[0]:
|
|
||||||
continue
|
|
||||||
if (has_fire and "Wasser" in c.elements) or (not has_fire and "Feuer" in c.elements):
|
|
||||||
rects.append(Rect2(v.position, v.size))
|
|
||||||
return rects
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cg78afyod2873
|
|
||||||
@@ -36,10 +36,6 @@ func build() -> void:
|
|||||||
p.perk = "" if i == 0 else keys[i - 1]
|
p.perk = "" if i == 0 else keys[i - 1]
|
||||||
app.save())
|
app.save())
|
||||||
pr.add_child(perk_box)
|
pr.add_child(perk_box)
|
||||||
var intro := ["Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★).",
|
|
||||||
"Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.",
|
|
||||||
"Kapitel 3: drei Merkmale, volle Tische und die stärksten Decks."]
|
|
||||||
Style.clabel(col, intro[chapter - 1], 14, Style.MUTED)
|
|
||||||
var offset := (chapter - 1) * Campaign.STAGES.size()
|
var offset := (chapter - 1) * Campaign.STAGES.size()
|
||||||
for k in Campaign.STAGES.size():
|
for k in Campaign.STAGES.size():
|
||||||
var i := offset + k
|
var i := offset + k
|
||||||
@@ -50,31 +46,27 @@ func build() -> void:
|
|||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
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))
|
Style.GOLD if next else null, 2 if next else 0))
|
||||||
col.add_child(panel)
|
col.add_child(panel)
|
||||||
var r := Style.row(panel, 10)
|
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)
|
r.custom_minimum_size = Vector2(0, 50)
|
||||||
Style.clabel(r, str(i + 1), 18, Style.GOLD if next else Style.MUTED, "center", true, 36)
|
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
|
||||||
Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 290)
|
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
|
||||||
var dots := Control.new()
|
var dots := Control.new()
|
||||||
dots.custom_minimum_size = Vector2(50, 0)
|
dots.custom_minimum_size = Vector2(60, 0)
|
||||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
var elems: Array = st.elements
|
var elems: Array = st.elements
|
||||||
dots.draw.connect(func():
|
dots.draw.connect(func():
|
||||||
for m in elems.size():
|
for m in elems.size():
|
||||||
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
||||||
r.add_child(dots)
|
r.add_child(dots)
|
||||||
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
|
||||||
var table: String = "Duell" if extra == 0 else "%d am Tisch" % (extra + 2)
|
|
||||||
var perks_text: String = ", ".join(st.perks.map(func(pk): return Game.PERKS[pk]))
|
|
||||||
var target := Campaign.target_rating(i, p.target_rating())
|
|
||||||
Style.clabel(r, "%s · Stärke %.1f · %d Münzen\n★ %s" % [table, target, st.reward, perks_text], 12, Style.MUTED, "left", false, 360)
|
|
||||||
if done:
|
if done:
|
||||||
Style.clabel(r, "abgeschlossen", 13, Style.GREEN, "right", false, 100)
|
|
||||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||||
elif next:
|
elif next:
|
||||||
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
|
||||||
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
||||||
else:
|
else:
|
||||||
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
|
||||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||||
b.disabled = true
|
b.disabled = true
|
||||||
if p.campaign_stage >= Campaign.total():
|
if p.campaign_stage >= Campaign.total():
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
class_name ChallengeScreen
|
|
||||||
extends Control
|
|
||||||
## Wöchentliche Herausforderung: feste Regelvariante mit einmaliger Belohnung.
|
|
||||||
|
|
||||||
var app
|
|
||||||
var msg: Label
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var p: Profile = app.profile
|
|
||||||
var ch := Challenge.current()
|
|
||||||
var done: bool = p.challenge_week == Challenge.week()
|
|
||||||
var col := Style.column(self, 760, 60, 14)
|
|
||||||
Style.clabel(col, "Herausforderung der Woche", 36, Style.GOLD, "center", true)
|
|
||||||
Style.clabel(col, "Jede Woche eine andere Regel. Die Belohnung gibt es einmal pro Woche, danach die normale.", 16, Style.MUTED)
|
|
||||||
Style.spacer(col, 20)
|
|
||||||
var panel := PanelContainer.new()
|
|
||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 10, Style.GREEN if done else Style.GOLD, 2))
|
|
||||||
col.add_child(panel)
|
|
||||||
var inner := VBoxContainer.new()
|
|
||||||
inner.add_theme_constant_override("separation", 10)
|
|
||||||
panel.add_child(inner)
|
|
||||||
Style.spacer(inner, 6)
|
|
||||||
Style.clabel(inner, ch.name, 30, Style.TEXT, "center", true)
|
|
||||||
Style.clabel(inner, ch.text, 18, Style.MUTED)
|
|
||||||
Style.clabel(inner, "Belohnung: %d Münzen%s" % [ch.reward, " · diese Woche schon bestanden ✓" if done else ""], 16, Style.GOLD)
|
|
||||||
Style.spacer(inner, 6)
|
|
||||||
Style.spacer(col, 10)
|
|
||||||
msg = Style.clabel(col, "", 16, Style.ORANGE)
|
|
||||||
var deck_err := Challenge.deck_allowed(ch, p.deck_cards())
|
|
||||||
if deck_err != "":
|
|
||||||
msg.text = deck_err + " (aktives Deck: %s)" % p.deck_name()
|
|
||||||
var br := Style.row(col, 20)
|
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
var b := Style.cbutton(br, "Antreten", Vector2(200, 50), func(): app.show_screen(BattleScreen.weekly(ch)), Color("285a3c"))
|
|
||||||
b.disabled = deck_err != "" or not p.deck_valid()[0]
|
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 50), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cxheynenqemse
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
class_name WorkshopScreen
|
class_name CollectionScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Werkstatt: Karten zerlegen (Staub) und herstellen.
|
## Sammlung: alle Karten mit Besitzstand; ausgewählte Karten lassen sich
|
||||||
|
## zerlegen (Staub) oder herstellen.
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var filter := ""
|
var filter := ""
|
||||||
@@ -18,7 +19,7 @@ var b_extras: Button
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Style.label(self, "Werkstatt", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||||
Style.label(self, "Karte anklicken, dann zerlegen oder herstellen. Überschuss: alles über %d Kopien." % Game.MAX_COPIES,
|
Style.label(self, "Karte anklicken, dann zerlegen oder herstellen. Überschuss: alles über %d Kopien." % Game.MAX_COPIES,
|
||||||
14, Style.MUTED, Rect2(240, 18, 600, 24))
|
14, Style.MUTED, Rect2(240, 18, 600, 24))
|
||||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||||
1
scripts/ui/collection_screen.gd.uid
Normal file
1
scripts/ui/collection_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dsqeag3p0ehbq
|
||||||
@@ -290,7 +290,7 @@ func add(card: Cards.Card) -> void:
|
|||||||
var owned: int = app.profile.collection.get(card.id, 0)
|
var owned: int = app.profile.collection.get(card.id, 0)
|
||||||
var have := deck.count(card.id)
|
var have := deck.count(card.id)
|
||||||
if owned == 0:
|
if owned == 0:
|
||||||
say("%s fehlt in deiner Sammlung (Werkstatt)" % card.name, false)
|
say("%s fehlt in deiner Sammlung" % card.title(), false)
|
||||||
elif deck.size() >= Game.DECK_SIZE:
|
elif deck.size() >= Game.DECK_SIZE:
|
||||||
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
||||||
elif have >= mini(owned, Game.MAX_COPIES):
|
elif have >= mini(owned, Game.MAX_COPIES):
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
class_name HotseatScreen
|
|
||||||
extends Control
|
|
||||||
## Lokal zu mehreren am selben Rechner: Spielerzahl, KI-Zahl und Decks wählen.
|
|
||||||
|
|
||||||
var app
|
|
||||||
var n_humans := 2
|
|
||||||
var n_ai := 0
|
|
||||||
var deck_choice: Array = [] # je Mensch: Slot-Index oder -1 für Zufallsdeck
|
|
||||||
var human_buttons: Array = []
|
|
||||||
var ai_buttons: Array = []
|
|
||||||
var deck_rows: VBoxContainer
|
|
||||||
var b_start: Button
|
|
||||||
var hint: Label
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var col := Style.column(self, 760, 40, 10)
|
|
||||||
Style.clabel(col, "Lokal zu mehreren", 36, Style.GOLD, "center", true)
|
|
||||||
Style.clabel(col, "Alle spielen am selben Rechner und reichen die Maus weiter. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
|
|
||||||
Style.spacer(col, 10)
|
|
||||||
Style.clabel(col, "Spieler", 20, Style.TEXT, "center", true)
|
|
||||||
var hr := Style.row(col, 10)
|
|
||||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
for n in range(2, 5):
|
|
||||||
human_buttons.append(Style.cbutton(hr, "%d Spieler" % n, Vector2(150, 44), func(): set_humans(n)))
|
|
||||||
Style.clabel(col, "KI-Gegner", 20, Style.TEXT, "center", true)
|
|
||||||
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, 44), func(): set_ai(n)))
|
|
||||||
Style.spacer(col, 6)
|
|
||||||
Style.clabel(col, "Decks", 20, Style.TEXT, "center", true)
|
|
||||||
deck_rows = VBoxContainer.new()
|
|
||||||
deck_rows.add_theme_constant_override("separation", 6)
|
|
||||||
col.add_child(deck_rows)
|
|
||||||
Style.spacer(col, 6)
|
|
||||||
hint = Style.clabel(col, "", 15, Style.ORANGE)
|
|
||||||
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), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
for i in 4:
|
|
||||||
deck_choice.append(-1)
|
|
||||||
var p: Profile = app.profile
|
|
||||||
if p.deck_valid()[0]:
|
|
||||||
deck_choice[0] = p.active_deck
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
|
|
||||||
|
|
||||||
func set_humans(n: int) -> void:
|
|
||||||
n_humans = n
|
|
||||||
n_ai = mini(n_ai, Game.MAX_OPPONENTS + 1 - n_humans)
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func set_ai(n: int) -> void:
|
|
||||||
n_ai = n
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func valid_slots() -> Array:
|
|
||||||
var out := []
|
|
||||||
for i in Profile.DECK_SLOTS:
|
|
||||||
if app.profile.deck_valid(i)[0]:
|
|
||||||
out.append(i)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
|
||||||
var p: Profile = app.profile
|
|
||||||
for i in human_buttons.size():
|
|
||||||
human_buttons[i].disabled = (i + 2) == n_humans
|
|
||||||
for i in ai_buttons.size():
|
|
||||||
ai_buttons[i].disabled = i == n_ai or n_humans + i > Game.MAX_OPPONENTS + 1
|
|
||||||
Style.clear(deck_rows)
|
|
||||||
var slots := valid_slots()
|
|
||||||
for h in n_humans:
|
|
||||||
var r := Style.row(deck_rows, 12)
|
|
||||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
Style.clabel(r, "Spieler %d" % (h + 1), 16, Style.TEXT, "right", true, 140)
|
|
||||||
var ob := OptionButton.new()
|
|
||||||
ob.custom_minimum_size = Vector2(340, 36)
|
|
||||||
ob.add_item("Zufallsdeck", -1)
|
|
||||||
for slot in slots:
|
|
||||||
var elems := Game.deck_elements(p.deck_cards(slot))
|
|
||||||
ob.add_item("%s (%s)" % [p.deck_name(slot), ", ".join(elems)], slot)
|
|
||||||
ob.select(ob.get_item_index(deck_choice[h]) if deck_choice[h] >= 0 and slots.has(deck_choice[h]) else 0)
|
|
||||||
var idx := h
|
|
||||||
ob.item_selected.connect(func(i: int): deck_choice[idx] = ob.get_item_id(i))
|
|
||||||
r.add_child(ob)
|
|
||||||
hint.text = "%d am Tisch" % (n_humans + n_ai)
|
|
||||||
|
|
||||||
|
|
||||||
func start() -> void:
|
|
||||||
var p: Profile = app.profile
|
|
||||||
var r := Game.make_rng()
|
|
||||||
var decks := []
|
|
||||||
for h in n_humans:
|
|
||||||
var slot: int = deck_choice[h]
|
|
||||||
if slot >= 0 and p.deck_valid(slot)[0]:
|
|
||||||
decks.append(p.deck_cards(slot))
|
|
||||||
else:
|
|
||||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
|
||||||
Game.shuffle(pool, r)
|
|
||||||
decks.append(Game.build_opponent_deck(r, [pool[0], pool[1]]))
|
|
||||||
app.show_screen(BattleScreen.hotseat(decks, n_ai))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://crlne4y0uiygl
|
|
||||||
@@ -22,7 +22,6 @@ func _ready() -> void:
|
|||||||
net.my_deck = app.profile.deck.duplicate()
|
net.my_deck = app.profile.deck.duplicate()
|
||||||
var col := Style.column(self, 760, 40, 10)
|
var col := Style.column(self, 760, 40, 10)
|
||||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
||||||
Style.clabel(col, "Im selben Netz: einer eröffnet, die anderen treten mit seiner Adresse bei. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
|
|
||||||
Style.spacer(col, 10)
|
Style.spacer(col, 10)
|
||||||
var hr := Style.row(col, 12)
|
var hr := Style.row(col, 12)
|
||||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
@@ -40,7 +39,7 @@ func _ready() -> void:
|
|||||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
||||||
Style.spacer(col, 6)
|
Style.spacer(col, 6)
|
||||||
Style.clabel(col, "KI-Gegner (nur Host)", 16, Style.MUTED)
|
Style.clabel(col, "KI-Gegner", 16, Style.MUTED)
|
||||||
var ar := Style.row(col, 10)
|
var ar := Style.row(col, 10)
|
||||||
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
for n in range(0, 5):
|
for n in range(0, 5):
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ extends Control
|
|||||||
var app
|
var app
|
||||||
var b_single: Button
|
var b_single: Button
|
||||||
var b_campaign: Button
|
var b_campaign: Button
|
||||||
var b_hotseat: Button
|
|
||||||
var b_online: Button
|
var b_online: Button
|
||||||
var b_tutorial: Button
|
|
||||||
var b_challenge: Button
|
|
||||||
var b_resume: Button
|
var b_resume: Button
|
||||||
var coins_label: Label
|
var coins_label: Label
|
||||||
var coll_label: Label
|
var coll_label: Label
|
||||||
@@ -35,23 +32,14 @@ func _ready() -> void:
|
|||||||
center.add_child(grid)
|
center.add_child(grid)
|
||||||
col.add_child(center)
|
col.add_child(center)
|
||||||
var sz := Vector2(270, 44)
|
var sz := Vector2(270, 44)
|
||||||
var p0: Profile = app.profile
|
|
||||||
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
|
|
||||||
if FileAccess.file_exists(app.battle_save_path()):
|
if FileAccess.file_exists(app.battle_save_path()):
|
||||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||||
b_tutorial = Style.cbutton(grid, "Anleitung 2: Fallen" if part2 else "Anleitung", sz,
|
|
||||||
func(): app.show_screen(BattleScreen.guided(2 if part2 else 1)),
|
|
||||||
Color("5a4628") if not (p0.tutorial_done and p0.tutorial2_done) else null)
|
|
||||||
b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
||||||
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
||||||
b_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
|
|
||||||
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||||
b_challenge = Style.cbutton(grid, "Herausforderung", sz, func(): app.show_screen(ChallengeScreen.new()), Color("285a3c"))
|
|
||||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
||||||
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
||||||
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
|
Style.cbutton(grid, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
|
||||||
Style.cbutton(grid, "Erfolge", sz, func(): app.show_screen(AchievementsScreen.new()))
|
|
||||||
Style.cbutton(grid, "Regeln", sz, func(): app.show_screen(RulesScreen.new()))
|
|
||||||
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
||||||
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
||||||
Style.spacer(col, 10)
|
Style.spacer(col, 10)
|
||||||
@@ -77,10 +65,7 @@ func refresh() -> void:
|
|||||||
var ok: Array = p.deck_valid()
|
var ok: Array = p.deck_valid()
|
||||||
b_single.disabled = not ok[0]
|
b_single.disabled = not ok[0]
|
||||||
b_campaign.disabled = not ok[0]
|
b_campaign.disabled = not ok[0]
|
||||||
b_tutorial.disabled = not ok[0]
|
|
||||||
b_online.disabled = not ok[0]
|
b_online.disabled = not ok[0]
|
||||||
b_challenge.disabled = not ok[0]
|
|
||||||
b_challenge.text = tr("Herausforderung") + (" ✓" if p.challenge_week == Challenge.week() else "")
|
|
||||||
var total := 0
|
var total := 0
|
||||||
for cid in p.collection:
|
for cid in p.collection:
|
||||||
total += p.collection[cid]
|
total += p.collection[cid]
|
||||||
@@ -95,12 +80,6 @@ func refresh() -> void:
|
|||||||
else:
|
else:
|
||||||
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
|
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
|
||||||
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
|
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
|
||||||
if ok[0] and not p.tutorial_done:
|
|
||||||
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
|
|
||||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
|
||||||
elif ok[0] and not p.tutorial2_done:
|
|
||||||
warn_label.text = "Anleitung 2 zeigt Mulligan, Fallen und die Feldkombo Dampf."
|
|
||||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
|
||||||
var notes := p.take_notices()
|
var notes := p.take_notices()
|
||||||
notice_label.text = " · ".join(notes)
|
notice_label.text = " · ".join(notes)
|
||||||
if not notes.is_empty():
|
if not notes.is_empty():
|
||||||
|
|||||||
@@ -1,109 +1,79 @@
|
|||||||
class_name OpponentSelectScreen
|
class_name OpponentSelectScreen
|
||||||
extends Control
|
extends Control
|
||||||
## Einzelspieler: Schwierigkeit, Gegnerzahl und Vorschau der Gegner (Element, Stärke).
|
## Einzelspieler: Schwierigkeit wählen und die Gegnerzahl schieben. Die Gegnerdecks
|
||||||
|
## werden erst beim Start zufällig gezogen (siehe BattleScreen und DeckGen).
|
||||||
|
|
||||||
var app
|
var app
|
||||||
var rating_label: Label
|
var rating_label: Label
|
||||||
var tip_label: Label
|
|
||||||
var diff_buttons := {}
|
var diff_buttons := {}
|
||||||
var previews := {} # n -> Array von Game.Player
|
var count_slider: HSlider
|
||||||
var preview_labels := {}
|
var count_label: Label
|
||||||
var b_tip: Button
|
var info_label: Label
|
||||||
var b_reroll: Button
|
|
||||||
var rerolls_left := 3
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var col := Style.column(self, 1000, 30, 10)
|
var col := Style.column(self, 1000, 60, 14)
|
||||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||||
Style.clabel(col, "Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben. Du gewinnst, wenn alle anderen besiegt sind." % Game.START_LIFE, 15, Style.MUTED)
|
Style.spacer(col, 10)
|
||||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||||
var dr := Style.row(col, 10)
|
var dr := Style.row(col, 10)
|
||||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
for key in ["leicht", "normal", "schwer"]:
|
for key in ["leicht", "normal", "schwer"]:
|
||||||
var k: String = key
|
var k: String = key
|
||||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
||||||
rating_label = Style.clabel(col, "", 14, Style.MUTED)
|
rating_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||||
var tr_row := Style.row(col, 10)
|
Style.spacer(col, 24)
|
||||||
tr_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
|
||||||
tip_label = Style.clabel(tr_row, "", 14, Style.GOLD, "right", false, 520)
|
var sr := Style.row(col, 12)
|
||||||
b_tip = Style.cbutton(tr_row, "Übernehmen", Vector2(140, 32), take_tip)
|
sr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.spacer(col, 2)
|
Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
|
||||||
for i in range(1, Game.MAX_OPPONENTS + 1):
|
count_slider = HSlider.new()
|
||||||
var n := i
|
count_slider.min_value = 1
|
||||||
var r := Style.row(col, 16)
|
count_slider.max_value = Game.MAX_OPPONENTS
|
||||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
count_slider.step = 1
|
||||||
Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c"))
|
count_slider.value = 1
|
||||||
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
count_slider.tick_count = Game.MAX_OPPONENTS
|
||||||
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
count_slider.ticks_on_borders = true
|
||||||
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
count_slider.custom_minimum_size = Vector2(420, 32)
|
||||||
preview_labels[n].autowrap_mode = TextServer.AUTOWRAP_WORD
|
count_slider.value_changed.connect(func(_v): refresh())
|
||||||
preview_labels[n].custom_minimum_size = Vector2(600, 0)
|
sr.add_child(count_slider)
|
||||||
preview_labels[n].size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
|
||||||
Style.spacer(col, 4)
|
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||||
|
Style.spacer(col, 30)
|
||||||
var br := Style.row(col, 16)
|
var br := Style.row(col, 16)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
b_reroll = Style.cbutton(br, "Neu würfeln", Vector2(190, 40), reroll)
|
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
||||||
Style.cbutton(br, "Zurück", Vector2(150, 40), func(): app.show_screen(MenuScreen.new()))
|
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
|
||||||
refresh(true)
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func set_difficulty(key: String) -> void:
|
func set_difficulty(key: String) -> void:
|
||||||
app.profile.settings.difficulty = key
|
app.profile.settings.difficulty = key
|
||||||
app.save()
|
app.save()
|
||||||
refresh(true)
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func take_tip() -> void:
|
func opponents() -> int:
|
||||||
var tip: String = app.profile.suggest_difficulty()
|
return int(count_slider.value)
|
||||||
if tip != "":
|
|
||||||
set_difficulty(tip)
|
|
||||||
|
|
||||||
|
|
||||||
## Höchstens dreimal neu würfeln, sonst ließen sich schwache Gegner erwürfeln.
|
func start() -> void:
|
||||||
func reroll() -> void:
|
app.show_screen(BattleScreen.new(opponents()))
|
||||||
if rerolls_left <= 0:
|
|
||||||
return
|
|
||||||
rerolls_left -= 1
|
|
||||||
refresh(true)
|
|
||||||
|
|
||||||
|
|
||||||
func start(n: int) -> void:
|
func refresh() -> void:
|
||||||
app.show_screen(BattleScreen.against(previews[n]))
|
|
||||||
|
|
||||||
|
|
||||||
## Baut die Gegner für jede Anzahl vor und zeigt Element und Stärke; der Kampf nutzt genau diese.
|
|
||||||
func refresh(reroll: bool = false) -> void:
|
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var d := p.difficulty()
|
|
||||||
for key in diff_buttons:
|
for key in diff_buttons:
|
||||||
var b: Button = diff_buttons[key]
|
var b: Button = diff_buttons[key]
|
||||||
var active: bool = key == p.settings.difficulty
|
var active: bool = key == p.settings.difficulty
|
||||||
b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2))
|
b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
|
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||||
var arche: String = tr("schwache Archetypen") if d.tier == "leicht" else (tr("starke Archetypen") if d.tier == "schwer" else tr("alle Archetypen"))
|
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f") % [p.deck_rating(), p.target_rating()]
|
||||||
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s") % [
|
var n := opponents()
|
||||||
p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
|
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
||||||
var tip := p.suggest_difficulty()
|
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||||
tip_label.text = (tr("Tipp: %s") % ((tr("%d Siege in Folge, versuche %s") % [p.records.streak, tr(Profile.DIFFICULTIES[tip].name)]) if p.records.streak >= 3
|
|
||||||
else (tr("%d Niederlagen in Folge, versuche %s") % [p.records.loss_streak, tr(Profile.DIFFICULTIES[tip].name)]))) if tip != "" else ""
|
|
||||||
b_tip.visible = tip != ""
|
|
||||||
if reroll:
|
|
||||||
var r := Game.make_rng()
|
|
||||||
for n in range(1, Game.MAX_OPPONENTS + 1):
|
|
||||||
var opps := []
|
|
||||||
for i in n:
|
|
||||||
opps.append(Game.random_opponent(r, i + 1, Game.START_LIFE, p.target_rating(), str(d.tier)))
|
|
||||||
previews[n] = opps
|
|
||||||
b_reroll.text = tr("Neu würfeln (%d)") % rerolls_left
|
|
||||||
b_reroll.disabled = rerolls_left <= 0
|
|
||||||
for n in previews:
|
|
||||||
var parts: Array[String] = []
|
|
||||||
for o in previews[n]:
|
|
||||||
parts.append("%s (%.1f)" % [o.title, Game.deck_rating(o.deck)])
|
|
||||||
preview_labels[n].text = ", ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
if event.is_action_pressed("ui_cancel"):
|
||||||
app.show_screen(MenuScreen.new())
|
app.show_screen(MenuScreen.new())
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
class_name RulesScreen
|
|
||||||
extends Control
|
|
||||||
|
|
||||||
var app
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
Style.label(self, "Regeln", 36, Style.GOLD, Rect2(0, 18, 1280, 50), "center", true)
|
|
||||||
var scroll := ScrollContainer.new()
|
|
||||||
scroll.position = Vector2(60, 78)
|
|
||||||
scroll.size = Vector2(1160, 640)
|
|
||||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
||||||
add_child(scroll)
|
|
||||||
var text := Label.new()
|
|
||||||
text.text = Style.rules_text()
|
|
||||||
text.autowrap_mode = TextServer.AUTOWRAP_WORD
|
|
||||||
text.custom_minimum_size = Vector2(1140, 0)
|
|
||||||
text.add_theme_font_size_override("font_size", 15)
|
|
||||||
scroll.add_child(text)
|
|
||||||
Style.button(self, "Zurück", Rect2(560, 735, 160, 44), func(): app.show_screen(MenuScreen.new()))
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
|
||||||
app.show_screen(MenuScreen.new())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bkemrytc2pjem
|
|
||||||
@@ -267,49 +267,6 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
|
|||||||
ci.draw_style_box(right, Rect2(rect.position + Vector2(half, 0), Vector2(rect.size.x - half, rect.size.y)))
|
ci.draw_style_box(right, Rect2(rect.position + Vector2(half, 0), Vector2(rect.size.x - half, rect.size.y)))
|
||||||
|
|
||||||
|
|
||||||
static func rules_text() -> String:
|
|
||||||
var lines := [
|
|
||||||
"Ziel: Besiege alle anderen. Du verlierst, wenn dein Leben auf 0 fällt.",
|
|
||||||
"Du spielst gegen 1 bis 5 Magier, jeder gegen jeden, alle nach denselben Regeln:",
|
|
||||||
"%d Leben, pro Zug 1 Energie mehr (höchstens 10) und 1 Karte. Die Zugreihenfolge wird ausgelost." % Game.START_LIFE,
|
|
||||||
"Wer nicht beginnt, startet mit einer Karte mehr und hat im ersten Zug 1 Energie extra.",
|
|
||||||
"Fallen liegen verdeckt (höchstens 2) und lösen aus, wenn dich jemand angreift oder ein Wesen ausspielt.",
|
|
||||||
"",
|
|
||||||
"Karten kosten Energie. Wesen kommen aufs Feld (max. 5) und greifen ab dem nächsten Zug an.",
|
|
||||||
"Zauber wirken sofort. Ein Wesen greift ein gegnerisches Wesen oder direkt den Gegner an;",
|
|
||||||
"beide Seiten verursachen ihren Angriffswert als Schaden.",
|
|
||||||
"",
|
|
||||||
"Elemente: Feuer (Schaden), Wasser (Kontrolle), Erde (Zähigkeit), Luft (Tempo).",
|
|
||||||
"Kombos verbinden zwei Elemente, auch das gleiche doppelt (Feuer+Feuer = Inferno).",
|
|
||||||
"Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.",
|
|
||||||
Cards.RESONANCE_TEXT,
|
|
||||||
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
|
|
||||||
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an das stärkste gegnerische Wesen, Blitz 2 Schaden",
|
|
||||||
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen +1/+1.",
|
|
||||||
"",
|
|
||||||
"Schlüsselwörter:",
|
|
||||||
]
|
|
||||||
for k in Cards.KEYWORDS:
|
|
||||||
lines.append(" %s: %s" % [k, Cards.KEYWORD_HELP[k]])
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
|
|
||||||
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
|
|
||||||
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
|
|
||||||
lines.append("Elementbooster: 5 Karten eines Elements (und seiner Kombos) für %d Münzen." % Profile.ELEMENT_BOOSTER_COST)
|
|
||||||
lines.append("Deck-Code: im Deckbau unter „Code“ kopieren und mit anderen teilen.")
|
|
||||||
lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
|
||||||
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
|
||||||
lines.append("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.")
|
|
||||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
|
||||||
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
|
||||||
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
|
||||||
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
|
|
||||||
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
|
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
|
||||||
|
|
||||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://cf42h6bsh8lvg
|
|
||||||
@@ -93,11 +93,10 @@ func _initialize() -> void:
|
|||||||
test_ki_besser_als_gierig()
|
test_ki_besser_als_gierig()
|
||||||
test_ki_spiele_enden()
|
test_ki_spiele_enden()
|
||||||
test_spielstand_migration()
|
test_spielstand_migration()
|
||||||
test_erfolge_und_tagesaufgabe()
|
test_tagesaufgabe()
|
||||||
test_kampagne()
|
test_kampagne()
|
||||||
test_fallen()
|
test_fallen()
|
||||||
test_ki_sieht_fallen_nicht()
|
test_ki_sieht_fallen_nicht()
|
||||||
test_hotseat()
|
|
||||||
test_zufallssamen()
|
test_zufallssamen()
|
||||||
test_deck_slots_und_werkstatt()
|
test_deck_slots_und_werkstatt()
|
||||||
test_spielstand_sicherung()
|
test_spielstand_sicherung()
|
||||||
@@ -109,7 +108,7 @@ func _initialize() -> void:
|
|||||||
test_deck_umbenennen_und_auffuellen()
|
test_deck_umbenennen_und_auffuellen()
|
||||||
test_neue_fallen()
|
test_neue_fallen()
|
||||||
test_kapitel_zwei_und_merkmalwahl()
|
test_kapitel_zwei_und_merkmalwahl()
|
||||||
test_herausforderung_und_bestenliste()
|
test_bestenliste()
|
||||||
test_deckstaerke_und_schwierigkeit()
|
test_deckstaerke_und_schwierigkeit()
|
||||||
test_starter_gegen_passende_gegner()
|
test_starter_gegen_passende_gegner()
|
||||||
test_kurve_statistik_vorschlag_verbesserung()
|
test_kurve_statistik_vorschlag_verbesserung()
|
||||||
@@ -649,29 +648,25 @@ func test_spielstand_migration() -> void:
|
|||||||
var alt := {"coins": 300, "collection": {"f_kobold": 2, "unbekannt": 1}, "deck": ["f_kobold", "f_kobold"]}
|
var alt := {"coins": 300, "collection": {"f_kobold": 2, "unbekannt": 1}, "deck": ["f_kobold", "f_kobold"]}
|
||||||
var p := Profile.from_dict(alt)
|
var p := Profile.from_dict(alt)
|
||||||
check(p.coins == 300 and p.collection == {"f_kobold": 2} and p.deck.size() == 2, "Version 1 gelesen")
|
check(p.coins == 300 and p.collection == {"f_kobold": 2} and p.deck.size() == 2, "Version 1 gelesen")
|
||||||
check(p.stats.games == 0 and p.achievements.is_empty() and p.campaign_stage == 0, "Standardwerte ergänzt")
|
check(p.stats.games == 0 and p.campaign_stage == 0, "Standardwerte ergänzt")
|
||||||
check(p.settings.volume == 0.8 and p.settings.animations, "Einstellungen Standard")
|
check(p.settings.volume == 0.8 and p.settings.animations, "Einstellungen Standard")
|
||||||
p.stats.wins = 3
|
p.stats.wins = 3
|
||||||
p.achievements.append("erster_sieg")
|
|
||||||
p.campaign_stage = 2
|
p.campaign_stage = 2
|
||||||
p.settings.volume = 0.3
|
p.settings.volume = 0.3
|
||||||
var d := p.to_dict()
|
var d := p.to_dict()
|
||||||
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
||||||
var p2 := Profile.from_dict(JSON.parse_string(JSON.stringify(d)))
|
var p2 := Profile.from_dict(JSON.parse_string(JSON.stringify(d)))
|
||||||
check(p2.stats.wins == 3 and p2.achievements == ["erster_sieg"] and p2.campaign_stage == 2, "Version 2 gelesen")
|
check(p2.stats.wins == 3 and p2.campaign_stage == 2, "Version 2 gelesen")
|
||||||
check(is_equal_approx(p2.settings.volume, 0.3), "Einstellungen gelesen")
|
check(is_equal_approx(p2.settings.volume, 0.3), "Einstellungen gelesen")
|
||||||
|
|
||||||
|
|
||||||
func test_erfolge_und_tagesaufgabe() -> void:
|
func test_tagesaufgabe() -> void:
|
||||||
Profile.today_override = "2026-09-16"
|
Profile.today_override = "2026-09-16"
|
||||||
var p := Profile.starter()
|
var p := Profile.starter()
|
||||||
var coins := p.coins
|
var coins := p.coins
|
||||||
var reward := p.record_battle(true, 1, p.deck_cards())
|
var reward := p.record_battle(true, 1, p.deck_cards())
|
||||||
check(reward == Profile.REWARD_WIN[1], "Duell-Belohnung")
|
check(reward == Profile.REWARD_WIN[1], "Duell-Belohnung")
|
||||||
check("erster_sieg" in p.achievements and "duellant" in p.achievements, "Erfolge Erster Sieg und Duellant")
|
check(p.coins == coins + reward + (p.daily_task().reward if p.daily.done else 0), "Münzen verbucht")
|
||||||
check(not ("tischherr" in p.achievements), "Herr des Tisches noch offen")
|
|
||||||
check(p.coins == coins + reward + 100 + (p.daily_task().reward if p.daily.done else 0), "Münzen inklusive Erfolge")
|
|
||||||
check(p.notices.size() >= 2, "Meldungen vorhanden")
|
|
||||||
p.take_notices()
|
p.take_notices()
|
||||||
check(p.notices.is_empty(), "Meldungen abgeholt")
|
check(p.notices.is_empty(), "Meldungen abgeholt")
|
||||||
check(p.daily.date == "2026-09-16" and p.daily.task != "", "Tagesaufgabe gesetzt")
|
check(p.daily.date == "2026-09-16" and p.daily.task != "", "Tagesaufgabe gesetzt")
|
||||||
@@ -691,14 +686,12 @@ func test_erfolge_und_tagesaufgabe() -> void:
|
|||||||
p.refresh_daily()
|
p.refresh_daily()
|
||||||
check(p.daily.date == "2026-09-17" and not p.daily.done and p.daily.progress == 0, "neuer Tag, neue Aufgabe")
|
check(p.daily.date == "2026-09-17" and not p.daily.done and p.daily.progress == 0, "neuer Tag, neue Aufgabe")
|
||||||
Profile.today_override = ""
|
Profile.today_override = ""
|
||||||
# Mono-Deck-Erfolg
|
|
||||||
var mono := []
|
var mono := []
|
||||||
for cid in ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche", "f_glutpfeil", "f_elementar"]:
|
for cid in ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche", "f_glutpfeil", "f_elementar"]:
|
||||||
for i in 3:
|
for i in 3:
|
||||||
mono.append(Cards.by_id(cid))
|
mono.append(Cards.by_id(cid))
|
||||||
mono = mono.slice(0, 20)
|
mono = mono.slice(0, 20)
|
||||||
p.record_battle(true, 3, mono)
|
p.record_battle(true, 3, mono)
|
||||||
check("elementarist" in p.achievements and "tischherr" in p.achievements, "Elementarist und Herr des Tisches")
|
|
||||||
check(p.stats.wins == 3 and p.stats.wins_by_opp["1"] == 2, "Statistik")
|
check(p.stats.wins == 3 and p.stats.wins_by_opp["1"] == 2, "Statistik")
|
||||||
|
|
||||||
|
|
||||||
@@ -720,7 +713,7 @@ func test_kampagne() -> void:
|
|||||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
||||||
p.campaign_stage = 9
|
p.campaign_stage = 9
|
||||||
p.record_battle(true, 5, p.deck_cards(), 9)
|
p.record_battle(true, 5, p.deck_cards(), 9)
|
||||||
check(p.campaign_stage == 10 and "kampagnenheld" in p.achievements, "Kampagne abgeschlossen")
|
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
|
||||||
var sim := Game.new_battle_with(Profile.starter().deck_cards(), Campaign.opponents(0, r), r)
|
var sim := Game.new_battle_with(Profile.starter().deck_cards(), Campaign.opponents(0, r), r)
|
||||||
var guard := 0
|
var guard := 0
|
||||||
while sim.winner == "" and guard < 300:
|
while sim.winner == "" and guard < 300:
|
||||||
@@ -810,34 +803,6 @@ func test_ki_sieht_fallen_nicht() -> void:
|
|||||||
check(g.clone_for(h).players[hi].traps.size() == 1, "eigene Fallen bleiben in der eigenen Kopie")
|
check(g.clone_for(h).players[hi].traps.size() == 1, "eigene Fallen bleiben in der eigenen Kopie")
|
||||||
|
|
||||||
|
|
||||||
func test_hotseat() -> void:
|
|
||||||
var r := RandomNumberGenerator.new()
|
|
||||||
r.seed = 3
|
|
||||||
var d := Profile.starter().deck_cards()
|
|
||||||
var g := Game.new_hotseat([d, d], 1, r)
|
|
||||||
check(g.players.size() == 3 and g.humans.size() == 2, "zwei Menschen und eine KI")
|
|
||||||
check(g.humans[0].name == "Spieler 1" and g.humans[1].name == "Spieler 2", "Spielernamen")
|
|
||||||
var ai = g.players.filter(func(p): return not p.is_human)[0]
|
|
||||||
g.damage(ai, 99)
|
|
||||||
g.check_end()
|
|
||||||
check(g.winner == "", "zwei Menschen übrig: weiter")
|
|
||||||
g.damage(g.humans[1], 99)
|
|
||||||
g.check_end()
|
|
||||||
check(g.winner == "human" and g.winner_player == g.humans[0], "Spieler 1 gewinnt")
|
|
||||||
var g2 := Game.new_hotseat([d, d], 1, r)
|
|
||||||
for hu in g2.humans:
|
|
||||||
g2.damage(hu, 99)
|
|
||||||
g2.check_end()
|
|
||||||
check(g2.winner == "opponents" and g2.winner_player != null and not g2.winner_player.is_human, "KI gewinnt, wenn alle Menschen tot sind")
|
|
||||||
var g3 := Game.new_hotseat([d, d, d], 0, r)
|
|
||||||
check(g3.players.size() == 3 and g3.opponents.size() == 2, "drei Menschen ohne KI")
|
|
||||||
var guard := 0
|
|
||||||
while g3.winner == "" and guard < 300:
|
|
||||||
AI.take_turn(g3, g3.current(), true)
|
|
||||||
guard += 1
|
|
||||||
check(g3.winner == "human" and g3.winner_player != null, "Hotseat-Partie endet mit Sieger")
|
|
||||||
|
|
||||||
|
|
||||||
func test_zufallssamen() -> void:
|
func test_zufallssamen() -> void:
|
||||||
Game.test_seed = 77
|
Game.test_seed = 77
|
||||||
var a := Game.make_rng().randi()
|
var a := Game.make_rng().randi()
|
||||||
@@ -864,7 +829,7 @@ func test_deck_slots_und_werkstatt() -> void:
|
|||||||
check(m.decks[0].cards == ["f_kobold", "f_kobold"] and m.active_deck == 0 and m.dust == 0, "Version 2 wird zu Slot 1")
|
check(m.decks[0].cards == ["f_kobold", "f_kobold"] and m.active_deck == 0 and m.dust == 0, "Version 2 wird zu Slot 1")
|
||||||
var v3 := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
var v3 := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
||||||
check(v3.decks[1].cards == ["f_kobold"] and v3.deck_name(1) == "Deck 2", "Version 3 gelesen")
|
check(v3.decks[1].cards == ["f_kobold"] and v3.deck_name(1) == "Deck 2", "Version 3 gelesen")
|
||||||
# Werkstatt
|
# Zerlegen und Herstellen
|
||||||
p.collection["f_kobold"] = 5
|
p.collection["f_kobold"] = 5
|
||||||
check(p.dismantle("f_kobold") == "" and p.collection["f_kobold"] == 4 and p.dust == 5, "Zerlegen bringt Staub")
|
check(p.dismantle("f_kobold") == "" and p.collection["f_kobold"] == 4 and p.dust == 5, "Zerlegen bringt Staub")
|
||||||
check(p.dismantle("f_kobold", 3) != "" and p.collection["f_kobold"] == 4, "Deckbedarf schützt Karten")
|
check(p.dismantle("f_kobold", 3) != "" and p.collection["f_kobold"] == 4, "Deckbedarf schützt Karten")
|
||||||
@@ -1019,8 +984,6 @@ func test_statistik_pity_tagesaufgaben() -> void:
|
|||||||
check(p.card_stats["f_kobold"].games == 1 and p.card_stats["f_kobold"].wins == 1 and p.card_stats["e_wall"].games == 1, "Partien und Siege je Deckkarte")
|
check(p.card_stats["f_kobold"].games == 1 and p.card_stats["f_kobold"].wins == 1 and p.card_stats["e_wall"].games == 1, "Partien und Siege je Deckkarte")
|
||||||
p.record_battle(false, 1, deck, -1, [])
|
p.record_battle(false, 1, deck, -1, [])
|
||||||
check(p.card_stats["f_kobold"].games == 2 and p.card_stats["f_kobold"].wins == 1, "Niederlage zählt Partie")
|
check(p.card_stats["f_kobold"].games == 2 and p.card_stats["f_kobold"].wins == 1, "Niederlage zählt Partie")
|
||||||
var top := p.top_cards(3)
|
|
||||||
check(top.size() == 3 and top[0].wins == 1, "Bestenliste der Karten")
|
|
||||||
# Pity-Zähler
|
# Pity-Zähler
|
||||||
p.coins = 100000
|
p.coins = 100000
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
@@ -1169,48 +1132,30 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
|
|||||||
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Du: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Du: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
|
||||||
p.campaign_stage = 19
|
p.campaign_stage = 19
|
||||||
p.record_battle(true, 5, p.deck_cards(), 19)
|
p.record_battle(true, 5, p.deck_cards(), 19)
|
||||||
check(p.campaign_stage == 20 and "elementmeister" in p.achievements, "Kapitel 2 abgeschlossen")
|
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
|
||||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
||||||
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
||||||
|
|
||||||
|
|
||||||
func test_herausforderung_und_bestenliste() -> void:
|
func test_bestenliste() -> void:
|
||||||
Profile.today_override = "2026-09-16"
|
|
||||||
var ch := Challenge.current()
|
|
||||||
check(ch.has("rules") and ch.n_opp >= 1, "Herausforderung der Woche")
|
|
||||||
var weeks := {}
|
|
||||||
for d in range(1, 43, 7):
|
|
||||||
Profile.today_override = "2026-%02d-%02d" % [1 + d / 28, 1 + d % 28]
|
|
||||||
weeks[Challenge.current().id] = true
|
|
||||||
check(weeks.size() >= 4, "Herausforderungen wechseln wöchentlich")
|
|
||||||
Profile.today_override = "2026-09-16"
|
Profile.today_override = "2026-09-16"
|
||||||
var mono := []
|
var mono := []
|
||||||
for i in 20:
|
for i in 20:
|
||||||
mono.append(Cards.by_id("w_reiter"))
|
mono.append(Cards.by_id("w_reiter"))
|
||||||
check(Challenge.deck_allowed(Challenge.LIST[5], Profile.starter().deck_cards()) != "", "Reine Lehre lehnt Zwei-Element-Deck ab")
|
|
||||||
check(Challenge.deck_allowed(Challenge.LIST[5], mono) == "", "Mono-Deck erlaubt")
|
|
||||||
var r := RandomNumberGenerator.new()
|
var r := RandomNumberGenerator.new()
|
||||||
r.seed = 4
|
r.seed = 4
|
||||||
var g := Game.new_battle(mono, 3, r, Challenge.LIST[0].rules)
|
var g := Game.new_battle(mono, 3, r, {"everyone_perk": "start_energie", "start_life": 20})
|
||||||
check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Eile: 20 Leben und Startenergie für alle")
|
check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Regelvariante: 20 Leben und Startenergie für alle")
|
||||||
check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie")
|
check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie")
|
||||||
var p := Profile.starter()
|
var p := Profile.starter()
|
||||||
var coins := p.coins
|
p.record_battle(true, 3, mono, -1, [], 7)
|
||||||
var reward := p.record_battle(true, 3, mono, -1, [], 7, Challenge.LIST[0])
|
p.record_battle(true, 3, mono, -1, [], 9)
|
||||||
check(reward == Challenge.LIST[0].reward and p.challenge_week == Challenge.week() and "wochenheld" in p.achievements, "Herausforderung belohnt")
|
|
||||||
reward = p.record_battle(true, 3, mono, -1, [], 9, Challenge.LIST[0])
|
|
||||||
check(reward == Profile.REWARD_WIN[3], "nur einmal pro Woche")
|
|
||||||
check(p.records.streak == 2 and p.records.best_streak == 2 and p.records.fastest_win == 7, "Serie und schnellster Sieg")
|
check(p.records.streak == 2 and p.records.best_streak == 2 and p.records.fastest_win == 7, "Serie und schnellster Sieg")
|
||||||
p.record_battle(false, 1, mono, -1, [], 12)
|
p.record_battle(false, 1, mono, -1, [], 12)
|
||||||
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
|
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
|
||||||
for i in 5:
|
for i in 5:
|
||||||
p.record_battle(true, 1, mono, -1, [], 10)
|
p.record_battle(true, 1, mono, -1, [], 10)
|
||||||
check("serie" in p.achievements, "Siegesserie-Erfolg")
|
check(p.records.best_streak == 5, "neue beste Serie")
|
||||||
var path := "user://test_statistik.csv"
|
|
||||||
check(p.export_stats(path) == "", "Statistik exportiert")
|
|
||||||
var text := FileAccess.get_file_as_string(path)
|
|
||||||
check(text.begins_with("karte;seltenheit;gespielt;partien;siege") and "beste_serie;5" in text, "CSV-Inhalt")
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
||||||
Profile.today_override = ""
|
Profile.today_override = ""
|
||||||
|
|
||||||
|
|
||||||
@@ -1240,7 +1185,7 @@ func test_deckstaerke_und_schwierigkeit() -> void:
|
|||||||
check(is_equal_approx(p.target_rating(), rs * 1.2) and p.difficulty().smart, "Schwer: 1,2-fach mit Vorausschau-KI")
|
check(is_equal_approx(p.target_rating(), rs * 1.2) and p.difficulty().smart, "Schwer: 1,2-fach mit Vorausschau-KI")
|
||||||
p.settings.difficulty = "leicht"
|
p.settings.difficulty = "leicht"
|
||||||
check(not p.difficulty().smart and is_equal_approx(p.target_rating(), rs * 0.9), "Leicht: einfache KI, 0,9-fach")
|
check(not p.difficulty().smart and is_equal_approx(p.target_rating(), rs * 0.9), "Leicht: einfache KI, 0,9-fach")
|
||||||
var reward := p.record_battle(true, 1, starter, -1, [], 5, {}, p.difficulty().reward)
|
var reward := p.record_battle(true, 1, starter, -1, [], 5, p.difficulty().reward)
|
||||||
check(reward == int(round(Profile.REWARD_WIN[1] * 0.75)), "Leicht: weniger Münzen")
|
check(reward == int(round(Profile.REWARD_WIN[1] * 0.75)), "Leicht: weniger Münzen")
|
||||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
||||||
check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert")
|
check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert")
|
||||||
@@ -1297,17 +1242,17 @@ func test_kurve_statistik_vorschlag_verbesserung() -> void:
|
|||||||
var mono := starter
|
var mono := starter
|
||||||
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
|
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
|
||||||
for i in 3:
|
for i in 3:
|
||||||
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 4)
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 4)
|
||||||
check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag")
|
check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag")
|
||||||
p.records.streak = 0
|
p.records.streak = 0
|
||||||
p.records.margins = []
|
p.records.margins = []
|
||||||
for i in 3:
|
for i in 3:
|
||||||
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 20)
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 20)
|
||||||
check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei deutliche Siege: Schwer vorschlagen")
|
check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei deutliche Siege: Schwer vorschlagen")
|
||||||
p.records.streak = 0
|
p.records.streak = 0
|
||||||
p.records.margins = []
|
p.records.margins = []
|
||||||
for i in 5:
|
for i in 5:
|
||||||
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 2)
|
p.record_battle(true, 1, mono, -1, [], 6, 1.0, 2)
|
||||||
check(p.suggest_difficulty() == "schwer" and p.records.margins.size() == 5, "fünf Siege egal wie knapp")
|
check(p.suggest_difficulty() == "schwer" and p.records.margins.size() == 5, "fünf Siege egal wie knapp")
|
||||||
p.settings.difficulty = "schwer"
|
p.settings.difficulty = "schwer"
|
||||||
p.take_notices()
|
p.take_notices()
|
||||||
@@ -1367,7 +1312,7 @@ func test_ki_zeitbudget() -> void:
|
|||||||
check(act.type in ["play", "attack", "end"] and ms < 80, "Zeitbudget greift (%d ms)" % ms)
|
check(act.type in ["play", "attack", "end"] and ms < 80, "Zeitbudget greift (%d ms)" % ms)
|
||||||
check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").combo() == "Sturm", "Gischtläufer für Sturm")
|
check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").combo() == "Sturm", "Gischtläufer für Sturm")
|
||||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
|
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
|
||||||
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
check(v.settings.has("big_font") and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
||||||
|
|
||||||
|
|
||||||
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
## Elementbooster enthalten nur Karten des Elements; Deck-Codes sind umkehrbar und robust.
|
||||||
@@ -1437,7 +1382,7 @@ func test_kampf_speichern() -> void:
|
|||||||
check(s2.players.all(func(p): return not (p.hand is Array) and not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
check(s2.players.all(func(p): return not (p.hand is Array) and not (p.traps is Array) and not (p.deck is Array)), "Zuschauer-Sicht ohne Hände, Decks und Fallen")
|
||||||
|
|
||||||
|
|
||||||
## Jeder tr("...")-Text in der Oberfläche und jeder Anleitungsschritt hat einen englischen Eintrag.
|
## Jeder tr("...")-Text in der Oberfläche hat einen englischen Eintrag.
|
||||||
func test_uebersetzung_vollstaendig() -> void:
|
func test_uebersetzung_vollstaendig() -> void:
|
||||||
var keys := {}
|
var keys := {}
|
||||||
var csv := FileAccess.get_file_as_string("res://translations/ui.csv")
|
var csv := FileAccess.get_file_as_string("res://translations/ui.csv")
|
||||||
@@ -1472,8 +1417,4 @@ func test_uebersetzung_vollstaendig() -> void:
|
|||||||
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
for m in rx_cards.search_all(FileAccess.get_file_as_string("res://scripts/cards.gd")):
|
||||||
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
||||||
missing.append(m.get_string(1))
|
missing.append(m.get_string(1))
|
||||||
for stepset in [BattleTutorial.STEPS1, BattleTutorial.STEPS2]:
|
|
||||||
for st in stepset:
|
|
||||||
if not keys.has(st[0]):
|
|
||||||
missing.append(st[0])
|
|
||||||
check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing])
|
check(missing.is_empty(), "fehlende Übersetzungen: %s" % [missing])
|
||||||
|
|||||||
227
tests/ui_test.gd
227
tests/ui_test.gd
@@ -164,11 +164,15 @@ func _ready() -> void:
|
|||||||
add_child(main)
|
add_child(main)
|
||||||
await frames(3)
|
await frames(3)
|
||||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||||
var suite := [test_menu_und_regeln, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
var suite := [test_menu, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung,
|
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung,
|
||||||
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
|
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
|
||||||
if not fast:
|
if not fast:
|
||||||
suite += [test_hotseat, test_netzwerk, test_host_wechsel, test_zuschauer, test_kampf.bind(2, 3), test_kampf_abbrechen]
|
suite += [test_kampf.bind(2, 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") != "":
|
||||||
|
suite += [test_netzwerk, test_host_wechsel, test_zuschauer]
|
||||||
suite.append(test_profil_zuruecksetzen)
|
suite.append(test_profil_zuruecksetzen)
|
||||||
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
|
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
|
||||||
if only != "":
|
if only != "":
|
||||||
@@ -183,14 +187,10 @@ func _ready() -> void:
|
|||||||
get_tree().quit(1 if failed > 0 else 0)
|
get_tree().quit(1 if failed > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
func test_menu_und_regeln() -> void:
|
func test_menu() -> void:
|
||||||
check(screen() is MenuScreen, "Menü sichtbar")
|
check(screen() is MenuScreen, "Menü sichtbar")
|
||||||
|
check(find_button("Einzelspieler") != null and find_button("Beenden") != null, "Menüknöpfe vorhanden")
|
||||||
await shot("01_menu")
|
await shot("01_menu")
|
||||||
await click(find_button("Regeln"))
|
|
||||||
check(screen() is RulesScreen, "Regeln geöffnet")
|
|
||||||
await shot("02_regeln")
|
|
||||||
await click(find_button("Zurück"))
|
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
|
||||||
|
|
||||||
|
|
||||||
func test_booster() -> void:
|
func test_booster() -> void:
|
||||||
@@ -198,13 +198,8 @@ func test_booster() -> void:
|
|||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BoosterScreen, "Boosterbildschirm")
|
check(scr is BoosterScreen, "Boosterbildschirm")
|
||||||
var coins: int = main.profile.coins
|
var coins: int = main.profile.coins
|
||||||
var ach_before: int = main.profile.achievements.size()
|
|
||||||
await click(scr.b_buy)
|
await click(scr.b_buy)
|
||||||
var bonus := 0
|
check(main.profile.coins == coins - Profile.BOOSTER_COST, "Booster bezahlt")
|
||||||
for a in Profile.ACHIEVEMENTS:
|
|
||||||
if a.id in main.profile.achievements.slice(ach_before):
|
|
||||||
bonus += a.reward
|
|
||||||
check(main.profile.coins == coins - Profile.BOOSTER_COST + bonus, "Booster bezahlt (plus Erfolgsmünzen %d)" % bonus)
|
|
||||||
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
|
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
|
||||||
await shot("03_booster_zu")
|
await shot("03_booster_zu")
|
||||||
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
|
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
|
||||||
@@ -373,25 +368,38 @@ func human_turn(scr) -> void:
|
|||||||
func start_battle(n_opp: int) -> void:
|
func start_battle(n_opp: int) -> void:
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
|
||||||
await click(find_button("%d Gegner" % n_opp))
|
screen().count_slider.value = n_opp
|
||||||
|
await frames(1)
|
||||||
|
await click(find_button("Starten"))
|
||||||
|
|
||||||
|
|
||||||
func test_gegnerauswahl() -> void:
|
func test_gegnerauswahl() -> void:
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
var sel = screen()
|
var sel = screen()
|
||||||
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
||||||
check("Deckstärke" in sel.rating_label.text, "Deckstärke angezeigt")
|
check("Deckstärke" in sel.rating_label.text and "Gegnerdecks" in sel.rating_label.text
|
||||||
check(sel.preview_labels[3].text.count("(") == 3 and sel.previews[3].size() == 3, "Vorschau zeigt drei Gegner mit Stärke")
|
and not ("Archetypen" in sel.rating_label.text or "KI" in sel.rating_label.text), "unter der Schwierigkeit nur die zwei Stärken")
|
||||||
for i in 3:
|
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")
|
||||||
await click(sel.b_reroll)
|
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.rerolls_left == 0 and sel.b_reroll.disabled, "höchstens dreimal neu würfeln")
|
check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
|
||||||
|
var rect: Rect2 = sel.count_slider.get_global_rect()
|
||||||
|
await click_at(Vector2(rect.end.x - 4, rect.get_center().y)) # ganz rechts anklicken
|
||||||
|
check(sel.opponents() == Game.MAX_OPPONENTS and sel.count_label.text == "%d Gegner" % Game.MAX_OPPONENTS, "Mausklick am rechten Rand stellt %d Gegner ein" % Game.MAX_OPPONENTS)
|
||||||
|
sel.count_slider.grab_focus()
|
||||||
|
await key(KEY_LEFT)
|
||||||
|
check(sel.opponents() == Game.MAX_OPPONENTS - 1, "Pfeiltaste links nimmt einen Gegner weg")
|
||||||
|
sel.count_slider.value = 4
|
||||||
|
await frames(1)
|
||||||
|
check(sel.opponents() == 4 and sel.count_label.text == "4 Gegner" and "5 am Tisch" in sel.info_label.text
|
||||||
|
and str(Profile.REWARD_WIN[4]) in sel.info_label.text, "Regler auf 4: fünf am Tisch mit Belohnung")
|
||||||
await click(find_button("Schwer"))
|
await click(find_button("Schwer"))
|
||||||
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
|
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
|
||||||
var previewed: Array = sel.previews[1].map(func(o): return o.title)
|
|
||||||
await shot("07_gegnerauswahl")
|
await shot("07_gegnerauswahl")
|
||||||
await click(find_button("1 Gegner"))
|
sel.count_slider.value = 1
|
||||||
|
await frames(1)
|
||||||
|
await click(find_button("Starten"))
|
||||||
var hard = screen()
|
var hard = screen()
|
||||||
check(hard.game.opponents.map(func(o): return o.title) == previewed, "Kampf nutzt die vorgeschauten Gegner")
|
check(hard is BattleScreen and hard.game.opponents.size() == 1, "Kampf mit einem Gegner gestartet")
|
||||||
var own := Game.deck_rating(main.profile.deck_cards())
|
var own := Game.deck_rating(main.profile.deck_cards())
|
||||||
var opp: Game.Player = hard.game.opponents[0]
|
var opp: Game.Player = hard.game.opponents[0]
|
||||||
var opp_rating := Game.deck_rating(opp.deck + opp.hand)
|
var opp_rating := Game.deck_rating(opp.deck + opp.hand)
|
||||||
@@ -399,22 +407,13 @@ func test_gegnerauswahl() -> void:
|
|||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
await click(find_button("Leicht"))
|
await click(find_button("Leicht"))
|
||||||
await click(find_button("1 Gegner"))
|
await click(find_button("Starten"))
|
||||||
var easy = screen()
|
var easy = screen()
|
||||||
opp = easy.game.opponents[0]
|
opp = easy.game.opponents[0]
|
||||||
check(Game.deck_rating(opp.deck + opp.hand) <= own * 1.0 + 0.15 and not easy.smart_ai, "Leicht: schwächeres Deck, einfache KI")
|
check(Game.deck_rating(opp.deck + opp.hand) <= own * 1.0 + 0.15 and not easy.smart_ai, "Leicht: schwächeres Deck, einfache KI")
|
||||||
await give_up()
|
await give_up()
|
||||||
await click(find_button("Einzelspieler"))
|
await click(find_button("Einzelspieler"))
|
||||||
await click(find_button("Normal"))
|
await click(find_button("Normal"))
|
||||||
main.profile.records.streak = 3
|
|
||||||
main.profile.records.margins = [20, 22, 18]
|
|
||||||
screen().refresh()
|
|
||||||
check(screen().b_tip.visible and "Schwer" in screen().tip_label.text, "Vorschlag nach drei deutlichen Siegen")
|
|
||||||
await click(find_button("Übernehmen"))
|
|
||||||
check(main.profile.settings.difficulty == "schwer", "Vorschlag übernommen")
|
|
||||||
main.profile.records.streak = 0
|
|
||||||
main.profile.records.margins = []
|
|
||||||
await click(find_button("Normal"))
|
|
||||||
await click(find_button("Zurück"))
|
await click(find_button("Zurück"))
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
for n in [1, 5]:
|
for n in [1, 5]:
|
||||||
@@ -565,23 +564,10 @@ func test_einstellungen() -> void:
|
|||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
func test_erfolge() -> void:
|
func test_sammlung() -> void:
|
||||||
await click(find_button("Erfolge"))
|
await click(find_button("Sammlung"))
|
||||||
check(screen() is AchievementsScreen, "Erfolge geöffnet")
|
|
||||||
var csv := "user://statistik.csv"
|
|
||||||
if FileAccess.file_exists(csv):
|
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(csv))
|
|
||||||
await click(find_button("Statistik exportieren"))
|
|
||||||
check(FileAccess.file_exists(csv) and "Gespeichert" in screen().export_label.text, "Statistik exportiert")
|
|
||||||
await shot("14_erfolge")
|
|
||||||
await click(find_button("Zurück"))
|
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
|
||||||
|
|
||||||
|
|
||||||
func test_werkstatt() -> void:
|
|
||||||
await click(find_button("Werkstatt"))
|
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is WorkshopScreen, "Werkstatt geöffnet")
|
check(scr is CollectionScreen, "Sammlung geöffnet")
|
||||||
var p: Profile = main.profile
|
var p: Profile = main.profile
|
||||||
p.dust = 0
|
p.dust = 0
|
||||||
var in_decks := {}
|
var in_decks := {}
|
||||||
@@ -604,8 +590,8 @@ func test_werkstatt() -> void:
|
|||||||
await click(phoenix)
|
await click(phoenix)
|
||||||
await click(scr.b_craft)
|
await click(scr.b_craft)
|
||||||
check(p.collection.get("ff_phoenix", 0) == 1 and p.dust == 5000 - Profile.DUST_CRAFT["legendär"], "Phönix hergestellt")
|
check(p.collection.get("ff_phoenix", 0) == 1 and p.dust == 5000 - Profile.DUST_CRAFT["legendär"], "Phönix hergestellt")
|
||||||
check(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Werkstatt gespeichert")
|
check(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Sammlung gespeichert")
|
||||||
await shot("20_werkstatt")
|
await shot("20_sammlung")
|
||||||
await click(find_button("Zurück"))
|
await click(find_button("Zurück"))
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
@@ -639,41 +625,6 @@ func test_gamepad_menue() -> void:
|
|||||||
check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder")
|
check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder")
|
||||||
|
|
||||||
|
|
||||||
func test_anleitung2() -> void:
|
|
||||||
check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen")
|
|
||||||
check(find_button("Anleitung 2: Fallen") != null, "Menü bietet Teil 2 an")
|
|
||||||
await click(find_button("Anleitung 2: Fallen"))
|
|
||||||
var scr = screen()
|
|
||||||
check(scr is BattleScreen and scr.tutorial and scr.tutorial_part == 2, "Anleitung 2 gestartet")
|
|
||||||
scr.ai_delay = 0.0
|
|
||||||
var g: Game = scr.game
|
|
||||||
check(g.opponents[0].name == "Mara" and g.human.hand.any(func(c): return c.id == "w_falle") and g.human.hand.any(func(c): return c.id == "w_reiter"), "Mara, Frostfalle und Wellenreiter auf der Hand")
|
|
||||||
await frames(2)
|
|
||||||
check(scr.mulligan_box.visible and scr.tutorial_step == 0, "Schritt 1: Mulligan-Maske")
|
|
||||||
await click(scr.b_keep)
|
|
||||||
check(scr.tutorial_step == 1 and "Frostfalle" in scr.tutorial_text.text, "Schritt 2: Falle legen")
|
|
||||||
await shot("29_anleitung2")
|
|
||||||
# Zug 1 hat nur 1 Energie: Zug beenden, dann Falle legen
|
|
||||||
await key(KEY_SPACE)
|
|
||||||
await wait_for_human(scr)
|
|
||||||
check(scr.highlight_layer.get_child_count() >= 1, "Falle hervorgehoben")
|
|
||||||
var idx: int = g.human.hand.map(func(c): return c.id).find("w_falle")
|
|
||||||
check(idx >= 0 and idx < 9, "Falle auf der Hand")
|
|
||||||
await key(KEY_1 + idx)
|
|
||||||
check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3")
|
|
||||||
# Mara läuft in die Falle: zwei Züge abwarten
|
|
||||||
for i in 2:
|
|
||||||
await clear_mulligan(scr)
|
|
||||||
if g.current() == g.human and g.winner == "":
|
|
||||||
await key(KEY_SPACE)
|
|
||||||
await wait_for_human(scr)
|
|
||||||
if scr.tutorial_step >= 3:
|
|
||||||
break
|
|
||||||
check(scr.tutorial_step >= 3 and g.log.any(func(l): return l.begins_with("Falle von Du")), "Mara löst die Frostfalle aus, Schritt 4")
|
|
||||||
await give_up()
|
|
||||||
check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt")
|
|
||||||
|
|
||||||
|
|
||||||
func test_profil_zuruecksetzen() -> void:
|
func test_profil_zuruecksetzen() -> void:
|
||||||
await click(find_button("Einstellungen"))
|
await click(find_button("Einstellungen"))
|
||||||
await click(find_button("Profil zurücksetzen"))
|
await click(find_button("Profil zurücksetzen"))
|
||||||
@@ -683,64 +634,10 @@ func test_profil_zuruecksetzen() -> void:
|
|||||||
check(not scr.confirm.visible, "abgebrochen")
|
check(not scr.confirm.visible, "abgebrochen")
|
||||||
await click(find_button("Profil zurücksetzen"))
|
await click(find_button("Profil zurücksetzen"))
|
||||||
await click(find_button("Ja, zurücksetzen"))
|
await click(find_button("Ja, zurücksetzen"))
|
||||||
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and not main.profile.tutorial_done, "Profil zurückgesetzt")
|
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and main.profile.collection.size() > 0, "Profil zurückgesetzt")
|
||||||
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
|
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
|
||||||
|
|
||||||
|
|
||||||
func test_herausforderung() -> void:
|
|
||||||
await click(find_button("Herausforderung"))
|
|
||||||
var scr = screen()
|
|
||||||
check(scr is ChallengeScreen, "Herausforderung geöffnet")
|
|
||||||
var ch := Challenge.current()
|
|
||||||
var allowed := Challenge.deck_allowed(ch, main.profile.deck_cards()) == ""
|
|
||||||
check(find_button("Antreten").disabled == not allowed, "Antreten je nach Deck")
|
|
||||||
await shot("28_herausforderung")
|
|
||||||
if allowed:
|
|
||||||
await click(find_button("Antreten"))
|
|
||||||
var b = screen()
|
|
||||||
check(b is BattleScreen and b.challenge == ch and b.game.opponents.size() == ch.n_opp, "Herausforderungskampf")
|
|
||||||
var everyone: String = ch.rules.get("everyone_perk", "")
|
|
||||||
check(everyone == "" or b.game.players.all(func(p): return everyone in p.perks), "Regel gilt für alle")
|
|
||||||
await give_up()
|
|
||||||
else:
|
|
||||||
await click(find_button("Zurück"))
|
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
|
||||||
|
|
||||||
|
|
||||||
func test_hotseat() -> void:
|
|
||||||
await click(find_button("Lokal zu mehreren"))
|
|
||||||
var scr = screen()
|
|
||||||
check(scr is HotseatScreen, "Hotseat-Auswahl")
|
|
||||||
await click(find_button("3 Spieler"))
|
|
||||||
await click(find_button("1 KI"))
|
|
||||||
check(scr.n_humans == 3 and scr.n_ai == 1, "3 Menschen, 1 KI")
|
|
||||||
await shot("17_hotseat")
|
|
||||||
await click(find_button("Start"))
|
|
||||||
var b = screen()
|
|
||||||
check(b is BattleScreen and b.is_hotseat() and b.game.players.size() == 4, "Hotseat-Kampf gestartet")
|
|
||||||
b.ai_delay = 0.0
|
|
||||||
var guard := 0
|
|
||||||
while not b.handover.visible and b.game.winner == "" and guard < 400:
|
|
||||||
if b.game.current() == b.perspective:
|
|
||||||
await clear_mulligan(b)
|
|
||||||
if b.game.current() == b.perspective:
|
|
||||||
await click(b.b_end)
|
|
||||||
await frames(1)
|
|
||||||
guard += 1
|
|
||||||
check(b.handover.visible, "Übergabe erscheint")
|
|
||||||
var cur: Game.Player = b.game.current()
|
|
||||||
check(cur.is_human and cur != b.perspective, "anderer Mensch ist dran")
|
|
||||||
await shot("18_hotseat_uebergabe")
|
|
||||||
await click(find_button("Weiter"))
|
|
||||||
check(b.perspective == cur and not b.handover.visible, "Perspektive gewechselt")
|
|
||||||
check(b.hand_views.size() == cur.hand.size() and b.name_label.text == cur.name, "Hand und Name des neuen Spielers")
|
|
||||||
check(b.mulligan_box.visible, "neuer Spieler bekommt seine Mulligan-Maske")
|
|
||||||
await clear_mulligan(b)
|
|
||||||
check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch")
|
|
||||||
await shot("19_hotseat_kampf")
|
|
||||||
await leave_battle()
|
|
||||||
|
|
||||||
|
|
||||||
func test_sprache() -> void:
|
func test_sprache() -> void:
|
||||||
await click(find_button("Einstellungen"))
|
await click(find_button("Einstellungen"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
@@ -765,46 +662,35 @@ func test_sprache() -> void:
|
|||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
func test_anleitung() -> void:
|
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
|
||||||
check(not main.profile.tutorial_done, "Anleitung noch offen")
|
func test_kampf_steuerung() -> void:
|
||||||
await click(find_button("Anleitung"))
|
await start_battle(1)
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
check(scr is BattleScreen and scr.tutorial, "Anleitung gestartet")
|
check(scr is BattleScreen, "Kampf gestartet")
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
|
await wait_for_human(scr)
|
||||||
var g: Game = scr.game
|
var g: Game = scr.game
|
||||||
check(g.players[0] == g.human and g.opponents[0].name == "Fenn" and g.opponents[0].perks.is_empty(), "Fenn ohne Merkmal, Mensch beginnt")
|
|
||||||
await frames(2)
|
|
||||||
check(scr.mulligan_box.visible, "Mulligan-Maske im ersten Zug")
|
|
||||||
var views = scr.mulligan_layer.get_children()
|
|
||||||
await click(views[0])
|
|
||||||
check(scr.mulligan_selected == [0], "Karte zum Tausch gewählt")
|
|
||||||
await shot("22_anleitung_mulligan")
|
|
||||||
await click(scr.b_swap)
|
|
||||||
check(not scr.mulligan_box.visible and g.human.mulligan_done, "getauscht")
|
|
||||||
check(g.log.any(func(l): return "tauscht 1 Karte" in l), "Tausch im Verlauf")
|
|
||||||
check(scr.tutorial_step == 0 and "Energie" in scr.tutorial_text.text, "Schritt 1 sichtbar")
|
|
||||||
var idx := -1
|
var idx := -1
|
||||||
for i in g.human.hand.size():
|
for i in g.human.hand.size():
|
||||||
if g.human.hand[i].is_creature() and g.human.hand[i].cost == 1:
|
if g.human.hand[i].is_creature() and g.can_play(g.human, g.human.hand[i])[0]:
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
check(idx < 0 or scr.highlight_layer.get_child_count() >= 1, "spielbare Wesen hervorgehoben")
|
check(idx >= 0, "spielbares Wesen auf der Hand")
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
var played_id: String = g.human.hand[idx].id
|
var played_id: String = g.human.hand[idx].id
|
||||||
|
var board_before: int = g.human.board.size()
|
||||||
await key(KEY_1 + idx)
|
await key(KEY_1 + idx)
|
||||||
check(g.human.board.size() == 1, "Wesen per Taste gespielt")
|
check(g.human.board.size() == board_before + 1, "Wesen per Taste gespielt")
|
||||||
check(scr.tutorial_step == 1, "Schritt 2 nach dem Ausspielen")
|
|
||||||
check(scr.highlight_layer.get_child_count() == 1 and scr.highlight_layer.get_child(0).get_rect().intersects(scr.b_end.get_rect()), "Zug-beenden-Knopf hervorgehoben")
|
|
||||||
# Rückgängig: Wesen zurück auf die Hand
|
# Rückgängig: Wesen zurück auf die Hand
|
||||||
check(scr.b_undo.visible, "Rückgängig angeboten")
|
check(scr.b_undo.visible, "Rückgängig angeboten")
|
||||||
var energy: int = g.human.energy
|
var energy: int = g.human.energy
|
||||||
await click(scr.b_undo)
|
await click(scr.b_undo)
|
||||||
g = scr.game
|
g = scr.game
|
||||||
check(g.human.board.is_empty() and g.human.hand.any(func(c): return c.id == played_id) and g.human.energy == energy + 1, "Rückgängig stellt Hand und Energie her")
|
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == played_id) and g.human.energy > energy, "Rückgängig stellt Hand und Energie her")
|
||||||
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
|
||||||
await key(KEY_1 + idx)
|
await key(KEY_1 + idx)
|
||||||
check(g.human.board.size() == 1, "erneut gespielt")
|
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
||||||
await shot("22_anleitung")
|
await shot("22_kampf_steuerung")
|
||||||
# Gamepad: Zeiger bewegen, Karte spielen
|
# Gamepad: Zeiger bewegen, Karte spielen
|
||||||
await pad(JOY_BUTTON_DPAD_RIGHT)
|
await pad(JOY_BUTTON_DPAD_RIGHT)
|
||||||
check(scr.gp_zone == "hand" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweiter Handkarte")
|
check(scr.gp_zone == "hand" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweiter Handkarte")
|
||||||
@@ -816,9 +702,9 @@ func test_anleitung() -> void:
|
|||||||
if pad_idx >= 0:
|
if pad_idx >= 0:
|
||||||
while scr.gp_index != pad_idx:
|
while scr.gp_index != pad_idx:
|
||||||
await pad(JOY_BUTTON_DPAD_RIGHT)
|
await pad(JOY_BUTTON_DPAD_RIGHT)
|
||||||
var board_before: int = g.human.board.size()
|
var board_before2: int = g.human.board.size()
|
||||||
await pad(JOY_BUTTON_A)
|
await pad(JOY_BUTTON_A)
|
||||||
check(g.human.board.size() == board_before + 1, "Gamepad A spielt das Wesen")
|
check(g.human.board.size() == board_before2 + 1, "Gamepad A spielt das Wesen")
|
||||||
scr.ai_delay = 0.75
|
scr.ai_delay = 0.75
|
||||||
await key(KEY_SPACE)
|
await key(KEY_SPACE)
|
||||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||||
@@ -832,16 +718,11 @@ func test_anleitung() -> void:
|
|||||||
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
|
check(g.current() == g.human and w < 60, "Gegnerzug übersprungen (%d Bilder)" % w)
|
||||||
scr.ai_delay = 0.0
|
scr.ai_delay = 0.0
|
||||||
await clear_mulligan(scr)
|
await clear_mulligan(scr)
|
||||||
check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug")
|
|
||||||
var step_before: int = scr.tutorial_step
|
|
||||||
await click(scr.b_tut_next)
|
|
||||||
check(scr.tutorial_step >= step_before + 1, "Weiter-Knopf (Schritt %d)" % step_before)
|
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
||||||
await key(KEY_ESCAPE)
|
await key(KEY_ESCAPE)
|
||||||
check(not scr.confirm.visible, "Esc schließt den Dialog")
|
check(not scr.confirm.visible, "Esc schließt den Dialog")
|
||||||
await give_up()
|
await give_up()
|
||||||
check(main.profile.tutorial_done, "Anleitung als erledigt gespeichert")
|
|
||||||
|
|
||||||
|
|
||||||
func test_netzwerk() -> void:
|
func test_netzwerk() -> void:
|
||||||
@@ -1028,7 +909,7 @@ func test_zuschauer() -> void:
|
|||||||
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() and not b.mulligan_box.visible and not b.b_end.visible,
|
check(b.game != null and b.game.turn >= 2 and b.hand_views.is_empty() and not b.mulligan_box.visible and not b.b_end.visible,
|
||||||
"Zuschauer sieht die Partie ohne Hände und ohne Züge (Zug %d)" % (b.game.turn if b.game != null else 0))
|
"Zuschauer sieht die Partie ohne Hände und ohne Züge (Zug %d)" % (b.game.turn if b.game != null else 0))
|
||||||
await shot("35_zuschauer")
|
await shot("35_zuschauer")
|
||||||
check(not b.handover.visible and not b.is_hotseat(), "Zuschauer bekommt keine Hotseat-Übergabe")
|
check(not b.b_end.visible and not b.b_undo.visible, "Zuschauer hat keine Zugknöpfe")
|
||||||
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
|
if screen() is BattleScreen: # die Bots spielen schnell, die Partie kann schon vorbei sein
|
||||||
await leave_battle()
|
await leave_battle()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,18 +3,12 @@ Dinger,Dinger
|
|||||||
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all
|
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all
|
||||||
Einzelspieler,Single player
|
Einzelspieler,Single player
|
||||||
Kampagne,Campaign
|
Kampagne,Campaign
|
||||||
Lokal zu mehreren,Local multiplayer
|
|
||||||
Netzwerkspiel,Network game
|
Netzwerkspiel,Network game
|
||||||
Booster öffnen,Open boosters
|
Booster öffnen,Open boosters
|
||||||
Deck bauen,Build deck
|
Deck bauen,Build deck
|
||||||
Werkstatt,Workshop
|
|
||||||
Erfolge,Achievements
|
|
||||||
Regeln,Rules
|
|
||||||
Einstellungen,Settings
|
Einstellungen,Settings
|
||||||
Anleitung,Tutorial
|
|
||||||
Beenden,Quit
|
Beenden,Quit
|
||||||
Zurück,Back
|
Zurück,Back
|
||||||
Weiter,Continue
|
|
||||||
Start,Start
|
Start,Start
|
||||||
Speichern,Save
|
Speichern,Save
|
||||||
Leeren,Clear
|
Leeren,Clear
|
||||||
@@ -57,9 +51,7 @@ Hand tauschen?,Swap cards?
|
|||||||
Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game.
|
Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game.
|
||||||
Fallen,Traps
|
Fallen,Traps
|
||||||
Du bist dran,Your turn
|
Du bist dran,Your turn
|
||||||
Herausforderung,Challenge
|
|
||||||
Antreten,Enter
|
Antreten,Enter
|
||||||
Statistik exportieren,Export statistics
|
|
||||||
Gegnerzüge überspringen,Skip AI turns
|
Gegnerzüge überspringen,Skip AI turns
|
||||||
Rückgängig,Undo
|
Rückgängig,Undo
|
||||||
Kapitel 1,Chapter 1
|
Kapitel 1,Chapter 1
|
||||||
@@ -95,44 +87,19 @@ Schwierigkeit,Difficulty
|
|||||||
Leicht,Easy
|
Leicht,Easy
|
||||||
Normal,Normal
|
Normal,Normal
|
||||||
Schwer,Hard
|
Schwer,Hard
|
||||||
"Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s","Your deck strength %.1f · Opponent decks %.1f · %s · Coins x%.2f · AI: %s"
|
|
||||||
schwache Archetypen,weaker archetypes
|
|
||||||
starke Archetypen,stronger archetypes
|
|
||||||
alle Archetypen,all archetypes
|
|
||||||
Vorausschau,lookahead
|
|
||||||
einfach,simple
|
|
||||||
Stärke %.1f,Strength %.1f
|
Stärke %.1f,Strength %.1f
|
||||||
"Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten","Deck strength: average of rarity, keywords and effects"
|
"Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten","Deck strength: average of rarity, keywords and effects"
|
||||||
Tipp: %s,Tip: %s
|
|
||||||
Übernehmen,Apply
|
|
||||||
Neu würfeln,Reroll
|
Neu würfeln,Reroll
|
||||||
Deck verbessern: %s ins aktive Deck (%s),Improve deck: add %s to the active deck (%s)
|
Deck verbessern: %s ins aktive Deck (%s),Improve deck: add %s to the active deck (%s)
|
||||||
Anleitung 2: Fallen,Tutorial 2: Traps
|
|
||||||
Neu würfeln (%d),Reroll (%d)
|
|
||||||
Profil zurücksetzen,Reset profile
|
Profil zurücksetzen,Reset profile
|
||||||
Große Schrift,Large text
|
Große Schrift,Large text
|
||||||
Verwerfen,Discard
|
Verwerfen,Discard
|
||||||
Weiter bearbeiten,Keep editing
|
Weiter bearbeiten,Keep editing
|
||||||
Ungespeicherte Änderungen,Unsaved changes
|
Ungespeicherte Änderungen,Unsaved changes
|
||||||
"Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",Welcome! Your hand is at the bottom. Every card costs energy (◆); you have 1 on your first turn. Click a creature that costs 1 to play it.
|
|
||||||
Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.,Creatures attack from the next turn on. End your turn with the button on the right or the space bar.
|
|
||||||
Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.,"Green frames mark creatures ready to attack. Click a creature, then Fenn or one of his creatures. Guardians must fall first."
|
|
||||||
Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.,Resonance: creatures sharing an element boost each other's attack. Play a second fire or earth creature.
|
|
||||||
Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.,Field combo: a creature next to one of yours with a different element triggers an effect. Lava deals 1 damage to the strongest enemy creature.
|
|
||||||
"Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.",Spells resolve immediately. Spark or Fireball can hit Fenn directly. Pick targets by clicking; right-click cancels.
|
|
||||||
Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!,Fenn loses at 0 life. Swap weak opening hands on your first turn (mulligan). Good luck!
|
|
||||||
"Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.",Mulligan: on your first turn you may put cards back once and redraw. Decide in the dialog what to keep.
|
|
||||||
"Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.",Traps lie face down and trigger during enemy turns. Set the Frost Trap once you have 2 energy.
|
|
||||||
"Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.","End your turn and wait. If Mara attacks you or a creature, the attack fizzles and her creature freezes."
|
|
||||||
Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.,Field combo Steam: a water creature next to a fire creature freezes the strongest enemy creature. Play both in sequence.
|
|
||||||
"Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!",Undo (U) takes back a creature you just played as long as nothing was triggered. Good luck against Mara!
|
|
||||||
"%d Siege in Folge, versuche %s","%d wins in a row, try %s"
|
|
||||||
"%d Niederlagen in Folge, versuche %s","%d losses in a row, try %s"
|
|
||||||
Duell,Duel
|
Duell,Duel
|
||||||
%d am Tisch,%d at the table
|
%d am Tisch,%d at the table
|
||||||
%d Spieler am Tisch,%d players at the table
|
%d Spieler am Tisch,%d players at the table
|
||||||
%d Münzen,%d coins
|
%d Münzen,%d coins
|
||||||
Stärke %.1f · %d Münzen,Strength %.1f · %d coins
|
|
||||||
Abbrechen,Cancel
|
Abbrechen,Cancel
|
||||||
"Ja, zurücksetzen","Yes, reset"
|
"Ja, zurücksetzen","Yes, reset"
|
||||||
"Speichern und zurück","Save and go back"
|
"Speichern und zurück","Save and go back"
|
||||||
@@ -217,3 +184,10 @@ Kapitel %d,Chapter %d
|
|||||||
Zuschauen,Watch
|
Zuschauen,Watch
|
||||||
Zuschauer,Spectator
|
Zuschauer,Spectator
|
||||||
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
"Zuschauer: Hände sind verdeckt","Spectator: hands are hidden"
|
||||||
|
Deine Deckstärke %.1f · Gegnerdecks %.1f,Your deck strength %.1f · Opponent decks %.1f
|
||||||
|
Starten,Start
|
||||||
|
1 Gegner,1 opponent
|
||||||
|
%d Gegner,%d opponents
|
||||||
|
Übernehmen,Apply
|
||||||
|
KI-Gegner,AI opponents
|
||||||
|
Sammlung,Collection
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user