Files
cards/tests/ui_test.gd
T
team3 1d563b9eec
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped
update
2026-10-14 17:35:09 +02:00

1079 lines
55 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
## Oberflächentest mit echtem Fenster und Screenshots:
## DINGER_SHOTS=/pfad/zum/ordner godot --path . tests/ui_test.tscn
const SAVE := "user://test_ui_spielstand.json"
const DEVICE := 7 # Gerätenummer der Testereignisse; alles andere fängt der InputFilter ab
var main: Control
var shots := ""
var passed := 0
var failed := 0
func check(cond: bool, msg: String) -> void:
if cond:
passed += 1
else:
failed += 1
printerr("FEHLER: " + msg)
print("%d Prüfungen bestanden, Abbruch beim ersten Fehler" % passed)
get_tree().quit(1)
## Lässt nur Ereignisse des Tests durch; echte Maus und Tastatur des Nutzers werden abgefangen,
## bevor Knöpfe oder Bildschirme sie sehen. Sitzt als letzter Knoten im Baum und bekommt _input
## darum als Erster.
class InputFilter extends Node:
func _input(event: InputEvent) -> void:
if event.device != DEVICE:
get_viewport().set_input_as_handled()
## Testereignis an das Fenster, mit der eigenen Gerätenummer.
func push(ev: InputEvent) -> void:
ev.device = DEVICE
get_viewport().push_input(ev)
func frames(n: int = 2) -> void:
for i in n:
await get_tree().process_frame
func shot(name: String) -> void:
if shots == "":
return
await frames(10) # Einblenden und Austeilen abwarten
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(shots.path_join(name + ".png"))
func click_at(pos: Vector2) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
push(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
push(up)
await frames(2)
func click(node: Control) -> void:
check(node != null, "Klickziel vorhanden")
if node != null:
await click_at(node.get_global_rect().get_center())
func right_click() -> void:
await right_click_at(Vector2(5, 5))
func double_click_at(pos: Vector2) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
down.double_click = true
push(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
up.double_click = false
push(up)
await frames(2)
func right_click_at(pos: Vector2, button: int = MOUSE_BUTTON_RIGHT) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = button
down.pressed = true
push(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
push(up)
await frames(2)
func key(code: int) -> void:
var down := InputEventKey.new()
down.keycode = code
down.pressed = true
push(down)
var up := InputEventKey.new()
up.keycode = code
up.pressed = false
push(up)
await frames(2)
func pad(button: int) -> void:
var ev := InputEventJoypadButton.new()
ev.button_index = button
ev.pressed = true
push(ev)
await frames(2)
## Mulligan-Maske wegklicken (Hand behalten), falls sie offen ist.
func hover(node: Control) -> void:
var ev := InputEventMouseMotion.new()
ev.position = node.get_global_rect().get_center()
ev.global_position = ev.position
push(ev)
await frames(2)
func screen():
return main.screen
## Jeder Abschnitt startet im Menü, auch wenn ein vorheriger abgebrochen ist.
func ensure_menu() -> void:
if TranslationServer.get_locale().begins_with("en"):
main.set_language("de")
if not (screen() is MenuScreen):
main.show_screen(MenuScreen.new())
await frames(2)
func find_button(text: String, root: Node = null) -> Button:
if root == null:
root = screen()
for c in root.get_children():
if c is Button and (c.text == text or c.tr(c.text) == text) and c.is_visible_in_tree():
return c
var found := find_button(text, c)
if found != null:
return found
return null
## Wartet, bis keine Kachel mehr animiert wird (Angriffe verschieben Kacheln kurz).
func settle(scr) -> void:
for i in 40:
if not scr.tiles.values().any(func(t): return t.busy):
return
await frames(1)
func wait_for_human(scr) -> void:
var g: Game = scr.game
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
await frames(1)
func _ready() -> void:
Game.test_seed = 4242 # wiederholbare Partien und Booster
shots = OS.get_environment("DINGER_SHOTS")
var abs := ProjectSettings.globalize_path(SAVE)
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(SAVE + suffix):
DirAccess.remove_absolute(abs + suffix)
main = load("res://scenes/main.tscn").instantiate()
main.save_path = SAVE
add_child(main)
add_child(InputFilter.new()) # nach main, damit er im Baum zuletzt steht
main.sfx.muted = true # Tests bleiben still
await frames(3)
var suite := [test_start, test_menu, test_gamepad_menue, test_booster, test_gegnerauswahl, test_einstellungen,
test_kampagne, test_sammlung, test_skilltree, test_cheats, test_deck, test_sprache, test_kampf_steuerung, test_kampf_elemente_und_verschmelzen, test_kampf_grenzen]
suite += [test_kampf.bind(3), test_kampf_abbrechen]
suite.append(test_profil_zuruecksetzen)
var only := OS.get_environment("DINGER_ONLY") # z. B. DINGER_ONLY=test_sammlung,test_kampf
if only != "":
suite = suite.filter(func(t: Callable): return t.get_method() in only.split(","))
for t in suite:
await ensure_menu()
await t.call()
for suffix in ["", ".bak", ".corrupt"]:
if FileAccess.file_exists(SAVE + suffix):
DirAccess.remove_absolute(abs + suffix)
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
get_tree().quit(1 if failed > 0 else 0)
## Neues Profil: die neutralen Karten als Sammlung und Stapel, direkt ins Menü.
func test_start() -> void:
main.reset_profile()
await frames(2)
var p: Profile = main.profile
check(screen() is MenuScreen and p.collection.size() == 110 and p.deck.size() == Game.DECK_MIN and p.deck.all(func(id): return Cards.by_id(id).is_neutral()), "neues Profil: 110 Neutrale, 30 im Stapel")
await shot("00_start")
check(Profile.load_or_new(SAVE).collection.size() == 110, "gespeichert")
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")
func test_booster() -> void:
await click(find_button("Booster öffnen"))
var scr = screen()
check(scr is BoosterScreen, "Boosterbildschirm")
var coins: int = main.profile.coins
await click(scr.b_buy)
check(main.profile.coins == coins - Profile.BOOSTER_COST, "Booster bezahlt")
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
await shot("03_booster_zu")
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
await click(backs[0])
check(scr.revealed.size() == 1, "eine Karte aufgedeckt")
await click(scr.b_all)
check(scr.revealed.size() == 5, "alle aufgedeckt")
await shot("04_booster_offen")
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Kauf gespeichert")
for res in scr.cards:
if res.new or res.upgraded:
check(main.profile.owns(res.card.id) and main.profile.level(res.card.id) == Cards.RARITIES.find(res.rarity), "neue oder aufgewertete Karte trägt ihre Stufe")
else:
check(res.dust > 0, "sonst gibt es Staub")
check(scr.b_buy.disabled == (main.profile.coins < Profile.BOOSTER_COST), "Kaufknopf folgt dem Münzstand")
check(not scr.pack_tiles["Feuer"].visible and scr.pack_tiles[Cards.NEUTRAL].visible and scr.pack_tiles[Cards.NEUTRAL].position.x == 530, "Feuer-Pack bis Kapitel 2 unsichtbar, Neutral mittig")
check(scr.summary_label.text.ends_with("Staub") and scr.pack_tiles[Cards.NEUTRAL].pulse, "Fazit steht, das gekaufte Pack pulsiert")
var saved_coins: int = main.profile.coins
main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
main.profile.coins = Profile.ELEMENT_BOOSTER_COST
scr.refresh()
check(scr.pack_tiles["Feuer"].visible and not scr.pack_buttons["Feuer"].disabled and "200" in scr.pack_tiles["Feuer"].hint and not scr.pack_tiles["Wasser"].visible, "Kapitel 2 offen: Feuer kaufbar, Wasser unsichtbar")
await click(scr.pack_buttons["Feuer"])
check(main.profile.coins == 0 and scr.cards.size() == 5 and scr.cards.all(func(res): return res.card.elements[0] == "Feuer"), "Feuer-Booster: fünf Feuerkarten")
await click(scr.b_all)
await shot("04b_booster_feuer")
main.profile.coins = Profile.BOOSTER_COST * 5
scr.refresh()
check(not scr.pack_tiles[Cards.NEUTRAL].buy_buttons[1].disabled and scr.pack_tiles[Cards.NEUTRAL].buy_buttons[2].disabled, "×5 bezahlbar, ×10 nicht")
await click(scr.pack_tiles[Cards.NEUTRAL].buy_buttons[1])
check(scr.cards.size() == 25 and main.profile.coins == 0 and scr.revealed.is_empty() and scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack).size() == 25, "fünf Packs auf einmal: 25 verdeckte Karten")
check(Cards.RARITIES.find(scr.cards[0].rarity) >= Cards.RARITIES.find(scr.cards[24].rarity), "beste Stufe zuerst")
await frames(30)
await shot("04c_booster_fuenf_zu")
await click(scr.b_all)
check(scr.revealed.size() == 25 and scr.summary_label.text.begins_with(str(scr.cards.filter(func(x): return x.new).size())) and "Staub" in scr.summary_label.text, "alle aufgedeckt, Fazit zählt neu und Staub")
await frames(40)
await shot("04d_booster_fuenf_offen")
main.profile.campaign_stage = 0
main.profile.coins = saved_coins
main.save()
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func target_view(scr, valid: Array) -> Control:
for v in scr.enemy_views:
if v.creature in valid:
return v
for v in scr.own_views:
if v.creature in valid:
return v
for p in scr.hero_panels:
if p in valid:
return scr.hero_panels[p]
return null
## Zieht eine Karte oder Kachel auf eine andere und lässt sie dort los; danach wartet das Ergebnis
## über dem Ziel auf accept_fuse oder „Abbrechen“.
func drag_fuse(scr, from: Control, to: Control) -> void:
await frames(20) # Kacheln sind noch unterwegs (Einfliegen, Nachrücken)
await settle(scr)
var a := from.get_global_rect().get_center()
var b := to.get_global_rect().get_center()
var fo = from.card if from is CardView else from.creature
var to_o = to.card if to is CardView else to.creature
var down := InputEventMouseButton.new()
down.position = a
down.global_position = a
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
push(down)
await frames(1)
for i in range(1, 6):
var m := InputEventMouseMotion.new()
m.position = a.lerp(b, i / 5.0)
m.global_position = m.position
m.button_mask = MOUSE_BUTTON_MASK_LEFT
push(m)
await frames(1)
if scr.drag_ghost == null or scr.drag_target == null:
print("DEBUG drag: ghost=", scr.drag_ghost, " target=", scr.drag_target, " press=", scr.press_target, " dran=", scr.my_turn(),
" overlay=", scr.any_overlay(), " mana=", scr.game.human.energy, " kann=", scr.game.can_fuse(scr.game.human, fo, to_o), " from=", from.get_global_rect(), " to=", to.get_global_rect())
check(scr.drag_ghost != null and scr.drag_target == to_o and to.drop, "Karte schwebt über einem Verschmelzungsziel, das golden gerahmt ist")
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
up.position = b
up.global_position = b
push(up)
await frames(2)
var want: Cards.Card = Game.fusion_result(scr.game.human, fo, to_o)
var shown: Array = scr.menu_layer.get_children().filter(func(c): return c is CardView)
check(scr.fuse_pending == [fo, to_o] and scr.menu_layer.visible and shown.size() == 1 and shown[0].card.name == want.name and shown[0].card.attack == want.attack and shown[0].card.health == want.health, "nach dem Loslassen schwebt %s (%d/%d) über dem Ziel und wartet auf Bestätigung" % [want.name, want.attack, want.health])
check(shown[0].position.y + shown[0].size.y < to.get_global_rect().position.y and not to.drop, "das Ergebnis steht über dem Ziel, der goldene Rahmen ist weg")
## Bestätigt die offene Verschmelzungsfrage; die neue Karte gleitet von oben in die Hand.
func accept_fuse(scr) -> void:
var pair: Array = scr.fuse_pending
check(pair != null, "eine Verschmelzung wartet")
var before: Array = scr.game.human.hand.duplicate()
var btn := find_button("Verschmelzen", scr.menu_layer)
check(btn != null, "Knopf Verschmelzen")
var at := btn.get_global_rect().get_center()
var down := InputEventMouseButton.new()
down.position = at
down.global_position = at
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
push(down)
var up: InputEventMouseButton = down.duplicate()
up.pressed = false
push(up)
await frames(1)
var fresh: Array = scr.hand_views.filter(func(v): return not (v.card in before))
check(fresh.size() == 1 and (fresh[0].position.y < BattleLayout.HAND_Y - 40 or not scr.fx.enabled), "die neue Karte gleitet von oben in die Hand")
await frames(2)
## Legt, was bezahlbar ist.
func play_hand(scr) -> void:
var g: Game = scr.game
for i in 12:
var playable: Array = scr.hand_views.filter(func(v): return g.can_play(g.human, v.card)[0])
if playable.is_empty():
break
var card: Cards.Card = playable[0].card
var board_before: int = g.human.board.size()
await click(playable[0])
check(g.human.board.size() > board_before, "Karte gespielt: " + card.name)
func human_turn(scr) -> void:
var g: Game = scr.game
# erst legen, was bezahlbar ist, dann mit dem Rest ziehen (und wieder legen)
for round in 2:
await play_hand(scr)
for i in 6:
if g.winner != "" or not g.can_draw(g.human)[0]:
break
var mana: int = g.human.energy
var hand: int = g.human.hand.size()
var cost: int = g.draw_cost(g.human)
await click(scr.deck)
check(g.human.energy == mana - cost and g.human.hand.size() == hand + 1, "gezogen: eine Karte")
var tried := [] # Wesen ohne lohnendes Ziel (0 Angriff ohne Vorteil) nicht wieder versuchen
for i in 12:
var ready: Array = scr.own_views.filter(func(v): return v.creature.can_attack() and not (v.creature in tried))
if ready.is_empty() or g.winner != "":
break
var cr: Game.Creature = ready[0].creature
var any_target: bool = g.can_attack(cr, g.opponent)[0] or g.opponent.board.any(func(c): return g.can_attack(cr, c)[0])
if not any_target:
tried.append(cr)
continue
await settle(scr)
await hover(ready[0])
var under = get_viewport().gui_get_hovered_control()
await click(ready[0])
if scr.selected != cr:
print("DEBUG Auswahl fehlgeschlagen: rect=", ready[0].get_global_rect(), " scale=", ready[0].scale,
" busy=", ready[0].busy, " unter Maus=", under, " Meldung='", scr.msg_label.text,
"' Zug Mensch=", g.current().is_human, " kann=", cr.can_attack(), " selected=", scr.selected)
check(scr.selected == cr, "Wesen gewählt")
var targets := []
for o in [g.opponent]:
if o.alive() and g.can_attack(cr, o)[0]:
targets.append(o)
for c in o.board:
if g.can_attack(cr, c)[0]:
targets.append(c)
await settle(scr)
await click(target_view(scr, targets))
check(scr.selected == null and cr.attacks_done > 0, "Angriff ausgeführt")
if g.winner == "":
await key(KEY_SPACE)
func start_battle() -> void:
await click(find_button("Einzelspieler"))
check(screen() is OpponentSelectScreen, "Gegnerauswahl")
await click(find_button("Starten"))
func test_gegnerauswahl() -> void:
await click(find_button("Einzelspieler"))
var sel = screen()
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
check(find_button("Neu würfeln") == null and find_button("Feuer") == null and find_button("Starten") != null, "nur Schwierigkeit und Starten")
await click(find_button("Schwer"))
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
check("Rare" in sel.info_label.text and "Merkmal" in sel.info_label.text and "×2" in sel.info_label.text, "Schwer: Gegnerkarten Rare, Merkmal und Münzfaktor stehen dabei: " + sel.info_label.text)
await shot("07_gegnerauswahl")
await click(find_button("Starten"))
var hard = screen()
check(hard is BattleScreen and hard.game.players.size() == 2, "Duell gestartet")
var opp: Game.Player = hard.game.opponent
check(opp.max_life == Game.START_LIFE and hard.smart_ai and opp.perks.size() == 1 and opp.looks.values().all(func(l): return l == 1) and opp.title == Cards.NEUTRAL, "Schwer: 30 Leben, Vorausschau-KI, ein Merkmal, Neutral in Rare")
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
var mine: Array = main.profile.deck_cards()
check(hard.game.human.hand.all(func(c): return c.id in mine), "die Starthand kommt ungefiltert vom eigenen Stapel")
await shot("05_starthand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht links oben")
await shot("11_kampf")
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Leicht"))
await click(find_button("Starten"))
var easy = screen()
opp = easy.game.opponent
check(opp.max_life == Game.START_LIFE and not easy.smart_ai and opp.perks.is_empty() and opp.looks.values().all(func(l): return l == 0), "Leicht: 30 Leben, einfache KI, kein Merkmal, Common")
check(easy.game.players[0].max_energy == Game.START_MANA and easy.game.human.life == Game.START_LIFE, "gleiche Regeln für beide")
await give_up()
await click(find_button("Einzelspieler"))
await click(find_button("Normal"))
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## 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:
var scr = screen()
await key(KEY_ESCAPE)
check(scr.options.visible, "Esc öffnet die Optionen")
await click(find_button("Aufgeben", scr.options))
check(scr.confirm.visible and not scr.options.visible, "Aufgeben fragt nach")
await click(find_button("Weiterspielen"))
check(not scr.confirm.visible and screen() == scr, "Weiterspielen bleibt im Kampf")
await key(KEY_ESCAPE)
await click(find_button("Aufgeben", scr.options))
await click(find_button("Ja, aufgeben"))
check(screen() is MenuScreen, "Aufgeben führt ins Menü")
func test_einstellungen() -> void:
await click(find_button("Einstellungen"))
var scr = screen()
check(scr is SettingsScreen, "Einstellungen geöffnet")
scr.volume.value = 0.3
scr.speed.value = 2.0
scr.anim.button_pressed = false
await frames(1)
check(is_equal_approx(main.profile.settings.volume, 0.3) and is_equal_approx(main.profile.settings.ai_speed, 2.0), "Regler gespeichert")
check(main.profile.settings.animations == false, "Animationen abschaltbar")
check(is_equal_approx(Profile.load_or_new(SAVE).settings.volume, 0.3), "Einstellungen auf Platte")
scr.anim.button_pressed = true
scr.big_font.button_pressed = true
await frames(2)
check(main.profile.settings.big_font and Style.font_scale > 1.0 and screen() is SettingsScreen, "große Schrift aktiv")
screen().big_font.button_pressed = false
await frames(2)
check(is_equal_approx(Style.font_scale, 1.0), "normale Schrift")
var scr_b = screen()
await shot("13_einstellungen")
scr = scr_b
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Skilltree: Level und Punkte aus der Erfahrung, Lernen verbraucht Punkte, Knöpfe folgen dem Stand.
func test_skilltree() -> void:
main.profile.xp = Skills.xp_for_level(3)
main.profile.skills = {}
main.save()
await click(find_button("Skilltree"))
var scr = screen()
check(scr is SkillScreen and scr.level_label.text == "Level 3" and scr.points_label.text == "Skillpunkte: 3", "Skilltree: Level 3, drei Punkte")
check(scr.learn_buttons.size() == Skills.SKILLS.size() and not scr.learn_buttons["packglueck"].disabled and scr.xp_label.text == "0 / 400 EP bis Level 4", "alle Skills mit Lernen-Knopf, Fortschritt bis Level 4")
check(scr.now_labels["packglueck"].text == "Jetzt: Chancen 45/25/15/10/5 %" and scr.next_labels["packglueck"].text == "Nächste Stufe: Chancen 37/28/17/12/6 %", "Wirkung jetzt und nächste Stufe in Zahlen")
await click(scr.learn_buttons["packglueck"])
check(main.profile.skill("packglueck") == 1 and scr.points_label.text == "Skillpunkte: 2" and scr.rank_labels["packglueck"].text == "Stufe 1 / 3", "Packglück gelernt, Punkt verbraucht")
await click(scr.learn_buttons["gratisziehen"])
check(scr.learn_buttons["gratisziehen"].disabled and scr.rank_labels["gratisziehen"].text == "Stufe 1 / 1" and scr.next_labels["gratisziehen"].text == "Höchste Stufe erreicht", "Freies Ziehen voll: Knopf aus")
await shot("28_skilltree")
check(Profile.load_or_new(SAVE).skill("packglueck") == 1, "gespeichert")
await click(find_button("Zurück"))
check(screen() is MenuScreen and screen().coll_label.text.ends_with("Level 3"), "zurück im Menü, Level steht dabei")
main.profile.xp = 0
main.profile.skills = {}
main.save()
## Cheats: Knöpfe setzen Münzen, Level, Kapitel und Sammlung sofort und speichern.
func test_cheats() -> void:
await click(find_button("Cheats"))
var scr = screen()
check(scr is CheatScreen and scr.status_label.text.begins_with("Münzen %d" % main.profile.coins), "Cheats geöffnet, Stand steht oben")
var coins: int = main.profile.coins
await click(find_button("+1.000 Münzen"))
check(main.profile.coins == coins + 1000 and Profile.load_or_new(SAVE).coins == coins + 1000 and scr.msg_label.text == "Gespeichert", "+1.000 Münzen, gespeichert")
await click(find_button("+1 Level"))
check(main.profile.player_level() == 1 and main.profile.skill_points() == 1, "+1 Level: ein Skillpunkt")
await click(find_button("Kapitel 3 frei"))
check(main.profile.campaign_stage == 20 and main.profile.unlocked_elements().size() == 3, "Kapitel 3 frei: Neutral, Feuer, Wasser")
await click(find_button("Alle Karten Ultimate Rare"))
check(main.profile.collection.size() == Cards.all().size() and main.profile.level("f_5_5") == 4, "alle 550 Karten in Ultimate Rare")
await shot("29_cheats")
await click(find_button("Sammlung zurücksetzen"))
check(main.profile.collection.size() == 110 and main.profile.deck.size() == Game.DECK_MIN, "Sammlung wieder am Start")
await click(find_button("Kampagne zurücksetzen"))
await click(find_button("Erfahrung auf 0"))
check(main.profile.campaign_stage == 0 and main.profile.xp == 0, "Kampagne und Erfahrung zurück")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Deck: eigene Karten rein und raus, Auffüllen nach Filter, Speichern. Beginnt mit einem frischen Profil.
func test_deck() -> void:
main.reset_profile()
await frames(2)
await click(find_button("Deck"))
var scr = screen()
check(scr is DeckScreen, "Deck geöffnet")
var p: Profile = main.profile
check(scr.grid.views().size() == p.collection.size() and scr.count_label.text == "Stapel: %d Karten (%d bis %d)" % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX], "alle eigenen Karten, Zähler stimmt")
check(scr.strength_label.text == "Stärke 100", "Startsammlung aus Grundkarten: Stärke 100")
p.collection[p.deck[0]] = 4
scr.refresh()
await frames(2)
check(scr.strength_label.text == "Stärke %d" % roundi((29 * 100 + 500) / 30.0), "eine Ultimate Rare hebt die Stärke: " + scr.strength_label.text)
check(scr.grid.views()[0].card.id == p.deck[0] and scr.grid.views()[0].rarity == "Ultimate Rare", "höchste Stufe steht vorn")
p.collection[p.deck[0]] = 0
scr.refresh()
await frames(2)
var views: Array = scr.grid.views()
check(views.size() == 110 and views.slice(0, Game.DECK_MIN).all(func(v): return not v.dim and v.marked) and views[Game.DECK_MIN].dim and not views[Game.DECK_MIN].marked and p.deck.size() == Game.DECK_MIN, "Stapelkarten vorn mit Haken, der Rest dunkel")
check(scr.curve.counts.size() == 11 and scr.curve.counts.reduce(func(a, v): return a + v, 0) == Game.DECK_MIN and scr.curve.counts[0] == 2, "Manakurve: 30 Karten verteilt, zwei zu 0 Mana")
check(scr.share.counts == {Cards.NEUTRAL: Game.DECK_MIN} and scr.share.shown() == [Cards.NEUTRAL], "Elementanteile: nur Neutral")
check(views[0].card.cost <= views[1].card.cost and views[Game.DECK_MIN].card.cost <= views[Game.DECK_MIN + 1].card.cost, "innerhalb von Stapel und Rest nach Kosten")
await click(scr.cost_buttons[3])
views = scr.grid.views()
check(scr.cost_filter == 3 and views.size() == 15 and views.all(func(v): return v.card.cost == 3), "Manafilter 3: nur die 15 Karten zu 3 Mana")
await click(scr.cost_buttons[3])
views = scr.grid.views()
check(scr.cost_filter == -1 and views.size() == 110, "derselbe Knopf hebt den Manafilter auf")
check(scr.element_buttons.size() == 5 and scr.element_buttons["Feuer"].tooltip_text == "Feuer" and not scr.element_buttons["Feuer"].visible and scr.element_buttons[Cards.NEUTRAL].visible and scr.element_buttons[Cards.NEUTRAL].position.x == 108, "Elementfilter als Symbolknöpfe, nur offene Elemente")
await hover(views[2])
check(scr.preview_holder.get_child_count() == 1 and scr.preview_holder.get_child(0).card.id == views[2].card.id, "Zeigen stellt die Karte groß in die Vorschau")
var first_id: String = views[0].card.id
await click(views[0])
check(not (first_id in p.deck) and p.deck.size() == Game.DECK_MIN - 1 and scr.grid.views().any(func(v): return v.card.id == first_id and v.dim and not v.marked), "Klick nimmt die Karte aus dem Stapel, sie wird dunkel und verliert den Haken")
check(scr.curve.counts.reduce(func(a, v): return a + v, 0) == Game.DECK_MIN - 1 and scr.msg_label.text == "Klick auf eine Karte: rein oder raus.", "Manakurve folgt, Hinweis steht")
var back_view = scr.grid.views().filter(func(v): return v.card.id == first_id)[0]
scr.grid.ensure_control_visible(back_view) # sie liegt jetzt hinter den Stapelkarten, außerhalb des Sichtbereichs
await frames(2)
await click(back_view)
check(first_id in p.deck and p.deck.size() == Game.DECK_MIN, "Klick nimmt sie wieder hinein")
await shot("19_deck")
await click(find_button("Leeren"))
check(p.deck.is_empty(), "geleert")
for id in ["w_0_1", "w_0_2", "w_0_3", "w_0_4", "w_0_5"]:
p.collection[id] = 0
main.profile.campaign_stage = 2 * Campaign.STAGES_PER_CHAPTER # Wasser offen
scr.refresh()
await frames(1)
check(scr.element_buttons["Wasser"].visible and scr.element_buttons["Wasser"].position.x == 108 + 2 * 40, "Kapitel 3 offen: Wasserfilter an dritter Stelle")
await click(scr.element_buttons["Wasser"])
await click(find_button("Auffüllen"))
check(p.deck.size() == 5 and p.deck.all(func(id): return Cards.by_id(id).elements[0] == "Wasser"), "Auffüllen hält sich an den Filter: nur die fünf Wasserkarten")
check(scr.share.shown() == ["Wasser"] and scr.share.counts["Wasser"] == 5, "Elementanteile: fünf Wasser")
await click(find_button("Alle"))
await click(find_button("Auffüllen"))
check(p.deck.size() == Game.DECK_MAX and Profile.load_or_new(SAVE).deck == p.deck, "ohne Filter bis 60, gespeichert")
main.profile.campaign_stage = 0
main.save()
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func test_sammlung() -> void:
await click(find_button("Sammlung"))
var scr = screen()
check(scr is CollectionScreen, "Sammlung geöffnet")
var p: Profile = main.profile
check(scr.grid.views().size() == 110 and scr.grid.views().all(func(v): return v.card.is_neutral()), "nur die Karten offener Elemente: zu Beginn die 110 Neutralen")
check(find_button("Feuer") == null and find_button("Neutral") != null, "Filter nur für offene Elemente")
check(scr.grid.views().all(func(v): return not v.dim), "zu Beginn gehören dir alle gezeigten Karten")
p.dust = 5000
scr.search_box.text = "Tapferer Knappe"
scr.search_box.text_changed.emit("Tapferer Knappe")
await frames(1)
var funke = scr.grid.views().filter(func(v): return v.card.id == "n_2_1")[0]
await click(funke)
check(scr.selected.id == "n_2_1" and p.rarity("n_2_1") == "Common" and scr.b_upgrade.visible, "eigene Karte gewählt, noch Common")
await click(scr.b_upgrade)
check(p.level("n_2_1") == 1 and p.dust == 5000 - Profile.DUST_UPGRADE["Rare"], "auf Rare aufgewertet")
scr.search_box.text = ""
scr.search_box.text_changed.emit("")
await frames(1)
var front = scr.grid.views().filter(func(v): return v.card.id == "n_2_1")[0]
check(front.rarity == "Rare" and front.card.attack == 3 and front.card.health == 2 and scr.grid.views()[0].rarity != "Common", "die Rare zeigt +1/+1, aufgewertete stehen vorn: %s %d/%d" % [front.rarity, front.card.attack, front.card.health])
check(Profile.load_or_new(SAVE).level("n_2_1") == 1, "Sammlung gespeichert")
scr.search_box.text = "Tapferer Knappe"
scr.search_box.text_changed.emit("Tapferer Knappe")
await frames(1)
var dust: int = p.dust
await click(scr.b_dismantle)
check(p.level("n_2_1") == 0 and p.dust == dust + Profile.DUST_DISMANTLE["Rare"], "wieder zerlegt")
# Herstellen einer fehlenden Karte (Feuer: erst ab Kapitel 2 zu sehen)
scr.search_box.text = ""
scr.search_box.text_changed.emit("")
main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
scr.refresh()
await frames(1)
var owned_shown: int = scr.grid.views().filter(func(v): return not v.dim).size()
check(scr.grid.views().size() == 220 and owned_shown < 220 and scr.grid.views().slice(0, owned_shown).all(func(v): return not v.dim) and scr.grid.views().slice(owned_shown).all(func(v): return v.dim), "Kapitel 2 offen: auch Feuer zu sehen, eigene vorn, fehlende abgedunkelt")
scr.search_box.text = "Infernalischer Drache"
scr.search_box.text_changed.emit("Infernalischer Drache")
await frames(1)
await click(scr.grid.views().filter(func(v): return v.card.id == "f_5_5")[0])
check(scr.b_craft.visible and not scr.b_upgrade.visible, "fehlende Karte: nur Herstellen")
dust = p.dust
await click(scr.b_craft)
check(p.owns("f_5_5") and p.dust == dust - Profile.CRAFT_COST, "hergestellt")
await shot("20_sammlung")
main.profile.campaign_stage = 0
main.save()
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func test_gamepad_menue() -> void:
check(get_viewport().gui_get_focus_owner() == null, "ohne Gamepad kein Fokus")
await pad(JOY_BUTTON_DPAD_DOWN)
var f := get_viewport().gui_get_focus_owner()
check(f is Button and main.pad_mode, "Gamepad setzt den Fokus auf einen Knopf")
var down := InputEventJoypadButton.new()
down.button_index = JOY_BUTTON_DPAD_DOWN
down.pressed = true
down.device = DEVICE
Input.parse_input_event(down)
await frames(2)
var f2 := get_viewport().gui_get_focus_owner()
check(f2 is Button and f2 != f, "Steuerkreuz wechselt den Knopf")
var target_text: String = f2.text
check(InputMap.action_get_events("ui_accept").any(func(e): return e is InputEventJoypadButton), "A ist auf ui_accept gemappt")
var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A
accept.pressed = true
push(accept)
await frames(1)
var release := InputEventJoypadButton.new()
release.button_index = JOY_BUTTON_A
release.pressed = false
push(release)
await frames(3)
check(not (screen() is MenuScreen) or target_text == "Beenden", "A öffnet den fokussierten Bildschirm (%s)" % target_text)
await hover(main)
check(not main.pad_mode and get_viewport().gui_get_focus_owner() == null, "Maus nimmt den Fokus wieder")
func test_profil_zuruecksetzen() -> void:
await click(find_button("Einstellungen"))
await click(find_button("Profil zurücksetzen"))
var scr = screen()
check(scr.confirm.visible, "Rückfrage")
await click(find_button("Abbrechen"))
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 main.profile.collection.size() == 110 and main.profile.campaign_stage == 0, "Profil zurückgesetzt: neutrale Startsammlung, zurück im Menü")
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
func test_sprache() -> void:
await click(find_button("Einstellungen"))
var scr = screen()
scr.language.select(1)
scr.language.item_selected.emit(1)
await frames(2)
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("Abwerfen +%d ◆") == "Discard +%d ◆" and TranslationServer.translate("Nicht genug Mana") == "Not enough mana", "dynamische Texte übersetzt")
check(find_button("Back") != null, "Knöpfe übersetzt")
await shot("26_englisch")
await click(find_button("Back"))
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"))
var scr2 = screen()
scr2.language.select(0)
scr2.language.item_selected.emit(0)
await frames(2)
check(find_button("Zurück") != null, "zurück auf Deutsch")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
## Steuerung im Kampf: Tastatur, Rückgängig, Gamepad, Überspringen, Esc.
func test_kampf_steuerung() -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
g.human.energy = 20 # die Starthand ist zufällig, hier soll nur die Steuerung geprüft werden
g.human.hand = [Cards.by_id("f_3_2").copy()]
scr.refresh()
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
# Ziehen: eine Karte vom eigenen Stapel, das erste Ziehen ist frei
var mana: int = g.human.energy
var deck_before: int = g.human.deck.size()
await click(scr.deck)
check(g.human.energy == mana and g.human.hand.size() == 2 and scr.hand_views.size() == 2 and g.human.deck.size() == deck_before - 1, "gezogen: eine Karte, frei")
check(g.draw_cost(g.human) == 1 and scr.deck.cost == 1, "das nächste Ziehen kostet 1")
# Ausspielen kostet die Kartenkosten; das Wesen greift noch nicht an
var board_before: int = g.human.board.size()
mana = g.human.energy
await key(KEY_1)
check(g.human.board.size() == board_before + 1 and g.human.energy == mana - Cards.by_id("f_3_2").cost, "Wesen per Taste gespielt, Kosten bezahlt")
check(not g.human.board.back().can_attack(), "frisch gelegt greift es noch nicht an")
# Rückgängig: Wesen zurück auf die Hand, Mana zurück
check(scr.b_undo.visible, "Rückgängig angeboten")
await click(scr.b_undo)
g = scr.game
check(g.human.board.size() == board_before and g.human.hand.any(func(c): return c.id == "f_3_2") and g.human.energy == mana, "Rückgängig stellt Hand und Mana her")
check(not scr.b_undo.visible and g.log.back() == "Rückgängig.", "Rückgängig verbraucht")
await key(KEY_1)
check(g.human.board.size() == board_before + 1, "erneut gespielt")
# Ziehen per Taste
mana = g.human.energy
var hand_before: int = g.human.hand.size()
await key(KEY_Z)
check(g.human.energy == mana - 1 and g.human.hand.size() == hand_before + 1, "Z zieht, das zweite Ziehen für 1")
await shot("22_kampf_steuerung")
# Gamepad: A auf dem Stapel zieht
await pad(JOY_BUTTON_DPAD_LEFT)
check(scr.gp_zone == "deck" and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf dem Stapel")
hand_before = g.human.hand.size()
await pad(JOY_BUTTON_A)
check(g.human.hand.size() == hand_before + 1, "Gamepad A zieht")
scr.ai_delay = 0.75
await key(KEY_SPACE)
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
await frames(2)
check(scr.b_skip.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 key(KEY_ESCAPE)
check(scr.options.visible and find_button("Hauptmenü", scr.options) != null and find_button("Beenden", scr.options) != null, "Esc öffnet die Optionen")
await key(KEY_ESCAPE)
check(not scr.options.visible, "Esc schließt die Optionen")
# Einstellungen aus dem Kampf und zurück
await key(KEY_ESCAPE)
await click(find_button("Einstellungen", scr.options))
check(screen() is SettingsScreen and is_instance_valid(scr), "Einstellungen geöffnet, der Kampf bleibt erhalten")
await click(find_button("Zurück"))
check(screen() == scr and scr.game == g and not scr.options.visible, "zurück im selben Kampf")
await give_up()
## Grenzen im Kampf: volle Hand, volles Feld, kein Mana.
func test_kampf_grenzen() -> void:
await start_battle()
var scr = screen()
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
g.human.energy = 100
scr.refresh()
await frames(1)
# Keine Grenze für Handkarten: acht weitere ziehen
var hand_before: int = g.human.hand.size()
for i in 8:
await click(scr.deck)
check(g.human.hand.size() == hand_before + 8 and scr.deck.enabled and scr.hand_views.size() == hand_before + 8, "acht Karten nachgezogen, Ziehen bleibt möglich")
# Feld voll spielen
var guard := 0
while g.human.board.size() < Game.MAX_BOARD and guard < 30:
guard += 1
if g.human.hand.is_empty():
await click(scr.deck)
await key(KEY_1)
check(g.human.board.size() == Game.MAX_BOARD, "Feld mit %d Wesen voll" % Game.MAX_BOARD)
if g.human.hand.is_empty():
await click(scr.deck)
await key(KEY_1)
check(scr.msg_label.text == tr("Dein Feld ist voll"), "Meldung bei vollem Feld: " + scr.msg_label.text)
await shot("25_kampf_voll")
# Verschmelzen per Ziehen macht wieder Platz
var before: int = g.human.board.size()
var hand_n: int = g.human.hand.size()
await drag_fuse(scr, scr.own_views[0], scr.own_views[1])
await accept_fuse(scr)
check(g.human.board.size() == before - 2 and g.human.hand.size() == hand_n + 1, "Verschmelzen macht zwei Plätze frei, das Ergebnis liegt auf der Hand")
# Ohne Mana kein Ziehen
g.human.energy = 0
g.human.draws = 1
scr.refresh()
await frames(1)
check(not scr.deck.enabled, "ohne Mana kein zweites Ziehen")
await give_up()
## Elementboni, verdeckt legen, Kartenmenü und Verschmelzen (per Ziehen und per Menü) im Kampf.
func test_kampf_elemente_und_verschmelzen() -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampf gestartet")
scr.ai_delay = 0.0
await wait_for_human(scr)
var g: Game = scr.game
g.human.board.clear()
scr.opponent().board.clear() # was die KI im ersten Zug gelegt hat, stört die Paare
var put := func(p: Game.Player, id: String, hidden: bool = false) -> Game.Creature:
var cr := Game.Creature.new(Cards.by_id(id).copy(), p, g.next_uid)
g.next_uid += 1
cr.ready = true
cr.hidden = hidden
p.board.append(cr)
return cr
var wolf_cr: Game.Creature = put.call(g.human, "f_3_2")
wolf_cr.health = 4 # übersteht die Konter von Falke und Funke, damit der Test ihn weiter nutzen kann
wolf_cr.max_health = 4
put.call(g.human, "f_4_3")
put.call(g.human, "w_3_3")
var falke: Game.Creature = put.call(scr.opponent(), "l_3_1")
var secret: Game.Creature = put.call(scr.opponent(), "f_2_1", true)
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
g.human.energy = 20
g.human.looks = {"w_3_3": 3}
scr.refresh()
await frames(2)
check(scr.element_view.text() == "Feuer 2 (Doppelschlag) · Wasser 1", "Liste: nur Elemente mit offenen Wesen, Feuerpaar: " + scr.element_view.text())
check(scr.opp_element_view.text() == "Luft 1", "die Liste des Gegners zählt nur seine offenen Wesen: " + scr.opp_element_view.text())
check(g.human.board[0].keywords() == ["Doppelschlag"] and scr.tiles[secret.uid].secret, "Feuerpaar gibt Doppelschlag; fremdes Verdecktes zeigt den Rücken")
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen, ohne Verschmelzen
await hover(scr.hand_views[0])
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Abwerfen +1 ◆", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü mit Verdeckt legen und Abwerfen")
check(find_button("Verschmelzen mit …", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
await shot("23_kartenmenue")
await right_click()
check(not scr.menu_layer.visible, "Rechtsklick schließt das Menü")
await right_click_at(Vector2(1150, 300)) # leere Fläche
check(scr.menu_layer.visible and find_button("Zug beenden", scr.menu_layer) != null, "Rechtsklick ins Leere bietet Zug beenden an")
await right_click()
# Mittlere Maustaste legt die Handkarte verdeckt
var mana: int = g.human.energy
await right_click_at(scr.hand_views[0].get_global_rect().get_center(), MOUSE_BUTTON_MIDDLE)
var mine_hidden: Game.Creature = g.human.board[3]
check(mine_hidden.hidden and g.human.energy == mana - 1 and scr.element_view.text().begins_with("Feuer 2 (Doppelschlag)"), "mittlere Maustaste legt verdeckt: zählt nicht als offen")
# Rechtsklick auf ein eigenes offenes Wesen: nichts zu wählen, also kein Menü
await right_click_at(scr.own_views[0].get_global_rect().get_center())
check(not scr.menu_layer.visible, "kein Menü ohne Einträge")
var wolf: Game.Creature = g.human.board[0]
while scr.own_views[3].busy:
await frames(1)
await click(scr.own_views[3])
check(mine_hidden.hidden and scr.msg_label.text == "Lodernder Funke kann erst im nächsten Zug aufgedeckt werden", "im Legezug nicht aufzudecken: " + scr.msg_label.text)
mine_hidden.ready = true
await click(scr.own_views[3])
check(not mine_hidden.hidden and scr.element_view.text().begins_with("Feuer 3 (Doppelschlag)"), "aufgedeckt: drei offene Feuer: " + scr.element_view.text())
check(wolf.power() == 3 and falke.power() == 3 and mine_hidden.can_attack(), "Werte bleiben die der Karten, aufgedeckt greift sofort an")
await shot("23b_drei_feuer")
# Angriffsziel: Feuer gegen Luft schlägt doppelt, der Falke kontert halb; verdeckte ohne Anzeige, Held gesperrt
await click(scr.own_views[0])
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == -1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: der Falke kontert halbiert, nichts gegen Verdeckte")
check(scr.tiles[falke.uid].shown_stats() == [2, falke.health], "der Falke zeigt seinen Konter gegen den Wolf: 3 halbiert, aufgerundet 2")
await hover(scr.tiles[falke.uid])
check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() * 2, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt doppelt zu")
await shot("23c_angreifer_gewaehlt")
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
await click(scr.tiles[secret.uid])
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
check(wolf.attacks_left() == 1, "Doppelschlag: ein Angriff bleibt")
await double_click_at(scr.own_views[0].get_global_rect().get_center())
check(scr.msg_label.text == "Erst die Wesen" and scr.opponent().life == 30, "Doppelklick zielt auf das Leben, hier stehen noch Wesen: " + scr.msg_label.text)
# Verschmelzen per Ziehen: den Aschekrieger auf den Wellenreiter ziehen, erst abbrechen, dann bestätigen
mana = g.human.energy
var before: int = g.human.board.size()
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
await shot("24a_verschmelzen_frage")
await click(find_button("Abbrechen", scr.menu_layer))
check(scr.fuse_pending == null and not scr.menu_layer.visible and g.human.board.size() == before and g.human.energy == mana, "Abbrechen lässt beide Wesen und das Mana, wie sie waren")
await drag_fuse(scr, scr.own_views[1], scr.own_views[2])
await accept_fuse(scr)
var dampf_cards: Array = g.human.hand.filter(func(c): return c.name == "Dampf")
check(g.human.board.size() == before - 2 and dampf_cards.size() == 1 and g.human.energy == mana, "beide Wesen verlassen das Brett, Dampf liegt auf der Hand, kostenlos")
var dampf: Cards.Card = dampf_cards[0]
var dampf_views: Array = scr.hand_views.filter(func(v): return v.card == dampf)
check(dampf.look == 3 and dampf.attack == 8 and dampf.health == 7 and dampf.cost == 4 and dampf_views.size() == 1 and dampf_views[0].rarity == "Ultra Rare", "Dampf 5/4 für 4 Mana aus den Grundwerten, Stufe 3 des Reiters macht 8/7")
await shot("24_verschmelzen")
# Verschmelzen auf der Hand per Ziehen
g.human.hand = [Cards.by_id("f_2_1").copy(), Cards.by_id("f_2_1").copy(), Cards.by_id("l_1_1").copy()]
scr.refresh()
await frames(1)
await drag_fuse(scr, scr.hand_views[0], scr.hand_views[1])
await accept_fuse(scr)
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Funken auf der Hand zu Inferno 3/2")
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
check(find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and g.human.hand[0].cost == 2, "Gamepad-Menü: Verdeckt legen, Inferno kostet 2")
await click(find_button("Verschmelzen mit …", scr.menu_layer))
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
await click(scr.hand_views[1])
check(g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz", "Inferno und Hauch zu Blitz")
# Handkarte mit Wesen: Blitz auf den Kobold ziehen
var board_n: int = g.human.board.size()
await drag_fuse(scr, scr.hand_views[0], scr.own_views[0])
await accept_fuse(scr)
check(g.human.board.size() == board_n - 1 and g.human.hand.size() == 1 and g.human.hand[0].name == "Blitz" and g.human.hand[0].base_attack() == 4, "Handkarte und Wesen verschmelzen: der Kobold verlässt das Brett, Blitz 4/4 bleibt auf der Hand")
await give_up()
func test_kampagne() -> void:
await click(find_button("Kampagne"))
var scr = screen()
check(scr is CampaignScreen, "Kampagne geöffnet")
await shot("15_kampagne")
check(scr.node_buttons.filter(func(b): return b.disabled).size() == Campaign.STAGES_PER_CHAPTER - 1 and find_button("Kämpfen") != null, "neun Stationen gesperrt, die erste offen")
check(find_button("Kapitel 5") != null and find_button("Kapitel 5").disabled, "fünf Kapitel, das letzte gesperrt")
await click(scr.node_buttons[3])
check(scr.selected == 0, "gesperrte Station lässt sich nicht wählen")
check(find_button("Kapitel 2").disabled and scr.perk_box == null, "Kapitel 2 und Merkmal noch gesperrt")
await click(find_button("Kämpfen"))
var battle = screen()
check(battle is BattleScreen and battle.stage == 0, "Kampagnenkampf gestartet")
check(battle.game.opponent.name == "Fenn", "Fenn ist der Gegner")
check(battle.game.opponent.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 give_up()
# Kapitel 1 als abgeschlossen setzen: Kapitel 2 und Merkmalwahl
main.profile.campaign_stage = Campaign.STAGES_PER_CHAPTER
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.opponent.perks.size() == 1 and b2.game.opponent.life == 22, "Kapitel-2-Gegner mit einem Merkmal und 22 Leben")
check(b2.game.opponent.deck.all(func(id): return Cards.by_id(id).elements[0] == "Feuer") and b2.game.opponent.looks.values().all(func(l): return l == 0), "Kapitel 2: nur Feuer in Common")
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(shot_turn: int) -> void:
await start_battle()
var scr = screen()
check(scr is BattleScreen, "Kampfbildschirm")
scr.ai_delay = 0.0
var g: Game = scr.game
check(g.players.size() == 2 and g.log[0].begins_with("Zugreihenfolge"), "Spiel gestartet, Reihenfolge ausgelost")
await wait_for_human(scr)
check(g.current() == g.human, "Spieler ist dran")
check(scr.hand_views.size() == g.human.hand.size(), "Handkarten sichtbar")
await shot("07_kampf_start")
# Kurze Partie: einfache KI, Gegner fast tot, drei bereite Drachen räumen auf
scr.smart_ai = false
g.opponent.life = 1
for i in 3:
var drache := Game.Creature.new(Cards.by_id("f_5_5").copy(), g.human, 9100 + i)
drache.ready = true
g.human.board.append(drache)
scr.refresh()
var coins: int = main.profile.coins
var guard := 0
while g.winner == "" and guard < 60:
guard += 1
var turn_before := g.turn
await human_turn(scr)
if g.winner != "":
break
check(g.current() != g.human or g.turn > turn_before, "Zug beendet")
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
if guard == shot_turn:
await shot("08_kampf_zug%d" % (shot_turn + 1))
check(g.winner != "", "Spiel beendet")
await frames(3)
await shot("09_kampf_ende")
check(scr.reward > 0 and main.profile.coins >= coins + scr.reward, "Belohnung gutgeschrieben")
check(scr.fast_turns > 0 and scr.reward == Profile.battle_coins(g.winner == "human", g.human.turns, g.human.cards_played, scr.fast_turns, main.profile.difficulty().reward), "Münzen aus Zügen, Karten und schnellen Zügen")
check(scr.detail_label.visible and tr("%d Züge") % g.human.turns in scr.detail_label.text and "= " in scr.detail_label.text, "Aufschlüsselung im Ergebnisbild")
check(Profile.load_or_new(SAVE).coins == main.profile.coins, "Belohnung gespeichert")
await click(find_button("Zurück zum Menü"))
check(screen() is MenuScreen, "zurück im Menü")
func test_kampf_abbrechen() -> void:
await start_battle()
var scr = screen()
scr.ai_delay = 0.0
var g: Game = scr.game
await wait_for_human(scr)
check(g.opponent != null and g.human.max_energy == Game.START_MANA, "Duell läuft")
# Kartenvorschau: aufgewertete Karten zeigen ihre Seltenheit
g.human.energy = 10
var wolf := Game.Creature.new(Cards.by_id("f_3_2"), g.human, 9000)
wolf.ready = true
g.human.board.append(wolf)
g.human.hand.append(Cards.by_id("w_3_3"))
g.human.looks["w_3_3"] = Cards.RARITIES.find("Super Rare")
scr.refresh()
var reiter_view = scr.hand_views.filter(func(v): return v.card.id == "w_3_3")[0]
check(reiter_view.rarity == "Super Rare", "Handkarte trägt die eigene Stufe")
await hover(scr.element_view) # erst weg von der Hand, damit das Betreten sicher ausgelöst wird
await hover(reiter_view)
var previews = scr.preview_holder.get_children().filter(func(c): return c is CardView)
check(previews.size() == 1 and previews[0].rarity == "Super Rare", "Vorschau ebenfalls")
await shot("21_kampf_vorschau")
if not scr.own_views.is_empty():
await click(scr.own_views[0])
await right_click()
check(scr.selected == null, "Rechtsklick bricht ab")
await hover(scr.hand_views[0])
if scr.preview_holder.get_child_count() == 0:
print("DEBUG Hover: overlay=", scr.any_overlay(), " hand=", scr.hand_views.size(),
" dran=", g.current().name, " Sieger=", g.winner, " hovered=", get_viewport().gui_get_hovered_control())
check(scr.preview_holder.get_child_count() > 0, "Vorschau bei Hover")
await key(KEY_ESCAPE)
await click(find_button("Verlauf", scr.options))
check(scr.log_overlay.visible and scr.any_overlay(), "Verlauf öffnet als Überlagerung")
await key(KEY_ESCAPE)
check(not scr.log_overlay.visible, "Esc schließt den Verlauf")
# Runden laufen lassen, damit die Gegner ihre Züge machen
for round in 4:
await key(KEY_SPACE)
var wait := 0
while g.current() != g.human and g.winner == "" and wait < 600:
await frames(1)
wait += 1
var ai_attacked: bool = g.log.any(func(l): return l.begins_with("Gegner") and " spielt " in l) \
and g.log.any(func(l): return " greift " in l)
check(ai_attacked, "Gegner spielen Karten und greifen an")
check(scr.tiles.size() == g.players.reduce(func(acc, p): return acc + p.board.size(), 0), "eine Kachel je Wesen")
await shot("10_kampf_runden")