Anleitung mit Hervorhebungen, Rückgängig, Kapitel 2, Herausforderungen, Fallen, Wiederverbinden, CI
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
11
README.md
11
README.md
@@ -26,10 +26,16 @@ Der Spielstand liegt unter `user://spielstand.json`
|
|||||||
- Kampagnen-Bosse mit Merkmalen, Kartenstatistik, zwölf Tagesaufgaben, Pity-Zähler für mythische Karten
|
- Kampagnen-Bosse mit Merkmalen, Kartenstatistik, zwölf Tagesaufgaben, Pity-Zähler für mythische Karten
|
||||||
- Netzwerkspiel im lokalen Netz (Host führt das Spiel, Mitspieler sehen nur ihre Hand)
|
- Netzwerkspiel im lokalen Netz (Host führt das Spiel, Mitspieler sehen nur ihre Hand)
|
||||||
- Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch
|
- Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch
|
||||||
|
- Geführte Anleitung mit Hervorhebungen, Rückgängig für Fehlklicks, Überspringen von Gegnerzügen
|
||||||
|
- Zwei Kampagnenkapitel (Bosse mit ein bis drei Merkmalen), wählbares eigenes Merkmal nach Kapitel 1
|
||||||
|
- Wöchentliche Herausforderung mit Regelvarianten, Bestenliste (Serie, schnellster Sieg), Statistik-Export als CSV
|
||||||
|
- Zehn Fallenkarten, Netzwerk mit Wiederverbinden (Platz bleibt 60 s frei)
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tests/run_headless.sh` führt Import und Headless-Prüfungen aus (auch in `.github/workflows/tests.yml`).
|
`tests/run_headless.sh` führt Import und Headless-Prüfungen aus. `.github/workflows/tests.yml` läuft beides in CI:
|
||||||
|
die Headless-Prüfungen direkt, den Fenster-Test unter Xvfb (lokal ohne Xvfb nicht geprüft).
|
||||||
|
Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren.
|
||||||
|
|
||||||
```
|
```
|
||||||
godot --headless --path . --import # einmalig: Ressourcen importieren
|
godot --headless --path . --import # einmalig: Ressourcen importieren
|
||||||
@@ -58,5 +64,6 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
|||||||
- `scripts/campaign.gd` – Kampagnenstufen
|
- `scripts/campaign.gd` – Kampagnenstufen
|
||||||
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte), Erfolge, Einstellungen, Regeln, Ton
|
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte), Erfolge, Einstellungen, Regeln, Ton
|
||||||
- `scenes/main.tscn` – Startszene
|
- `scenes/main.tscn` – Startszene
|
||||||
- `scripts/net.gd` – Netzwerkspiel (ENet)
|
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||||
|
- `scripts/challenge.gd` – wöchentliche Herausforderungen
|
||||||
- `translations/ui.csv` – englische Oberflächentexte
|
- `translations/ui.csv` – englische Oberflächentexte
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
|
||||||
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich
|
- Lokal zu mehreren am selben Rechner und im lokalen Netz möglich
|
||||||
- Geführte erste Partie als Einstieg
|
- Geführte erste Partie als Einstieg
|
||||||
|
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel
|
||||||
|
|
||||||
## Sammeln
|
## Sammeln
|
||||||
- Booster öffnen
|
- Booster öffnen
|
||||||
|
|||||||
@@ -2,27 +2,30 @@ class_name AchievementsScreen
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
var app
|
var app
|
||||||
|
var export_label: Label
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var col := Style.column(self, 900, 20, 5)
|
var col := Style.column(self, 900, 12, 4)
|
||||||
Style.clabel(col, "Erfolge", 36, Style.GOLD, "center", true)
|
Style.clabel(col, "Erfolge", 32, Style.GOLD, "center", true)
|
||||||
var st: Dictionary = p.stats
|
var st: Dictionary = p.stats
|
||||||
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
|
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
|
||||||
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.STAGES.size()], 16, Style.MUTED)
|
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.total()], 16, Style.MUTED)
|
||||||
var task := p.daily_task()
|
var task := p.daily_task()
|
||||||
var daily_text: String = ("Tagesaufgabe erledigt: %s ✓" % task.text) if p.daily.done else \
|
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]
|
"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, daily_text, 16, Style.BLUE)
|
||||||
Style.spacer(col, 8)
|
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:
|
for a in Profile.ACHIEVEMENTS:
|
||||||
var done: bool = a.id in p.achievements
|
var done: bool = a.id in p.achievements
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
|
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
|
||||||
col.add_child(panel)
|
col.add_child(panel)
|
||||||
var r := Style.row(panel, 12)
|
var r := Style.row(panel, 12)
|
||||||
r.custom_minimum_size = Vector2(0, 42)
|
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, "✓" 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.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, a.text, 15, Style.MUTED, "left", false, 380)
|
||||||
@@ -34,10 +37,18 @@ func _ready() -> void:
|
|||||||
for row in top:
|
for row in top:
|
||||||
parts.append("%s %d/%d" % [row.card.name, row.wins, row.games])
|
parts.append("%s %d/%d" % [row.card.name, row.wins, row.games])
|
||||||
Style.clabel(col, "Beste Karten (Siege/Partien): " + " · ".join(parts), 14, Style.MUTED)
|
Style.clabel(col, "Beste Karten (Siege/Partien): " + " · ".join(parts), 14, Style.MUTED)
|
||||||
Style.spacer(col, 6)
|
export_label = Style.clabel(col, "", 13, Style.GREEN)
|
||||||
var br := Style.row(col)
|
Style.spacer(col, 2)
|
||||||
|
var br := Style.row(col, 16)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
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:
|
func _input(event: InputEvent) -> void:
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ var preview_timer := 0.0
|
|||||||
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||||
var mulligan_selected: Array = []
|
var mulligan_selected: Array = []
|
||||||
var tutorial_step := 0
|
var tutorial_step := 0
|
||||||
|
var challenge := {} # wöchentliche Herausforderung oder leer
|
||||||
|
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||||
|
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||||
|
var undo_log_len := 0
|
||||||
|
var undo_t := 0.0
|
||||||
|
|
||||||
# Ansichten (auch für Tests)
|
# Ansichten (auch für Tests)
|
||||||
var tiles := {}
|
var tiles := {}
|
||||||
@@ -70,6 +75,22 @@ var tutorial_panel: Panel
|
|||||||
var tutorial_text: RichTextLabel
|
var tutorial_text: RichTextLabel
|
||||||
var b_tut_next: Button
|
var b_tut_next: Button
|
||||||
var wait_label: Label
|
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)
|
||||||
|
|
||||||
const TUTORIAL_STEPS := [
|
const TUTORIAL_STEPS := [
|
||||||
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
||||||
@@ -107,6 +128,12 @@ static func guided() -> BattleScreen:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
static func weekly(ch: Dictionary) -> BattleScreen:
|
||||||
|
var b := BattleScreen.new(int(ch.n_opp), -1)
|
||||||
|
b.challenge = ch
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
func is_hotseat() -> bool:
|
func is_hotseat() -> bool:
|
||||||
return game != null and game.humans.size() > 1 and online_seat < 0
|
return game != null and game.humans.size() > 1 and online_seat < 0
|
||||||
|
|
||||||
@@ -146,14 +173,24 @@ func _ready() -> void:
|
|||||||
var opps := Campaign.opponents(0, r)
|
var opps := Campaign.opponents(0, r)
|
||||||
opps[0].perks = []
|
opps[0].perks = []
|
||||||
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
||||||
|
elif not challenge.is_empty():
|
||||||
|
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, challenge.rules)
|
||||||
elif stage >= 0:
|
elif stage >= 0:
|
||||||
var r := Game.make_rng()
|
var r := Game.make_rng()
|
||||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r)
|
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r, false, own_rules())
|
||||||
else:
|
else:
|
||||||
game = Game.new_battle(app.profile.deck_cards(), n_opp)
|
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
||||||
_begin()
|
_begin()
|
||||||
|
|
||||||
|
|
||||||
|
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar).
|
||||||
|
func own_rules() -> Dictionary:
|
||||||
|
var p: Profile = app.profile
|
||||||
|
if p.perk != "" and p.perk_unlocked():
|
||||||
|
return {"human_perks": [p.perk]}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
func _begin() -> void:
|
func _begin() -> void:
|
||||||
n_opp = game.players.size() - 1
|
n_opp = game.players.size() - 1
|
||||||
if is_online():
|
if is_online():
|
||||||
@@ -207,6 +244,11 @@ func _build_static() -> void:
|
|||||||
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
||||||
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
||||||
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
||||||
|
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
|
||||||
|
b_skip.add_theme_font_size_override("font_size", 14)
|
||||||
|
b_skip.visible = false
|
||||||
|
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
|
||||||
|
b_undo.visible = false
|
||||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||||
wait_label.visible = false
|
wait_label.visible = false
|
||||||
hero_layer = _layer()
|
hero_layer = _layer()
|
||||||
@@ -214,6 +256,7 @@ func _build_static() -> void:
|
|||||||
trap_layer = _layer()
|
trap_layer = _layer()
|
||||||
hand_layer = _layer()
|
hand_layer = _layer()
|
||||||
preview_holder = _layer()
|
preview_holder = _layer()
|
||||||
|
highlight_layer = _layer()
|
||||||
zoom_layer = _layer()
|
zoom_layer = _layer()
|
||||||
fx_layer = _layer()
|
fx_layer = _layer()
|
||||||
# Mulligan
|
# Mulligan
|
||||||
@@ -380,6 +423,11 @@ func _input(event: InputEvent) -> void:
|
|||||||
KEY_SPACE:
|
KEY_SPACE:
|
||||||
if my_turn() and not any_overlay():
|
if my_turn() and not any_overlay():
|
||||||
end_turn()
|
end_turn()
|
||||||
|
elif b_skip.visible:
|
||||||
|
skip_ai = true
|
||||||
|
KEY_U:
|
||||||
|
if b_undo.visible:
|
||||||
|
undo()
|
||||||
KEY_ENTER, KEY_KP_ENTER:
|
KEY_ENTER, KEY_KP_ENTER:
|
||||||
if handover.visible:
|
if handover.visible:
|
||||||
take_over()
|
take_over()
|
||||||
@@ -414,6 +462,12 @@ func _process(delta: float) -> void:
|
|||||||
preview_timer -= delta
|
preview_timer -= delta
|
||||||
if preview_timer <= 0.0:
|
if preview_timer <= 0.0:
|
||||||
show_preview(null, false)
|
show_preview(null, false)
|
||||||
|
if undo_t > 0.0:
|
||||||
|
undo_t -= delta
|
||||||
|
if undo_t <= 0.0:
|
||||||
|
clear_undo()
|
||||||
|
if is_online() and net.is_host:
|
||||||
|
net.tick()
|
||||||
if game.winner != "":
|
if game.winner != "":
|
||||||
if not ended:
|
if not ended:
|
||||||
ended = true
|
ended = true
|
||||||
@@ -431,19 +485,24 @@ func _process(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
if is_client():
|
if is_client():
|
||||||
return
|
return
|
||||||
|
if skip_ai:
|
||||||
|
ai_timer = 0.0
|
||||||
|
else:
|
||||||
ai_timer -= delta
|
ai_timer -= delta
|
||||||
if ai_timer > 0:
|
if ai_timer > 0:
|
||||||
return
|
return
|
||||||
var action := AI.choose_action(game, cur)
|
var action := AI.choose_action(game, cur)
|
||||||
AI.apply_action(game, cur, action)
|
AI.apply_action(game, cur, action)
|
||||||
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
|
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
|
||||||
|
if game.current().is_human:
|
||||||
|
skip_ai = false
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
func _finish() -> void:
|
func _finish() -> void:
|
||||||
var won := game.winner == "human"
|
var won := game.winner == "human"
|
||||||
if counts_for_profile():
|
if counts_for_profile():
|
||||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids)
|
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge)
|
||||||
if tutorial:
|
if tutorial:
|
||||||
app.profile.tutorial_done = true
|
app.profile.tutorial_done = true
|
||||||
app.save()
|
app.save()
|
||||||
@@ -480,15 +539,52 @@ func play(card: Cards.Card, target) -> void:
|
|||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
|
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
|
||||||
return
|
return
|
||||||
|
var before: Game = game.clone() if card.is_creature() and not is_online() else null
|
||||||
|
var log_len := game.log.size()
|
||||||
var err := game.play_card(perspective, card, target)
|
var err := game.play_card(perspective, card, target)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(err)
|
say(tr(err))
|
||||||
|
return
|
||||||
|
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte, Kombos, Fallen)
|
||||||
|
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||||
|
undo_state = before
|
||||||
|
undo_log_len = log_len
|
||||||
|
undo_t = 6.0
|
||||||
|
else:
|
||||||
|
clear_undo()
|
||||||
|
|
||||||
|
|
||||||
|
func clear_undo() -> void:
|
||||||
|
undo_state = null
|
||||||
|
undo_t = 0.0
|
||||||
|
b_undo.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
## Nimmt das letzte Ausspielen eines Wesens zurück (nur solange nichts Verdecktes offenbart wurde).
|
||||||
|
func undo() -> void:
|
||||||
|
if undo_state == null:
|
||||||
|
return
|
||||||
|
var idx := game.players.find(perspective)
|
||||||
|
var restored := undo_state
|
||||||
|
restored.quiet = false
|
||||||
|
restored.rng = game.rng
|
||||||
|
restored.log = game.log.slice(0, undo_log_len)
|
||||||
|
restored.say("Rückgängig.")
|
||||||
|
game = restored
|
||||||
|
perspective = game.players[idx]
|
||||||
|
if is_online() and net.is_host:
|
||||||
|
net.game = game
|
||||||
|
clear_undo()
|
||||||
|
cancel()
|
||||||
|
build_hero_boxes()
|
||||||
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
func end_turn() -> void:
|
func end_turn() -> void:
|
||||||
if not my_turn():
|
if not my_turn():
|
||||||
return
|
return
|
||||||
cancel()
|
cancel()
|
||||||
|
clear_undo()
|
||||||
zoom_player = null
|
zoom_player = null
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "end"})
|
net.send_action({"type": "end"})
|
||||||
@@ -552,7 +648,7 @@ func on_click(kind: String, obj) -> void:
|
|||||||
if obj in g.valid_targets(perspective, card.target_type()):
|
if obj in g.valid_targets(perspective, card.target_type()):
|
||||||
play(card, obj)
|
play(card, obj)
|
||||||
else:
|
else:
|
||||||
say("Abgebrochen")
|
say(tr("Abgebrochen"))
|
||||||
zoom_player = null
|
zoom_player = null
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
@@ -560,12 +656,13 @@ func on_click(kind: String, obj) -> void:
|
|||||||
var cr = selected
|
var cr = selected
|
||||||
selected = null
|
selected = null
|
||||||
if kind == "enemy" or kind == "hero":
|
if kind == "enemy" or kind == "hero":
|
||||||
|
clear_undo()
|
||||||
if is_client():
|
if is_client():
|
||||||
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
|
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
|
||||||
else:
|
else:
|
||||||
var err := g.attack(cr, obj)
|
var err := g.attack(cr, obj)
|
||||||
if err != "":
|
if err != "":
|
||||||
say(err)
|
say(tr(err))
|
||||||
zoom_player = null
|
zoom_player = null
|
||||||
after_action()
|
after_action()
|
||||||
return
|
return
|
||||||
@@ -573,18 +670,18 @@ func on_click(kind: String, obj) -> void:
|
|||||||
"hand":
|
"hand":
|
||||||
var ok := g.can_play(perspective, obj)
|
var ok := g.can_play(perspective, obj)
|
||||||
if not ok[0]:
|
if not ok[0]:
|
||||||
say(ok[1])
|
say(tr(ok[1]))
|
||||||
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
|
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
|
||||||
pending = obj
|
pending = obj
|
||||||
say("Wähle ein Ziel")
|
say(tr("Wähle ein Ziel"))
|
||||||
else:
|
else:
|
||||||
play(obj, null)
|
play(obj, null)
|
||||||
"own":
|
"own":
|
||||||
if obj.can_attack():
|
if obj.can_attack():
|
||||||
selected = obj
|
selected = obj
|
||||||
say("Wähle ein Angriffsziel")
|
say(tr("Wähle ein Angriffsziel"))
|
||||||
else:
|
else:
|
||||||
say("%s kann nicht angreifen" % obj.name)
|
say(tr("%s kann nicht angreifen") % obj.name)
|
||||||
after_action()
|
after_action()
|
||||||
|
|
||||||
|
|
||||||
@@ -739,6 +836,51 @@ func tutorial_next() -> void:
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Hebt hervor, was der aktuelle Schritt verlangt.
|
||||||
|
func _tutorial_highlights(mine: bool) -> void:
|
||||||
|
Style.clear(highlight_layer)
|
||||||
|
if not mine or any_overlay():
|
||||||
|
return
|
||||||
|
var g := game
|
||||||
|
var h := perspective
|
||||||
|
var rects: Array[Rect2] = []
|
||||||
|
match TUTORIAL_STEPS[tutorial_step][1]:
|
||||||
|
"played_creature":
|
||||||
|
for v in hand_views:
|
||||||
|
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
||||||
|
rects.append(Rect2(v.position, v.size))
|
||||||
|
"turn_passed":
|
||||||
|
rects.append(b_end.get_rect())
|
||||||
|
"attacked":
|
||||||
|
for v in own_views:
|
||||||
|
if v.creature.can_attack():
|
||||||
|
rects.append(Rect2(v.slot, v.size))
|
||||||
|
if not own_views.is_empty() and own_views.any(func(v): return v.creature.can_attack()):
|
||||||
|
for o in others():
|
||||||
|
if o.alive() and hero_panels.has(o):
|
||||||
|
rects.append(hero_panels[o].get_rect())
|
||||||
|
"resonance":
|
||||||
|
for v in 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 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 hand_views:
|
||||||
|
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
||||||
|
rects.append(Rect2(v.position, v.size))
|
||||||
|
for r in rects:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
func _tutorial_refresh() -> void:
|
func _tutorial_refresh() -> void:
|
||||||
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
|
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
|
||||||
tutorial_step += 1
|
tutorial_step += 1
|
||||||
@@ -876,8 +1018,12 @@ func refresh() -> void:
|
|||||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||||
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
||||||
deck_label.text = "Deck %d" % h.deck.size()
|
deck_label.text = "Deck %d" % h.deck.size()
|
||||||
status_label.text = "Zug %d · %s" % [g.turn, "Du bist dran" if mine else g.current().name + " ist dran …"]
|
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
||||||
b_end.disabled = not mine
|
b_end.disabled = not mine
|
||||||
|
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||||
|
b_skip.visible = ai_running
|
||||||
|
b_end.visible = not ai_running
|
||||||
|
b_undo.visible = undo_state != null and mine
|
||||||
var lines: Array[String] = []
|
var lines: Array[String] = []
|
||||||
var start := maxi(0, g.log.size() - 60)
|
var start := maxi(0, g.log.size() - 60)
|
||||||
for i in range(start, g.log.size()):
|
for i in range(start, g.log.size()):
|
||||||
@@ -891,6 +1037,7 @@ func refresh() -> void:
|
|||||||
log_box.text = "\n".join(lines)
|
log_box.text = "\n".join(lines)
|
||||||
if tutorial:
|
if tutorial:
|
||||||
_tutorial_refresh()
|
_tutorial_refresh()
|
||||||
|
_tutorial_highlights(mine)
|
||||||
|
|
||||||
Style.clear(zoom_layer)
|
Style.clear(zoom_layer)
|
||||||
if zoom_player != null and zoom_player.alive():
|
if zoom_player != null and zoom_player.alive():
|
||||||
|
|||||||
@@ -2,52 +2,88 @@ class_name CampaignScreen
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
var app
|
var app
|
||||||
|
var chapter := 1
|
||||||
|
var perk_box: OptionButton
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var p: Profile = app.profile
|
var p: Profile = app.profile
|
||||||
var col := Style.column(self, 1000, 24, 6)
|
chapter = 2 if p.campaign_stage >= Campaign.STAGES.size() else 1
|
||||||
Style.clabel(col, "Kampagne", 36, Style.GOLD, "center", true)
|
build()
|
||||||
Style.clabel(col, "Zehn Magier warten. Jede Stufe bringt mehr Gegner an den Tisch, alle nach denselben Regeln.", 16, Style.MUTED)
|
|
||||||
Style.spacer(col, 6)
|
|
||||||
for i in Campaign.STAGES.size():
|
func build() -> void:
|
||||||
var st: Dictionary = Campaign.STAGES[i]
|
Style.clear(self)
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var col := Style.column(self, 1000, 16, 5)
|
||||||
|
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
||||||
|
var tabs := Style.row(col, 10)
|
||||||
|
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
var b1 := Style.cbutton(tabs, "Kapitel 1", Vector2(160, 36), func(): set_chapter(1), Color("285a3c") if chapter == 1 else null)
|
||||||
|
var b2 := Style.cbutton(tabs, "Kapitel 2", Vector2(160, 36), func(): set_chapter(2), Color("285a3c") if chapter == 2 else null)
|
||||||
|
b2.disabled = p.campaign_stage < Campaign.STAGES.size()
|
||||||
|
if p.perk_unlocked():
|
||||||
|
var pr := Style.row(tabs, 8)
|
||||||
|
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
||||||
|
perk_box = OptionButton.new()
|
||||||
|
perk_box.custom_minimum_size = Vector2(300, 36)
|
||||||
|
perk_box.add_item("keins")
|
||||||
|
var keys: Array = Game.PERKS.keys()
|
||||||
|
for k in keys:
|
||||||
|
perk_box.add_item(Game.PERKS[k])
|
||||||
|
perk_box.select(keys.find(p.perk) + 1 if p.perk != "" else 0)
|
||||||
|
perk_box.item_selected.connect(func(i: int):
|
||||||
|
p.perk = "" if i == 0 else keys[i - 1]
|
||||||
|
app.save())
|
||||||
|
pr.add_child(perk_box)
|
||||||
|
Style.clabel(col, "Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★)." if chapter == 1
|
||||||
|
else "Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.", 14, Style.MUTED)
|
||||||
|
var offset := 0 if chapter == 1 else Campaign.STAGES.size()
|
||||||
|
for k in Campaign.STAGES.size():
|
||||||
|
var i := offset + k
|
||||||
|
var st := Campaign.stage(i)
|
||||||
var done := i < p.campaign_stage
|
var done := i < p.campaign_stage
|
||||||
var next := i == p.campaign_stage
|
var next := i == p.campaign_stage
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
||||||
Style.GOLD if next else null, 2 if next else 0))
|
Style.GOLD if next else null, 2 if next else 0))
|
||||||
col.add_child(panel)
|
col.add_child(panel)
|
||||||
var r := Style.row(panel, 12)
|
var r := Style.row(panel, 10)
|
||||||
r.custom_minimum_size = Vector2(0, 50)
|
r.custom_minimum_size = Vector2(0, 50)
|
||||||
Style.clabel(r, str(i + 1), 20, Style.GOLD if next else Style.MUTED, "center", true, 40)
|
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], 17, Style.TEXT if not done else Style.MUTED, "left", true, 300)
|
Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 290)
|
||||||
var dots := Control.new()
|
var dots := Control.new()
|
||||||
dots.custom_minimum_size = Vector2(60, 0)
|
dots.custom_minimum_size = Vector2(50, 0)
|
||||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
var elems: Array = st.elements
|
var elems: Array = st.elements
|
||||||
dots.draw.connect(func():
|
dots.draw.connect(func():
|
||||||
for k in elems.size():
|
for m in elems.size():
|
||||||
dots.draw_circle(Vector2(10 + k * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[k]]))
|
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
||||||
r.add_child(dots)
|
r.add_child(dots)
|
||||||
var table: String = "Duell" if st.extra == 0 else "%d am Tisch" % (st.extra + 2)
|
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
||||||
Style.clabel(r, "%s · %d Münzen\n★ %s" % [table, st.reward, Game.PERKS[st.perk]], 13, Style.MUTED, "left", false, 300)
|
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]))
|
||||||
|
Style.clabel(r, "%s · %d Münzen\n★ %s" % [table, st.reward, perks_text], 12, Style.MUTED, "left", false, 360)
|
||||||
if done:
|
if done:
|
||||||
Style.clabel(r, "abgeschlossen", 14, Style.GREEN, "right", false, 110)
|
Style.clabel(r, "abgeschlossen", 13, Style.GREEN, "right", false, 100)
|
||||||
Style.cbutton(r, "Wiederholen", Vector2(140, 38), _start.bind(i))
|
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||||
elif next:
|
elif next:
|
||||||
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
|
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
||||||
Style.cbutton(r, "Kämpfen", Vector2(140, 38), _start.bind(i), Color("285a3c"))
|
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
||||||
else:
|
else:
|
||||||
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
|
Style.clabel(r, "", 13, Style.GREEN, "right", false, 100)
|
||||||
var b := Style.cbutton(r, "Gesperrt", Vector2(140, 38), func(): pass)
|
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||||
b.disabled = true
|
b.disabled = true
|
||||||
if p.campaign_stage >= Campaign.STAGES.size():
|
if p.campaign_stage >= Campaign.total():
|
||||||
Style.clabel(col, "Kampagne abgeschlossen. Der Archimagier ist besiegt!", 18, Style.GREEN, "center", true)
|
Style.clabel(col, "Beide Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
||||||
Style.spacer(col, 4)
|
|
||||||
var br := Style.row(col)
|
var br := Style.row(col)
|
||||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
||||||
|
|
||||||
|
|
||||||
|
func set_chapter(c: int) -> void:
|
||||||
|
chapter = c
|
||||||
|
build()
|
||||||
|
|
||||||
|
|
||||||
func _start(stage: int) -> void:
|
func _start(stage: int) -> void:
|
||||||
|
|||||||
42
scripts/ui/challenge_screen.gd
Normal file
42
scripts/ui/challenge_screen.gd
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
class_name ChallengeScreen
|
||||||
|
extends Control
|
||||||
|
## Wöchentliche Herausforderung: feste Regelvariante mit einmaliger Belohnung.
|
||||||
|
|
||||||
|
var app
|
||||||
|
var msg: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var p: Profile = app.profile
|
||||||
|
var ch := Challenge.current()
|
||||||
|
var done: bool = p.challenge_week == Challenge.week()
|
||||||
|
var col := Style.column(self, 760, 60, 14)
|
||||||
|
Style.clabel(col, "Herausforderung der Woche", 36, Style.GOLD, "center", true)
|
||||||
|
Style.clabel(col, "Jede Woche eine andere Regel. Die Belohnung gibt es einmal pro Woche, danach die normale.", 16, Style.MUTED)
|
||||||
|
Style.spacer(col, 20)
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 10, Style.GREEN if done else Style.GOLD, 2))
|
||||||
|
col.add_child(panel)
|
||||||
|
var inner := VBoxContainer.new()
|
||||||
|
inner.add_theme_constant_override("separation", 10)
|
||||||
|
panel.add_child(inner)
|
||||||
|
Style.spacer(inner, 6)
|
||||||
|
Style.clabel(inner, ch.name, 30, Style.TEXT, "center", true)
|
||||||
|
Style.clabel(inner, ch.text, 18, Style.MUTED)
|
||||||
|
Style.clabel(inner, "Belohnung: %d Münzen%s" % [ch.reward, " · diese Woche schon bestanden ✓" if done else ""], 16, Style.GOLD)
|
||||||
|
Style.spacer(inner, 6)
|
||||||
|
Style.spacer(col, 10)
|
||||||
|
msg = Style.clabel(col, "", 16, Style.ORANGE)
|
||||||
|
var deck_err := Challenge.deck_allowed(ch, p.deck_cards())
|
||||||
|
if deck_err != "":
|
||||||
|
msg.text = deck_err + " (aktives Deck: %s)" % p.deck_name()
|
||||||
|
var br := Style.row(col, 20)
|
||||||
|
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
var b := Style.cbutton(br, "Antreten", Vector2(200, 50), func(): app.show_screen(BattleScreen.weekly(ch)), Color("285a3c"))
|
||||||
|
b.disabled = deck_err != "" or not p.deck_valid()[0]
|
||||||
|
Style.cbutton(br, "Zurück", Vector2(160, 50), func(): app.show_screen(MenuScreen.new()))
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
app.show_screen(MenuScreen.new())
|
||||||
1
scripts/ui/challenge_screen.gd.uid
Normal file
1
scripts/ui/challenge_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cxheynenqemse
|
||||||
@@ -7,6 +7,7 @@ var b_campaign: Button
|
|||||||
var b_hotseat: Button
|
var b_hotseat: Button
|
||||||
var b_online: Button
|
var b_online: Button
|
||||||
var b_tutorial: Button
|
var b_tutorial: Button
|
||||||
|
var b_challenge: Button
|
||||||
var coins_label: Label
|
var coins_label: Label
|
||||||
var coll_label: Label
|
var coll_label: Label
|
||||||
var deck_label: Label
|
var deck_label: Label
|
||||||
@@ -40,6 +41,7 @@ func _ready() -> void:
|
|||||||
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
||||||
b_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
|
b_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
|
||||||
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||||
|
b_challenge = Style.cbutton(grid, "Herausforderung", sz, func(): app.show_screen(ChallengeScreen.new()), Color("285a3c"))
|
||||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
||||||
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
||||||
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
|
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
|
||||||
@@ -61,20 +63,22 @@ func refresh() -> void:
|
|||||||
b_campaign.disabled = not ok[0]
|
b_campaign.disabled = not ok[0]
|
||||||
b_tutorial.disabled = not ok[0]
|
b_tutorial.disabled = not ok[0]
|
||||||
b_online.disabled = not ok[0]
|
b_online.disabled = not ok[0]
|
||||||
|
b_challenge.disabled = not ok[0]
|
||||||
|
b_challenge.text = tr("Herausforderung") + (" ✓" if p.challenge_week == Challenge.week() else "")
|
||||||
var total := 0
|
var total := 0
|
||||||
for cid in p.collection:
|
for cid in p.collection:
|
||||||
total += p.collection[cid]
|
total += p.collection[cid]
|
||||||
coins_label.text = "Münzen: %d · Staub: %d" % [p.coins, p.dust]
|
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
||||||
coll_label.text = "Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d" % [
|
coll_label.text = tr("Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d") % [
|
||||||
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.STAGES.size()]
|
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.total()]
|
||||||
var elems := Game.deck_elements(p.deck_cards())
|
var elems := Game.deck_elements(p.deck_cards())
|
||||||
deck_label.text = "Aktives Deck: %s (%s)" % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else "leer"]
|
deck_label.text = tr("Aktives Deck: %s (%s)") % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else tr("leer")]
|
||||||
var task := p.daily_task()
|
var task := p.daily_task()
|
||||||
if p.daily.done:
|
if p.daily.done:
|
||||||
daily_label.text = "Tagesaufgabe erledigt: %s ✓" % task.text
|
daily_label.text = tr("Tagesaufgabe erledigt: %s ✓") % task.text
|
||||||
else:
|
else:
|
||||||
daily_label.text = "Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
|
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
|
||||||
warn_label.text = "" if ok[0] else "Aktives Deck nicht spielbar: " + ok[1]
|
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
|
||||||
if ok[0] and not p.tutorial_done:
|
if ok[0] and not p.tutorial_done:
|
||||||
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
|
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
|
||||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
||||||
|
|||||||
@@ -1105,7 +1105,7 @@ func test_neue_fallen() -> void:
|
|||||||
g.play_card(o, C("f_wolf"))
|
g.play_card(o, C("f_wolf"))
|
||||||
check(o.board.is_empty(), "Aschefalle tötet den 3/2 Wolf beim Ausspielen")
|
check(o.board.is_empty(), "Aschefalle tötet den 3/2 Wolf beim Ausspielen")
|
||||||
# Sandfalle friert das ausgespielte Wesen ein, Spiegelfalle beschwört einen Windgeist
|
# Sandfalle friert das ausgespielte Wesen ein, Spiegelfalle beschwört einen Windgeist
|
||||||
s = setup(["e_sandfalle", "l_spiegel"], ["e_troll"])
|
s = setup(["e_sandfalle", "l_spiegel"], ["e_troll", "w_reiter"])
|
||||||
g = s[0]
|
g = s[0]
|
||||||
h = s[1]
|
h = s[1]
|
||||||
o = s[2][0]
|
o = s[2][0]
|
||||||
@@ -1113,8 +1113,9 @@ func test_neue_fallen() -> void:
|
|||||||
g.play_card(h, C("l_spiegel"))
|
g.play_card(h, C("l_spiegel"))
|
||||||
g.end_turn()
|
g.end_turn()
|
||||||
g.play_card(o, C("e_troll"))
|
g.play_card(o, C("e_troll"))
|
||||||
check(o.board.size() == 1 and o.board[0].frozen, "Sandfalle friert ein")
|
check(o.board.size() == 1 and o.board[0].frozen and h.traps.size() == 1, "Sandfalle friert ein, eine Falle je Auslöser")
|
||||||
check(h.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist")
|
g.play_card(o, C("w_reiter")) # Wasser neben Erde: Schlamm heilt nur, der Windgeist überlebt
|
||||||
|
check(h.board.size() == 1 and h.board[0].name == "Windgeist", "Spiegelfalle beschwört Windgeist beim zweiten Wesen")
|
||||||
check(h.traps.is_empty(), "beide Fallen verbraucht")
|
check(h.traps.is_empty(), "beide Fallen verbraucht")
|
||||||
# Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt
|
# Nebelfalle bricht ab und zieht, Dornenfalle verletzt und heilt
|
||||||
s = setup(["w_nebelfalle", "e_dornen"])
|
s = setup(["w_nebelfalle", "e_dornen"])
|
||||||
|
|||||||
136
tests/ui_test.gd
136
tests/ui_test.gd
@@ -154,8 +154,8 @@ func _ready() -> void:
|
|||||||
add_child(main)
|
add_child(main)
|
||||||
await frames(3)
|
await frames(3)
|
||||||
for t in [test_menu_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
for t in [test_menu_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_hotseat,
|
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_herausforderung,
|
||||||
test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]:
|
test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]:
|
||||||
await ensure_menu()
|
await ensure_menu()
|
||||||
await t.call()
|
await t.call()
|
||||||
for suffix in ["", ".bak", ".corrupt"]:
|
for suffix in ["", ".bak", ".corrupt"]:
|
||||||
@@ -358,6 +358,17 @@ func test_gegnerauswahl() -> void:
|
|||||||
await give_up()
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
|
## Kampf verlassen: nach Spielende über den Ergebnisdialog, sonst per Aufgeben.
|
||||||
|
func leave_battle() -> void:
|
||||||
|
var scr = screen()
|
||||||
|
if scr is BattleScreen and scr.game != null and scr.game.winner != "":
|
||||||
|
await frames(2)
|
||||||
|
await click(find_button("Zurück zum Menü"))
|
||||||
|
check(screen() is MenuScreen, "Ergebnisdialog führt ins Menü")
|
||||||
|
else:
|
||||||
|
await give_up()
|
||||||
|
|
||||||
|
|
||||||
func give_up() -> void:
|
func give_up() -> void:
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
await clear_mulligan(scr)
|
await clear_mulligan(scr)
|
||||||
@@ -390,6 +401,11 @@ func test_einstellungen() -> void:
|
|||||||
func test_erfolge() -> void:
|
func test_erfolge() -> void:
|
||||||
await click(find_button("Erfolge"))
|
await click(find_button("Erfolge"))
|
||||||
check(screen() is AchievementsScreen, "Erfolge geöffnet")
|
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 shot("14_erfolge")
|
||||||
await click(find_button("Zurück"))
|
await click(find_button("Zurück"))
|
||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
@@ -427,6 +443,26 @@ func test_werkstatt() -> void:
|
|||||||
check(screen() is MenuScreen, "zurück im Menü")
|
check(screen() is MenuScreen, "zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
func test_hotseat() -> void:
|
||||||
await click(find_button("Lokal zu mehreren"))
|
await click(find_button("Lokal zu mehreren"))
|
||||||
var scr = screen()
|
var scr = screen()
|
||||||
@@ -458,7 +494,7 @@ func test_hotseat() -> void:
|
|||||||
await clear_mulligan(b)
|
await clear_mulligan(b)
|
||||||
check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch")
|
check(b.hero_panels.size() == 3 and not b.hero_panels.has(cur), "drei andere am Tisch")
|
||||||
await shot("19_hotseat_kampf")
|
await shot("19_hotseat_kampf")
|
||||||
await give_up()
|
await leave_battle()
|
||||||
|
|
||||||
|
|
||||||
func test_sprache() -> void:
|
func test_sprache() -> void:
|
||||||
@@ -469,10 +505,12 @@ func test_sprache() -> void:
|
|||||||
await frames(2)
|
await frames(2)
|
||||||
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
|
check(TranslationServer.get_locale().begins_with("en") and main.profile.settings.language == "en", "Sprache auf Englisch")
|
||||||
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
|
check(TranslationServer.translate("Zurück") == "Back" and TranslationServer.translate("Einzelspieler") == "Single player", "Übersetzung geladen")
|
||||||
|
check(TranslationServer.translate("Zug %d · Du bist dran") == "Turn %d · Your turn" and TranslationServer.translate("Nicht genug Energie") == "Not enough energy", "dynamische Texte übersetzt")
|
||||||
check(find_button("Back") != null, "Knöpfe übersetzt")
|
check(find_button("Back") != null, "Knöpfe übersetzt")
|
||||||
await shot("26_englisch")
|
await shot("26_englisch")
|
||||||
await click(find_button("Back"))
|
await click(find_button("Back"))
|
||||||
check(find_button("Single player") != null and find_button("Settings") != null, "Menü auf Englisch")
|
check(find_button("Single player") != null and find_button("Settings") != null, "Menü auf Englisch")
|
||||||
|
check(screen().coins_label.text.begins_with("Coins:"), "zusammengesetzte Texte übersetzt")
|
||||||
await click(find_button("Settings"))
|
await click(find_button("Settings"))
|
||||||
var scr2 = screen()
|
var scr2 = screen()
|
||||||
scr2.language.select(0)
|
scr2.language.select(0)
|
||||||
@@ -506,14 +544,36 @@ func test_anleitung() -> void:
|
|||||||
if g.human.hand[i].is_creature() and g.human.hand[i].cost == 1:
|
if g.human.hand[i].is_creature() and g.human.hand[i].cost == 1:
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
|
check(idx < 0 or scr.highlight_layer.get_child_count() >= 1, "spielbare Wesen hervorgehoben")
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
|
var played_id: String = g.human.hand[idx].id
|
||||||
await key(KEY_1 + idx)
|
await key(KEY_1 + idx)
|
||||||
check(g.human.board.size() == 1, "Wesen per Taste gespielt")
|
check(g.human.board.size() == 1, "Wesen per Taste gespielt")
|
||||||
check(scr.tutorial_step == 1, "Schritt 2 nach dem Ausspielen")
|
check(scr.tutorial_step == 1, "Schritt 2 nach dem Ausspielen")
|
||||||
|
check(scr.highlight_layer.get_child_count() == 1 and scr.highlight_layer.get_child(0).get_rect().intersects(scr.b_end.get_rect()), "Zug-beenden-Knopf hervorgehoben")
|
||||||
|
# Rückgängig: Wesen zurück auf die Hand
|
||||||
|
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(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")
|
await shot("22_anleitung")
|
||||||
|
scr.ai_delay = 0.75
|
||||||
await key(KEY_SPACE)
|
await key(KEY_SPACE)
|
||||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||||
await wait_for_human(scr)
|
await frames(2)
|
||||||
|
check(scr.b_skip.visible and not scr.b_end.visible, "Überspringen-Knopf während des Gegnerzugs")
|
||||||
|
await click(scr.b_skip)
|
||||||
|
var w := 0
|
||||||
|
while g.current() != g.human and g.winner == "" and w < 120:
|
||||||
|
await frames(1)
|
||||||
|
w += 1
|
||||||
|
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")
|
check(scr.tutorial_step >= 2 or idx < 0, "Schritt 3 im zweiten Zug")
|
||||||
var step_before: int = scr.tutorial_step
|
var step_before: int = scr.tutorial_step
|
||||||
await click(scr.b_tut_next)
|
await click(scr.b_tut_next)
|
||||||
@@ -532,6 +592,8 @@ func test_netzwerk() -> void:
|
|||||||
check(scr is LobbyScreen, "Lobby geöffnet")
|
check(scr is LobbyScreen, "Lobby geöffnet")
|
||||||
await click(find_button("Spiel eröffnen"))
|
await click(find_button("Spiel eröffnen"))
|
||||||
check(main.net.is_host and main.net.connected, "Spiel eröffnet")
|
check(main.net.is_host and main.net.connected, "Spiel eröffnet")
|
||||||
|
if FileAccess.file_exists("user://bot_quit"):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://bot_quit"))
|
||||||
var args := ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--", "--ip=127.0.0.1", "--name=Bot"]
|
var args := ["--headless", "--path", ProjectSettings.globalize_path("res://"), "tests/net_bot.tscn", "--", "--ip=127.0.0.1", "--name=Bot"]
|
||||||
var pid := OS.create_process(OS.get_executable_path(), args)
|
var pid := OS.create_process(OS.get_executable_path(), args)
|
||||||
check(pid > 0, "Bot-Prozess gestartet")
|
check(pid > 0, "Bot-Prozess gestartet")
|
||||||
@@ -559,19 +621,46 @@ func test_netzwerk() -> void:
|
|||||||
check(b.game.turn >= 3 and b.game.winner == "", "mehrere Runden gespielt, Partie läuft (Zug %d)" % b.game.turn)
|
check(b.game.turn >= 3 and b.game.winner == "", "mehrere Runden gespielt, Partie läuft (Zug %d)" % b.game.turn)
|
||||||
check(b.game.log.any(func(l): return l.begins_with("Bot spielt") or l.begins_with("Bot behält") or l.begins_with("Bot tauscht")), "Bot hat gehandelt")
|
check(b.game.log.any(func(l): return l.begins_with("Bot spielt") or l.begins_with("Bot behält") or l.begins_with("Bot tauscht")), "Bot hat gehandelt")
|
||||||
await shot("24_netzwerk_kampf")
|
await shot("24_netzwerk_kampf")
|
||||||
OS.kill(pid)
|
# Bot verlässt geordnet: Platz bleibt frei, Züge werden übersprungen
|
||||||
|
var quit_file := "user://bot_quit"
|
||||||
|
var f := FileAccess.open(quit_file, FileAccess.WRITE)
|
||||||
|
f.store_string("x")
|
||||||
|
f.close()
|
||||||
var w3 := 0
|
var w3 := 0
|
||||||
while b.game.winner == "" and w3 < 1500:
|
while main.net.waiting.is_empty() and w3 < 600:
|
||||||
await frames(1)
|
await frames(1)
|
||||||
w3 += 1
|
w3 += 1
|
||||||
if b.game.winner != "":
|
check(main.net.waiting.has("Bot") and b.game.log.any(func(l): return "Verbindung verloren" in l), "Platz wird freigehalten")
|
||||||
check(b.game.winner == "human" and b.game.log.any(func(l): return "verlassen" in l), "Trennung des Bots zählt als Aufgabe")
|
var turn_before: int = b.game.turn
|
||||||
await frames(2)
|
for i in 2:
|
||||||
check(b.overlay.visible, "Spielende angezeigt")
|
await clear_mulligan(b)
|
||||||
await click(find_button("Zurück zum Menü"))
|
if b.game.current() == b.perspective and b.game.winner == "":
|
||||||
else:
|
await click(b.b_end)
|
||||||
print("HINWEIS: Bot-Trennung nach 25 s noch nicht erkannt, Aufgeben stattdessen")
|
var w4 := 0
|
||||||
await give_up()
|
while b.game.current() != b.perspective and b.game.winner == "" and w4 < 300:
|
||||||
|
await frames(1)
|
||||||
|
w4 += 1
|
||||||
|
check(b.game.turn >= turn_before + 1 and b.game.log.any(func(l): return "übersprungen" in l), "Züge des fehlenden Spielers werden übersprungen")
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(quit_file))
|
||||||
|
await frames(5)
|
||||||
|
var pid2 := OS.create_process(OS.get_executable_path(), args)
|
||||||
|
var w5 := 0
|
||||||
|
while not main.net.waiting.is_empty() and w5 < 900:
|
||||||
|
await frames(1)
|
||||||
|
w5 += 1
|
||||||
|
check(main.net.waiting.is_empty() and main.net.seats.size() == 2 and b.game.log.any(func(l): return l == "Bot ist zurück."), "Bot wieder verbunden, Platz zurück")
|
||||||
|
var turn2: int = b.game.turn
|
||||||
|
for i in 2:
|
||||||
|
await clear_mulligan(b)
|
||||||
|
if b.game.current() == b.perspective and b.game.winner == "":
|
||||||
|
await click(b.b_end)
|
||||||
|
var w6 := 0
|
||||||
|
while b.game.current() != b.perspective and b.game.winner == "" and w6 < 900:
|
||||||
|
await frames(1)
|
||||||
|
w6 += 1
|
||||||
|
check(b.game.turn >= turn2 + 1, "Partie läuft nach der Rückkehr weiter")
|
||||||
|
OS.kill(pid2)
|
||||||
|
await leave_battle()
|
||||||
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
check(screen() is MenuScreen and not main.net.connected, "Netzwerk getrennt, zurück im Menü")
|
||||||
|
|
||||||
|
|
||||||
@@ -581,12 +670,31 @@ func test_kampagne() -> void:
|
|||||||
check(scr is CampaignScreen, "Kampagne geöffnet")
|
check(scr is CampaignScreen, "Kampagne geöffnet")
|
||||||
await shot("15_kampagne")
|
await shot("15_kampagne")
|
||||||
check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen")
|
check(find_button("Gesperrt") != null and find_button("Kämpfen") != null, "Stufen gesperrt und offen")
|
||||||
|
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
|
||||||
await click(find_button("Kämpfen"))
|
await click(find_button("Kämpfen"))
|
||||||
var battle = screen()
|
var battle = screen()
|
||||||
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
|
||||||
check(battle.game.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner")
|
check(battle.game.opponents.size() == 1 and battle.game.opponents[0].name == "Fenn", "Fenn ist der Gegner")
|
||||||
|
check(battle.game.opponents[0].perks == ["feuer_angriff"] and battle.game.log.any(func(l): return l.begins_with("Fenn: Feuerwesen")), "Boss-Merkmal aktiv")
|
||||||
await shot("16_kampagne_kampf")
|
await shot("16_kampagne_kampf")
|
||||||
await give_up()
|
await give_up()
|
||||||
|
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
|
||||||
|
main.profile.campaign_stage = Campaign.STAGES.size()
|
||||||
|
await click(find_button("Kampagne"))
|
||||||
|
var scr2 = screen()
|
||||||
|
check(scr2.chapter == 2 and scr2.perk_box != null, "Kapitel 2 offen, Merkmal wählbar")
|
||||||
|
scr2.perk_box.select(1)
|
||||||
|
scr2.perk_box.item_selected.emit(1)
|
||||||
|
check(main.profile.perk == Game.PERKS.keys()[0], "Merkmal gewählt")
|
||||||
|
await shot("27_kampagne_kapitel2")
|
||||||
|
await click(find_button("Kämpfen"))
|
||||||
|
var b2 = screen()
|
||||||
|
check(b2 is BattleScreen and b2.stage == 10 and b2.game.opponents[0].perks.size() == 2, "Kapitel-2-Boss mit zwei Merkmalen")
|
||||||
|
check(b2.game.human.perks == [main.profile.perk], "eigenes Merkmal im Kampf")
|
||||||
|
await give_up()
|
||||||
|
main.profile.campaign_stage = 0
|
||||||
|
main.profile.perk = ""
|
||||||
|
main.save()
|
||||||
|
|
||||||
|
|
||||||
func test_kampf(n_opp: int, shot_turn: int) -> void:
|
func test_kampf(n_opp: int, shot_turn: int) -> void:
|
||||||
|
|||||||
@@ -57,3 +57,37 @@ Hand tauschen?,Swap cards?
|
|||||||
Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game.
|
Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game.
|
||||||
Fallen,Traps
|
Fallen,Traps
|
||||||
Du bist dran,Your turn
|
Du bist dran,Your turn
|
||||||
|
Herausforderung,Challenge
|
||||||
|
Antreten,Enter
|
||||||
|
Statistik exportieren,Export statistics
|
||||||
|
Gegnerzüge überspringen,Skip AI turns
|
||||||
|
Rückgängig,Undo
|
||||||
|
Kapitel 1,Chapter 1
|
||||||
|
Kapitel 2,Chapter 2
|
||||||
|
"Münzen: %d · Staub: %d","Coins: %d · Dust: %d"
|
||||||
|
"Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d","Collection: %d of %d cards (%d total) · Campaign: %d/%d"
|
||||||
|
Aktives Deck: %s (%s),Active deck: %s (%s)
|
||||||
|
leer,empty
|
||||||
|
Tagesaufgabe erledigt: %s ✓,Daily task done: %s ✓
|
||||||
|
Tagesaufgabe: %s (%d/%d) · +%d Münzen,Daily task: %s (%d/%d) · +%d coins
|
||||||
|
"Aktives Deck nicht spielbar: ","Active deck not playable: "
|
||||||
|
Zug %d · Du bist dran,Turn %d · Your turn
|
||||||
|
Zug %d · %s ist dran …,Turn %d · %s is playing …
|
||||||
|
Wähle ein Ziel,Choose a target
|
||||||
|
Wähle ein Angriffsziel,Choose an attack target
|
||||||
|
Abgebrochen,Cancelled
|
||||||
|
%s kann nicht angreifen,%s cannot attack
|
||||||
|
Nicht dein Zug,Not your turn
|
||||||
|
Nicht genug Energie,Not enough energy
|
||||||
|
Dein Feld ist voll,Your board is full
|
||||||
|
Kein gültiges Ziel,No valid target
|
||||||
|
Ungültiges Ziel,Invalid target
|
||||||
|
Höchstens 2 Fallen,At most 2 traps
|
||||||
|
Ein Wächter muss zuerst angegriffen werden,A guardian must be attacked first
|
||||||
|
Sieg!,Victory!
|
||||||
|
Niederlage,Defeat
|
||||||
|
+%d Münzen,+%d coins
|
||||||
|
"Deck braucht genau %d Karten (%d)","Deck needs exactly %d cards (%d)"
|
||||||
|
Höchstens %dx dieselbe Karte,At most %dx the same card
|
||||||
|
Karte nicht oft genug in der Sammlung,Not enough copies in your collection
|
||||||
|
Höchstens %d Elemente im Deck,At most %d elements in a deck
|
||||||
|
|||||||
|
Binary file not shown.
Reference in New Issue
Block a user