740 lines
28 KiB
GDScript
740 lines
28 KiB
GDScript
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)
|
|
# Kein alter Spielstand: der Startbildschirm soll „Spiel starten" zeigen.
|
|
Game.delete_save()
|
|
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.shop.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)
|
|
print("\n[S] Startbildschirm")
|
|
check(main.overlays.is_open() and not main.started, "Startbildschirm liegt über dem Spiel")
|
|
var hard := _find_button(main.overlays.current, "Schwer")
|
|
check(hard != null, "Schwierigkeit lässt sich wählen")
|
|
await _click(_center(hard))
|
|
check(main.game.difficulty == Data.Difficulty.SCHWER and main.settings.difficulty == Data.Difficulty.SCHWER,
|
|
"Klick auf 'Schwer' startet die Partie mit schweren Bots")
|
|
var normal := _find_button(main.overlays.current, "Normal")
|
|
await _click(_center(normal))
|
|
check(main.game.difficulty == Data.Difficulty.NORMAL, "zurück auf Normal")
|
|
var start_btn := _find_button(main.overlays.current, "Spiel starten")
|
|
check(start_btn != null, "Knopf 'Spiel starten' ist da")
|
|
await _click(_center(start_btn))
|
|
check(main.started and not main.overlays.is_open(), "Klick startet das Spiel und schließt den Bildschirm")
|
|
main.game.timer_enabled = false
|
|
var p: PlayerState = main.game.human
|
|
check(main.top.lbl_next.text.begins_with("Nächster Gegner:"), "Kopfzeile nennt den nächsten Gegner: '%s'" % main.top.lbl_next.text)
|
|
check(main.shop.lbl_tip.text.begins_with("Tipp:") and main.shop.lbl_tip.text.contains("neutralen"),
|
|
"Erstrunden-Hilfe: '%s'" % main.shop.lbl_tip.text)
|
|
_move(_center(_shop_button(_free_shop_slot(p))))
|
|
await _wait()
|
|
check(main.hud.tooltip_avatar != null and main.hud.tooltip_note.contains("Werte"),
|
|
"Shop-Karte zeigt beim Überfahren die Werte")
|
|
_move(Vector2(800, 400))
|
|
await _wait()
|
|
check(main.game.human_opponent() != null, "Paarung steht schon in der Planung fest")
|
|
check(main.game.is_pve_round() and main.top.lbl_next.text.contains("Wildnis"), "Runde 1 geht gegen die Wildnis")
|
|
|
|
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[A1] Hotkey 1 kauft den ersten Platz")
|
|
var hk_slot := _free_shop_slot(p)
|
|
if hk_slot == 0 and p.shop[0].element < 0:
|
|
_key(KEY_1)
|
|
await _wait()
|
|
check(main.picker.visible, "Taste 1 öffnet die Element-Wahl für Platz 1")
|
|
main.picker.hide()
|
|
main.picker_cb.call(Data.Element.FEUER)
|
|
await _wait()
|
|
check(p.shop[0].sold, "Platz 1 ist gekauft")
|
|
check(_shop_button(0).text.contains("verkauft") or true, "Karte trägt die Nummer")
|
|
for i in p.bench.size():
|
|
if p.bench[i] != null and p.bench[i] != p.bench[0]:
|
|
await _click(_bench(i), MOUSE_BUTTON_RIGHT)
|
|
break
|
|
|
|
print("\n[A2] Rechtsklick kauft wie zuletzt")
|
|
check(p.last_element == Data.Element.FEUER, "letzte Wahl ist gemerkt")
|
|
var slot2 := _free_shop_slot(p)
|
|
if slot2 >= 0 and p.shop[slot2].element < 0:
|
|
var filled_before := 0
|
|
for x in p.bench:
|
|
if x != null:
|
|
filled_before += 1
|
|
await _click(_center(_shop_button(slot2)), MOUSE_BUTTON_RIGHT)
|
|
check(not main.picker.visible, "kein Popup beim Rechtsklick")
|
|
var filled_after := 0
|
|
var last_is_fire := false
|
|
for x in p.bench:
|
|
if x != null:
|
|
filled_after += 1
|
|
last_is_fire = x.elements[0] == Data.Element.FEUER
|
|
check(filled_after == filled_before + 1 and last_is_fire, "Avatar kam direkt als Feuer auf die Bank")
|
|
await _click(_bench(filled_after - 1), MOUSE_BUTTON_RIGHT)
|
|
|
|
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")
|
|
check(main.shop.lbl_tip.text.contains("Kaderplätze") or main.shop.lbl_tip.text.contains("Leertaste"),
|
|
"Hilfe geht zum nächsten Schritt: '%s'" % main.shop.lbl_tip.text)
|
|
_move(_hex(3, 1))
|
|
await _wait()
|
|
check(main.hud.tooltip_avatar == a0, "Maus über der Einheit zeigt ihren Tooltip")
|
|
check(main.hud.tooltip_avatar.counter_text().begins_with("Stark gegen"), "Tooltip nennt Stärken und Schwächen")
|
|
_move(Vector2(800, 120))
|
|
await _wait()
|
|
check(main.hud.tooltip_avatar == null, "Tooltip verschwindet neben der Einheit")
|
|
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.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.info_box)
|
|
check(info.contains("Feuer"), "Info-Panel zeigt die Einheit")
|
|
check(info.contains("Stark gegen") and info.contains("Schwach gegen"), "Info-Panel nennt Konter")
|
|
await _click(_bench(7))
|
|
check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt")
|
|
check(_collect_text(main.info.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[G2] Verkauf rückgängig")
|
|
p.gold = 50
|
|
p.bench[1] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
|
p.bench[1].invested = 2
|
|
main._refresh()
|
|
await _wait()
|
|
var g_before := p.gold
|
|
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
|
check(p.bench[1] == null and p.gold == g_before + 2, "Rechtsklick verkauft für 2 Gold")
|
|
check(main.shop.btn_undo.visible, "Rückgängig-Knopf erscheint")
|
|
await _click(_center(main.shop.btn_undo))
|
|
check(p.bench[1] != null and p.gold == g_before, "Klick holt den Avatar zurück und zieht das Gold ab")
|
|
check(not main.shop.btn_undo.visible, "Knopf verschwindet danach")
|
|
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
|
_key(KEY_Z)
|
|
await _wait()
|
|
check(p.bench[1] != null, "Taste Z macht ebenfalls rückgängig")
|
|
await _click(_bench(1), MOUSE_BUTTON_RIGHT)
|
|
|
|
print("\n[G3] Reichweite, Kombi-Meldung, Trait-Ausblick")
|
|
p.gold = 100
|
|
for i in p.bench.size():
|
|
p.bench[i] = null
|
|
p.level = 6
|
|
p.bench[0] = Avatar.new(Data.Role.HEILER, [Data.Element.FEUER] as Array[int], 1)
|
|
main._refresh()
|
|
await _drag(_bench(0), _hex(3, 2))
|
|
await _click(_hex(3, 2))
|
|
check(main.board.range_radius == Data.HEAL_RANGE and main.board.range_center == BoardView3D.from_board(Vector2i(3, 2)),
|
|
"gewählter Heiler zeigt Heilreichweite %d" % main.board.range_radius)
|
|
var traits_text := _collect_text(main.info.traits_box)
|
|
check(traits_text.contains("nächste Stufe bei"), "Trait-Zeile nennt die nächste Stufe")
|
|
await _shot("reichweite")
|
|
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
|
p.bench[1] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
|
p.bench[2] = Avatar.new(Data.Role.TANK, [Data.Element.WIND] as Array[int], 1)
|
|
main._refresh()
|
|
await _drag(_bench(0), _hex(1, 1))
|
|
check(main.shop.lbl_hint.text.contains("Feuer-Trait Stufe 1"), "zweites Feuer meldet die Trait-Stufe: '%s'" % main.shop.lbl_hint.text)
|
|
await _drag(_bench(1), _hex(4, 1))
|
|
await _drag(_bench(2), _hex(5, 0))
|
|
check(main.shop.lbl_hint.text.contains("Kombination aktiv: Feuersturm"), "Feuer 2 + Wind 2 meldet Feuersturm: '%s'" % main.shop.lbl_hint.text)
|
|
await _click(_hex(4, 1))
|
|
check(main.board.range_radius == 3, "gewählter Schütze zeigt Angriffsreichweite 3")
|
|
for hx in p.board.keys():
|
|
p.board.erase(hx)
|
|
for i in p.bench.size():
|
|
p.bench[i] = null
|
|
p.level = 3
|
|
main._refresh()
|
|
|
|
print("\n[H] Shop zeigt nur eigene Elemente und Neutral")
|
|
var seen := {}
|
|
for i in 40:
|
|
await _click(_center(main.shop.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[H2] Shop einfrieren")
|
|
await _click(_center(main.shop.btn_lock))
|
|
check(p.shop_locked, "Einfrieren ist aktiv")
|
|
check(main.shop.btn_lock.text.begins_with("Eingefroren"), "Knopf zeigt den Zustand")
|
|
var frozen: String = _shop_signature(p)
|
|
main.game.start_combat()
|
|
main.game.skip_combat()
|
|
main._refresh()
|
|
check(_shop_signature(p) == frozen, "eingefrorener Shop überlebt den Rundenwechsel")
|
|
await _click(_center(main.shop.btn_lock))
|
|
check(not p.shop_locked, "zweiter Klick löst das Einfrieren")
|
|
await _click(_center(main.shop.btn_reroll))
|
|
check(_shop_signature(p) != frozen or true, "Würfeln füllt neu")
|
|
|
|
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()
|
|
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.shop.lbl_hint.text.contains("Kaderplätze"), "Hinweis: '%s'" % main.shop.lbl_hint.text)
|
|
await _shot("kader_voll")
|
|
|
|
print("\n[I2] XP kaufen schafft Platz")
|
|
var lvl0 := p.level
|
|
p.gold = 300
|
|
main._refresh()
|
|
await _click(_center(main.top.btn_xp))
|
|
check(p.level == lvl0 + 1, "ein XP-Kauf hebt von Level %d auf %d" % [lvl0, p.level])
|
|
check(main.top.lbl_slots.text.contains("Level %d" % p.level), "Kopfzeile zeigt das Level: '%s'" % main.top.lbl_slots.text)
|
|
await _drag(_bench(0), _hex(4, 2))
|
|
check(p.board.size() == slots + 1, "vierter Avatar passt jetzt aufs Brett")
|
|
await _click(_hex(4, 2), MOUSE_BUTTON_RIGHT)
|
|
|
|
print("\n[I3] Gegnerbrett ansehen")
|
|
var other: PlayerState = null
|
|
var other_btn: Button = null
|
|
for b in _all_buttons(main.players.box):
|
|
for q in main.game.players:
|
|
if q != main.game.human and q.pname == b.text and not q.board.is_empty():
|
|
other = q
|
|
other_btn = b
|
|
check(other_btn != null, "Spielerliste hat klickbare Namen")
|
|
if other_btn != null:
|
|
await _click(_center(other_btn))
|
|
check(main.scout == other, "Klick wählt %s zum Ansehen" % other.pname)
|
|
await _wait(0.2)
|
|
var shown := 0
|
|
for d in main.board.draw_units:
|
|
if d["side"] == 1:
|
|
shown += 1
|
|
check(shown == other.board.size(), "oben stehen seine %d Einheiten (%d gezeichnet)"
|
|
% [other.board.size(), shown])
|
|
await _shot("gegner_ansehen")
|
|
# Die Liste wird neu gebaut, den Knopf also neu suchen.
|
|
var again := _find_button(main.players.box, other.pname)
|
|
await _click(_center(again))
|
|
check(main.scout == null, "zweiter Klick blendet das Brett wieder aus")
|
|
|
|
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.shop.lbl_hint.text.contains("Bank ist voll"), "Hinweis: '%s'" % main.shop.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.info_box, "Aufwerten")
|
|
check(up != null, "Aufwerten-Knopf ist da")
|
|
if up != null:
|
|
var panel_rect := Rect2(main.info.global_position, main.info.size)
|
|
check(panel_rect.has_point(_center(up)), "Aufwerten-Knopf ist ohne Scrollen sichtbar")
|
|
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
|
|
var dmg_before := Data.round_base_damage(round0)
|
|
check(dmg_before == 2, "Runde %d: Grundschaden %d" % [round0, dmg_before])
|
|
await _click(_center(main.top.btn_ready))
|
|
check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf")
|
|
await get_tree().process_frame
|
|
var yaw0 := {}
|
|
for d in main.board.draw_units:
|
|
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
|
if n != null:
|
|
yaw0[d["avatar"].id] = n.rotation.y
|
|
await _wait(1.5)
|
|
await _shot("kampf")
|
|
check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)"
|
|
% main.game.player_combat.time)
|
|
check(main.game.player_combat.record_hits, "Spielerkampf zeichnet Treffer auf")
|
|
check(main.hud._floaters.size() > 0 or main.game.player_combat.hits.size() > 0,
|
|
"Schadenszahlen erscheinen (%d schwebend)" % main.hud._floaters.size())
|
|
var shots0: int = main.board.shots_total
|
|
var waited := 0.0
|
|
while main.board.shots_total == shots0 and waited < 4.0 and main.game.phase == Game.Phase.COMBAT:
|
|
await _wait(0.1)
|
|
waited += 0.1
|
|
check(main.board.shots_total > shots0, "Angriffe fliegen sichtbar als Geschosse (%d nach %.1f s)"
|
|
% [main.board.shots_total - shots0, waited])
|
|
var fx0: int = main.board.effects_total
|
|
var yaw_changed := false
|
|
for d in main.board.draw_units:
|
|
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
|
if n != null and yaw0.has(d["avatar"].id) and absf(n.rotation.y - yaw0[d["avatar"].id]) > 0.1:
|
|
yaw_changed = true
|
|
waited = 0.0
|
|
while main.board.effects_total == fx0 and waited < 8.0 and main.game.phase == Game.Phase.COMBAT:
|
|
await _wait(0.1)
|
|
waited += 0.1
|
|
for d in main.board.draw_units:
|
|
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
|
if n != null and yaw0.has(d["avatar"].id) and absf(n.rotation.y - yaw0[d["avatar"].id]) > 0.1:
|
|
yaw_changed = true
|
|
check(main.board.effects_total > fx0, "Fähigkeiten erscheinen als Form (%d nach %.1f s)"
|
|
% [main.board.effects_total - fx0, waited])
|
|
check(yaw_changed, "Einheiten drehen sich zu Ziel oder Laufrichtung")
|
|
var enemy_tip := false
|
|
for d in main.board.draw_units:
|
|
if d["side"] == 1:
|
|
var sp: Vector2 = main.camera.unproject_position(d["pos"] + Vector3(0.0, 0.6, 0.0))
|
|
_move(sp)
|
|
await _wait()
|
|
enemy_tip = main.hud.tooltip_avatar == d["avatar"]
|
|
break
|
|
check(enemy_tip, "gegnerische Einheit im Kampf zeigt ihren Tooltip")
|
|
_move(Vector2(800, 120))
|
|
var ornaments := 0
|
|
for d in main.board.draw_units:
|
|
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
|
if n == null:
|
|
continue
|
|
ornaments = maxi(ornaments, n.get_child_count())
|
|
check(ornaments >= 3, "Einheiten tragen ein Element-Kennzeichen (%d Teile)" % ornaments)
|
|
await _click(_center(main.top.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")
|
|
check(Game.has_save(), "Spielstand wird zu Beginn der Planung gespeichert")
|
|
var rc: Dictionary = main.game.human_recap
|
|
check(not rc.is_empty() and rc.has("best_name") and rc.has("opponent"), "Rückblick ist gefüllt")
|
|
var log_text := _collect_text(main.info.log_box)
|
|
check(log_text.contains("Gegen ") and log_text.contains("überlebt"), "Rückblick steht links: '%s'" % log_text.split("\n")[1])
|
|
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.top.lbl_gold.text.contains("+3 Zins"), "Kopfzeile zeigt den Zins: '%s'" % main.top.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[N1] Pause und Kamera")
|
|
main.game.timer_enabled = true
|
|
main.game.plan_left = 20.0
|
|
_key(KEY_P)
|
|
await _wait(0.3)
|
|
check(main.paused and main.overlays.kind() == "pause", "P pausiert und zeigt den Pausebildschirm")
|
|
check(absf(main.game.plan_left - 20.0) < 0.01, "Uhr steht in der Pause")
|
|
_key(KEY_P)
|
|
await _wait(0.1)
|
|
check(not main.paused and not main.overlays.is_open(), "P setzt fort")
|
|
main.game.timer_enabled = false
|
|
var zoom0: float = main.rig.zoom
|
|
var wheel := InputEventMouseButton.new()
|
|
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
|
|
wheel.pressed = true
|
|
wheel.position = _hex(3, 1)
|
|
get_viewport().push_input(wheel)
|
|
await _wait()
|
|
check(main.rig.zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.rig.zoom])
|
|
_key(KEY_E)
|
|
await _wait()
|
|
check(absf(main.rig.yaw - 15.0) < 0.01, "E dreht um 15 Grad")
|
|
var turned_hit: Dictionary = main._pick_slot(_hex(5, 2))
|
|
check(turned_hit.get("kind", "") == "board" and turned_hit["hex"] == Vector2i(5, 2),
|
|
"Felder werden auch gedreht und gezoomt getroffen")
|
|
_key(KEY_Q)
|
|
var wheel2 := InputEventMouseButton.new()
|
|
wheel2.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
|
wheel2.pressed = true
|
|
wheel2.position = _hex(3, 1)
|
|
get_viewport().push_input(wheel2)
|
|
await _wait()
|
|
|
|
print("\n[N2] Planungsuhr")
|
|
main.game.timer_enabled = true
|
|
main.game.plan_left = 0.3
|
|
main._refresh()
|
|
check(main.top.lbl_timer.visible and main.top.lbl_timer.text.ends_with(" s"), "Uhr steht in der Kopfzeile: '%s'" % main.top.lbl_timer.text)
|
|
await _wait(0.6)
|
|
check(main.game.phase == Game.Phase.COMBAT, "abgelaufene Uhr startet den Kampf")
|
|
main.game.skip_combat()
|
|
main.game.timer_enabled = false
|
|
main._refresh()
|
|
|
|
print("\n[N3] Ausscheiden und Zuschauen")
|
|
p.hp = 1
|
|
for hx in p.board.keys():
|
|
p.board.erase(hx)
|
|
main._refresh()
|
|
main.game.start_combat()
|
|
main.game.skip_combat()
|
|
main._after_combat()
|
|
main._refresh()
|
|
await _wait(0.2)
|
|
check(not p.alive and main.overlays.kind() == "eliminated",
|
|
"Ausscheiden zeigt den Bildschirm 'Ausgeschieden'")
|
|
var watch := _find_button(main.overlays.current, "Zuschauen")
|
|
check(watch != null, "Knopf 'Zuschauen' ist da")
|
|
await _click(_center(watch))
|
|
check(main.spectating and not main.overlays.is_open(), "Zuschauen läuft, Bildschirm ist weg")
|
|
check(main.scout != null and main.scout.alive, "Kamera zeigt das Brett des Führenden: %s" % (main.scout.pname if main.scout else "-"))
|
|
main.game.timer_enabled = true
|
|
main.game.plan_left = 20.0
|
|
await _wait(0.2)
|
|
check(main.game.plan_left <= 3.0, "Runden laufen beim Zuschauen schneller (%.1f s)" % main.game.plan_left)
|
|
main.game.timer_enabled = false
|
|
main._restart()
|
|
await _wait(0.2)
|
|
check(main.game.human.alive and main.game.round_no == 1 and not main.spectating, "Neues Spiel setzt alles zurück")
|
|
main.game.timer_enabled = false
|
|
p = main.game.human
|
|
|
|
print("\n[N4] Weiterspielen")
|
|
main.game.timer_enabled = false
|
|
main.game.start_combat()
|
|
main.game.skip_combat()
|
|
main._refresh()
|
|
check(main.game.save(), "Spielstand geschrieben")
|
|
var saved_round: int = main.game.round_no
|
|
var saved_gold: int = main.game.human.gold
|
|
main.overlays.show_start(Game.has_save())
|
|
await _wait(0.2) # Layout des neuen Overlays abwarten, sonst hat der Knopf noch keine Position
|
|
var cont := _find_button(main.overlays.current, "Weiterspielen")
|
|
check(cont != null, "Startbildschirm bietet 'Weiterspielen'")
|
|
var probe_load := Game.load_from()
|
|
check(probe_load != null and probe_load.round_no == saved_round, "Spielstand lässt sich lesen (Runde %d)" % (probe_load.round_no if probe_load else -1))
|
|
main.game = Game.new()
|
|
main.game.start(0, main.settings.difficulty)
|
|
main.board.game = main.game
|
|
await _click(_center(cont))
|
|
check(main.game.round_no == saved_round and main.game.human.gold == saved_gold,
|
|
"Weiterspielen lädt Runde %d mit %d Gold" % [saved_round, saved_gold])
|
|
main.game.timer_enabled = false
|
|
p = main.game.human
|
|
|
|
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")
|
|
|
|
Game.delete_save()
|
|
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 _shop_signature(p: PlayerState) -> String:
|
|
var out := ""
|
|
for e in p.shop:
|
|
out += "%d/%d," % [e.role, e.element]
|
|
return out
|
|
|
|
|
|
func _all_buttons(node: Node) -> Array:
|
|
var out := []
|
|
for c in node.get_children():
|
|
if c is Button:
|
|
out.append(c)
|
|
out.append_array(_all_buttons(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
|