saumwacht-godot/tools/luneraria/verify_m2.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

171 lines
7.7 KiB
GDScript

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(hud.hero_card != null, "hero card 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))
# G3 HUD: the hero card opens the hero window (recruiting, then values, items, talents).
await click(hud.hero_card)
await frames(4)
check(hud.modal_open() and hud.hero_detail.recruit_buttons.size() == 3, "hero card opens the hero window")
var recruit: Button = button_with(hud.hero_detail, "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_detail, "TOP"))
check(state.players[1].draft.hero.lane == "top", "hero lane changed by click")
hud.close_modal()
await frames(4)
await click(hud._shop_buttons[state.base_ids(state.players[1].bund)[0]])
await frames(4)
await click(hud._chips.values()[0] if not hud._chips.is_empty() else null)
await frames(4)
var grotto_button: Button = hud.unit_detail._lanes.get(LuneMatch.GROTTO) if hud.unit_detail.visible else null
check(grotto_button != null and not grotto_button.disabled, "unit details offer 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._purchase_tabs.tabs[1])
await frames(6)
var champion_cards: Dictionary = hud.champion_panel.cards
check(hud.champion_panel.visible and champion_cards.size() == 3 and (champion_cards["ch_schutz"] as LuneUnitCard).cost == 150, "champion tab lists the champions")
await shot("m2-hud-hero-round1")
await click(hud._purchase_tabs.tabs[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)