Files
aetherbound-guild/runtime/tests/p9_1/battle_readability_continuity_test.gd
T

249 lines
11 KiB
GDScript

extends SceneTree
const MainScene = preload("res://main.tscn")
const VIEWPORT_SIZE := Vector2i(844, 390)
var failures: Array[String] = []
var viewport: SubViewport
var capture_root := ""
var verified_events := 0
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
viewport = SubViewport.new()
viewport.size = VIEWPORT_SIZE
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(viewport)
capture_root = OS.get_environment("ABG_P9_1_BATTLE_CAPTURE_ROOT")
if not capture_root.is_empty():
DirAccess.make_dir_recursive_absolute(capture_root)
var game := await create_game()
await prepare_exceptional_battle_three(game)
verify_approach(game, ["Hearthguard", "Farstring", "Kindhand"])
game.advance_battle_phase()
expect(game.facade.enemy_animation_state() == "telegraph", "battle three pressure is telegraphed")
expect(game.facade.battle_event_text().contains("艾连"), "battle three telegraph names the current lead")
game.advance_battle_phase()
await verify_bound_impact(game, 2, "", "Hearthguard", "guard", "guard", "front")
game.advance_battle_phase()
verify_accessibility_invariance(game, "Farstring", "draw_release")
await verify_bound_impact(game, 3, "Farstring", "enemy", "draw_release", "hit", "enemy")
await capture_state("01-battle-three-farstring")
game.advance_battle_phase()
await verify_bound_impact(game, 4, "Kindhand", "Hearthguard", "lantern_heal", "heal", "front")
await capture_state("02-battle-three-kindhand")
game.advance_battle_phase()
expect(game.facade.later_battle_animation_state("Hearthguard") == "basic_attack", "battle three lead performs the finishing action")
game.advance_battle_phase()
expect(game.facade.enemy_animation_state() == "death", "battle three enemy uses the death animation")
game.advance_battle_phase()
expect(game.page == game.AppPage.SESSION_RESULT, "battle three reaches its victory result")
game.open_route_board()
game.choose_route("R6-03")
game.open_route_review()
game.commit_route()
game.advance_battle_phase()
expect(game.facade.enemy_animation_state() == "telegraph", "route pressure is telegraphed before impact")
game.advance_battle_phase()
await verify_bound_impact(game, 2, "", "Kindhand", "idle", "hit", "added")
expect(game.facade.added_rear_sprite.position.x < game.facade.added_rear_base_position.x, "third-position damage recoils the correct actor")
await capture_state("03-battle-four-marked-third")
game.advance_battle_phase()
await verify_bound_impact(game, 3, "Farstring", "enemy", "draw_release", "hit", "enemy")
game.advance_battle_phase()
await verify_bound_impact(game, 4, "Kindhand", "Kindhand", "lantern_heal", "heal", "added")
resolve_remaining_battle(game, 4, 7)
expect(game.page == game.AppPage.SECOND_CYCLE_RESULT, "battle four reaches its victory result")
game.open_training()
game.choose_training("rear")
game.open_training_review()
game.commit_training()
verify_approach(game, ["Hearthguard", "Farstring", "Kindhand"])
game.advance_battle_phase()
await verify_bound_impact(game, 1, "Farstring", "enemy", "draw_release", "hit", "enemy")
await capture_state("04-battle-five-trained-farstring")
resolve_remaining_battle(game, 1, 7)
expect(game.page == game.AppPage.THIRD_CYCLE_RESULT, "trained Farstring path reaches the final result")
await destroy_game(game)
await verify_training_action("front", 2, "Hearthguard", "", "Hearthguard", "guard", "guard", "front")
await verify_training_action("added", 5, "Kindhand", "Kindhand", "Kindhand", "lantern_heal", "heal", "added")
viewport.queue_free()
await settle(90)
if failures.is_empty():
print("ABG_P9_1_BATTLE_READABILITY_CONTINUITY_OK battles=3,4,5 events=%d formation=HG,FS,KH training=HG,FS,KH reduced_motion=true mute_captions=true low_power=true vfx=guard,hit,heal added_recoil=true bounds=844x390" % verified_events)
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func create_game() -> Control:
var game := MainScene.instantiate()
game.auto_boot = false
game.audio_enabled = false
viewport.add_child(game)
await settle(10)
game.locale = "zh_CN"
game.muted = true
game.boot_verified = true
game.page = game.AppPage.MAIN_MENU
game.render()
return game
func prepare_exceptional_battle_three(game: Control) -> void:
start_pair(game, "Farstring", "Kindhand")
resolve_remaining_battle(game, 0, 6)
game.open_adaptation()
game.choose_adaptation("EQ-002")
game.begin_second_battle()
resolve_remaining_battle(game, 0, 6)
game.open_guild_return()
game.choose_guild_option("recruit_rear")
game.begin_third_battle()
expect(game.facade.visible_member_ids() == ["Hearthguard", "Farstring", "Kindhand"], "Guild re-formation creates the declared visible order")
func prepare_exceptional_battle_five(game: Control, training_slot: String) -> void:
await prepare_exceptional_battle_three(game)
resolve_remaining_battle(game, 0, 7)
game.open_route_board()
game.choose_route("R6-03")
game.open_route_review()
game.commit_route()
resolve_remaining_battle(game, 0, 7)
game.open_training()
game.choose_training(training_slot)
game.open_training_review()
game.commit_training()
func verify_training_action(slot: String, phase: int, member_id: String, source_id: String, target_id: String, expected_state: String, expected_vfx: String, expected_slot: String) -> void:
var game := await create_game()
await prepare_exceptional_battle_five(game, slot)
for current_phase in range(phase):
game.advance_battle_phase()
expect(game.battle_phase == phase, "%s training reaches phase %d" % [member_id, phase])
await verify_bound_impact(game, phase, source_id, target_id, expected_state, expected_vfx, expected_slot)
await destroy_game(game)
func verify_approach(game: Control, members: Array[String]) -> void:
expect(game.battle_phase == 0, "battle opens in the approach phase")
expect(game.facade.enemy_animation_state() == "advance", "enemy advances during approach")
for member_id in members:
expect(game.facade.later_battle_animation_state(member_id) == "advance", "%s advances without an identity swap" % member_id)
var caption: String = game.facade.battle_event_text()
expect(caption.contains("推进") or caption.contains("进入"), "approach phase has a readable caption")
func verify_accessibility_invariance(game: Control, member_id: String, expected_state: String) -> void:
var snapshot: Dictionary = game.facade.session_state.snapshot()
var caption: String = game.facade.battle_event_text()
var duration: float = game.phase_duration(game.battle_phase)
var state: String = game.facade.later_battle_animation_state(member_id)
game.reduced_motion = true
game.muted = true
game.facade.presentation_settings.low_power = true
game.render()
expect(game.facade.session_state.snapshot() == snapshot, "presentation settings do not mutate the domain snapshot")
expect(game.facade.battle_event_text() == caption, "mute/captions preserve the event caption")
expect(is_equal_approx(game.phase_duration(game.battle_phase), duration), "low power preserves battle duration")
expect(game.facade.later_battle_animation_state(member_id) == state and state == expected_state, "reduced motion preserves authored action state")
expect(game.facade.animation_frame_index(member_id, expected_state, 0.25) > 0, "reduced motion keeps essential action frames")
game.reduced_motion = false
game.facade.presentation_settings.low_power = false
game.render()
func verify_bound_impact(game: Control, phase: int, source_id: String, target_id: String, expected_state: String, expected_vfx: String, expected_slot: String) -> void:
expect(game.battle_phase == phase, "impact is checked in phase %d" % phase)
var action_member := source_id if not source_id.is_empty() else target_id
var actual_state: String = game.facade.enemy_animation_state() if action_member == "enemy" else game.facade.later_battle_animation_state(action_member)
expect(actual_state == expected_state, "phase %d binds %s to %s" % [phase, action_member, expected_state])
var facts_before: int = game.facade.session_state.facts.size()
game.commit_phase_impact(phase)
expect(game.facade.session_state.facts.size() == facts_before + 1, "phase %d commits one semantic event" % phase)
if game.facade.session_state.facts.size() <= facts_before:
return
var event: Dictionary = game.facade.session_state.facts[-1]
var feedback: Dictionary = game.active_battle_feedback
expect(String(event.get("source_member_id", "")) == source_id, "phase %d semantic source is stable" % phase)
expect(String(event.get("target_member_id", "")) == target_id, "phase %d semantic target is stable" % phase)
expect(String(feedback.get("source_member_id", "")) == source_id, "phase %d feedback preserves source identity" % phase)
expect(String(feedback.get("target_member_id", "")) == target_id, "phase %d feedback preserves target identity" % phase)
expect(String(feedback.get("target", "")) == expected_slot, "phase %d feedback targets the current visible slot" % phase)
expect(String(feedback.get("cue", "")) == String(event.get("cue", "")), "phase %d feedback preserves the impact cue" % phase)
expect(String(feedback.get("vfx", "")) == expected_vfx, "phase %d uses %s VFX" % [phase, expected_vfx])
var label := find_control(game, "BattleFeedbackLabel")
var vfx := find_control(game, "BattleFeedbackVFX")
expect(label != null and vfx != null, "phase %d renders number and VFX" % phase)
if label != null:
expect(String(label.get_meta("target_member_id", "")) == target_id, "phase %d number remains bound to target identity" % phase)
expect(label.get_global_rect().position.y >= 164.0, "phase %d number does not cover the event ribbon")
if vfx != null:
expect(String(vfx.get_meta("vfx", "")) == expected_vfx, "phase %d rendered VFX matches payload" % phase)
game.battle_feedback_clock = 0.10
game.update_battle_feedback_visual()
verified_events += 1
func start_pair(game: Control, first: String, second: String) -> void:
game.start_new_game()
game.open_recruit_selection()
game.toggle_recruit(first)
game.toggle_recruit(second)
game.review_automatic_formation()
game.commit_formation()
func resolve_remaining_battle(game: Control, current_phase: int, phase_count: int) -> void:
for phase in range(current_phase, phase_count):
game.advance_battle_phase()
func capture_state(state_name: String) -> void:
if capture_root.is_empty():
return
await settle(7)
await RenderingServer.frame_post_draw
var image := viewport.get_texture().get_image()
var error := image.save_png(capture_root.path_join(state_name + ".png"))
if error != OK:
failures.append("capture %s failed with %s" % [state_name, error])
func destroy_game(game: Control) -> void:
game.queue_free()
await settle(60)
func find_control(node: Node, node_name: String) -> Control:
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
return node
for child in node.get_children():
var found := find_control(child, node_name)
if found != null:
return found
return null
func settle(frames: int) -> void:
for frame in range(frames):
await process_frame
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P9.1 battle readability/continuity: %s" % message)