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>
171 lines
7.7 KiB
GDScript
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)
|