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
|
||||
- name: Import
|
||||
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
|
||||
- name: Ergebnis
|
||||
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
|
||||
- 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)
|
||||
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern, Erfolge, Tagesaufgabe
|
||||
- Lokal zu mehreren: bis zu 4 Menschen am selben Rechner plus KI
|
||||
- Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
|
||||
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern je Kapitel, Tagesaufgabe
|
||||
- Booster, fünf Deck-Slots, Sammlung (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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)
|
||||
- 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
|
||||
- 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
|
||||
- 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)
|
||||
- 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)
|
||||
- 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-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
|
||||
- Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
||||
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
||||
- 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()`)
|
||||
- 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
|
||||
- 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)
|
||||
- Große Schrift: Kartennamen neben dem Kostenkreis, Kartentext läuft nicht mehr über, breitere Knöpfe in Booster, Deckbau und Werkstatt
|
||||
- Übersetzungsprüfung: jeder tr()-Text und jeder Anleitungsschritt braucht einen englischen Eintrag
|
||||
- Große Schrift: Kartennamen neben dem Kostenkreis, Kartentext läuft nicht mehr über, breitere Knöpfe in Booster, Deckbau und Sammlung
|
||||
|
||||
## Tests
|
||||
|
||||
`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).
|
||||
`DINGER_FAST=1` lässt den Fenster-Test ohne Hotseat, Netzwerk und lange Kämpfe laufen (etwa ein Drittel der Zeit).
|
||||
Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren.
|
||||
`DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit).
|
||||
|
||||
```
|
||||
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/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_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
|
||||
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
|
||||
@@ -94,14 +88,12 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
||||
## Aufbau
|
||||
|
||||
- `scripts/cards.gd` – Elemente, Kombos, Kartenklassen; Pool aus `data/karten.json`
|
||||
- `scripts/game.gd` – Spielregeln, Züge, 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/netstate.gd` – Zustandsbilder fürs Netzwerk
|
||||
- `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/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
|
||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||
- `scripts/challenge.gd` – wöchentliche Herausforderungen
|
||||
- `translations/ui.csv` – englische Oberflächentexte
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
- PC-Spiel
|
||||
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
|
||||
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
|
||||
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
||||
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich, auch als Zuschauer
|
||||
- Kampagne gegen benannte Magier und Tagesaufgaben als Fortschritt
|
||||
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
|
||||
- Ein unterbrochener Kampf lässt sich später fortsetzen
|
||||
- Geführte erste Partie als Einstieg
|
||||
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel
|
||||
- Bestenliste als Langzeitziel
|
||||
|
||||
## Sammeln
|
||||
- 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 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 players: Array = [] # in Zugreihenfolge
|
||||
var winner_player: Player = null
|
||||
@@ -746,19 +746,3 @@ static func new_battle_with(deck: Array, opps: Array, r: RandomNumberGenerator =
|
||||
p.max_life = 35
|
||||
g.start(fixed_order)
|
||||
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
|
||||
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 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": ""},
|
||||
"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_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 := [
|
||||
{"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},
|
||||
@@ -64,18 +47,14 @@ var collection := {} # id -> Anzahl
|
||||
var decks: Array = [] # DECK_SLOTS Einträge {"name": String, "cards": Array von ids}
|
||||
var active_deck := 0
|
||||
var stats := {"games": 0, "wins": 0, "boosters": 0, "wins_by_opp": {}}
|
||||
var achievements: Array = []
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
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 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 booster_since_mythic := 0
|
||||
var tutorial_done := false
|
||||
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 challenge_week := -1 # Woche der zuletzt bestandenen Herausforderung
|
||||
|
||||
## Das aktive Deck (Kartenliste des gewählten Slots).
|
||||
var deck: Array:
|
||||
@@ -153,8 +132,6 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
p.stats.boosters = int(st.get("boosters", 0))
|
||||
for k in st.get("wins_by_opp", {}):
|
||||
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))
|
||||
var dl = d.get("daily", {})
|
||||
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]
|
||||
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.tutorial_done = bool(d.get("tutorial_done", false))
|
||||
if version >= 5:
|
||||
p.perk = str(d.get("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)),
|
||||
"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))}
|
||||
p.challenge_week = int(d.get("challenge_week", -1))
|
||||
p.tutorial2_done = bool(d.get("tutorial2_done", false))
|
||||
return p
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
||||
"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,
|
||||
"booster_since_mythic": booster_since_mythic, "tutorial_done": tutorial_done,
|
||||
"perk": perk, "records": records, "challenge_week": challenge_week, "tutorial2_done": tutorial2_done}
|
||||
"booster_since_mythic": booster_since_mythic,
|
||||
"perk": perk, "records": records}
|
||||
|
||||
|
||||
## 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]
|
||||
|
||||
|
||||
## 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:
|
||||
return Cards.all().filter(func(c): return collection.get(c.id, 0) > 0)
|
||||
|
||||
@@ -298,7 +260,7 @@ func distinct_owned() -> int:
|
||||
return n
|
||||
|
||||
|
||||
# --- Werkstatt --------------------------------------------------------------------
|
||||
# --- Zerlegen und Herstellen ------------------------------------------------------
|
||||
|
||||
## Zerlegt n Kopien in Staub. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func dismantle(cid: String, n: int = 1) -> String:
|
||||
@@ -323,7 +285,6 @@ func craft(cid: String) -> String:
|
||||
dust -= cost
|
||||
collection[cid] = collection.get(cid, 0) + 1
|
||||
daily_event("craft")
|
||||
check_achievements()
|
||||
return ""
|
||||
|
||||
|
||||
@@ -407,17 +368,16 @@ func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Arra
|
||||
collection[c.id] = collection.get(c.id, 0) + 1
|
||||
stats.boosters += 1
|
||||
daily_event("booster")
|
||||
check_achievements()
|
||||
return cards
|
||||
|
||||
|
||||
# --- Kampfergebnis, Erfolge, Tagesaufgabe -------------------------------------------
|
||||
# --- Kampfergebnis, Tagesaufgabe -------------------------------------------
|
||||
|
||||
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
||||
## 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 = [],
|
||||
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
|
||||
records.margins.append(maxi(remaining_life, 0) if won else 0)
|
||||
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.loss_streak += 1
|
||||
var tip := suggest_difficulty()
|
||||
if tip != "" and stage < 0 and challenge.is_empty():
|
||||
if tip != "" and stage < 0:
|
||||
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])
|
||||
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
|
||||
reward = Campaign.stage(stage).reward
|
||||
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
|
||||
daily_event("game")
|
||||
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")
|
||||
for e in Game.deck_elements(deck_used):
|
||||
daily_event("win_element:" + e)
|
||||
check_achievements({"won": won, "n_opp": n_opp, "deck": deck_used})
|
||||
return reward
|
||||
|
||||
|
||||
@@ -476,55 +431,6 @@ func _stat(cid: String) -> Dictionary:
|
||||
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:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
@@ -606,26 +512,6 @@ func perk_unlocked() -> bool:
|
||||
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:
|
||||
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:
|
||||
if screen.handover.visible:
|
||||
screen.take_over()
|
||||
return
|
||||
if screen.mulligan.visible:
|
||||
screen.do_mulligan([])
|
||||
return
|
||||
@@ -120,6 +117,19 @@ func confirm() -> void:
|
||||
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:
|
||||
Style.clear(screen.cursor_layer)
|
||||
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)
|
||||
var item = items[index]
|
||||
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.size = r.size + Vector2(12, 12)
|
||||
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
class_name BattleScreen
|
||||
extends Control
|
||||
## 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 n_opp: int
|
||||
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 online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||
var net: Net
|
||||
@@ -28,11 +25,6 @@ var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
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 gp_zone: String:
|
||||
get:
|
||||
@@ -40,8 +32,6 @@ var gp_zone: String:
|
||||
var gp_index: int:
|
||||
get:
|
||||
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 smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
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 confirm: Control
|
||||
var b_save_leave: Button
|
||||
var handover: Control
|
||||
var handover_label: Label
|
||||
var mulligan: BattleMulligan
|
||||
var mulligan_box: Control:
|
||||
get:
|
||||
@@ -102,26 +90,9 @@ var mulligan_selected: Array:
|
||||
get:
|
||||
return mulligan.selected
|
||||
var cursor_layer: Control
|
||||
var tutorial_panel: Panel
|
||||
var tutorial_text: RichTextLabel
|
||||
var b_tut_next: Button
|
||||
var wait_label: Label
|
||||
var b_skip: 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:
|
||||
@@ -129,13 +100,6 @@ func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||
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:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
b.net_play = true
|
||||
@@ -143,38 +107,14 @@ static func online(seat: int) -> BattleScreen:
|
||||
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).
|
||||
static func resume(data: Dictionary) -> BattleScreen:
|
||||
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.saved_game = Game.from_snapshot(data.game)
|
||||
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:
|
||||
return net_play
|
||||
|
||||
@@ -189,7 +129,7 @@ func is_client() -> bool:
|
||||
|
||||
|
||||
func counts_for_profile() -> bool:
|
||||
return not is_hotseat() and not is_online()
|
||||
return not is_online()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -213,24 +153,9 @@ func _ready() -> void:
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif saved_game != null:
|
||||
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:
|
||||
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())
|
||||
elif not preset_opponents.is_empty():
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), preset_opponents, null, false, own_rules())
|
||||
else:
|
||||
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()
|
||||
@@ -273,22 +198,6 @@ func _build_static() -> void:
|
||||
log_box.add_theme_font_size_override("normal_font_size", 13)
|
||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
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)
|
||||
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")
|
||||
@@ -313,7 +222,6 @@ func _build_static() -> void:
|
||||
trap_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
highlight_layer = _layer()
|
||||
zoom_layer = _layer()
|
||||
fx_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.add_theme_font_size_override("font_size", 14)
|
||||
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:
|
||||
@@ -355,15 +257,15 @@ func _overlay() -> Control:
|
||||
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:
|
||||
return counts_for_profile() and not tutorial
|
||||
return counts_for_profile()
|
||||
|
||||
|
||||
func save_battle() -> void:
|
||||
if not resumable() or game == null or game.winner != "":
|
||||
return
|
||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "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)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(d))
|
||||
@@ -380,12 +282,6 @@ func leave() -> void:
|
||||
net.leave()
|
||||
if resumable() and game != null and game.winner == "":
|
||||
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())
|
||||
|
||||
|
||||
@@ -475,7 +371,7 @@ func my_turn() -> 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:
|
||||
@@ -506,14 +402,10 @@ func _input(event: InputEvent) -> void:
|
||||
if b_undo.visible:
|
||||
undo()
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
if handover.visible:
|
||||
take_over()
|
||||
elif mulligan.visible:
|
||||
if mulligan.visible:
|
||||
do_mulligan([])
|
||||
elif overlay.visible:
|
||||
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():
|
||||
var idx: int = event.keycode - KEY_1
|
||||
@@ -552,12 +444,7 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
var cur := game.current()
|
||||
if cur.is_human:
|
||||
if cur != perspective and is_hotseat() and not handover.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:
|
||||
if cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible:
|
||||
show_mulligan()
|
||||
return
|
||||
if is_client():
|
||||
@@ -606,13 +493,8 @@ func _exit_tree() -> void:
|
||||
func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
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
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor, game.human.life)
|
||||
if tutorial:
|
||||
if tutorial_part == 1:
|
||||
app.profile.tutorial_done = true
|
||||
else:
|
||||
app.profile.tutorial2_done = true
|
||||
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, factor, game.human.life)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
@@ -623,17 +505,6 @@ func _finish() -> void:
|
||||
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:
|
||||
msg_t = 2.5
|
||||
msg_label.text = m
|
||||
@@ -908,30 +779,6 @@ func _note_events(evs: Array) -> void:
|
||||
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 --------------------------------------------------------------------
|
||||
|
||||
func refresh() -> void:
|
||||
@@ -1084,9 +931,6 @@ func refresh() -> void:
|
||||
else:
|
||||
lines.append(line)
|
||||
log_box.text = "\n".join(lines)
|
||||
if tutorial:
|
||||
_tutorial_refresh()
|
||||
_tutorial_highlights(mine)
|
||||
pad.draw_cursor()
|
||||
|
||||
Style.clear(zoom_layer)
|
||||
@@ -1110,7 +954,6 @@ func refresh() -> void:
|
||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
||||
overlay.visible = true
|
||||
confirm.visible = false
|
||||
handover.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]
|
||||
app.save())
|
||||
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()
|
||||
for k in Campaign.STAGES.size():
|
||||
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,
|
||||
Style.GOLD if next else null, 2 if next else 0))
|
||||
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)
|
||||
Style.clabel(r, str(i + 1), 18, Style.GOLD if next else Style.MUTED, "center", true, 36)
|
||||
Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 290)
|
||||
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
|
||||
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
|
||||
var dots := Control.new()
|
||||
dots.custom_minimum_size = Vector2(50, 0)
|
||||
dots.custom_minimum_size = Vector2(60, 0)
|
||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var elems: Array = st.elements
|
||||
dots.draw.connect(func():
|
||||
for m in elems.size():
|
||||
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
||||
r.add_child(dots)
|
||||
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:
|
||||
Style.clabel(r, "abgeschlossen", 13, Style.GREEN, "right", false, 100)
|
||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||
elif next:
|
||||
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
||||
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
||||
else:
|
||||
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||
b.disabled = true
|
||||
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
|
||||
## Werkstatt: Karten zerlegen (Staub) und herstellen.
|
||||
## Sammlung: alle Karten mit Besitzstand; ausgewählte Karten lassen sich
|
||||
## zerlegen (Staub) oder herstellen.
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
@@ -18,7 +19,7 @@ var b_extras: Button
|
||||
|
||||
|
||||
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,
|
||||
14, Style.MUTED, Rect2(240, 18, 600, 24))
|
||||
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 have := deck.count(card.id)
|
||||
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:
|
||||
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
||||
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()
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
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)
|
||||
var hr := Style.row(col, 12)
|
||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
@@ -40,7 +39,7 @@ func _ready() -> void:
|
||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
||||
Style.spacer(col, 6)
|
||||
Style.clabel(col, "KI-Gegner (nur Host)", 16, Style.MUTED)
|
||||
Style.clabel(col, "KI-Gegner", 16, Style.MUTED)
|
||||
var ar := Style.row(col, 10)
|
||||
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for n in range(0, 5):
|
||||
|
||||
@@ -4,10 +4,7 @@ extends Control
|
||||
var app
|
||||
var b_single: Button
|
||||
var b_campaign: Button
|
||||
var b_hotseat: Button
|
||||
var b_online: Button
|
||||
var b_tutorial: Button
|
||||
var b_challenge: Button
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
@@ -35,23 +32,14 @@ func _ready() -> void:
|
||||
center.add_child(grid)
|
||||
col.add_child(center)
|
||||
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()):
|
||||
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_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_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, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
||||
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.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, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
|
||||
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
||||
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
||||
Style.spacer(col, 10)
|
||||
@@ -77,10 +65,7 @@ func refresh() -> void:
|
||||
var ok: Array = p.deck_valid()
|
||||
b_single.disabled = not ok[0]
|
||||
b_campaign.disabled = not ok[0]
|
||||
b_tutorial.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
|
||||
for cid in p.collection:
|
||||
total += p.collection[cid]
|
||||
@@ -95,12 +80,6 @@ func refresh() -> void:
|
||||
else:
|
||||
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])
|
||||
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()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -1,109 +1,79 @@
|
||||
class_name OpponentSelectScreen
|
||||
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 rating_label: Label
|
||||
var tip_label: Label
|
||||
var diff_buttons := {}
|
||||
var previews := {} # n -> Array von Game.Player
|
||||
var preview_labels := {}
|
||||
var b_tip: Button
|
||||
var b_reroll: Button
|
||||
var rerolls_left := 3
|
||||
var count_slider: HSlider
|
||||
var count_label: Label
|
||||
var info_label: Label
|
||||
|
||||
|
||||
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, "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)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in ["leicht", "normal", "schwer"]:
|
||||
var k: String = key
|
||||
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)
|
||||
var tr_row := Style.row(col, 10)
|
||||
tr_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
tip_label = Style.clabel(tr_row, "", 14, Style.GOLD, "right", false, 520)
|
||||
b_tip = Style.cbutton(tr_row, "Übernehmen", Vector2(140, 32), take_tip)
|
||||
Style.spacer(col, 2)
|
||||
for i in range(1, Game.MAX_OPPONENTS + 1):
|
||||
var n := i
|
||||
var r := Style.row(col, 16)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(150, 46), func(): start(n), Color("285a3c"))
|
||||
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
||||
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
||||
preview_labels[n].autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
preview_labels[n].custom_minimum_size = Vector2(600, 0)
|
||||
preview_labels[n].size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.spacer(col, 4)
|
||||
rating_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 24)
|
||||
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
|
||||
var sr := Style.row(col, 12)
|
||||
sr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
|
||||
count_slider = HSlider.new()
|
||||
count_slider.min_value = 1
|
||||
count_slider.max_value = Game.MAX_OPPONENTS
|
||||
count_slider.step = 1
|
||||
count_slider.value = 1
|
||||
count_slider.tick_count = Game.MAX_OPPONENTS
|
||||
count_slider.ticks_on_borders = true
|
||||
count_slider.custom_minimum_size = Vector2(420, 32)
|
||||
count_slider.value_changed.connect(func(_v): refresh())
|
||||
sr.add_child(count_slider)
|
||||
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
|
||||
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 30)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_reroll = Style.cbutton(br, "Neu würfeln", Vector2(190, 40), reroll)
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 40), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh(true)
|
||||
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
|
||||
func set_difficulty(key: String) -> void:
|
||||
app.profile.settings.difficulty = key
|
||||
app.save()
|
||||
refresh(true)
|
||||
refresh()
|
||||
|
||||
|
||||
func take_tip() -> void:
|
||||
var tip: String = app.profile.suggest_difficulty()
|
||||
if tip != "":
|
||||
set_difficulty(tip)
|
||||
func opponents() -> int:
|
||||
return int(count_slider.value)
|
||||
|
||||
|
||||
## Höchstens dreimal neu würfeln, sonst ließen sich schwache Gegner erwürfeln.
|
||||
func reroll() -> void:
|
||||
if rerolls_left <= 0:
|
||||
return
|
||||
rerolls_left -= 1
|
||||
refresh(true)
|
||||
func start() -> void:
|
||||
app.show_screen(BattleScreen.new(opponents()))
|
||||
|
||||
|
||||
func start(n: int) -> 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:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
var d := p.difficulty()
|
||||
for key in diff_buttons:
|
||||
var b: Button = diff_buttons[key]
|
||||
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("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 · %s · Münzen x%.2f · KI: %s") % [
|
||||
p.deck_rating(), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
|
||||
var tip := p.suggest_difficulty()
|
||||
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)
|
||||
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f") % [p.deck_rating(), p.target_rating()]
|
||||
var n := opponents()
|
||||
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
||||
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
|
||||
|
||||
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())
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
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) --------------------------
|
||||
|
||||
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_spiele_enden()
|
||||
test_spielstand_migration()
|
||||
test_erfolge_und_tagesaufgabe()
|
||||
test_tagesaufgabe()
|
||||
test_kampagne()
|
||||
test_fallen()
|
||||
test_ki_sieht_fallen_nicht()
|
||||
test_hotseat()
|
||||
test_zufallssamen()
|
||||
test_deck_slots_und_werkstatt()
|
||||
test_spielstand_sicherung()
|
||||
@@ -109,7 +108,7 @@ func _initialize() -> void:
|
||||
test_deck_umbenennen_und_auffuellen()
|
||||
test_neue_fallen()
|
||||
test_kapitel_zwei_und_merkmalwahl()
|
||||
test_herausforderung_und_bestenliste()
|
||||
test_bestenliste()
|
||||
test_deckstaerke_und_schwierigkeit()
|
||||
test_starter_gegen_passende_gegner()
|
||||
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 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.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")
|
||||
p.stats.wins = 3
|
||||
p.achievements.append("erster_sieg")
|
||||
p.campaign_stage = 2
|
||||
p.settings.volume = 0.3
|
||||
var d := p.to_dict()
|
||||
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
|
||||
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")
|
||||
|
||||
|
||||
func test_erfolge_und_tagesaufgabe() -> void:
|
||||
func test_tagesaufgabe() -> void:
|
||||
Profile.today_override = "2026-09-16"
|
||||
var p := Profile.starter()
|
||||
var coins := p.coins
|
||||
var reward := p.record_battle(true, 1, p.deck_cards())
|
||||
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(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")
|
||||
check(p.coins == coins + reward + (p.daily_task().reward if p.daily.done else 0), "Münzen verbucht")
|
||||
p.take_notices()
|
||||
check(p.notices.is_empty(), "Meldungen abgeholt")
|
||||
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()
|
||||
check(p.daily.date == "2026-09-17" and not p.daily.done and p.daily.progress == 0, "neuer Tag, neue Aufgabe")
|
||||
Profile.today_override = ""
|
||||
# Mono-Deck-Erfolg
|
||||
var mono := []
|
||||
for cid in ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche", "f_glutpfeil", "f_elementar"]:
|
||||
for i in 3:
|
||||
mono.append(Cards.by_id(cid))
|
||||
mono = mono.slice(0, 20)
|
||||
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")
|
||||
|
||||
|
||||
@@ -720,7 +713,7 @@ func test_kampagne() -> void:
|
||||
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
|
||||
p.campaign_stage = 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 guard := 0
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
Game.test_seed = 77
|
||||
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")
|
||||
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")
|
||||
# Werkstatt
|
||||
# Zerlegen und Herstellen
|
||||
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", 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")
|
||||
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")
|
||||
var top := p.top_cards(3)
|
||||
check(top.size() == 3 and top[0].wins == 1, "Bestenliste der Karten")
|
||||
# Pity-Zähler
|
||||
p.coins = 100000
|
||||
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")
|
||||
p.campaign_stage = 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())))
|
||||
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
|
||||
|
||||
|
||||
func test_herausforderung_und_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")
|
||||
func test_bestenliste() -> void:
|
||||
Profile.today_override = "2026-09-16"
|
||||
var mono := []
|
||||
for i in 20:
|
||||
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()
|
||||
r.seed = 4
|
||||
var g := Game.new_battle(mono, 3, r, Challenge.LIST[0].rules)
|
||||
check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Eile: 20 Leben und Startenergie für alle")
|
||||
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), "Regelvariante: 20 Leben und Startenergie für alle")
|
||||
check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie")
|
||||
var p := Profile.starter()
|
||||
var coins := p.coins
|
||||
var reward := p.record_battle(true, 3, mono, -1, [], 7, Challenge.LIST[0])
|
||||
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")
|
||||
p.record_battle(true, 3, mono, -1, [], 7)
|
||||
p.record_battle(true, 3, mono, -1, [], 9)
|
||||
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)
|
||||
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
|
||||
for i in 5:
|
||||
p.record_battle(true, 1, mono, -1, [], 10)
|
||||
check("serie" in p.achievements, "Siegesserie-Erfolg")
|
||||
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))
|
||||
check(p.records.best_streak == 5, "neue beste Serie")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
|
||||
check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert")
|
||||
@@ -1297,17 +1242,17 @@ func test_kurve_statistik_vorschlag_verbesserung() -> void:
|
||||
var mono := starter
|
||||
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
|
||||
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")
|
||||
p.records.streak = 0
|
||||
p.records.margins = []
|
||||
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")
|
||||
p.records.streak = 0
|
||||
p.records.margins = []
|
||||
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")
|
||||
p.settings.difficulty = "schwer"
|
||||
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(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())))
|
||||
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.
|
||||
@@ -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")
|
||||
|
||||
|
||||
## 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:
|
||||
var keys := {}
|
||||
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")):
|
||||
if not keys.has(m.get_string(1)) and not (m.get_string(1) in missing):
|
||||
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])
|
||||
|
||||
227
tests/ui_test.gd
227
tests/ui_test.gd
@@ -164,11 +164,15 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
await frames(3)
|
||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||
var suite := [test_menu_und_regeln, 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,
|
||||
var suite := [test_menu, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||
test_kampagne, test_sammlung, test_sprache, test_kampf_steuerung,
|
||||
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
|
||||
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)
|
||||
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_host_wechsel,test_zuschauer
|
||||
if only != "":
|
||||
@@ -183,14 +187,10 @@ func _ready() -> void:
|
||||
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(find_button("Einzelspieler") != null and find_button("Beenden") != null, "Menüknöpfe vorhanden")
|
||||
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:
|
||||
@@ -198,13 +198,8 @@ func test_booster() -> void:
|
||||
var scr = screen()
|
||||
check(scr is BoosterScreen, "Boosterbildschirm")
|
||||
var coins: int = main.profile.coins
|
||||
var ach_before: int = main.profile.achievements.size()
|
||||
await click(scr.b_buy)
|
||||
var bonus := 0
|
||||
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(main.profile.coins == coins - Profile.BOOSTER_COST, "Booster bezahlt")
|
||||
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
|
||||
await shot("03_booster_zu")
|
||||
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:
|
||||
await click(find_button("Einzelspieler"))
|
||||
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:
|
||||
await click(find_button("Einzelspieler"))
|
||||
var sel = screen()
|
||||
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
||||
check("Deckstärke" in sel.rating_label.text, "Deckstärke angezeigt")
|
||||
check(sel.preview_labels[3].text.count("(") == 3 and sel.previews[3].size() == 3, "Vorschau zeigt drei Gegner mit Stärke")
|
||||
for i in 3:
|
||||
await click(sel.b_reroll)
|
||||
check(sel.rerolls_left == 0 and sel.b_reroll.disabled, "höchstens dreimal neu würfeln")
|
||||
check("Deckstärke" in sel.rating_label.text and "Gegnerdecks" in sel.rating_label.text
|
||||
and not ("Archetypen" in sel.rating_label.text or "KI" in sel.rating_label.text), "unter der Schwierigkeit nur die zwei Stärken")
|
||||
check(find_button("Neu würfeln") == null and find_button("1 Gegner") == null and find_button("Starten") != null, "kein Würfeln und keine Gegnerknöpfe, nur Starten")
|
||||
check(sel.count_slider.min_value == 1 and sel.count_slider.max_value == Game.MAX_OPPONENTS and sel.count_slider.step == 1, "Gegnerzahl als Schieberegler von 1 bis %d" % Game.MAX_OPPONENTS)
|
||||
check(sel.opponents() == 1 and sel.count_label.text == "1 Gegner" and "Duell" in sel.info_label.text, "Regler beginnt beim Duell")
|
||||
var rect: Rect2 = sel.count_slider.get_global_rect()
|
||||
await click_at(Vector2(rect.end.x - 4, rect.get_center().y)) # ganz rechts anklicken
|
||||
check(sel.opponents() == Game.MAX_OPPONENTS and sel.count_label.text == "%d Gegner" % Game.MAX_OPPONENTS, "Mausklick am rechten Rand stellt %d Gegner ein" % Game.MAX_OPPONENTS)
|
||||
sel.count_slider.grab_focus()
|
||||
await key(KEY_LEFT)
|
||||
check(sel.opponents() == Game.MAX_OPPONENTS - 1, "Pfeiltaste links nimmt einen Gegner weg")
|
||||
sel.count_slider.value = 4
|
||||
await frames(1)
|
||||
check(sel.opponents() == 4 and sel.count_label.text == "4 Gegner" and "5 am Tisch" in sel.info_label.text
|
||||
and str(Profile.REWARD_WIN[4]) in sel.info_label.text, "Regler auf 4: fünf am Tisch mit Belohnung")
|
||||
await click(find_button("Schwer"))
|
||||
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 click(find_button("1 Gegner"))
|
||||
sel.count_slider.value = 1
|
||||
await frames(1)
|
||||
await click(find_button("Starten"))
|
||||
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 opp: Game.Player = hard.game.opponents[0]
|
||||
var opp_rating := Game.deck_rating(opp.deck + opp.hand)
|
||||
@@ -399,22 +407,13 @@ func test_gegnerauswahl() -> void:
|
||||
await give_up()
|
||||
await click(find_button("Einzelspieler"))
|
||||
await click(find_button("Leicht"))
|
||||
await click(find_button("1 Gegner"))
|
||||
await click(find_button("Starten"))
|
||||
var easy = screen()
|
||||
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")
|
||||
await give_up()
|
||||
await click(find_button("Einzelspieler"))
|
||||
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"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
for n in [1, 5]:
|
||||
@@ -565,23 +564,10 @@ func test_einstellungen() -> void:
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_erfolge() -> void:
|
||||
await click(find_button("Erfolge"))
|
||||
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"))
|
||||
func test_sammlung() -> void:
|
||||
await click(find_button("Sammlung"))
|
||||
var scr = screen()
|
||||
check(scr is WorkshopScreen, "Werkstatt geöffnet")
|
||||
check(scr is CollectionScreen, "Sammlung geöffnet")
|
||||
var p: Profile = main.profile
|
||||
p.dust = 0
|
||||
var in_decks := {}
|
||||
@@ -604,8 +590,8 @@ func test_werkstatt() -> void:
|
||||
await click(phoenix)
|
||||
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(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Werkstatt gespeichert")
|
||||
await shot("20_werkstatt")
|
||||
check(Profile.load_or_new(SAVE).collection.get("ff_phoenix", 0) == 1, "Sammlung gespeichert")
|
||||
await shot("20_sammlung")
|
||||
await click(find_button("Zurück"))
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
await click(find_button("Einstellungen"))
|
||||
await click(find_button("Profil zurücksetzen"))
|
||||
@@ -683,64 +634,10 @@ func test_profil_zuruecksetzen() -> void:
|
||||
check(not scr.confirm.visible, "abgebrochen")
|
||||
await click(find_button("Profil 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")
|
||||
|
||||
|
||||
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:
|
||||
await click(find_button("Einstellungen"))
|
||||
var scr = screen()
|
||||
@@ -765,46 +662,35 @@ func test_sprache() -> void:
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_anleitung() -> void:
|
||||
check(not main.profile.tutorial_done, "Anleitung noch offen")
|
||||
await click(find_button("Anleitung"))
|
||||
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
|
||||
func test_kampf_steuerung() -> void:
|
||||
await start_battle(1)
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen and scr.tutorial, "Anleitung gestartet")
|
||||
check(scr is BattleScreen, "Kampf gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
await wait_for_human(scr)
|
||||
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
|
||||
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
|
||||
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:
|
||||
var played_id: String = g.human.hand[idx].id
|
||||
var board_before: int = g.human.board.size()
|
||||
await key(KEY_1 + idx)
|
||||
check(g.human.board.size() == 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")
|
||||
check(g.human.board.size() == board_before + 1, "Wesen per Taste gespielt")
|
||||
# Rückgängig: Wesen zurück auf die Hand
|
||||
check(scr.b_undo.visible, "Rückgängig angeboten")
|
||||
var energy: int = g.human.energy
|
||||
await click(scr.b_undo)
|
||||
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")
|
||||
await key(KEY_1 + idx)
|
||||
check(g.human.board.size() == 1, "erneut gespielt")
|
||||
await shot("22_anleitung")
|
||||
check(g.human.board.size() == board_before + 1, "erneut gespielt")
|
||||
await shot("22_kampf_steuerung")
|
||||
# Gamepad: Zeiger bewegen, Karte spielen
|
||||
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")
|
||||
@@ -816,9 +702,9 @@ func test_anleitung() -> void:
|
||||
if pad_idx >= 0:
|
||||
while scr.gp_index != pad_idx:
|
||||
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)
|
||||
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
|
||||
await key(KEY_SPACE)
|
||||
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)
|
||||
scr.ai_delay = 0.0
|
||||
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)
|
||||
check(scr.confirm.visible, "Esc öffnet Aufgeben")
|
||||
await key(KEY_ESCAPE)
|
||||
check(not scr.confirm.visible, "Esc schließt den Dialog")
|
||||
await give_up()
|
||||
check(main.profile.tutorial_done, "Anleitung als erledigt gespeichert")
|
||||
|
||||
|
||||
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,
|
||||
"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")
|
||||
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
|
||||
await leave_battle()
|
||||
else:
|
||||
|
||||
@@ -3,18 +3,12 @@ Dinger,Dinger
|
||||
Ein Kartenspiel: jeder gegen jeden,A card game: free-for-all
|
||||
Einzelspieler,Single player
|
||||
Kampagne,Campaign
|
||||
Lokal zu mehreren,Local multiplayer
|
||||
Netzwerkspiel,Network game
|
||||
Booster öffnen,Open boosters
|
||||
Deck bauen,Build deck
|
||||
Werkstatt,Workshop
|
||||
Erfolge,Achievements
|
||||
Regeln,Rules
|
||||
Einstellungen,Settings
|
||||
Anleitung,Tutorial
|
||||
Beenden,Quit
|
||||
Zurück,Back
|
||||
Weiter,Continue
|
||||
Start,Start
|
||||
Speichern,Save
|
||||
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.
|
||||
Fallen,Traps
|
||||
Du bist dran,Your turn
|
||||
Herausforderung,Challenge
|
||||
Antreten,Enter
|
||||
Statistik exportieren,Export statistics
|
||||
Gegnerzüge überspringen,Skip AI turns
|
||||
Rückgängig,Undo
|
||||
Kapitel 1,Chapter 1
|
||||
@@ -95,44 +87,19 @@ Schwierigkeit,Difficulty
|
||||
Leicht,Easy
|
||||
Normal,Normal
|
||||
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
|
||||
"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
|
||||
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
|
||||
Große Schrift,Large text
|
||||
Verwerfen,Discard
|
||||
Weiter bearbeiten,Keep editing
|
||||
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
|
||||
%d am Tisch,%d at the table
|
||||
%d Spieler am Tisch,%d players at the table
|
||||
%d Münzen,%d coins
|
||||
Stärke %.1f · %d Münzen,Strength %.1f · %d coins
|
||||
Abbrechen,Cancel
|
||||
"Ja, zurücksetzen","Yes, reset"
|
||||
"Speichern und zurück","Save and go back"
|
||||
@@ -217,3 +184,10 @@ Kapitel %d,Chapter %d
|
||||
Zuschauen,Watch
|
||||
Zuschauer,Spectator
|
||||
"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