249 lines
11 KiB
GDScript
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)
|