Files
mtgodot-poc/project/equip_model_test.gd
shenleiandClaude Opus 5 1933a5ceda fix(combat): 战斗逐项对齐 40250,修复移动 / 技能释放与武器挂点
- 连击类型由 combo_changed 驱动(SetComboSkillFlag,技能 122 等级)
- 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd 逐行移植)
- _register_hit 改为 map::insert 语义,修正命中上限
- 攻速同时缩放 GetAttackingElapsedTime 与攻击动作速率;按武器动作模式目录绑定攻击段
- 去掉本地连击超时,motion_bound 清命中表 / 连击段号
- __ProcessDataAttackSuccess:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、
  命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动(ui/hit_reaction.gd)
- ClassicSession::send_use_skill 不再夹带 CG_FLY_TARGETING
- mac 前进 / 后退方向与技能释放修复;武器按职业骨骼挂到手上
- 新增 hit_collision / physics_push / hit_view / net_world_push / weapon_attach 测试;
  gpu_pose_bounds / race_motion_assembly 适配 damage 随机变体
- 文档:CLIENT-GAP.md、CLIENT-GAP-FIX.md §3.5 / §3.7 与 C.5 增量 130

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 13:15:28 +09:00

258 lines
11 KiB
GDScript
Raw Permalink 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.
# equip_model_test —— P2 装备→模型 headless 自检(假 client + 真 item_list.txt + 桩 model)。
# godot --headless --path project --script equip_model_test.gd
extends SceneTree
const ItemListDB = preload("res://ui/item_list.gd")
const EquipModel = preload("res://ui/equip_model.gd")
class FakeClient extends Node:
signal inventory_changed(window: int, cell: int)
signal entity_main_set(vid: int)
signal entity_info(vid: int, entity: Dictionary)
var equip := []
var main_parts := [0, 0, 0, 5] # parts[3] = hair 5
func _init():
for i in 11: equip.append({"vnum": 0, "count": 0, "wear": i})
func get_equipment() -> Array: return equip
func get_main_vid() -> int: return 1000
var mount_vnum := 0
func get_entity(_v) -> Dictionary: return {"race": 0, "parts": main_parts, "mount_vnum": mount_vnum}
func set_wear(idx, vnum):
equip[idx] = {"vnum": vnum, "count": 1, "wear": idx}
inventory_changed.emit(2, 90 + idx) # window EQUIPMENT
# Node3D 桩:记录 weapon_gr2 / shield_gr2 / gr2_path / hair_gr2 被设成什么
class StubModel extends Node3D:
var weapon_gr2 := ""
var shield_gr2 := ""
var gr2_path := ""
var hair_gr2 := ""
var hair_skin := "-"
func _set(prop, val):
match String(prop):
"weapon_gr2": weapon_gr2 = val; return true
"shield_gr2": shield_gr2 = val; return true
"gr2_path": gr2_path = val; return true
"hair_gr2": hair_gr2 = val; return true
"hair_skin": hair_skin = val; return true
return false
# item_proto 桩:只给武器 subtypeWEAPON_DAGGER=1 BOW=2 FAN=5item_length.h EWeaponSubTypes
class FakeProto extends Node:
const SUB := {19: 0, 2000: 2, 4000: 1, 7000: 5}
func item(vnum: int) -> Dictionary:
if SUB.has(vnum):
return {"type": 1, "sub_type": SUB[vnum]}
return {}
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
await _run()
if _fail == 0:
print("PASS: equip_model_test (item_list + weapon swap)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var assets := AssetRoot.path()
var il_path := assets.path_join("locale/locale/common/item_list.txt")
# item_list.txt 解析
var il := ItemListDB.new()
if not FileAccess.file_exists(il_path):
print(" (skip: no item_list.txt)")
return
_ck(il.load_file(il_path), "item_list loaded")
_ck(il.count > 1000, "item_list count > 1000 (%d)" % il.count)
_ck(il.model(19) == "d:/ymir work/item/weapon/00010.gr2", "item_list.model(19), got '%s'" % il.model(19))
_ck(il.icon(19) == "icon/item/00010.tga", "item_list.icon(19)")
_ck(il.type_of(19) == "WEAPON", "item_list.type_of(19)")
# EquipModel:装备武器 vnum 19 -> model.weapon_gr2 = 解析后的真实路径
var fc := FakeClient.new()
var model := StubModel.new()
get_root().add_child(fc)
get_root().add_child(model)
var em: Node = EquipModel.new()
get_root().add_child(em)
em.setup(fc, il, func() -> Node: return model, assets)
em.main_getter = func() -> int: return 1000
fc.set_wear(4, 19) # WEAR_WEAPON
await process_frame
var wp: String = model.weapon_gr2
print(" weapon_gr2 -> '", wp, "'")
if wp == "":
# 资产里可能没有这把武器的 gr2 文件;至少确认解析逻辑跑到了(basevnum 约定)
_ck(true, "weapon resolved to '' (gr2 not in assets — resolution logic still exercised)")
else:
_ck(wp.ends_with("00010.gr2") and FileAccess.file_exists(wp), "weapon_gr2 points at a real 00010.gr2")
# 拆下武器 -> weapon_gr2 = ""
fc.set_wear(4, 0)
await process_frame
_ck(model.weapon_gr2 == "", "unequip -> weapon_gr2 cleared")
# armor_model_map 注入 -> gr2_path 切换
em.armor_model_map = {11209: "d:/ymir work/pc/warrior/warrior.gr2"}
fc.set_wear(0, 11209)
await process_frame
_ck(model.gr2_path == "d:/ymir work/pc/warrior/warrior.gr2", "armor_model_map -> gr2_path set")
# 左右手:ActorInstanceAttach.cpp AttachWeapon
# __IsRightHandWeapon: DAGGER / (FAN && 骑马) -> 右;BOW -> 否;其余 -> 右
# __IsLeftHandWeapon : DAGGER / (FAN && 骑马) / BOW -> 左(GetSubModelThing = 同一 gr2
# shield_gr2 槽 = PART_WEAPON_LEFT;参考端不渲染 WEAR_SHIELD(盾在 item_list 没模型)。
var fch := FakeClient.new()
var mh := StubModel.new()
var fp := FakeProto.new()
get_root().add_child(fch)
get_root().add_child(mh)
get_root().add_child(fp)
var emh: Node = EquipModel.new()
get_root().add_child(emh)
emh.setup(fch, il, func() -> Node: return mh, assets)
emh.main_getter = func() -> int: return 1000
emh.proto = fp
var sword_p: String = emh._resolve_weapon(19)
var bow_p: String = emh._resolve_weapon(2000)
var dagger_p: String = emh._resolve_weapon(4000)
var fan_p: String = emh._resolve_weapon(7000)
_ck(sword_p != "" and bow_p != "" and dagger_p != "" and fan_p != "", "weapon gr2 resolvable (19/2000/4000/7000)")
fch.set_wear(4, 19)
await process_frame
_ck(mh.weapon_gr2 == sword_p and mh.shield_gr2 == "", "sword -> right hand only (%s | %s)" % [mh.weapon_gr2, mh.shield_gr2])
fch.set_wear(4, 4000)
await process_frame
_ck(mh.weapon_gr2 == dagger_p and mh.shield_gr2 == dagger_p, "dagger -> both hands (%s | %s)" % [mh.weapon_gr2, mh.shield_gr2])
fch.set_wear(4, 2000)
await process_frame
_ck(mh.weapon_gr2 == "" and mh.shield_gr2 == bow_p, "bow -> left hand only (%s | %s)" % [mh.weapon_gr2, mh.shield_gr2])
fch.set_wear(4, 7000)
await process_frame
_ck(mh.weapon_gr2 == fan_p and mh.shield_gr2 == "", "fan on foot -> right hand only (%s | %s)" % [mh.weapon_gr2, mh.shield_gr2])
fch.mount_vnum = 20030
fch.entity_info.emit(1000, {})
await process_frame
_ck(mh.weapon_gr2 == fan_p and mh.shield_gr2 == fan_p, "fan mounted -> both hands (%s | %s)" % [mh.weapon_gr2, mh.shield_gr2])
fch.mount_vnum = 0
fch.set_wear(4, 19)
fch.set_wear(10, 19) # WEAR_SHIELD 不渲染
await process_frame
_ck(mh.shield_gr2 == "", "WEAR_SHIELD is not rendered (%s)" % mh.shield_gr2)
fch.set_wear(4, 0)
await process_frame
_ck(mh.weapon_gr2 == "" and mh.shield_gr2 == "", "unequip -> both hands cleared")
# 头盔(WEAR_HEAD=1)有模型 -> 覆盖 hair_gr2;拆下回角色发型(parts[3]=5
var em2: Node = EquipModel.new()
get_root().add_child(em2)
em2.armor_model_map = {}
em2.setup(fc, il, func() -> Node: return model, assets)
em2.main_getter = func() -> int: return 1000
model.hair_gr2 = ""
# 头盔 vnum 用一个 item_list 里有 model 的(19 有)
fc.set_wear(1, 19)
await process_frame
_ck(model.hair_gr2 == "" or model.hair_gr2.ends_with(".gr2"), "WEAR_HEAD 有模型 -> 覆盖 hair 槽")
# 拆头盔:应尝试从 parts[3] 恢复发型(有 race_spec 才成,无则保持不炸)
fc.set_wear(1, 0)
await process_frame
_ck(true, "拆头盔 -> 回 parts 发型(不炸)")
# armor shape 走 item_proto values[3](对齐 __ArmorVnumToShape
if ClassDB.class_exists("Metin2Proto"):
var pr: Object = ClassDB.instantiate("Metin2Proto")
get_root().add_child(pr)
var ip := assets.path_join("locale/locale/en/item_proto")
if FileAccess.file_exists(ip) and pr.call("load_item_proto", ip):
var em3: Node = EquipModel.new()
get_root().add_child(em3)
em3.setup(fc, il, func() -> Node: return model, assets)
em3.proto = pr
em3.race = 0 # warrior
# 11200 "Monk Plate Armour+0" -> values[3] == 3
_ck(em3._armor_shape_default(11200) == 3, "armor 11200 -> shape 3 (via proto values[3])")
# 11000 "Wolf Armour" -> values[3] == 0 -> 回退 =vnum
_ck(em3._armor_shape_default(11000) == 11000, "armor 11000 -> values[3]=0 回退 =vnum")
_ck(em3._armor_specular(11209) == 100, "armor 11209(+9) -> specular 100")
# FakeProto 的 subtype 与真 item_proto 一致
for v in FakeProto.SUB:
var it: Dictionary = pr.item(v)
_ck(int(it.get("type", -1)) == 1 and int(it.get("sub_type", -1)) == FakeProto.SUB[v],
"item_proto %d -> WEAPON sub_type %d (got %s/%s)" % [v, FakeProto.SUB[v], it.get("type"), it.get("sub_type")])
# 时装 / parts[] 优先:服务器把 costume vnum 写进主角 parts[ARMOR],压过装备槽
var seen := {"body": -1, "hair": ""}
var em4: Node = EquipModel.new()
get_root().add_child(em4)
# 用一个记录用的桩:_apply_body / hair 走真逻辑但我们看最终 vnum
em4.setup(fc, il, func() -> Node: return model, assets)
em4.main_getter = func() -> int: return 1000
em4.armor_model_map = {77777: "d:/x/costume.gr2", 11111: "d:/x/armor.gr2"}
# 装备槽是普通盔甲,但 parts[ARMOR] 是时装 -> 用时装
fc.equip[0] = {"vnum": 11111, "count": 1, "wear": 0}
fc.main_parts = [77777, 0, 0, 5]
em4._last_body_vnum = -1
em4.refresh()
_ck(model.gr2_path == "d:/x/costume.gr2", "parts[ARMOR] 时装压过装备槽 (%s)" % model.gr2_path)
# parts[ARMOR] 清 0 -> 回退装备槽
fc.main_parts = [0, 0, 0, 5]
em4._last_body_vnum = -1
em4.refresh()
_ck(model.gr2_path == "d:/x/armor.gr2", "parts[ARMOR]=0 -> 回退 WEAR_BODY (%s)" % model.gr2_path)
# _eff 直接测
_ck(em4._eff([9, 0, 0, 0], 0, [{"vnum": 5}], 0) == 9, "_eff: parts 非 0 优先")
_ck(em4._eff([0, 0, 0, 0], 0, [{"vnum": 5}], 0) == 5, "_eff: parts 0 -> 装备槽")
_ck(em4._eff([], 1, [{"vnum": 1}, {"vnum": 2}], 1) == 2, "_eff: 无 parts -> 装备槽")
# §4.1 修改 4:动物 / 怪物时装(shape 100-103)遮挡武器 + 头发
var em5: Node = EquipModel.new()
get_root().add_child(em5)
em5.setup(fc, il, func() -> Node: return model, assets)
em5.main_getter = func() -> int: return 1000
em5.armor_shape_of = func(v: int) -> int: return 101 if v == 88888 else v
fc.equip[4] = {"vnum": 19, "count": 1, "wear": 4} # 有武器
fc.equip[10] = {"vnum": 19, "count": 1, "wear": 10} # 有盾
fc.main_parts = [88888, 0, 0, 5] # parts[ARMOR] -> shape 101
model.weapon_gr2 = "x"; model.shield_gr2 = "x"; model.hair_gr2 = "x"
em5._last_weapon_vnum = -1
em5._last_body_vnum = -1; em5._last_head_vnum = -2
em5.refresh()
_ck(model.weapon_gr2 == "", "动物时装 -> weapon_gr2 清空 (%s)" % model.weapon_gr2)
_ck(model.shield_gr2 == "", "动物时装 -> shield_gr2 清空 (%s)" % model.shield_gr2)
_ck(model.hair_gr2 == "", "动物时装 -> hair_gr2 清空 (%s)" % model.hair_gr2)
# 脱下动物时装 -> 武器恢复解析(资产里 00010.gr2 可能不存在,至少不再是遮挡态)
fc.main_parts = [0, 0, 0, 5]
em5._last_weapon_vnum = -999
em5._last_body_vnum = -1; em5._last_head_vnum = -2
em5.refresh()
_ck(model.weapon_gr2 == "" or model.weapon_gr2.ends_with("00010.gr2"),
"脱时装 -> weapon 重新解析 (%s)" % model.weapon_gr2)
# 婚纱(shape 201)遮挡武器,但头发保留;捧花例外
var em6: Node = EquipModel.new()
get_root().add_child(em6)
em6.setup(fc, il, func() -> Node: return model, assets)
em6.main_getter = func() -> int: return 1000
em6.armor_shape_of = func(v: int) -> int: return 201 if v == 99999 else v
fc.equip[4] = {"vnum": 19, "count": 1, "wear": 4}
fc.main_parts = [99999, 0, 0, 5]
model.weapon_gr2 = "x"; model.hair_gr2 = "x"
em6._last_weapon_vnum = -1; em6._last_body_vnum = -1; em6._last_head_vnum = -2
em6.refresh()
_ck(model.weapon_gr2 == "", "婚纱 + 剑 -> weapon_gr2 清空")
_ck(model.hair_gr2 != "", "婚纱 -> 头发保留 (%s)" % model.hair_gr2)
# 换捧花(vnum 50201-> 武器恢复
fc.main_parts = [99999, 50201, 0, 5]
em6._last_weapon_vnum = -999; em6._last_body_vnum = -1; em6._last_head_vnum = -2
em6.refresh()
_ck(em6._last_weapon_vnum == 50201, "婚纱 + 捧花 -> 武器 vnum 保留 (%d)" % em6._last_weapon_vnum)