Compare commits

...

4 Commits

Author SHA1 Message Date
Team3
8944fdd017 update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled
2026-09-19 13:37:12 +02:00
Team3
89db101137 Erfolge entfernt, der Bildschirm zeigt nur noch Statistik
- Die dreizehn Erfolge samt Belohnungen, Meldungen und Spielstandfeld sind weg
- Der bisherige Erfolgs-Bildschirm heißt Statistik und zeigt Kämpfe, Tagesaufgabe,
  Bestenliste, beste Karten und den CSV-Export

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
2026-09-17 21:37:58 +02:00
Team3
43073351b0 Wöchentliche Herausforderung und die Netzwerk-Hinweise entfernt
- Challenge samt Bildschirm, Menüpunkt, Erfolg "Wochenheld" und Spielstandfeld entfernt;
  record_battle verliert den challenge-Parameter, die Bestenliste bleibt
- Lobby: Erklärzeile unter dem Titel weg, "KI-Gegner (nur Host)" heißt jetzt "KI-Gegner"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
2026-09-17 21:13:08 +02:00
Team3
0f2dc22ef6 Gegnerauswahl und Kampagne aufgeräumt, Hotseat entfernt
- Einzelspieler: Schwierigkeit, darunter nur die zwei Deckstärken, Gegnerzahl als
  Schieberegler, Starten und Zurück; kein Würfeln, die Gegnerdecks entstehen beim Start
- Kampagne: nur noch Name, Elementpunkte und Knopf, mit Innenabstand in den Zeilen
- Hotseat (Lokal zu mehreren) samt Übergabe-Vorhang, Auswahlbildschirm und Tests entfernt
- Netzwerktests laufen nur mit DINGER_NETZ=1, weil die Bot-Prozesse Minuten brauchen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
2026-09-17 21:05:09 +02:00
32 changed files with 189 additions and 1203 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
1 keys en
3 Ein Kartenspiel: jeder gegen jeden A card game: free-for-all
4 Einzelspieler Single player
5 Kampagne Campaign
Lokal zu mehreren Local multiplayer
6 Netzwerkspiel Network game
7 Booster öffnen Open boosters
8 Deck bauen Build deck
Werkstatt Workshop
Erfolge Achievements
Regeln Rules
9 Einstellungen Settings
Anleitung Tutorial
10 Beenden Quit
11 Zurück Back
Weiter Continue
12 Start Start
13 Speichern Save
14 Leeren Clear
51 Klicke Karten an die du zurücklegen willst. Einmal pro Partie. Click the cards you want to put back. Once per game.
52 Fallen Traps
53 Du bist dran Your turn
Herausforderung Challenge
54 Antreten Enter
Statistik exportieren Export statistics
55 Gegnerzüge überspringen Skip AI turns
56 Rückgängig Undo
57 Kapitel 1 Chapter 1
87 Leicht Easy
88 Normal Normal
89 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
90 Stärke %.1f Strength %.1f
91 Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten Deck strength: average of rarity, keywords and effects
Tipp: %s Tip: %s
Übernehmen Apply
92 Neu würfeln Reroll
93 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)
94 Profil zurücksetzen Reset profile
95 Große Schrift Large text
96 Verwerfen Discard
97 Weiter bearbeiten Keep editing
98 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
99 Duell Duel
100 %d am Tisch %d at the table
101 %d Spieler am Tisch %d players at the table
102 %d Münzen %d coins
Stärke %.1f · %d Münzen Strength %.1f · %d coins
103 Abbrechen Cancel
104 Ja, zurücksetzen Yes, reset
105 Speichern und zurück Save and go back
184 Zuschauen Watch
185 Zuschauer Spectator
186 Zuschauer: Hände sind verdeckt Spectator: hands are hidden
187 Deine Deckstärke %.1f · Gegnerdecks %.1f Your deck strength %.1f · Opponent decks %.1f
188 Starten Start
189 1 Gegner 1 opponent
190 %d Gegner %d opponents
191 Übernehmen Apply
192 KI-Gegner AI opponents
193 Sammlung Collection

Binary file not shown.