# 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「自动」生效,也是渲染别的玩家用的同一条路。 # # §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 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 := -2 # -1 是「发型被遮挡」的有效态,用 -2 当「未应用」哨兵 var _last_shield_vnum := -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] # (对齐客户端 __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()) # 远端 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(对齐 __ArmorVnumToShape:proto 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) if wpn_eff != _last_weapon_vnum: _last_weapon_vnum = wpn_eff model.set("weapon_gr2", _resolve_weapon(wpn_eff)) # --- 盾(左手挂点,同武器遮挡规则)-------------------------------------------- if eq.size() > WEAR_SHIELD: var sh_vnum := int(eq[WEAR_SHIELD].get("vnum", 0)) var sh_eff := 0 if PartHiding.weapon_hidden(raw_shape, is_poly, sh_vnum) else sh_vnum if sh_eff != _last_shield_vnum: _last_shield_vnum = sh_eff model.set("shield_gr2", _resolve_weapon(sh_eff)) # §4.1 修改 4 seam:40250 实体表暂无变身字段(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 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] 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)解析绝对路径。 # 依次试: 规格文件目录/ · 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 ""