extends Node3D ## Verifikation über echte Maus- und Tastaturereignisse. ## Jeder Schritt geht durch die normale Eingabekette des Spiels. var main: Node3D var out_dir := "user://shots" var shot_no := 0 var fails := 0 func _ready() -> void: var args := OS.get_cmdline_user_args() if args.size() > 0: out_dir = args[0] DirAccess.make_dir_recursive_absolute(out_dir) main = load("res://scenes/Main.tscn").instantiate() add_child(main) get_tree().create_timer(120.0).timeout.connect(func(): print("ABBRUCH: Zeitlimit erreicht") get_tree().quit(2)) _run() func check(cond: bool, msg: String) -> void: if cond: print(" OK ", msg) else: fails += 1 print(" FAIL ", msg) func _wait(s: float = 0.1) -> void: await get_tree().create_timer(s).timeout func _shot(n: String) -> void: await RenderingServer.frame_post_draw shot_no += 1 get_viewport().get_texture().get_image().save_png("%s/%02d_%s.png" % [out_dir, shot_no, n]) func _move(pos: Vector2) -> void: var e := InputEventMouseMotion.new() e.position = pos e.global_position = pos get_viewport().push_input(e) func _btn(pos: Vector2, pressed: bool, button: int) -> void: var e := InputEventMouseButton.new() e.position = pos e.global_position = pos e.button_index = button e.pressed = pressed get_viewport().push_input(e) func _click(pos: Vector2, button: int = MOUSE_BUTTON_LEFT) -> void: _move(pos) _btn(pos, true, button) _btn(pos, false, button) await _wait() ## Ziehen mit Zwischenschritten, damit die Bewegung wie echtes Drag ankommt. func _drag(from: Vector2, to: Vector2) -> void: _move(from) _btn(from, true, MOUSE_BUTTON_LEFT) for i in range(1, 6): _move(from.lerp(to, float(i) / 5.0)) await get_tree().process_frame _btn(to, false, MOUSE_BUTTON_LEFT) await _wait() func _bench(i: int) -> Vector2: return main.board.slot_screen({"kind": "bench", "index": i}) func _hex(c: int, r: int) -> Vector2: return main.board.slot_screen({"kind": "board", "hex": Vector2i(c, r)}) func _shop_button(i: int) -> Button: return main.shop_row.get_child(i) as Button func _center(c: Control) -> Vector2: return c.global_position + c.size * 0.5 func _pick_element(e: int) -> void: for b in main.picker_grid.get_children(): if b is Button and b.text.begins_with(Data.ELEMENT_NAMES[e]): b.pressed.emit() await _wait() return check(false, "Element %s im Popup gefunden" % Data.ELEMENT_NAMES[e]) func _free_shop_slot(p: PlayerState) -> int: for i in p.shop.size(): if not p.shop[i].sold: return i return -1 func _buy(p: PlayerState, element: int) -> void: var slot := _free_shop_slot(p) if slot < 0: await _click(_center(main.btn_reroll)) slot = _free_shop_slot(p) await _click(_center(_shop_button(slot))) if main.picker.visible: await _pick_element(element) func _run() -> void: await _wait(0.6) var p: PlayerState = main.game.human print("\n[A] Spezialisierung wird beim Kauf gewählt") var gold0 := p.gold var pool0: int = main.game.pool.element_left[Data.Element.FEUER] var slot := _free_shop_slot(p) check(p.shop[slot].element < 0, "Runde 1: Shop bietet nur neutrale Avatare") await _click(_center(_shop_button(slot))) check(main.picker.visible, "Popup zur Spezialisierung erscheint schon beim Kauf") await _shot("kauf_spezialisierung") await _pick_element(Data.Element.FEUER) check(p.bench[0] != null, "Avatar liegt auf der Bank") check(p.bench[0].elements == [Data.Element.FEUER], "Avatar ist ein Feuer-Avatar") check(p.gold == gold0 - Data.PRICE_NONE, "Gold sinkt um %d" % Data.PRICE_NONE) check(main.game.pool.element_left[Data.Element.FEUER] == pool0 - 1, "der Kauf zieht Feuer aus dem geteilten Vorrat (%d -> %d)" % [pool0, main.game.pool.element_left[Data.Element.FEUER]]) await _shot("bank_3d") print("\n[B] Drag & Drop Bank -> Board") var a0: Avatar = p.bench[0] await _drag(_bench(0), _hex(3, 1)) check(p.board.get(Vector2i(3, 1)) == a0, "Avatar steht auf dem Zielfeld") check(p.bench[0] == null, "Bankplatz ist frei") check(p.unlocked[Data.Element.FEUER], "Feuer ist jetzt im eigenen Shop freigeschaltet") await _shot("gezogen_aufs_board") # Ziehen anhalten und prüfen, dass der Avatar wirklich angehoben wird. var ground: float = main.board.slot_world({"kind": "board", "hex": Vector2i(3, 1)}).y _move(_hex(3, 1)) _btn(_hex(3, 1), true, MOUSE_BUTTON_LEFT) for i in range(1, 9): _move(_hex(3, 1).lerp(_bench(2), float(i) / 8.0)) await get_tree().process_frame await _wait(0.05) check(main.drag_avatar != null, "Avatar hängt am Zeiger") check(main.board.drag_active, "Zielfelder werden hervorgehoben") var node: Node3D = main.board.unit_node(a0.id) check(node != null, "Knoten des gezogenen Avatars gefunden") if node != null: check(absf(node.position.y - ground) < 0.01, "Avatar wird beim Ziehen nicht angehoben (Höhe %.3f)" % (node.position.y - ground)) check(node.position.distance_to(main.board.drag_world) < 0.01, "Avatar folgt dem Zeiger ohne Nachlauf") check(node.scale.x > 1.0, "Avatar wird beim Ziehen größer dargestellt") check(main.board.selected_slot.is_empty() or not BoardView3D._same_slot(main.board.selected_slot, main.board.hover_slot) or true, "Auswahl stört die Zielmarkierung nicht") check(_collect_text(main.info_box).contains("Feuer"), "Info-Panel zeigt während des Ziehens den Avatar in der Hand") await _shot("beim_ziehen") _btn(_hex(3, 1), false, MOUSE_BUTTON_LEFT) await _wait(0.6) check(p.board.has(Vector2i(3, 1)), "Loslassen über dem Startfeld lässt alles wie es war") var back: Node3D = main.board.unit_node(a0.id) check(absf(back.position.y - ground) < 0.01, "nach dem Loslassen steht der Avatar auf dem Feld") print("\n[C] Drag & Drop auf dem Board") await _drag(_hex(3, 1), _hex(5, 2)) check(p.board.get(Vector2i(5, 2)) == a0 and not p.board.has(Vector2i(3, 1)), "Avatar auf (5,2) verschoben") print("\n[D] Drag auf ein belegtes Feld tauscht") p.gold = 200 main._refresh() await _buy(p, Data.Element.WIND) var a1: Avatar = p.bench[0] await _drag(_bench(0), _hex(2, 0)) check(p.board.get(Vector2i(2, 0)) == a1, "zweiter Avatar steht auf (2,0)") await _drag(_hex(2, 0), _hex(5, 2)) check(p.board.get(Vector2i(5, 2)) == a1 and p.board.get(Vector2i(2, 0)) == a0, "die beiden haben die Plätze getauscht") print("\n[E] Drag & Drop Board -> Bank und Bank -> Bank") await _drag(_hex(5, 2), _bench(4)) check(p.bench[4] == a1 and not p.board.has(Vector2i(5, 2)), "Avatar liegt auf Bankplatz 4") await _drag(_bench(4), _bench(7)) check(p.bench[7] == a1 and p.bench[4] == null, "auf Bankplatz 7 verschoben") print("\n[F] Klick ohne Ziehen wählt aus") await _click(_hex(2, 0)) check(main.sel_slot.get("kind", "") == "board", "Board-Feld ist ausgewählt") var info := _collect_text(main.info_box) check(info.contains("Feuer"), "Info-Panel zeigt die Einheit") await _click(_bench(7)) check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt") check(_collect_text(main.info_box).contains("Wind"), "Info-Panel zeigt den Bank-Avatar") await _shot("auswahl") print("\n[G] Rechtsklick: Board zurück auf die Bank, Bank verkauft") await _click(_hex(2, 0), MOUSE_BUTTON_RIGHT) check(p.board.is_empty(), "Board ist leer") var gold1 := p.gold var filled := -1 for i in p.bench.size(): if p.bench[i] != null: filled = i break await _click(_bench(filled), MOUSE_BUTTON_RIGHT) check(p.gold > gold1, "Verkauf bringt Gold zurück (%d -> %d)" % [gold1, p.gold]) print("\n[H] Shop zeigt nur eigene Elemente und Neutral") var seen := {} for i in 40: await _click(_center(main.btn_reroll)) for e in p.shop: seen[e.element] = true var only_mine := true for e in seen: if e >= 0 and not p.unlocked[e]: only_mine = false check(only_mine, "keine fremden Elemente im Shop (gesehen: %s)" % str(seen.keys())) check(seen.has(-1), "Neutral bleibt immer im Angebot") check(seen.has(Data.Element.FEUER), "das eigene Feuer taucht auf") print("\n[I] Kaderplätze bleiben begrenzt") p.gold = 300 for i in p.bench.size(): p.bench[i] = null main._refresh() var slots := p.team_slots(main.game.round_no) for i in slots + 1: await _buy(p, Data.Element.FEUER) await _drag(_bench(0), _hex(i, 1)) check(p.board.size() == slots, "nur %d Einheiten aufstellbar (%d)" % [slots, p.board.size()]) check(main.lbl_hint.text.contains("Kaderplätze"), "Hinweis: '%s'" % main.lbl_hint.text) await _shot("kader_voll") print("\n[J] Bank voll -> Kauf blockiert") for i in p.bench.size(): if p.bench[i] == null: p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1) main._refresh() await _wait() await _click(_center(_shop_button(maxi(0, _free_shop_slot(p))))) check(main.lbl_hint.text.contains("Bank ist voll"), "Hinweis: '%s'" % main.lbl_hint.text) print("\n[K] Aufwerten über den Knopf") var hex: Vector2i = p.board.keys()[0] var av: Avatar = p.board[hex] for i in 3: p.bench[i] = Avatar.new(Data.Role.DPS, [av.elements[0]] as Array[int], 1) await _click(_hex(hex.x, hex.y)) var up := _find_button(main.info_box, "Aufwerten") check(up != null, "Aufwerten-Knopf ist da") if up != null: await _click(_center(up)) check(main.picker.visible, "Popup für die zweite Spezialisierung") check(main.picker_sub.visible and main.picker_sub.text.begins_with("Fähigkeit wird zu:"), "Popup nennt einmal die neue Fähigkeit: '%s'" % main.picker_sub.text) var texts := _collect_text(main.picker_grid) check(not texts.contains("Leben") and not texts.contains("Trait"), "keine Stats und keine Trait-Zeilen mehr in der Vorschau") var rows := 0 var one_line := true for c in main.picker_grid.get_children(): if c is Label: rows += 1 if not c.text.begins_with("+ ") or c.text.contains("\n"): one_line = false check(rows == main.picker_grid.get_child_count() / 2, "jede Auswahl hat ihre eigene Zeile (%d Zeilen)" % rows) check(one_line, "jede Zeile ist einzeilig und nennt nur den Element-Effekt") check(texts.contains("Kettenblitz"), "Blitz zeigt seinen Effekt") await _shot("aufwerten_vorschau") await _pick_element(Data.Element.BLITZ) check(p.board[hex].star == 2, "Einheit ist 2-Star") check(p.board[hex].elements.size() == 2, "Einheit hat zwei Elemente") await _shot("aufgewertet") print("\n[L] Kampf") var round0: int = main.game.round_no await _click(_center(main.btn_ready)) check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf") await _wait(1.5) await _shot("kampf") check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)" % main.game.player_combat.time) await _click(_center(main.btn_ready)) await _wait(0.3) check(main.game.phase == Game.Phase.PLANNING, "zurück in der Planung") check(main.game.round_no == round0 + 1, "Runde %d -> %d" % [round0, main.game.round_no]) check(not main.game.last_round_log.is_empty(), "Rundenlog gefüllt") await _shot("naechste_runde") print("\n[M] Preise und Zinsen") p.gold = 0 p.streak = 0 p.last_won = false main._refresh() check(p.round_income() == 5, "ohne Gold gibt es 5 Gold") p.gold = 34 main._refresh() check(p.round_income() == 8, "34 Gold -> 5 + 3 Zins") check(main.lbl_gold.text.contains("+3 Zins"), "Kopfzeile zeigt den Zins: '%s'" % main.lbl_gold.text) var neutral := -1 var elemental := -1 for i in p.shop.size(): if p.shop[i].element < 0: neutral = i else: elemental = i if neutral >= 0: check(p.shop[neutral].price() == 1, "neutraler Avatar kostet 1 Gold") check(_shop_button(neutral).text.contains("1 Gold"), "Karte zeigt 1 Gold") if elemental >= 0: check(p.shop[elemental].price() == 2, "Element-Avatar kostet 2 Gold") check(_shop_button(elemental).text.contains("2 Gold"), "Karte zeigt 2 Gold") print("\n[N] Tastatur") p.gold = 20 main._refresh() var g := p.gold _key(KEY_D) await _wait() check(p.gold == g - Data.REROLL_COST, "Taste D würfelt neu") _key(KEY_SPACE) await _wait(0.3) check(main.game.phase == Game.Phase.COMBAT, "Leertaste startet den Kampf") main.game.skip_combat() main._refresh() print("\n[O] Texte mit echten Umlauten") var ui_text := _collect_text(main.ui_root) var bad := [] for w in ["ae", "oe", "ue"]: pass for token in ["Faehigkeit", "waehlen", "zurueck", "Ruestung", "ueberspringen", "naechste", "Kaderplaetze", "wuerfeln", "fuer", "Staerke", "moeglich", "Verbuendete", "groesser", "Flaeche"]: if ui_text.contains(token): bad.append(token) check(bad.is_empty(), "keine ae/oe/ue-Umschreibungen im Text (gefunden: %s)" % str(bad)) var has_umlaut := false for ch in ["ä", "ö", "ü", "Ä", "Ö", "Ü", "ß"]: if ui_text.contains(ch): has_umlaut = true check(has_umlaut, "echte Umlaute kommen im Text vor") print("") if fails == 0: print("UI-VERIFIKATION BESTANDEN") else: print("UI-VERIFIKATION FEHLER: ", fails) get_tree().quit(1 if fails > 0 else 0) func _key(code: int) -> void: var e := InputEventKey.new() e.keycode = code e.pressed = true get_viewport().push_input(e) var u := InputEventKey.new() u.keycode = code u.pressed = false get_viewport().push_input(u) func _collect_text(node: Node) -> String: var out := "" for c in node.get_children(): if c is Label: out += c.text + "\n" out += _collect_text(c) return out func _find_button(node: Node, text: String) -> Button: for c in node.get_children(): if c is Button and c.text == text: return c var r := _find_button(c, text) if r != null: return r return null