update
This commit is contained in:
15
README.md
15
README.md
@@ -29,44 +29,38 @@ Der Spielstand liegt unter `user://spielstand.json`
|
||||
- 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 je Kapitel, Tagesaufgabe
|
||||
- Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
|
||||
- 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
|
||||
- Bestenliste (Serie, schnellster Sieg) und 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) werden laufend gespeichert und lassen sich im Menü fortsetzen
|
||||
- Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
||||
- Gamepad im Deckbau, in der Sammlung und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
|
||||
- Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`)
|
||||
- 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 die langen Kämpfe laufen (etwa ein Drittel der Zeit).
|
||||
Statistiken aus Spieltests lassen sich unter Statistik als CSV exportieren.
|
||||
|
||||
```
|
||||
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren (macht start.sh auch)
|
||||
@@ -100,7 +94,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Tagesaufgabe, Einstellungen
|
||||
- `scripts/campaign.gd` – Kampagnenstufen
|
||||
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Statistik, Einstellungen, Regeln, Ton
|
||||
- `scenes/main.tscn` – Startszene
|
||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||
- `translations/ui.csv` – englische Oberflächentexte
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
- 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
|
||||
- Bestenliste als Langzeitziel
|
||||
|
||||
## Sammeln
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class_name Profile
|
||||
extends RefCounted
|
||||
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Statistik, 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,7 +20,7 @@ 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}
|
||||
|
||||
@@ -53,9 +53,7 @@ var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": tru
|
||||
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": []}
|
||||
|
||||
## Das aktive Deck (Kartenliste des gewählten Slots).
|
||||
@@ -151,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):
|
||||
@@ -160,7 +157,6 @@ 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.tutorial2_done = bool(d.get("tutorial2_done", false))
|
||||
return p
|
||||
|
||||
|
||||
@@ -169,8 +165,8 @@ func to_dict() -> Dictionary:
|
||||
"decks": decks, "active_deck": active_deck,
|
||||
"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, "tutorial2_done": tutorial2_done}
|
||||
"booster_since_mythic": booster_since_mythic,
|
||||
"perk": perk, "records": records}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
@@ -252,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)
|
||||
|
||||
@@ -276,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:
|
||||
@@ -528,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()
|
||||
|
||||
|
||||
@@ -117,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():
|
||||
@@ -127,7 +140,7 @@ func draw_cursor() -> void:
|
||||
index = clampi(index, 0, items.size() - 1)
|
||||
var item = items[index]
|
||||
var r: Rect2 = Rect2(item.slot, item.size) if item is CreatureView else Rect2(item.position, item.size)
|
||||
var hl := BattleScreen.Highlight.new()
|
||||
var hl := Highlight.new()
|
||||
hl.position = r.position - Vector2(6, 6)
|
||||
hl.size = r.size + Vector2(12, 12)
|
||||
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
class_name BattleScreen
|
||||
extends Control
|
||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||
## Modi: Einzelspieler, Kampagne, Anleitung, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
|
||||
var app
|
||||
var n_opp: int
|
||||
var stage := -1 # Kampagnenstufe oder -1
|
||||
var tutorial := false
|
||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||
var net: Net
|
||||
@@ -26,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:
|
||||
@@ -96,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:
|
||||
@@ -130,13 +107,6 @@ 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
|
||||
|
||||
|
||||
## 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)))
|
||||
@@ -183,13 +153,6 @@ func _ready() -> void:
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif saved_game != null:
|
||||
game = saved_game
|
||||
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 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())
|
||||
@@ -235,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")
|
||||
@@ -275,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()
|
||||
@@ -313,7 +259,7 @@ func _overlay() -> Control:
|
||||
|
||||
## 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:
|
||||
@@ -336,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())
|
||||
|
||||
|
||||
@@ -466,8 +406,6 @@ func _input(event: InputEvent) -> void:
|
||||
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
|
||||
@@ -555,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 not tutorial else 1.0
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life)
|
||||
if tutorial:
|
||||
if tutorial_part == 1:
|
||||
app.profile.tutorial_done = true
|
||||
else:
|
||||
app.profile.tutorial2_done = true
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
@@ -846,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:
|
||||
@@ -1022,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)
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
class_name BattleTutorial
|
||||
extends RefCounted
|
||||
## Geführte Partien: Schritte, Bedingungen und Hervorhebungen (ausgelagert aus dem Kampfbildschirm).
|
||||
|
||||
const STEPS1 := [
|
||||
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
||||
"played_creature"],
|
||||
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
|
||||
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
|
||||
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
|
||||
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.", "combo"],
|
||||
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
|
||||
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
|
||||
]
|
||||
const STEPS2 := [
|
||||
["Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.", "mulligan"],
|
||||
["Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.", "trap_set"],
|
||||
["Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.", "trap_fired"],
|
||||
["Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.", "combo_dampf"],
|
||||
["Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!", ""],
|
||||
]
|
||||
|
||||
var screen # BattleScreen
|
||||
var part := 1
|
||||
var step := 0
|
||||
|
||||
|
||||
func _init(s, p: int = 1) -> void:
|
||||
screen = s
|
||||
part = p
|
||||
|
||||
|
||||
func steps() -> Array:
|
||||
return STEPS1 if part == 1 else STEPS2
|
||||
|
||||
|
||||
func stage_index() -> int:
|
||||
return 0 if part == 1 else 1
|
||||
|
||||
|
||||
## Teil 1: ein Wesen für 1 Energie auf der Hand, damit der erste Schritt immer möglich ist.
|
||||
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
|
||||
func inject_cards(g: Game) -> void:
|
||||
if part == 1 and not g.human.hand.any(func(c): return c.is_creature() and c.cost == 1):
|
||||
g.human.hand.append(Cards.by_id("f_kobold"))
|
||||
if part == 2:
|
||||
g.human.hand.append(Cards.by_id("w_falle"))
|
||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
||||
for i in 2:
|
||||
g.opponents[0].hand.append(Cards.by_id("w_reiter"))
|
||||
|
||||
|
||||
func condition(kind: String) -> bool:
|
||||
var g: Game = screen.game
|
||||
var h: Game.Player = g.human
|
||||
match kind:
|
||||
"played_creature":
|
||||
return g.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
||||
"turn_passed":
|
||||
return h.turns >= 2
|
||||
"attacked":
|
||||
return g.log.any(func(l): return l.begins_with(h.board[0].name + " greift") if not h.board.is_empty() else false) \
|
||||
or g.log.any(func(l): return l.ends_with("greift Fenn an."))
|
||||
"resonance":
|
||||
return h.board.any(func(c): return c.resonance > 0)
|
||||
"combo":
|
||||
return g.log.any(func(l): return l.begins_with("Feldkombo"))
|
||||
"spell":
|
||||
return g.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
||||
"mulligan":
|
||||
return h.mulligan_done
|
||||
"trap_set":
|
||||
return g.log.any(func(l): return l == "Du legt eine Falle.")
|
||||
"trap_fired":
|
||||
return g.log.any(func(l): return l.begins_with("Falle von Du"))
|
||||
"combo_dampf":
|
||||
return g.log.any(func(l): return l.begins_with("Feldkombo Dampf"))
|
||||
return false
|
||||
|
||||
|
||||
func advance() -> void:
|
||||
while step < steps().size() - 1 and condition(steps()[step][1]):
|
||||
step += 1
|
||||
|
||||
|
||||
func next() -> bool:
|
||||
if step < steps().size() - 1:
|
||||
step += 1
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func text() -> String:
|
||||
return "%d/%d %s" % [step + 1, steps().size(), screen.tr(steps()[step][0])]
|
||||
|
||||
|
||||
func finished() -> bool:
|
||||
return step >= steps().size() - 1
|
||||
|
||||
|
||||
## Rechtecke, die der aktuelle Schritt hervorhebt.
|
||||
func highlight_rects(mine: bool) -> Array[Rect2]:
|
||||
var rects: Array[Rect2] = []
|
||||
if not mine or screen.any_overlay():
|
||||
return rects
|
||||
var g: Game = screen.game
|
||||
var h: Game.Player = screen.perspective
|
||||
match steps()[step][1]:
|
||||
"played_creature":
|
||||
for v in screen.hand_views:
|
||||
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"turn_passed", "trap_fired":
|
||||
rects.append(screen.b_end.get_rect())
|
||||
"attacked":
|
||||
for v in screen.own_views:
|
||||
if v.creature.can_attack():
|
||||
rects.append(Rect2(v.slot, v.size))
|
||||
if screen.own_views.any(func(v): return v.creature.can_attack()):
|
||||
for o in screen.others():
|
||||
if o.alive() and screen.hero_panels.has(o):
|
||||
rects.append(screen.hero_panels[o].get_rect())
|
||||
"resonance":
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"combo":
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"spell":
|
||||
for v in screen.hand_views:
|
||||
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"trap_set":
|
||||
for v in screen.hand_views:
|
||||
if v.card.is_trap() and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"combo_dampf":
|
||||
var has_fire: bool = h.board.any(func(b): return "Feuer" in b.card.elements)
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if not c.is_creature() or not g.can_play(h, c)[0]:
|
||||
continue
|
||||
if (has_fire and "Wasser" in c.elements) or (not has_fire and "Feuer" in c.elements):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
return rects
|
||||
@@ -1 +0,0 @@
|
||||
uid://cg78afyod2873
|
||||
@@ -1,6 +1,7 @@
|
||||
class_name WorkshopScreen
|
||||
class_name CollectionScreen
|
||||
extends Control
|
||||
## Werkstatt: Karten zerlegen (Staub) und herstellen.
|
||||
## Sammlung: alle Karten mit Besitzstand; ausgewählte Karten lassen sich
|
||||
## zerlegen (Staub) oder herstellen.
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
@@ -18,7 +19,7 @@ var b_extras: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Werkstatt", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.label(self, "Karte anklicken, dann zerlegen oder herstellen. Überschuss: alles über %d Kopien." % Game.MAX_COPIES,
|
||||
14, Style.MUTED, Rect2(240, 18, 600, 24))
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
1
scripts/ui/collection_screen.gd.uid
Normal file
1
scripts/ui/collection_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsqeag3p0ehbq
|
||||
@@ -290,7 +290,7 @@ func add(card: Cards.Card) -> void:
|
||||
var owned: int = app.profile.collection.get(card.id, 0)
|
||||
var have := deck.count(card.id)
|
||||
if owned == 0:
|
||||
say("%s fehlt in deiner Sammlung (Werkstatt)" % card.name, false)
|
||||
say("%s fehlt in deiner Sammlung" % card.title(), false)
|
||||
elif deck.size() >= Game.DECK_SIZE:
|
||||
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
||||
elif have >= mini(owned, Game.MAX_COPIES):
|
||||
|
||||
@@ -5,7 +5,6 @@ var app
|
||||
var b_single: Button
|
||||
var b_campaign: Button
|
||||
var b_online: Button
|
||||
var b_tutorial: Button
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
@@ -33,21 +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_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
||||
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
||||
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
|
||||
Style.cbutton(grid, "Statistik", sz, func(): app.show_screen(StatsScreen.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)
|
||||
@@ -73,7 +65,6 @@ 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]
|
||||
var total := 0
|
||||
for cid in p.collection:
|
||||
@@ -89,12 +80,6 @@ func refresh() -> void:
|
||||
else:
|
||||
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
|
||||
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
|
||||
if ok[0] and not p.tutorial_done:
|
||||
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
|
||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
||||
elif ok[0] and not p.tutorial2_done:
|
||||
warn_label.text = "Anleitung 2 zeigt Mulligan, Fallen und die Feldkombo Dampf."
|
||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
class_name RulesScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Regeln", 36, Style.GOLD, Rect2(0, 18, 1280, 50), "center", true)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.position = Vector2(60, 78)
|
||||
scroll.size = Vector2(1160, 640)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(scroll)
|
||||
var text := Label.new()
|
||||
text.text = Style.rules_text()
|
||||
text.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
text.custom_minimum_size = Vector2(1140, 0)
|
||||
text.add_theme_font_size_override("font_size", 15)
|
||||
scroll.add_child(text)
|
||||
Style.button(self, "Zurück", Rect2(560, 735, 160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
@@ -1 +0,0 @@
|
||||
uid://bkemrytc2pjem
|
||||
@@ -1,46 +0,0 @@
|
||||
class_name StatsScreen
|
||||
extends Control
|
||||
## Statistik: Kämpfe, Tagesaufgabe, Bestenliste, beste Karten und CSV-Export.
|
||||
|
||||
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, "Statistik", 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)
|
||||
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_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
@@ -1 +0,0 @@
|
||||
uid://t0h7fpmpeuq4
|
||||
@@ -267,48 +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("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.")
|
||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
||||
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
||||
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
||||
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
|
||||
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
|
||||
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://cf42h6bsh8lvg
|
||||
@@ -829,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")
|
||||
@@ -984,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()
|
||||
@@ -1157,11 +1155,7 @@ func test_bestenliste() -> void:
|
||||
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)
|
||||
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 = ""
|
||||
|
||||
|
||||
@@ -1318,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.
|
||||
@@ -1388,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")
|
||||
@@ -1423,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])
|
||||
|
||||
116
tests/ui_test.gd
116
tests/ui_test.gd
@@ -164,8 +164,8 @@ 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_statistik, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2,
|
||||
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_kampf.bind(2, 3), test_kampf_abbrechen]
|
||||
@@ -187,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:
|
||||
@@ -568,23 +564,10 @@ func test_einstellungen() -> void:
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_statistik() -> void:
|
||||
await click(find_button("Statistik"))
|
||||
check(screen() is StatsScreen, "Statistik 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_statistik")
|
||||
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 := {}
|
||||
@@ -607,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ü")
|
||||
|
||||
@@ -642,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"))
|
||||
@@ -686,7 +634,7 @@ 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")
|
||||
|
||||
|
||||
@@ -714,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")
|
||||
@@ -765,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")
|
||||
@@ -781,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:
|
||||
|
||||
@@ -6,13 +6,9 @@ Kampagne,Campaign
|
||||
Netzwerkspiel,Network game
|
||||
Booster öffnen,Open boosters
|
||||
Deck bauen,Build deck
|
||||
Werkstatt,Workshop
|
||||
Regeln,Rules
|
||||
Einstellungen,Settings
|
||||
Anleitung,Tutorial
|
||||
Beenden,Quit
|
||||
Zurück,Back
|
||||
Weiter,Continue
|
||||
Start,Start
|
||||
Speichern,Save
|
||||
Leeren,Clear
|
||||
@@ -56,7 +52,6 @@ Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards
|
||||
Fallen,Traps
|
||||
Du bist dran,Your turn
|
||||
Antreten,Enter
|
||||
Statistik exportieren,Export statistics
|
||||
Gegnerzüge überspringen,Skip AI turns
|
||||
Rückgängig,Undo
|
||||
Kapitel 1,Chapter 1
|
||||
@@ -96,24 +91,11 @@ Stärke %.1f,Strength %.1f
|
||||
"Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten","Deck strength: average of rarity, keywords and effects"
|
||||
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
|
||||
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!
|
||||
Duell,Duel
|
||||
%d am Tisch,%d at the table
|
||||
%d Spieler am Tisch,%d players at the table
|
||||
@@ -208,4 +190,4 @@ Starten,Start
|
||||
%d Gegner,%d opponents
|
||||
Übernehmen,Apply
|
||||
KI-Gegner,AI opponents
|
||||
Statistik,Statistics
|
||||
Sammlung,Collection
|
||||
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user