# 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) -> model.weapon_gr2 = item_list.model(vnum) 解析后的真实路径 # WEAR_SHIELD(10)-> model.shield_gr2(左手刚体挂点,同 weapon) # 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「自动」生效,也是渲染别的玩家用的同一条路。 extends Node const RaceSpec = preload("res://ui/race_spec.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 # 兼容旧引用 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 := -1 var _last_shield_vnum := -1 var _last_hair_part := -1 var main_getter: Callable = Callable() # func() -> int:主角 vid(取 parts 用) # armor vnum -> race .msm 的 shape index。默认:有 proto 用 item_proto values[3] # (对齐客户端 __ArmorVnumToShape,SHAPE_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 := {} 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()) 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: return var eq: Array = client.get_equipment() if eq.size() <= WEAR_WEAPON: return var parts := _parts() # 主角 parts[](时装 / 别的玩家用);空 -> 全回退 eq # 武器:parts[WEAPON] 优先,为 0 回退 WEAR_WEAPON var wpn_vnum := _eff(parts, PART_WEAPON, eq, WEAR_WEAPON) if wpn_vnum != _last_weapon_vnum: _last_weapon_vnum = wpn_vnum model.set("weapon_gr2", _resolve_weapon(wpn_vnum)) if eq.size() > WEAR_SHIELD: var sh_vnum := int(eq[WEAR_SHIELD].get("vnum", 0)) if sh_vnum != _last_shield_vnum: _last_shield_vnum = sh_vnum model.set("shield_gr2", _resolve_weapon(sh_vnum)) # 身体:parts[ARMOR] 优先(= 普通盔甲或时装),为 0 回退 WEAR_BODY var body_vnum := _eff(parts, PART_ARMOR, eq, WEAR_BODY) if body_vnum != _last_body_vnum: _last_body_vnum = body_vnum _apply_body(model, body_vnum) # 头:parts[HEAD] 优先,为 0 回退 WEAR_HEAD;有模型的头盔覆盖发型槽,否则用发型 var head_vnum := _eff(parts, PART_HEAD, eq, WEAR_HEAD) if head_vnum != _last_head_vnum: _last_head_vnum = head_vnum _apply_head_or_hair(model, head_vnum) # 有效 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 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 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: 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) # --- 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 v is Array and v.size() > 3 and int(v[3]) != 0: 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: # 强化等级越高越亮(PARITY §2.7:fSpecular = bSpecular / 100) var sp := _armor_specular(vnum) if sp > 0: model.set("specular_power", sp / 100.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):覆盖 surface 0 var src: String = e.get("source_skin", "") var tgt: String = e.get("target_skin", "") if tgt != "" and tgt != src and model.has_method("set_surface_texture"): var t := _resolve_spec_asset(e, tgt) 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] for rel in ["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)解析绝对路径。 # 依次试: 规格文件目录/ · PathName(去盘符)+扫散包 · 按 basename 扫 pc// 与 pc2// 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 # 按 basename 在 pc// 和 pc2// 下找 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], "pc2/ymir work/pc2/%s/%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/..." 或散包相对路径 -> /(/)... 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 ""