Files
mtgodot-poc/project/ui/mob_view.gd
T

219 lines
7.0 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.
# MobView (真机字节校准收尾) —— 怪 / NPC 的真模型:Metin2Model + Metin2AnimPlayer。
#
# var mv := preload("res://ui/mob_view.gd").new()
# if mv.build(assets_root, proto, race): # race = mob_proto vnumGC_CHARACTER_ADD.race
# ...
# mv.set_anim_state("run") # wait/run/attack/damage/dead/skill/emotion
#
# 目录:<assets>/*/ymir work/{monster,npc}/<mob_proto.name>/
# <name>.gr2 = 基础网格,<name>.dds = 贴图,motlist.txt = 动作索引表:
# GENERAL WAIT 00.msa 65 (组 动作名 文件 权重)
extends Node3D
# net_world 的状态名 -> motlist.txt 的动作名(按优先级取第一个存在的)
const STATE_MOTIONS := {
"wait": ["WAIT", "WAIT1"],
"walk": ["WALK", "RUN"],
"run": ["RUN", "WALK"],
"attack": ["NORMAL_ATTACK", "NORMAL_ATTACK1", "SPECIAL_1"],
"combo": ["NORMAL_ATTACK1", "NORMAL_ATTACK"],
"skill": ["SPECIAL_1", "SKILL", "NORMAL_ATTACK"],
"damage": ["FRONT_DAMAGE", "DAMAGE", "BACK_DAMAGE"],
"dead": ["FRONT_DEAD", "DEAD", "BACK_DEAD"],
"emotion": ["WAIT"],
}
var model: Node # Metin2Model
var anim: Node # Metin2AnimPlayer
var _dir := ""
var _motions := {} # MOTION_NAME(String) -> 绝对 .msa 路径
var _state := ""
# root/npclist.txt: vnum -> 模型代号(bear_brown / tiger_big / stray_dog…)。
# 翻译版 mob_proto 把 szName 也本地化了("Brown Bear"),解不出目录,必须靠这张表。
static var _npclist: Dictionary = {}
static var _npclist_loaded := false
static func _load_npclist(assets_root: String) -> void:
if _npclist_loaded:
return
_npclist_loaded = true
for rel in ["root/npclist.txt", "npclist.txt", "locale/npclist.txt"]:
var p := assets_root.path_join(rel)
if not FileAccess.file_exists(p):
continue
var f := FileAccess.open(p, FileAccess.READ)
while f and not f.eof_reached():
var line := f.get_line().strip_edges()
if line == "":
continue
var parts := line.split("\t", false)
if parts.size() < 2:
parts = line.split(" ", false)
if parts.size() < 2:
continue
var vnum := int(parts[0])
var code := String(parts[1]).strip_edges()
if vnum > 0 and code != "":
_npclist[vnum] = code
break
func build(assets_root: String, proto: Node, race: int, pump := Callable()) -> bool:
if not ClassDB.class_exists("Metin2Model") or race < 1:
return false
_load_npclist(assets_root)
# 候选代号:npclist 优先,proto 名兜底
var stems: Array[String] = []
var code: String = _npclist.get(race, "")
if code != "":
stems.append(code)
for c: String in _folder_candidates(proto, race):
if not stems.has(c):
stems.append(c)
# 逐个代号:目录名从完整代号逐段回退(bear_brown -> bear),网格名保留完整代号
var mesh_stem := ""
for stem in stems:
var segs := stem.split("_")
for cut in range(segs.size(), 0, -1):
var folder := "_".join(segs.slice(0, cut))
var d := _find_dir(assets_root, folder)
if d != "":
_dir = d
mesh_stem = stem
break
if _dir != "":
break
if _dir == "":
return false
var gr2 := _dir.path_join(mesh_stem + ".gr2")
if not FileAccess.file_exists(gr2):
gr2 = _dir.path_join(_dir.get_file() + ".gr2") # 目录同名主网格
if not FileAccess.file_exists(gr2):
gr2 = _pick_base_gr2() # 目录里第一个非动作 .gr2
if gr2 == "":
return false
if pump.is_valid(): pump.call()
model = ClassDB.instantiate("Metin2Model")
model.name = "Metin2Model"
model.set("texture_dir", _dir)
model.set("gr2_path", gr2) # 重:解 gr2 + 建网格
if pump.is_valid(): pump.call()
add_child(model)
_load_motlist()
if ClassDB.class_exists("Metin2AnimPlayer"):
anim = ClassDB.instantiate("Metin2AnimPlayer")
anim.set("model_path", NodePath("../Metin2Model"))
anim.set("blend_time", 0.15)
add_child(anim)
set_anim_state("wait")
CharShadow.attach(self, 1.6) # 怪脚印大一点
return true
func set_anim_state(s: String) -> void:
if s == _state or anim == null:
return
_state = s
var msa := _motion_for(s)
if msa == "":
return
anim.set("loop", s in ["wait", "walk", "run"])
anim.set("anim_path", msa)
# 供 net_world._on_info 用:真模型没有统一的名字节点时补一个。
func set_display_name(n: String) -> void:
var tag := get_node_or_null("Label3D") as Label3D
if tag == null:
tag = Label3D.new()
tag.name = "Label3D"
tag.position.y = 2.1
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
tag.no_depth_test = true
tag.pixel_size = 0.006
add_child(tag)
tag.text = n
# --- 内部 -------------------------------------------------------------------
func _folder_candidates(proto: Node, race: int) -> Array:
if proto == null or not proto.has_method("mob"):
return []
var m: Dictionary = proto.mob(race)
if m.is_empty():
return []
var out: Array = []
var raws: Array[String] = [String(m.get("name", "")), String(m.get("locale_name", ""))]
for raw in raws:
var s: String = raw.strip_edges().to_lower()
if s == "":
continue
for c in [s.replace(" ", "_"), s.replace(" ", ""), s.replace(" ", "-")]:
if c != "" and not out.has(c):
out.append(c)
return out
func _motion_for(state: String) -> String:
for mo: String in STATE_MOTIONS.get(state, []):
if _motions.has(mo):
return _motions[mo]
# 兜底:目录里有 <mo>.msa 直接用
for mo: String in STATE_MOTIONS.get(state, []):
var p := _dir.path_join(mo.to_lower() + ".msa")
if FileAccess.file_exists(p):
return p
return _motions.get("WAIT", "")
func _load_motlist() -> void:
var ml := _dir.path_join("motlist.txt")
if not FileAccess.file_exists(ml):
return
var f := FileAccess.open(ml, FileAccess.READ)
while f and not f.eof_reached():
var parts := f.get_line().strip_edges().split(" ", false)
if parts.size() >= 3:
var motion := String(parts[1])
var file := String(parts[2])
var abs := _dir.path_join(file)
if FileAccess.file_exists(abs):
_motions[motion] = abs
func _pick_base_gr2() -> String:
var da := DirAccess.open(_dir)
if da == null:
return ""
for fn in da.get_files():
if not fn.ends_with(".gr2"):
continue
if fn.contains("_lod_"):
continue
# 动作文件通常是纯数字命名(00.gr2 / 20_1.gr2
var stem := fn.get_basename()
if stem.is_valid_int() or (stem.contains("_") and stem.split("_")[0].is_valid_int()):
continue
return _dir.path_join(fn)
return ""
func _find_dir(assets_root: String, folder: String) -> String:
# <assets>/*/ymir work/{monster,npc}/<folder>
var rels := [
"ymir work/monster/%s" % folder,
"ymir work/monster2/%s" % folder,
"ymir work/npc/%s" % folder,
"ymir work/npc2/%s" % folder,
"Monster/ymir work/monster/%s" % folder,
"Monster/ymir work/monster2/%s" % folder,
"NPC/ymir work/npc/%s" % folder,
"NPC/ymir work/npc2/%s" % folder,
]
for rel: String in rels:
var d := assets_root.path_join(rel)
if DirAccess.dir_exists_absolute(d):
return d
var da := DirAccess.open(assets_root)
if da:
for sub in da.get_directories():
for rel: String in rels:
var c := assets_root.path_join(sub).path_join(rel)
if DirAccess.dir_exists_absolute(c):
return c
return ""