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
+60 -11
View File
@@ -15,7 +15,8 @@ class FakeClient extends Node:
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
func get_entity(_v) -> Dictionary: return {"race": 0, "parts": main_parts}
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
@@ -36,6 +37,14 @@ class StubModel extends Node3D:
"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:
@@ -43,7 +52,7 @@ func _ck(c: bool, m: String) -> void:
printerr("FAIL: " + m)
func _init() -> void:
_run()
await _run()
if _fail == 0:
print("PASS: equip_model_test (item_list + weapon swap)")
quit(0)
@@ -97,15 +106,50 @@ func _run() -> void:
await process_frame
_ck(model.gr2_path == "d:/ymir work/pc/warrior/warrior.gr2", "armor_model_map -> gr2_path set")
# 盾(WEAR_SHIELD=10-> shield_gr2 = item_list.model 解析
# 用一个真存在的武器 vnum 当盾(资产里 00010.gr2 存在)
fc.set_wear(10, 19)
# 左右手: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
print(" shield_gr2 -> '", model.shield_gr2, "'")
_ck(model.shield_gr2 == "" or model.shield_gr2.ends_with("00010.gr2"), "WEAR_SHIELD -> shield_gr2")
fc.set_wear(10, 0)
_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(model.shield_gr2 == "", "unequip shield -> shield_gr2 cleared")
_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()
@@ -139,6 +183,11 @@ func _run() -> void:
# 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": ""}
@@ -174,7 +223,7 @@ func _run() -> void:
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_shield_vnum = -1
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)
@@ -182,7 +231,7 @@ func _run() -> void:
_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_shield_vnum = -999
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"),