Files
mtgodot-poc/project/mob_view_test.gd
T
shenleiandClaude Opus 5 f39a55fdd5 feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档
- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay
  只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续
  运行并记 BLOCKED;写本批次 release-manifest.json 后聚合
- run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理);
  soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数)
- 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试
- forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别
- 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求
- .gitignore 排除本地场景配置、凭据文件与运行输出

离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS,
node 夹具测试 PASS。未联网运行,未重建候选包。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-11 22:06:48 +09:00

114 lines
4.6 KiB
GDScript
Raw 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")
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 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])