# skill_motion_loop_test —— 40250 SetMotionLoopCount / MSA LoopData parity。 # godot --headless --path project --script skill_motion_loop_test.gd extends SceneTree const AssetRoot = preload("res://asset_root.gd") const PlayerView = preload("res://ui/player_view.gd") class FakeAnim extends Node: signal playback_finished var anim_path := "" var loop := true var time_scale := 1.0 var playing := true var motion_loop_count := -1 func set_motion_loop_count(count: int) -> void: motion_loop_count = count func get_motion_data() -> Dictionary: return {} var _fail := 0 func _ck(ok: bool, message: String) -> void: if not ok: _fail += 1 printerr("FAIL: " + message) func _touch(path: String) -> void: DirAccess.make_dir_recursive_absolute(path.get_base_dir()) var f := FileAccess.open(path, FileAccess.WRITE) f.store_string("") f.close() func _init() -> void: call_deferred("_run") func _run() -> void: await _test_player_view_payload() await _test_native_msa_segment_loop() if _fail == 0: print("PASS: skill_motion_loop_test") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _test_player_view_payload() -> void: var root := ProjectSettings.globalize_path("user://skill_motion_loop_test") var skill_dir := root.path_join("PC/ymir work/pc/warrior/skill") _touch(skill_dir.path_join("loop_skill.msa")) var pv: Node = PlayerView.new() get_root().add_child(pv) var anim := FakeAnim.new() pv.add_child(anim) pv.anim = anim pv._assets_root = root pv._race = 0 pv.motion_dir = root.path_join("PC/ymir work/pc/warrior/general") var played: bool = pv.play_skill_motion("loop_skill", 0, 3, true) _ck(played, "PlayerView accepts a skill motion from the resource directory") _ck(anim.motion_loop_count == 3, "PlayerView forwards uMotLoopCount to the animation player") _ck(pv.skill_motion_state() == {"loop_count": 3, "moving": true}, "PlayerView keeps loop count and moving-skill bit together") pv.free() await process_frame func _test_native_msa_segment_loop() -> void: if not ClassDB.class_exists("Metin2AnimPlayer"): print(" (skip native MSA loop test: animation extension unavailable)") return var assets := AssetRoot.path() var msa := assets.path_join("PC/ymir work/pc/assassin/skill/yeonsa.msa") if not FileAccess.file_exists(msa): print(" (skip native MSA loop test: yeonsa.msa unavailable)") return var anim: Node = ClassDB.instantiate("Metin2AnimPlayer") get_root().add_child(anim) if not anim.has_method("set_motion_loop_count") or not anim.has_method("get_motion_loop_count"): _ck(false, "Metin2AnimPlayer exposes SetMotionLoopCount") anim.free() await process_frame return anim.set("loop", false) anim.set("anim_path", msa) await process_frame var loop_data: Dictionary = anim.call("get_loop_data") _ck(bool(loop_data.get("present", false)), "MSA LoopData is exposed") _ck(int(loop_data.get("count", 0)) == 7, "yeonsa resource loop count is 7") anim.call("set_motion_loop_count", 2) _ck(int(anim.call("get_motion_loop_count")) == 2, "SetMotionLoopCount overrides the resource count") var previous: float = float(anim.call("get_time")) var wraps := 0 var count_after_wrap := -1 var finished := [false] anim.playback_finished.connect(func(): finished[0] = true) var frames := 0 while frames < 500 and not finished[0]: await process_frame var current: float = float(anim.call("get_time")) if current + 0.05 < previous: wraps += 1 count_after_wrap = int(anim.call("get_motion_loop_count")) previous = current frames += 1 _ck(wraps == 1, "loop count 2 performs one LoopData segment rewind") _ck(count_after_wrap == 1, "segment rewind decrements the remaining count") _ck(finished[0], "finite MSA loop returns to playback_finished after the tail") anim.set("anim_path", assets.path_join("PC/ymir work/pc/warrior/general/wait.msa")) await process_frame _ck(int(anim.call("get_motion_loop_count")) == 0, "normal motion does not inherit the previous skill loop override") anim.free() await process_frame