This commit is contained in:
team3
2026-09-16 13:21:45 +02:00
parent 8a188c5d5c
commit 49e8f2d95e
27 changed files with 1772 additions and 265 deletions

View File

@@ -14,6 +14,8 @@ func _ready() -> void:
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():
@@ -119,15 +121,32 @@ func _buy(p: PlayerState, element: int) -> void:
func _run() -> void:
await _wait(0.6)
print("\n[S] Startbildschirm")
check(main.overlay != null and not main.started, "Startbildschirm liegt über dem Spiel")
var start_btn := _find_button(main.overlay, "Spiel starten")
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 main.overlay == null, "Klick startet das Spiel und schließt den Bildschirm")
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
@@ -146,6 +165,22 @@ func _run() -> void:
% [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)
@@ -171,6 +206,15 @@ func _run() -> void:
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
@@ -229,6 +273,7 @@ func _run() -> void:
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")
@@ -246,6 +291,57 @@ func _run() -> void:
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:
@@ -341,6 +437,8 @@ func _run() -> void:
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:"),
@@ -371,6 +469,12 @@ func _run() -> void:
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)"
@@ -385,6 +489,33 @@ func _run() -> void:
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)
@@ -397,6 +528,7 @@ func _run() -> void:
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)
@@ -445,23 +577,23 @@ func _run() -> void:
main.game.plan_left = 20.0
_key(KEY_P)
await _wait(0.3)
check(main.paused and main.overlay != null, "P pausiert und zeigt den Pausebildschirm")
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 main.overlay == null, "P setzt fort")
check(not main.paused and not main.overlays.is_open(), "P setzt fort")
main.game.timer_enabled = false
var zoom0: float = main.cam_zoom
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.cam_zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.cam_zoom])
check(main.rig.zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.rig.zoom])
_key(KEY_E)
await _wait()
check(absf(main.cam_yaw - 15.0) < 0.01, "E dreht um 15 Grad")
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")
@@ -484,6 +616,57 @@ func _run() -> void:
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 := []
@@ -501,6 +684,7 @@ func _run() -> void:
has_umlaut = true
check(has_umlaut, "echte Umlaute kommen im Text vor")
Game.delete_save()
print("")
if fails == 0:
print("UI-VERIFIKATION BESTANDEN")