# mob_view_test —— 怪 / NPC 真模型(MobView)headless 自检。 # godot --headless --path project --script mob_view_test.gd # 需要 Metin2Model 扩展 + Metin2 资产(AssetRoot.path());缺就跳过对应检查。 extends SceneTree const MobView = preload("res://ui/mob_view.gd") const Audio = preload("res://audio.gd") var _fail := 0 var _resolution_done := false func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: _run() if _fail == 0: print("PASS: mob_view_test") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) class FakeAnim: extends Node var anim_path := "" var loop := false # 纯解析:不需要扩展和资产。旧客户端 CRaceData::GetMotionKey 找不到动作时 # SetLoopMotion/InterceptMotion 直接返回、保留当前动作;不能静默换成 WAIT。 func _check_motion_resolution() -> void: var mv: Node3D = MobView.new() mv._dir = "res://__no_such_mob_dir__" mv._motions = {"WAIT": "wait.msa", "NORMAL_ATTACK": "attack.msa", "FRONT_DEAD": "dead.msa"} var dead: Dictionary = mv.resolve_motion("dead") _ck(dead.get("requested_state") == "dead" and dead.get("motion") == "FRONT_DEAD" \ and dead.get("path") == "dead.msa" and dead.get("fallback_reason") == "", "dead -> FRONT_DEAD 无回退") var run: Dictionary = mv.resolve_motion("run") _ck(run.get("path") == "" and run.get("motion") == "" \ and run.get("fallback_reason") == "reference_keep_current_motion", "缺 RUN/WALK -> 保留当前动作(%s)" % run) _ck(mv.get_motion_path("run") == "", "缺 RUN/WALK 时 get_motion_path 不返回 WAIT") mv._motions = {"WAIT": "wait.msa"} _ck(mv.get_motion_path("dead") == "", "缺死亡动作时不能解析成 WAIT") mv._motions = {"WAIT": "wait.msa", "WALK": "walk.msa"} var alias: Dictionary = mv.resolve_motion("run") _ck(alias.get("path") == "walk.msa" and alias.get("fallback_reason") == "client_alias:WALK", "run -> WALK 标注别名回退") mv._motions = {"WAIT": "wait.msa"} var anim := FakeAnim.new() mv.anim = anim mv.set_anim_state("wait") mv.set_anim_state("run") _ck(anim.anim_path == "wait.msa" and anim.loop, "缺 run 时保留当前 WAIT 播放") _ck(mv.last_motion_resolution().get("fallback_reason") == "reference_keep_current_motion", "记录最后一次解析的回退原因") mv.anim = null anim.free() mv.free() _resolution_done = true func _run() -> void: _check_motion_resolution() # 脚本运行时错误只会中止函数、不计失败;用哨兵保证整段断言都执行过。 _ck(_resolution_done, "动作解析断言全部执行") if not ClassDB.class_exists("Metin2Model") or not ClassDB.class_exists("Metin2Proto"): print(" (skip: 扩展未注册)") return var assets := AssetRoot.path() if not DirAccess.dir_exists_absolute(assets.path_join("Monster")): print(" (skip: 无 Monster 资产)") return var proto: Object = ClassDB.instantiate("Metin2Proto") get_root().add_child(proto) var audio: Node = Audio.new() get_root().add_child(audio) audio.setup(assets) var mp := assets.path_join("locale/locale/en/mob_proto") if not FileAccess.file_exists(mp) or not proto.call("load_mob_proto", mp): print(" (skip: 无 mob_proto)") return # race 102 = Wolf -> assets/*/ymir work/monster/wolf/ var mv: Node3D = MobView.new() get_root().add_child(mv) var ok: bool = mv.build(assets, proto, 102) _ck(ok, "MobView.build(102 = Wolf) 成功") if not ok: return mv.set_audio(audio) _ck(mv._dir.get_file() == "wolf", "解到 wolf 目录(%s)" % mv._dir) _ck(mv.model != null and mv.model.get_class() == "Metin2Model", "Metin2Model 建好") _ck(mv._motions.size() >= 3, "motlist.txt 解出 %d 个动作" % mv._motions.size()) _ck(mv._motions.has("WAIT"), "有 WAIT 动作") mv.set_display_name("Wolf") _ck(mv.get_node_or_null("Label3D") != null and (mv.get_node("Label3D") as Label3D).text == "Wolf", "set_display_name -> nameplate") # 状态切换:run -> RUN / attack -> NORMAL_ATTACK(存在才断言路径变化) mv.set_anim_state("run") var s1: String = mv._state mv.set_anim_state("attack") _ck(mv._state == "attack" and s1 == "run", "set_anim_state 切换记录") _ck(mv._sound_instances.size() > 0, "MobView attack motion loads paired .mss instances") # 未知 race -> build 失败(返回 null 用占位) var mv2: Node3D = MobView.new() get_root().add_child(mv2) _ck(not mv2.build(assets, proto, 999999), "未知 race -> build 失败") # race 20009 = Old Man(NPC)——若有目录就该建成 var mv3: Node3D = MobView.new() get_root().add_child(mv3) var npc_ok: bool = mv3.build(assets, proto, 20009) print(" NPC 20009 (Old Man) build = %s (%s)" % [npc_ok, mv3._dir])