Files
2026-09-16 22:15:52 +09:00

158 lines
6.8 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mob_view_test —— 怪 / NPC 真模型(MobViewheadless 自检。
# 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")
const MsaMotion = preload("res://ui/msa_motion.gd")
var _fail := 0
var _resolution_done := false
var _move_speed_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
var time_scale := 1.0
# 纯解析:不需要扩展和资产。旧客户端 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
# CActorInstance::Move:走 / 跑按 m_fMovSpd = movSpd/100 播放;根运动速度
# = |.msa Accumulation| / MotionDurationwolf 03.msa255.45 / 0.6 = 425.75 cm/s)。
func _write_msa(path: String, duration: float, ay: float) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string("ScriptType MotionData\n\nMotionFileName \"x.gr2\"\n" +
"MotionDuration %f\nAccumulation 0.00\t%.2f\t0.00\n" % [duration, ay])
f.close()
func _check_move_speed() -> void:
var walk_p := "user://__mob_view_test_walk.msa"
var run_p := "user://__mob_view_test_run.msa"
_write_msa(walk_p, 0.866667, -75.81)
_write_msa(run_p, 0.6, -255.45)
_ck(absf(MsaMotion.move_speed(run_p) - 425.75) < 0.01, "run msa 速度 425.75%f" % MsaMotion.move_speed(run_p))
_ck(absf(MsaMotion.move_speed(walk_p) - 87.47) < 0.01, "walk msa 速度 87.47%f" % MsaMotion.move_speed(walk_p))
_ck(MsaMotion.move_speed("user://__no_such.msa") == 0.0, "缺文件 -> 0")
var mv: Node3D = MobView.new()
mv._dir = "res://__no_such_mob_dir__"
mv._motions = {"WAIT": "wait.msa", "WALK": walk_p, "RUN": run_p}
var sp: Vector2 = mv.get_move_motion_speeds()
_ck(absf(sp.x - 87.47) < 0.01 and absf(sp.y - 425.75) < 0.01, "get_move_motion_speeds%s" % sp)
var anim := FakeAnim.new()
mv.anim = anim
mv.set_move_speed(200)
mv.set_anim_state("wait")
_ck(anim.time_scale == 1.0, "wait 不受移动速度影响")
mv.set_anim_state("run")
_ck(anim.anim_path == run_p and anim.time_scale == 2.0, "run 按 movSpd/100 播放(%f" % anim.time_scale)
mv.set_move_speed(150)
_ck(anim.time_scale == 1.5, "跑动中改速立即生效(%f" % anim.time_scale)
mv.set_move_speed(1200)
_ck(anim.time_scale == 1.0, ">1100 保持原速(%f" % anim.time_scale)
mv.anim = null
anim.free()
mv.free()
DirAccess.remove_absolute(ProjectSettings.globalize_path(walk_p))
DirAccess.remove_absolute(ProjectSettings.globalize_path(run_p))
_move_speed_done = true
func _run() -> void:
_check_motion_resolution()
_check_move_speed()
_ck(_move_speed_done, "移动速度断言全部执行")
# 脚本运行时错误只会中止函数、不计失败;用哨兵保证整段断言都执行过。
_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 ManNPC)——若有目录就该建成
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])