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>
245 lines
12 KiB
GDScript
245 lines
12 KiB
GDScript
extends Node
|
|
## M1 acceptance in the real application with real mouse and key events: menu, planning,
|
|
## commit, combat, pause/speed, settlement, return to menu, restart, a complete match to an
|
|
## Ancient's fall, result screen, rematch, window sizes and repeated menu/match cycles.
|
|
## Only the combat clock is fast-forwarded (more fixed steps per frame); rules are untouched.
|
|
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/verify_m1.tscn
|
|
var main: Node
|
|
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("M1_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(8)
|
|
await RenderingServer.frame_post_draw
|
|
get_viewport().get_texture().get_image().save_png(out + name + ".png")
|
|
print("M1_SHOT ", name)
|
|
|
|
func click(control: Control) -> void:
|
|
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 key(code: Key) -> void:
|
|
for pressed: bool in [true, false]:
|
|
var event := InputEventKey.new()
|
|
event.keycode = code
|
|
event.physical_keycode = code
|
|
event.pressed = pressed
|
|
get_viewport().push_input(event)
|
|
await get_tree().process_frame
|
|
|
|
var core_shot_taken: bool = false
|
|
|
|
func fast_combat(game: Node3D, steps_per_frame: int = 40, capture: String = "") -> void:
|
|
var captured: bool = capture.is_empty()
|
|
while game.match_state.phase == LuneMatch.Phase.COMBAT:
|
|
for i: int in range(steps_per_frame):
|
|
if game.match_state.phase != LuneMatch.Phase.COMBAT:
|
|
break
|
|
# Slow down near an Ancient's fall to capture the siege in the live combat view.
|
|
if not core_shot_taken and _weak_core(game) >= 0:
|
|
core_shot_taken = true
|
|
var team: int = _weak_core(game)
|
|
var at: Vector2 = game.match_state.map.core_position(team)
|
|
game.camera.look_at_point(Vector3(at.x, 0, at.y), 22.0, true)
|
|
await shot("core-siege")
|
|
game.match_state.step_combat()
|
|
await get_tree().process_frame
|
|
if not captured and game.match_state.phase == LuneMatch.Phase.COMBAT and game.match_state.combat.elapsed > 25.0:
|
|
captured = true
|
|
await shot(capture)
|
|
|
|
func _weak_core(game: Node3D) -> int:
|
|
var combat: LuneCombat = game.match_state.combat
|
|
for team: int in range(2):
|
|
var core: LuneUnit = combat.buildings["blau_ancient" if team == 0 else "rot_ancient"]
|
|
if core.alive and core.hp < core.max_hp * .3:
|
|
return team
|
|
return -1
|
|
|
|
func inside(control: Control, label: String) -> void:
|
|
var view := Rect2(Vector2.ZERO, get_viewport().get_visible_rect().size)
|
|
check(control.is_visible_in_tree() and view.encloses(control.get_global_rect()), label)
|
|
|
|
func run() -> void:
|
|
Soundscape.enabled = false
|
|
var started: int = Time.get_ticks_msec()
|
|
main = load("res://scenes/luneraria.tscn").instantiate()
|
|
add_child(main)
|
|
var menu: MainMenu = main.menu
|
|
check(ProjectSettings.get_setting("application/config/name") == "Champions of Luneraria", "project renamed to Champions of Luneraria")
|
|
check(menu.title_label.text == "CHAMPIONS" and menu.subtitle_label.text.contains("LUNERARIA"), "menu shows the new title")
|
|
while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")):
|
|
await get_tree().process_frame
|
|
print("M1_MENU_READY_MS ", Time.get_ticks_msec() - started)
|
|
await click(menu.new_game_button)
|
|
while menu.is_transitioning():
|
|
await get_tree().process_frame
|
|
await click(menu.lane_choice.get_button(0))
|
|
check(menu.lane == "top", "lane choice by click")
|
|
await click(menu.faction_choice.cards[0])
|
|
check(menu.bund == "glutpakt", "Bund choice by click")
|
|
await click(menu.start_button)
|
|
var game: Node3D = main.game
|
|
check(is_instance_valid(game) and game.human_slot == 0 and game.match_state.players[0].bund == "glutpakt", "start button launches a match for Blau Top with the chosen Bund")
|
|
var focus_before: Vector3 = game.camera._goal_focus
|
|
# Clicks and game keys during the fade must not reach the board.
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = MOUSE_BUTTON_WHEEL_UP
|
|
event.pressed = true
|
|
event.position = Vector2(900, 500)
|
|
get_viewport().push_input(event, true)
|
|
await key(KEY_1)
|
|
await key(KEY_SPACE)
|
|
check(game.match_state.players[0].draft.roster.is_empty() and game.match_state.phase == LuneMatch.Phase.PLANNING and is_equal_approx(game.camera._goal_zoom, 24.0), "no click or key reaches the game during the menu fade")
|
|
await get_tree().create_timer(.5).timeout
|
|
check(not menu.visible, "menu hides after the fade")
|
|
var state: LuneMatch = game.match_state
|
|
var hud: Control = game.hud
|
|
check(state.players.size() == 6 and state.players.filter(func(p: Dictionary) -> bool: return p.is_human).size() == 1, "one human and five bots")
|
|
check(state.round_number == 1 and state.phase == LuneMatch.Phase.PLANNING, "match opens in planning")
|
|
var gold0: int = state.gold_of(0)
|
|
await click(hud._starter)
|
|
check(state.players[0].draft.roster.size() == 3 and state.gold_of(0) == gold0 - 180, "Startaufstellung buys three Glutpakt troops for 180 gold")
|
|
await key(KEY_1)
|
|
check(state.players[0].draft.roster.size() == 3, "key 1 without enough gold is rejected")
|
|
await click(hud._undo)
|
|
check(state.players[0].draft.roster.size() == 2, "undo removes the last purchase")
|
|
await frames(4)
|
|
# G3 HUD: select the troop in the army strip, then order it to Mid in the detail panel.
|
|
await click(hud._chips.values()[0] if not hud._chips.is_empty() else null)
|
|
await frames(3)
|
|
var lane_button: Button = hud.unit_detail._lanes.get("mid") if hud.unit_detail.visible else null
|
|
await click(lane_button)
|
|
check(state.players[0].draft.roster.any(func(e: Dictionary) -> bool: return e.lane == "mid"), "lane assignment button moves a troop to Mid")
|
|
await click(hud._shop_buttons["g01"])
|
|
check(state.players[0].draft.roster.size() == 3, "shop click buys a troop")
|
|
for size: Vector2i in [Vector2i(1280, 720), Vector2i(1920, 1080)]:
|
|
get_window().size = size
|
|
await frames(6)
|
|
inside(hud._ready_button, "Bereit inside %dx%d" % [size.x, size.y])
|
|
inside(hud.minimap, "minimap inside %dx%d" % [size.x, size.y])
|
|
inside(hud._gold, "gold readout inside %dx%d" % [size.x, size.y])
|
|
await shot("planning-%dx%d" % [size.x, size.y])
|
|
var draft_uids: Array = state.players[0].draft.roster.map(func(e: Dictionary) -> String: return e.uid)
|
|
await key(KEY_SPACE)
|
|
check(state.phase == LuneMatch.Phase.COMBAT, "Space commits planning and starts combat")
|
|
check(state.players[0].roster.map(func(e: Dictionary) -> String: return e.uid) == draft_uids, "committed roster equals the draft")
|
|
var troops_on_top: int = 0
|
|
var troops_on_mid: int = 0
|
|
for unit: LuneUnit in state.combat.units:
|
|
if unit.owner_slot == 0:
|
|
troops_on_top += int(unit.lane == "top")
|
|
troops_on_mid += int(unit.lane == "mid")
|
|
check(troops_on_top == 2 and troops_on_mid == 1, "troops fight on their assigned lanes")
|
|
check(not state.submit(0, {"type": "buy", "type_id": "g01"}).ok, "no purchases during combat")
|
|
await get_tree().create_timer(1.0).timeout
|
|
await key(KEY_P)
|
|
var paused_time: float = state.combat.elapsed
|
|
await get_tree().create_timer(.5).timeout
|
|
check(game.paused and is_equal_approx(state.combat.elapsed, paused_time), "P pauses the combat clock")
|
|
await key(KEY_P)
|
|
await click(hud._speed)
|
|
check(game.SPEEDS[game.speed_index] == 2, "speed button switches to 2x")
|
|
game.focus_lane("mid", true)
|
|
await shot("combat-mid-live")
|
|
await fast_combat(game)
|
|
check(state.phase == LuneMatch.Phase.PLANNING and state.round_number == 2, "combat settles into planning round 2")
|
|
check(state.players[0].roster.size() == 3, "troops return after combat")
|
|
check(not state.last_settlement.is_empty() and int(state.last_settlement.income) > 0, "settlement pays income")
|
|
await frames(4)
|
|
check(hud._report.text.begins_with("Welle 1 abgeschlossen"), "HUD reports the settlement")
|
|
await shot("settlement")
|
|
# Menu and restart: a fresh match without previous progress.
|
|
await click(find_menu_button(hud))
|
|
await frames(4)
|
|
check(menu.visible and not is_instance_valid(game), "Menü returns to the title and frees the match")
|
|
await click(menu.new_game_button)
|
|
while menu.is_transitioning():
|
|
await get_tree().process_frame
|
|
await click(menu.start_button)
|
|
await get_tree().create_timer(.5).timeout
|
|
game = main.game
|
|
state = game.match_state
|
|
hud = game.hud
|
|
check(state.round_number == 1 and state.players[0].roster.is_empty() and state.gold_of(0) == state.balance.start_gold, "new match starts without previous progress")
|
|
# A complete match: the human's planning uses the same bot planner through the same commands.
|
|
var rounds: int = 0
|
|
var shots_taken: Dictionary = {}
|
|
while state.phase != LuneMatch.Phase.OVER and rounds < 60:
|
|
LuneBot.plan(state, game.human_slot, "druck")
|
|
game._on_ready()
|
|
if not shots_taken.has("lane") and state.round_number >= 3:
|
|
shots_taken.lane = true
|
|
game.focus_lane("top", true)
|
|
await fast_combat(game, 40, "combat-top")
|
|
elif not shots_taken.has("overview") and state.round_number >= 5:
|
|
shots_taken.overview = true
|
|
game.camera.overview(true)
|
|
await fast_combat(game, 40, "overview-combat")
|
|
else:
|
|
await fast_combat(game)
|
|
rounds += 1
|
|
check(state.phase == LuneMatch.Phase.OVER, "a complete match ends by an Ancient's fall (%d rounds)" % rounds)
|
|
var destroyed: Array = state.combat.destroyed_cores
|
|
check(destroyed.size() >= 1 and state.core_hp(destroyed[0]) <= 0.0, "the losing Ancient is actually destroyed")
|
|
check(core_shot_taken, "the siege of the falling Ancient was captured live")
|
|
await get_tree().create_timer(.8).timeout
|
|
await shot("result")
|
|
check(hud._over.visible and hud._over_title.text in ["SIEG", "NIEDERLAGE", "UNENTSCHIEDEN"], "result screen shows the outcome")
|
|
print("M1_MATCH_RESULT ", state.end_note, " after ", state.round_number, " waves")
|
|
var rematch: Button = null
|
|
for node: Node in hud._over.find_children("*", "Button", true, false):
|
|
if (node as Button).text == "NEUE PARTIE":
|
|
rematch = node
|
|
await click(rematch)
|
|
await frames(3)
|
|
check(is_instance_valid(main.game) and main.game != game and main.game.match_state.round_number == 1, "rematch starts a fresh match")
|
|
# Repeated menu/match cycles must not leak objects.
|
|
var objects: Array[float] = []
|
|
for cycle: int in range(4):
|
|
main.return_to_menu()
|
|
await frames(6)
|
|
main.start_match({"seed": 100 + cycle})
|
|
await get_tree().create_timer(.5).timeout
|
|
await frames(4)
|
|
objects.append(Performance.get_monitor(Performance.OBJECT_COUNT))
|
|
check(objects[3] <= objects[1] * 1.03 + 50, "no growing object count over menu/match cycles (%s)" % str(objects))
|
|
var report := {"checks": checks, "failures": failures, "object_counts": objects}
|
|
var file := FileAccess.open("res://docs/luneraria/logs/m1-acceptance.json", FileAccess.WRITE)
|
|
file.store_string(JSON.stringify(report, "\t"))
|
|
file.close()
|
|
print("M1_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)
|
|
|
|
func find_menu_button(hud: Control) -> Button:
|
|
for node: Node in hud.find_children("*", "Button", true, false):
|
|
var caption: String = (node as Button).text
|
|
if node.has_meta("label"):
|
|
caption = (node.get_meta("label") as Label).text
|
|
if caption == "Menü" and (node as Button).is_visible_in_tree():
|
|
return node
|
|
return null
|