Hauptszene scenes/luneraria.tscn, Saumwacht bleibt über --legacy und die alten QA-Schalter erreichbar. Karte aus Bestandsassets plus neuem Turm, HUD mit Planung, Lanes und Minimap, Menü umbenannt, Einstellungen werden aus dem alten Benutzerordner übernommen. Simulation 6,6x schneller (Raster, Caches, faire Staffelung) und exakt spiegelsymmetrisch. Regeln 91/91, echte Anwendung 38/38, Legacy unverändert grün. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
246 lines
11 KiB
GDScript
246 lines
11 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)
|
|
var lane_button: Button = null
|
|
for child: Node in hud._roster.get_children():
|
|
for grand: Node in child.get_children():
|
|
if grand is Button and grand.text == "M":
|
|
lane_button = grand
|
|
break
|
|
if lane_button != null:
|
|
break
|
|
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):
|
|
if (node as Button).text == "Menü" and (node as Button).is_visible_in_tree():
|
|
return node
|
|
return null
|