# weapon_trace_test —— 40250 WeaponTrace 的轨迹/生命周期/接入回归。 # godot --headless --path project --script weapon_trace_test.gd extends SceneTree const WeaponTrace = preload("res://ui/weapon_trace.gd") const PlayerView = preload("res://ui/player_view.gd") var _fail := 0 func _ck(ok: bool, message: String) -> void: if not ok: _fail += 1 printerr("FAIL: " + message) func _init() -> void: await _run() if _fail == 0: print("PASS: weapon_trace_test") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: await _test_trace_lifecycle() await _test_player_view_integration() func _test_trace_lifecycle() -> void: var host := Node3D.new() get_root().add_child(host) var weapon := MeshInstance3D.new() var box := BoxMesh.new() box.size = Vector3(0.12, 0.12, 1.6) weapon.mesh = box host.add_child(weapon) var trace: Node = WeaponTrace.new() host.add_child(trace) await process_frame _ck(trace.attach_to_weapon(weapon), "box weapon resolves a trace tip") trace.turn_on() for i in 6: weapon.position = Vector3(float(i) * 0.05, 0.0, 0.0) trace._process(0.016) var active: Dictionary = trace.debug_state() _ck(active.get("playing", false), "TurnOn keeps trace active") _ck(int(active.get("short_points", 0)) >= 2 and int(active.get("long_points", 0)) >= 2, "Update samples both short and long world tracks") _ck(int(active.get("vertex_count", 0)) >= 4, "BuildVertex produces a triangle-strip vertex band") trace.turn_off() var before_fade: Dictionary = trace.debug_state() trace._process(0.05) var during_fade: Dictionary = trace.debug_state() _ck(not during_fade.get("playing", true), "TurnOff stops sampling") _ck(int(during_fade.get("long_points", 0)) > 0 and int(during_fade.get("long_points", 0)) <= int(before_fade.get("long_points", 0)), "TurnOff preserves the tail for lifetime fade") trace._process(0.20) var expired: Dictionary = trace.debug_state() _ck(int(expired.get("short_points", 0)) == 0 and int(expired.get("long_points", 0)) == 0, "expired trace samples are removed") trace.attach_to_weapon(weapon) var replaced: Dictionary = trace.debug_state() _ck(int(replaced.get("short_points", 0)) == 0 and int(replaced.get("long_points", 0)) == 0, "re-attaching a weapon clears the old actor trace") trace.free() host.free() await process_frame func _test_player_view_integration() -> void: if not ClassDB.class_exists("Metin2Model") or not ClassDB.class_exists("Metin2AnimPlayer"): print(" (skip PlayerView integration: animation extension unavailable)") return var assets := AssetRoot.path() var weapon_path := assets.path_join("item/ymir work/item/weapon/00010.gr2") if not FileAccess.file_exists(weapon_path): print(" (skip PlayerView integration: weapon asset unavailable)") return var pv: Node = PlayerView.new() get_root().add_child(pv) _ck(pv.build(assets, 0), "PlayerView builds for WeaponTrace integration") pv.set("weapon_gr2", weapon_path) for i in 4: await process_frame var attached: Dictionary = pv.weapon_trace_state() _ck(attached.get("present", false), "ordinary sword creates PlayerView WeaponTrace") pv.play_attack_motion(2, 14, 1.0) var attacking: Dictionary = pv.weapon_trace_state() _ck(attacking.get("playing", false), "attack motion turns WeaponTrace on") # playback_finished is the 40250 motion-queue boundary; invoke the same # callback deterministically instead of trying to interrupt an active once # motion with a loop request (PlayerView correctly defers that request). pv._on_playback_finished() var hidden: Dictionary = pv.weapon_trace_state() _ck(not hidden.get("playing", true), "new non-attack motion turns WeaponTrace off") pv.set_weapon_trace_enabled(false) await process_frame _ck(not pv.weapon_trace_state().get("present", true), "weapon type gate can destroy the trace") pv.free() if ClassDB.class_exists("Metin2Model"): Metin2Model.clear_texture_cache() if ClassDB.class_exists("Metin2AnimPlayer"): Metin2AnimPlayer.clear_clip_cache() await process_frame