Files
mtgodot-poc/project/ui/equip_model.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

582 lines
22 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.
# EquipModel (P2) —— 装备变化 → Metin2Model 部件切换。
#
# var em := preload("res://ui/equip_model.gd").new()
# add_child(em)
# em.setup(m2client, item_list, model_getter, assets_root, race)
#
# 监听 M2Client.inventory_changed(window == EQUIPMENT),读 get_equipment()
# WEAR_WEAPON(4) -> item_list.model(vnum) 解析后的真实路径,按左右手分给
# model.weapon_gr2PART_WEAPON/ model.shield_gr2PART_WEAPON_LEFT
# ActorInstanceAttach.cpp AttachWeapon:匕首双手、弓左手、骑马的扇双手)
# WEAR_SHIELD(10)-> 不渲染(参考端没有盾牌挂件,item_list 的盾也没有模型)
# WEAR_HEAD(1) -> 有模型的头盔 = 覆盖发型槽(Metin2 约定:头防替换头发)
# WEAR_BODY(0) -> race_spec.shape(armor_shape_of(vnum)) -> model.gr2_path (+ 换肤)
# armor_model_map[vnum] 若给了则优先
#
# 时装(costume):Metin2 不发独立的 costume 槽包,服务器把 costume 的 vnum 直接写进
# 主角实体的 parts[] 数组(ARMOR/HAIR)。所以每个渲染槽优先取 parts[],为 0 才回退
# get_equipment()。这样 costume「自动」生效,也是渲染别的玩家用的同一条路。
#
# §4.1 修改 4(部位遮挡):解出身体 shape 后过 PartHiding —— 动物 / 怪物时装
# shape 100-103)抑制武器 + 头发;婚纱(shape 201)非捧花时抑制武器;变身
# (seam,暂无实体字段)会把身体 shape 归 0 且清武器 / 头发。刷新顺序对齐
# InstanceBase.cpp ChangeArmor:身体 → 头发 → 武器(→ 盾)。
extends Node
const RaceSpec = preload("res://ui/race_spec.gd")
const PartHiding = preload("res://part_hiding.gd")
const EffectRegistry = preload("res://fx/effect_registry.gd")
const MotionEffectAnchor = preload("res://fx/motion_effect_anchor.gd")
const WEAR_BODY := 0
const WEAR_HEAD := 1
const WEAR_WEAPON := 4
const WEAR_SHIELD := 10
# CHR_EQUIPPART_*parts[] 下标)
const PART_ARMOR := 0
const PART_WEAPON := 1
const PART_HEAD := 2
const PART_HAIR := 3
const CHR_EQUIPPART_HAIR := 3 # 兼容旧引用
# item_length.h EWeaponSubTypesCItemData::GetWeaponType = bSubType
const WEAPON_SUB_DAGGER := 1
const WEAPON_SUB_BOW := 2
const WEAPON_SUB_TWO_HANDED := 3
const WEAPON_SUB_BELL := 4
const WEAPON_SUB_FAN := 5
const WEAPON_SUB_ARROW := 6
const ITEM_TYPE_WEAPON := 1
const ITEM_TYPE_ARMOR := 2
const ARMOR_BODY := 0
const HAND_RIGHT := 1
const HAND_LEFT := 2
const CLASS_OF := ["warrior", "assassin", "sura", "shaman"]
var client: Node
var item_list: RefCounted # ItemList
var proto: Node # Metin2Proto(有的话 armor shape 走 item_proto values[3]
var assets_root := ""
var race := -1
var _model_getter: Callable
var _spec: RefCounted # RaceSpec(惰性)
var _spec_tried := false
var _last_weapon_vnum := -1
var _last_body_vnum := -1
var _last_head_vnum := -2 # -1 是「发型被遮挡」的有效态,用 -2 当「未应用」哨兵
var _last_weapon_hands := -1
var _last_hair_part := -1
var main_getter: Callable = Callable() # func() -> int:主角 vid(取 parts 用)
var _remote_mode := false
var _remote_parts: Array = [] # 远端 PC 的 awPart[ARMOR..HAIR]
# armor vnum -> race .msm 的 shape index。默认:有 proto 用 item_proto values[3]
# (对齐客户端 __ArmorVnumToShapeSHAPE_VALUE_SLOT_INDEX=3),否则回退 =vnum。应用方可覆写。
var armor_shape_of: Callable = func(vnum: int) -> int: return _armor_shape_default(vnum)
# 应用方可注入:vnum -> 身体 gr2 路径(优先于 race_spec
var armor_model_map := {}
var effect_registry: RefCounted
var _weapon_refine_fx: Array[Node] = []
var _armor_refine_fx: Array[Node] = []
func setup(m2client: Node, il: RefCounted, model_getter: Callable, assets := "",
race_index := -1) -> void:
client = m2client
item_list = il
_model_getter = model_getter
assets_root = assets
race = race_index
if client and client.has_signal("inventory_changed"):
client.inventory_changed.connect(_on_inv_changed)
if client and client.has_signal("entity_main_set"):
client.entity_main_set.connect(func(v):
if race < 0 and client.has_method("get_entity"):
race = int(client.get_entity(v).get("race", -1))
refresh())
# GC_CHARACTER_UPDATE / GC_CHAR_ADD_INFO 改了主角 parts(换装 / 时装)-> 全刷
if client and client.has_signal("entity_info"):
client.entity_info.connect(func(v, _d):
if main_getter.is_valid() and int(main_getter.call()) == v:
refresh())
# 上 / 下马改变扇子的左右手(__IsLeftHandWeapon: FAN && IsMountingHorse
if client and client.has_signal("mount_changed"):
client.mount_changed.connect(func(v):
if main_getter.is_valid() and int(main_getter.call()) == v:
refresh())
# 远端 PC 不读取本地 get_equipment();参考端直接消费
# GC_CHAR_ADDITIONAL_INFO / GC_CHARACTER_UPDATE 的 awPart[4]。
func setup_remote(il: RefCounted, model_getter: Callable, assets := "",
race_index := -1, parts := [], proto_node: Node = null) -> void:
client = null
item_list = il
_model_getter = model_getter
assets_root = assets
race = race_index
proto = proto_node
_remote_mode = true
_remote_parts = parts.duplicate() if parts is Array else []
while _remote_parts.size() < 4:
_remote_parts.append(0)
if _remote_parts.size() > 4:
_remote_parts = _remote_parts.slice(0, 4)
refresh()
func set_race(r: int) -> void:
race = r
_spec = null
_spec_tried = false
func _on_inv_changed(window: int, _cell: int) -> void:
if window == 2: # mtnet::WINDOW_EQUIPMENT
refresh()
func refresh() -> void:
var model: Node = _model_getter.call() if _model_getter.is_valid() else null
if model == null or (client == null and not _remote_mode):
return
var eq: Array = client.get_equipment() if client != null and client.has_method("get_equipment") else []
if not _remote_mode and eq.size() <= WEAR_WEAPON:
return
var parts := _parts() # 主角 parts[](时装 / 别的玩家用);空 -> 全回退 eq
# --- 身体:parts[ARMOR] 优先(普通盔甲或时装),为 0 回退 WEAR_BODY -------------
var body_vnum := _eff(parts, PART_ARMOR, eq, WEAR_BODY)
# 遮挡判定用的 shape(对齐 __ArmorVnumToShapeproto values[3] 非 0,否则 =vnum
var raw_shape := int(armor_shape_of.call(body_vnum)) if armor_shape_of.is_valid() else body_vnum
var is_poly := _is_poly()
if body_vnum != _last_body_vnum:
_last_body_vnum = body_vnum
_apply_body(model, body_vnum)
# --- 头 / 发:动物·怪物时装(shape 100-103)或变身 -> 隐藏;否则头盔覆盖 / 发型 ----
var hair_off := PartHiding.hair_hidden(raw_shape, is_poly)
var head_vnum := _eff(parts, PART_HEAD, eq, WEAR_HEAD)
var head_key := -1 if hair_off else head_vnum
if head_key != _last_head_vnum:
_last_head_vnum = head_key
if hair_off:
model.set("hair_gr2", "")
model.set("hair_skin", "")
_last_hair_part = -1 # 恢复时强制重挂发型
else:
_apply_head_or_hair(model, head_vnum)
# --- 武器:parts[WEAPON] 优先,为 0 回退 WEAR_WEAPON;过 PartHiding 遮挡 ---------
var wpn_vnum := _eff(parts, PART_WEAPON, eq, WEAR_WEAPON)
var wpn_eff := PartHiding.effective_weapon(wpn_vnum, raw_shape, is_poly)
var hands := _weapon_hands(wpn_eff)
var weapon_changed := wpn_eff != _last_weapon_vnum or hands != _last_weapon_hands
if weapon_changed:
_last_weapon_vnum = wpn_eff
_last_weapon_hands = hands
# 左手用 GetSubModelThing()item .msm 加载在参考端被注释掉,恒为同一个 gr2
var wpath := _resolve_weapon(wpn_eff)
model.set("weapon_gr2", wpath if hands & HAND_RIGHT else "")
model.set("shield_gr2", wpath if hands & HAND_LEFT else "")
# SetArmor / SetWeapon both call __GetRefinedEffect.
_refresh_weapon_refine_effect(model, _last_weapon_vnum)
_refresh_armor_refine_effect(model, _last_body_vnum)
# ActorInstanceAttach.cpp __IsRightHandWeapon / __IsLeftHandWeapon -> HAND_* 位掩码
static func refine_effect_level(vnum: int, item: Dictionary) -> int:
if vnum <= 0 or item.is_empty():
return 0
var sockets := int(item.get("gain_socket_pct", 0))
if sockets != 3:
return 0
return maxi(vnum % 10, 0)
static func refine_effect_specs(vnum: int, item: Dictionary) -> Array:
var out: Array = []
if vnum <= 0 or item.is_empty():
return out
var item_type := int(item.get("type", -1))
var subtype := int(item.get("sub_type", -1))
var level := refine_effect_level(vnum, item)
if item_type == ITEM_TYPE_WEAPON:
if level < 7 or level > 9:
return out
var stem := "sword_%d" % level
match subtype:
WEAPON_SUB_DAGGER:
out.append({"slot": "right", "effect": "d:/ymir work/pc/common/effect/sword/%s_s.mse" % stem})
out.append({"slot": "left", "effect": "d:/ymir work/pc/common/effect/sword/%s_s.mse" % stem})
WEAPON_SUB_FAN:
out.append({"slot": "right", "effect": "d:/ymir work/pc/common/effect/sword/%s_f.mse" % stem})
WEAPON_SUB_BOW:
out.append({"slot": "left", "effect": "d:/ymir work/pc/common/effect/sword/%s_b.mse" % stem})
WEAPON_SUB_BELL, WEAPON_SUB_ARROW:
out.append({"slot": "right", "effect": "d:/ymir work/pc/common/effect/sword/%s_s.mse" % stem})
_:
# Sword and two-handed sword share the normal sword effect.
out.append({"slot": "right", "effect": "d:/ymir work/pc/common/effect/sword/%s.mse" % stem})
elif item_type == ITEM_TYPE_ARMOR and subtype == ARMOR_BODY:
# 40250 always layers these two Blue Steel armour effects, in addition
# to the regular +7/+8/+9 body effect.
if vnum >= 12010 and vnum <= 12049:
out.append({"slot": "body", "effect": "d:/ymir work/pc/common/effect/armor/armor-4-2-1.mse"})
out.append({"slot": "body", "effect": "d:/ymir work/pc/common/effect/armor/armor-4-2-2.mse"})
if level >= 7 and level <= 9:
out.append({"slot": "body", "effect": "d:/ymir work/pc/common/effect/armor/armor_%d.mse" % level})
return out
func _item(vnum: int) -> Dictionary:
if vnum > 0 and proto and proto.has_method("item"):
return proto.item(vnum)
return {}
func _ensure_effect_registry() -> RefCounted:
if effect_registry == null and assets_root != "":
effect_registry = EffectRegistry.new()
effect_registry.call("setup", assets_root)
return effect_registry
func _clear_refine_effects(nodes: Array[Node]) -> void:
for node in nodes:
if is_instance_valid(node) and not node.is_queued_for_deletion():
node.queue_free()
nodes.clear()
func _effect_bone(model: Node, slot: String) -> String:
match slot:
"right":
if model and "_right_bone" in model and String(model.get("_right_bone")) != "":
return String(model.get("_right_bone"))
return "equip_right_hand"
"left":
if model and "_left_bone" in model and String(model.get("_left_bone")) != "":
return String(model.get("_left_bone"))
return "equip_left"
"body": return "Bip01"
return ""
func _effect_part(model: Node, slot: String) -> Node3D:
if model == null:
return null
match slot:
"right": return model.find_child("Weapon", true, false) as Node3D
"left": return model.find_child("Shield", true, false) as Node3D
"body": return model as Node3D
return model as Node3D
func _spawn_bone_refine(registry: RefCounted, model: Node, spec: Dictionary) -> Node:
# Weapons with a non-identity Granny grip pre-transform (for
# example 03150): it moves the particles away even though the mesh is held.
if not model is Node3D or not ("anim" in model):
return null
var animator: Variant = model.get("anim")
if not animator is Node or not animator.has_method("get_effect_bone_pose"):
return null
var bone := _effect_bone(model, String(spec.get("slot", "")))
if bone == "":
return null
var effect: Node = registry.call("spawn", String(spec.get("effect", "")),
model as Node3D, false)
if effect == null:
return null
var anchor := MotionEffectAnchor.new()
effect.add_child(anchor)
var event := {"attaching": true, "following": true, "independent": false,
"bone": bone, "pos": Vector3.ZERO}
if anchor.configure(model as Node3D, event):
return effect
effect.free()
return null
func _spawn_refine_specs(model: Node, specs: Array, output: Array[Node]) -> void:
var registry := _ensure_effect_registry()
if registry == null or not registry.has_method("spawn"):
return
for spec in specs:
var parent := _effect_part(model, String(spec.get("slot", "")))
var effect: Node = _spawn_bone_refine(registry, model, spec)
if effect == null and parent != null:
effect = registry.call("spawn", String(spec.get("effect", "")), parent, false)
if effect:
output.append(effect)
func _refresh_weapon_refine_effect(model: Node, vnum: int) -> void:
_clear_refine_effects(_weapon_refine_fx)
_spawn_refine_specs(model, refine_effect_specs(vnum, _item(vnum)), _weapon_refine_fx)
func _refresh_armor_refine_effect(model: Node, vnum: int) -> void:
_clear_refine_effects(_armor_refine_fx)
_spawn_refine_specs(model, refine_effect_specs(vnum, _item(vnum)), _armor_refine_fx)
# A RemotePlayerView is assembled before NetWorld adds it to the SceneTree.
# Re-run only the effect attachment after _ready so skeleton-following anchors
# can resolve global transforms; do not reload body/weapon geometry.
func refresh_refine_effects() -> void:
var model: Node = _model_getter.call() if _model_getter.is_valid() else null
if model == null:
return
_refresh_armor_refine_effect(model, _last_body_vnum)
_refresh_weapon_refine_effect(model, _last_weapon_vnum)
func _weapon_hands(vnum: int) -> int:
if vnum == 0:
return 0
var sub := -1
if proto and proto.has_method("item"):
sub = int(proto.item(vnum).get("sub_type", -1))
if sub == WEAPON_SUB_DAGGER or (sub == WEAPON_SUB_FAN and _is_mounting()):
return HAND_RIGHT | HAND_LEFT
if sub == WEAPON_SUB_BOW:
return HAND_LEFT
return HAND_RIGHT
# IsMountingHorse:主角实体 mount_vnum 非 0。远端 PC 暂无坐骑状态输入(seam)。
func _is_mounting() -> bool:
if _remote_mode or not main_getter.is_valid() or client == null or not client.has_method("get_entity"):
return false
var vid: int = main_getter.call()
return vid != 0 and int(client.get_entity(vid).get("mount_vnum", 0)) != 0
# §4.1 修改 4 seam40250 实体表暂无变身字段(m2_client.cpp entity dict 无 poly_race /
# polymorph)。一旦 GC_CHARACTER_UPDATE / GC_CHAR_ADD_INFO 带上变身 race,这里改成读
# client.get_entity(main_getter.call()).get("poly_race", 0) != 0 即可。
func _is_poly() -> bool:
return false
# 有效 vnum:主角 parts[part_idx] 非 0 就用它(时装 / 远端玩家),否则用装备槽
func _eff(parts: Array, part_idx: int, eq: Array, wear_slot: int) -> int:
if parts.size() > part_idx and int(parts[part_idx]) != 0:
return int(parts[part_idx])
return int(eq[wear_slot].get("vnum", 0)) if eq.size() > wear_slot else 0
func _parts() -> Array:
if _remote_mode:
return _remote_parts
if not main_getter.is_valid() or client == null or not client.has_method("get_entity"):
return []
var vid: int = main_getter.call()
if vid == 0:
return []
var p = client.get_entity(vid).get("parts", [])
return p if p is Array else []
func _apply_head_or_hair(model: Node, head_vnum: int) -> void:
# 头盔有模型 -> 用它替换发型(Metin2:头防走 hair 槽)
if head_vnum != 0:
var hm := _resolve_weapon(head_vnum) # item_list.model 解析,复用武器解析器
if hm != "":
model.set("hair_gr2", hm)
model.set("hair_skin", "")
return
# 没头盔(或头盔无模型)-> 回到角色发型
_apply_hair_from_parts(model, true)
func _apply_hair_from_parts(model: Node, force := false) -> void:
if model == null:
return
var hp := _hair_part()
if hp < 0 or (hp == _last_hair_part and not force):
return
_last_hair_part = hp
# 时装假发 / 大编号 -> parts[HAIR] 是 item vnum,走 item_list.model(同头盔)
if hp >= 100 and item_list:
var wig := _resolve_weapon(hp)
if wig != "":
model.set("hair_gr2", wig)
model.set("hair_skin", "")
return
# 小编号 -> race .msm 的 hair shape index
var e := _spec_hair(hp)
if e.is_empty():
return
var m := _resolve_spec_asset(e, String(e.get("model", "")))
var ts := _resolve_spec_asset(e, String(e.get("target_skin", "")))
if m != "":
model.set("hair_gr2", m)
if ts != "":
model.set("hair_skin", ts)
func _hair_part() -> int:
if _remote_mode:
return int(_remote_parts[PART_HAIR]) if _remote_parts.size() > PART_HAIR else -1
if not main_getter.is_valid() or client == null or not client.has_method("get_entity"):
return -1
var vid: int = main_getter.call()
if vid == 0:
return -1
var e: Dictionary = client.get_entity(vid)
var parts = e.get("parts", [])
if parts is Array and parts.size() > CHR_EQUIPPART_HAIR:
return int(parts[CHR_EQUIPPART_HAIR])
return -1
func set_hair(index: int) -> void:
if _remote_mode:
_set_remote_part(PART_HAIR, index)
refresh()
return
var model: Node = _model_getter.call() if _model_getter.is_valid() else null
if model == null:
return
var e := _spec_hair(index)
if e.is_empty():
return
var m := _resolve_spec_asset(e, String(e.get("model", "")))
var ts := _resolve_spec_asset(e, String(e.get("target_skin", "")))
if m != "":
model.set("hair_gr2", m)
if ts != "":
model.set("hair_skin", ts)
func set_armor(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_ARMOR, index)
refresh()
func set_weapon(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_WEAPON, index)
refresh()
func set_head(index: int) -> void:
if not _remote_mode:
return
_set_remote_part(PART_HEAD, index)
refresh()
func _set_remote_part(part: int, value: int) -> void:
while _remote_parts.size() <= part:
_remote_parts.append(0)
_remote_parts[part] = value
# --- body ------------------------------------------------------------------
# armor vnum -> item_proto values[3](非 0);否则 =vnum(对齐 __ArmorVnumToShape
func _armor_shape_default(vnum: int) -> int:
if vnum > 1 and proto and proto.has_method("item"):
var it: Dictionary = proto.item(vnum)
var v = it.get("values", [])
if not it.is_empty() and v is Array and v.size() > 3:
return int(v[3])
return vnum
func _armor_specular(vnum: int) -> int:
if proto and proto.has_method("item"):
return int(proto.item(vnum).get("specular", 0))
return 0
func _apply_body(model: Node, vnum: int) -> void:
if model == null:
return
if model.has_method("clear_skin_textures"):
model.call("clear_skin_textures")
# 强化等级越高越亮(PARITY §2.7fSpecular = bSpecular / 100,无高光时重置为 0)
var sp := _armor_specular(vnum)
model.set("specular_power", sp / 100.0 if sp > 0 else 0.0)
if armor_model_map.has(vnum):
model.set("gr2_path", String(armor_model_map[vnum]))
return
var shape_idx: int = armor_shape_of.call(vnum) if armor_shape_of.is_valid() else vnum
var e := _spec_shape(shape_idx)
if e.is_empty():
return # 没有对应 shape:保持当前身体
var body := _resolve_spec_asset(e, String(e.get("model", "")))
if body != "":
model.set("gr2_path", body)
# 换肤(同模型不同 TargetSkin):按 40250 SetMaterialImagePointer 材质名映射
var src: String = e.get("source_skin", "")
var tgt: String = e.get("target_skin", "")
if tgt != "" and tgt != src:
var t := _resolve_spec_asset(e, tgt)
var s := _resolve_spec_asset(e, src)
if model.has_method("set_skin_texture"):
if t != "":
model.call("set_skin_texture", s if s != "" else src, t)
if model.has_method("set_surface_texture"):
if t != "":
model.call("set_surface_texture", 0, t)
func _spec_shape(idx: int) -> Dictionary:
_ensure_spec()
return _spec.shape(idx) if _spec else {}
func _spec_hair(idx: int) -> Dictionary:
_ensure_spec()
return _spec.hair(idx) if _spec else {}
func _ensure_spec() -> void:
if _spec_tried:
return
_spec_tried = true
if race < 0 or assets_root == "":
return
var cls: String = CLASS_OF[race & 3]
var suffix := "w" if race in [1, 3, 4, 6] else "m"
for rel in ["root/msm/%s_%s.msm" % [cls, suffix],
"PC/ymir work/pc/%s/%s.msm" % [cls, cls],
"pc2/ymir work/pc2/%s/%s.msm" % [cls, cls],
"season1/season1/pc/%s.msm" % cls]:
var p := _resolve_asset(rel)
if p != "":
var rs := RaceSpec.new()
if rs.load_file(p):
_spec = rs
return
# --- 路径解析 ------------------------------------------------------------
# 用 race_spec 的一条 shape/hair 记录 + 一个相对文件(model / *_skin)解析绝对路径。
# 依次试: 规格文件目录/<rel> · PathName(去盘符)+扫散包 · 按 basename 扫 pc/<cls>/ 与 pc2/<cls>/
func _resolve_spec_asset(e: Dictionary, rel: String) -> String:
if rel == "":
return ""
var sdir := String(e.get("spec_dir", ""))
if sdir != "":
var p := sdir.path_join(rel)
if FileAccess.file_exists(p):
return p
var byp := _resolve_asset(String(e.get("path", "")) + rel)
if byp != "":
return byp
# 规格覆盖层(例如 root/msm/warrior_m.msm)可能只给 Model 名而没有
# PathName。按 basename 回退时除职业根目录外,还必须查 hair/;否则
# HairIndex 1001..1012 能解析却加载失败,并静默保留 PlayerView 的
# hair_1_1 默认模型,造成选角发型遮脸。
var bn := rel.get_file()
if race >= 0:
var cls: String = CLASS_OF[race & 3]
for sub in [
"PC/ymir work/pc/%s/%s" % [cls, bn],
"PC/ymir work/pc/%s/hair/%s" % [cls, bn],
"pc2/ymir work/pc2/%s/%s" % [cls, bn],
"pc2/ymir work/pc2/%s/hair/%s" % [cls, bn],
]:
var q := _resolve_asset(sub)
if q != "":
return q
return ""
func _resolve_weapon(vnum: int) -> String:
if vnum == 0 or item_list == null:
return ""
var vpath: String = item_list.model(vnum)
if vpath == "":
var base := (vnum / 10) * 10
vpath = "d:/ymir work/item/weapon/%05d.gr2" % base
return _resolve_asset(vpath)
# "d:/ymir work/..." 或散包相对路径 -> <assets>/(<pack>/)...
func _resolve_asset(vpath: String) -> String:
if vpath == "" or assets_root == "":
return ""
var rel := vpath.replace("\\", "/")
if rel.length() >= 2 and rel[1] == ":":
rel = rel.substr(2)
rel = rel.lstrip("/")
var direct := assets_root.path_join(rel)
if FileAccess.file_exists(direct):
return direct
var da := DirAccess.open(assets_root)
if da:
for sub in da.get_directories():
var cand := assets_root.path_join(sub).path_join(rel)
if FileAccess.file_exists(cand):
return cand
return ""