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
This commit is contained in:
shenlei
2026-09-12 13:15:28 +09:00
co-authored by Claude Opus 5
parent 96c29d2bb7
commit 1933a5ceda
30 changed files with 2353 additions and 209 deletions
+42 -12
View File
@@ -5,8 +5,10 @@
# 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_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] 若给了则优先
@@ -34,6 +36,12 @@ 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_FAN := 5
const HAND_RIGHT := 1
const HAND_LEFT := 2
const CLASS_OF := ["warrior", "assassin", "sura", "shaman"]
var client: Node
@@ -47,7 +55,7 @@ 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_weapon_hands := -1
var _last_hair_part := -1
var main_getter: Callable = Callable() # func() -> int:主角 vid(取 parts 用)
var _remote_mode := false
@@ -77,6 +85,11 @@ func setup(m2client: Node, il: RefCounted, model_getter: Callable, assets := "",
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]。
@@ -139,17 +152,34 @@ func refresh() -> void:
# --- 武器: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:
var hands := _weapon_hands(wpn_eff)
if wpn_eff != _last_weapon_vnum or hands != _last_weapon_hands:
_last_weapon_vnum = wpn_eff
model.set("weapon_gr2", _resolve_weapon(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 "")
# --- 盾(左手挂点,同武器遮挡规则)--------------------------------------------
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))
# ActorInstanceAttach.cpp __IsRightHandWeapon / __IsLeftHandWeapon -> HAND_* 位掩码
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,这里改成读