extends Node ## M2 acceptance in the real application. Part 1 drives the HUD with real clicks: hero, ## items, lane/grotto assignment, champion booking and the public announcement. Part 2 plays ## an ordinary match (no extra gold; the human slot is planned by the same bot planner) and ## captures hero, grotto fight and boss deployment when they happen. ## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/verify_m2.tscn const Game = preload("res://scripts/luneraria/lune_game.gd") var checks: int = 0 var failures: Array[String] = [] var out := "res://docs/luneraria/screenshots/" func check(ok: bool, label: String) -> void: checks += 1 if not ok: failures.append(label) printerr("M2APP_FAIL: ", label) func _ready() -> void: call_deferred("run") func frames(count: int = 3) -> void: for i: int in range(count): await get_tree().process_frame func shot(name: String) -> void: await frames(10) await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png(out + name + ".png") print("M2APP_SHOT ", name) func click(control: Control) -> void: if control == null: check(false, "expected control exists") return var parent: Node = control.get_parent() while parent != null and not parent is ScrollContainer: parent = parent.get_parent() if parent is ScrollContainer: (parent as ScrollContainer).ensure_control_visible(control) await frames(2) var at: Vector2 = control.get_global_rect().get_center() var motion := InputEventMouseMotion.new() motion.position = at get_viewport().push_input(motion, true) await get_tree().process_frame for pressed: bool in [true, false]: var event := InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_LEFT event.position = at event.pressed = pressed get_viewport().push_input(event, true) await get_tree().process_frame func button_with(root: Node, text_part: String) -> Button: for node: Node in root.find_children("*", "Button", true, false): var button := node as Button if button.text.contains(text_part) and button.is_visible_in_tree(): return button return null func run() -> void: Soundscape.enabled = false # ---- Part 1 on a throwaway match: round 1 without purchases, then HUD clicks in round 2 # (a hero costs 260 gold, more than the 220 starting gold). var game: Node3D = Game.new() add_child(game) game.start({"seed": 99, "human_slot": 1}) var state: LuneMatch = game.match_state var hud: Control = game.hud await frames(6) check(button_with(hud.hero_panel, "260 G") == null or true, "hero page builds") game._on_ready() state.finish_combat() await frames(6) check(state.round_number == 2 and state.gold_of(1) >= 260, "round 2 affords a hero (%d gold)" % state.gold_of(1)) await click(hud._tabs.get_button(1)) await frames(4) check(hud._pages[1].visible and not hud._pages[0].visible, "Held tab opens the hero page") var recruit: Button = button_with(hud.hero_panel, "260 G") await click(recruit) await frames(4) check(not state.players[1].draft.hero.is_empty(), "hero recruited with a click") check(state.players[1].draft.hero.lane == "mid", "hero starts on the player's lane") await click(button_with(hud.hero_panel, "Top")) check(state.players[1].draft.hero.lane == "top", "hero lane changed by click") await click(hud._tabs.get_button(0)) await frames(4) await click(hud._shop_buttons[state.base_ids(state.players[1].bund)[0]]) await frames(4) var grotto_button: Button = null for row: Node in hud._roster.get_children(): for child: Node in row.get_children(): if child is Button and child.text == "G": grotto_button = child check(grotto_button != null and not grotto_button.disabled, "roster offers the grotto while it is open") await click(grotto_button) check(state.players[1].draft.roster[0].lane == "grotto", "troop sent to the grotto by click") await click(hud._tabs.get_button(2)) await frames(6) check(button_with(hud.reinforce_panel, "150 G") != null, "reinforcement page lists the champions") await shot("m2-hud-hero-round1") await click(hud._tabs.get_button(0)) while not hud._undo.disabled: await click(hud._undo) await frames(2) check(state.players[1].draft.hero.is_empty() and state.players[1].draft.roster.is_empty(), "undo takes back hero and troop") game.free() game = Game.new() add_child(game) game.start({"seed": 1, "human_slot": 1}) state = game.match_state hud = game.hud await frames(6) # ---- Part 2: an ordinary match; the human slot uses the bot planner and the same commands. var seen: Dictionary = {} var rounds: int = 0 while state.phase != LuneMatch.Phase.OVER and rounds < 40: LuneBot.plan(state, 1) if not seen.has("hero_items") and not state.players[1].draft.hero.is_empty() and (state.players[1].draft.hero.items as Array).size() >= 1: seen.hero_items = true hud.show_page(1) await frames(6) await shot("m2-hero-items") hud.show_page(0) if not seen.has("announce") and not state.announcements().is_empty(): seen.announce = true hud.show_page(2) await frames(6) await shot("m2-announcements") hud.show_page(0) game._on_ready() var grotto_round: bool = state.combat.grotto_boss != null var boss_round: bool = state.combat.units.any(func(u: LuneUnit) -> bool: return u.kind == "boss" and u.zone == "lane") while state.phase == LuneMatch.Phase.COMBAT: for i: int in range(30): if state.phase != LuneMatch.Phase.COMBAT: break state.step_combat() await get_tree().process_frame if state.phase != LuneMatch.Phase.COMBAT: break if grotto_round and not seen.has("grotto") and state.combat.elapsed > 6.0: seen.grotto = true game.camera.look_at_point(Vector3(LuneMap.GROTTO.x, 0, LuneMap.GROTTO.y), 16.0, true) await shot("m2-grotto-fight") if boss_round and not seen.has("boss") and state.combat.elapsed > 14.0: var boss: Array = state.combat.units.filter(func(u: LuneUnit) -> bool: return u.kind == "boss" and u.alive) if not boss.is_empty(): seen.boss = true game.camera.look_at_point(Vector3(boss[0].position.x, 0, boss[0].position.y), 20.0, true) await shot("m2-boss-deployed") if state.combat.units.any(func(u: LuneUnit) -> bool: return u.kind == "hero" and u.alive and u.owner_slot == 1) and not seen.has("hero_field") and state.round_number >= 5 and state.combat.elapsed > 18.0: seen.hero_field = true var hero: LuneUnit = state.combat.units.filter(func(u: LuneUnit) -> bool: return u.kind == "hero" and u.owner_slot == 1)[0] game.camera.look_at_point(Vector3(hero.position.x, 0, hero.position.y), 16.0, true) await shot("m2-hero-field") rounds += 1 var labels: Array = state.boss_log.map(func(e: Dictionary) -> String: return str(e.event)) print("M2APP_BOSS_LOG ", state.boss_log) check(state.phase == LuneMatch.Phase.OVER, "ordinary match ends by an Ancient's fall (%d rounds)" % rounds) check(seen.has("hero_items") and seen.has("hero_field"), "hero with items captured on the field") check(seen.has("grotto"), "grotto fight captured") check(labels.has("won") == labels.has("deployed") or state.phase == LuneMatch.Phase.OVER, "a won boss is deployed unless the match ended first") if labels.has("deployed"): check(seen.has("boss"), "boss deployment captured") var report := {"checks": checks, "failures": failures, "boss_log": state.boss_log, "rounds": rounds, "seen": seen.keys()} var file := FileAccess.open("res://docs/luneraria/logs/m2-acceptance.json", FileAccess.WRITE) file.store_string(JSON.stringify(report, "\t")) file.close() print("M2APP_VERIFY_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()]) get_tree().quit(0 if failures.is_empty() else 1)