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
+160
View File
@@ -0,0 +1,160 @@
# HitCollision —— CLIENT-GAP §3.5 修改 2`CActorInstance::__NormalAttackProcess` 用到的碰撞几何,
# 逐行移植(单位 = Metin2 cm,Z 为高度):
# IntersectLineSegments / FindNearestPointOnLineSegment / FindNearestPointOfParallelLineSegments /
# AdjustNearestPointsEterLib/lineintersect_utils.cppMY_EPSILON 0.1
# DetectCollisionDynamicZCylinderVSDynamicZCylinderGameLib/GameUtil.cpp
# .msm AttachingData CollisionType 3 = m_DefendingPointInstanceList 的防御球
extends RefCounted
const MY_EPSILON := 0.1
const COLLISION_TYPE_DEFENDING := 3 # NRaceData::COLLISION_TYPE_DEFENDING
const HUGE_RACE_VNUM := 2493 # IS_HUGE_RACEInstanceBase.cpp
static var _msm_cache := {}
# FindNearestPointOnLineSegment|L|² < ε² 时 Nearest = A1parameter 保持调用方原值。
# 返回 [Nearest, parameter]。
static func _nearest_on_segment(a1: Vector3, l: Vector3, b: Vector3, parameter: float) -> Array:
var d := l.length_squared()
if d < MY_EPSILON * MY_EPSILON:
return [a1, parameter]
var p := clampf((b - a1).dot(l) / d, 0.0, 1.0)
return [a1 + p * l, p]
static func _out_of_range(a: float) -> bool:
return a < 0.0 or a > 1.0
# FindNearestPointOfParallelLineSegmentss[] 已被 FindNearestPointOnLineSegment 夹到 [0,1]
# 所以前两个分支在出货代码里实际走不到;照抄保留)。
static func _parallel(a1: Vector3, a2: Vector3, la: Vector3, b1: Vector3, b2: Vector3, lb: Vector3) -> Array:
var r0 := _nearest_on_segment(a1, la, b1, 0.0)
var out_a: Vector3 = r0[0]
var s0: float = r0[1]
var s1: float = _nearest_on_segment(a1, la, b2, 0.0)[1]
var out_b := Vector3.ZERO
if s0 < 0.0 and s1 < 0.0:
out_a = a1
out_b = b2 if s0 < s1 else b1
elif s0 > 1.0 and s1 > 1.0:
out_a = a2
out_b = b1 if s0 < s1 else b2
else:
var temp := 0.5 * (clampf(s0, 0.0, 1.0) + clampf(s1, 0.0, 1.0))
out_a = a1 + temp * la
out_b = _nearest_on_segment(b1, lb, out_a, temp)[0]
return [out_a, out_b]
static func _adjust(a1: Vector3, la: Vector3, b1: Vector3, lb: Vector3, s: float, t: float) -> Array:
var out_a := a1 + s * la
var out_b := b1 + t * lb
var r: Array
if _out_of_range(s) and _out_of_range(t):
s = clampf(s, 0.0, 1.0)
out_a = a1 + s * la
r = _nearest_on_segment(b1, lb, out_a, t)
out_b = r[0]
t = r[1]
if _out_of_range(t):
t = clampf(t, 0.0, 1.0)
out_b = b1 + t * lb
r = _nearest_on_segment(a1, la, out_b, s)
out_a = r[0]
s = r[1]
r = _nearest_on_segment(b1, lb, out_a, t)
out_b = r[0]
elif _out_of_range(s):
s = clampf(s, 0.0, 1.0)
out_a = a1 + s * la
out_b = _nearest_on_segment(b1, lb, out_a, t)[0]
elif _out_of_range(t):
t = clampf(t, 0.0, 1.0)
out_b = b1 + t * lb
out_a = _nearest_on_segment(a1, la, out_b, s)[0]
return [out_a, out_b]
# 返回 [OutA, OutB]:两段上的最近点对。
static func intersect_line_segments(a1: Vector3, a2: Vector3, b1: Vector3, b2: Vector3) -> Array:
var la := a2 - a1
var lb := b2 - b1
var l11 := la.length_squared()
var l22 := lb.length_squared()
var eps2 := MY_EPSILON * MY_EPSILON
if l11 < eps2:
return [a1, _nearest_on_segment(b1, lb, a1, 0.0)[0]]
elif l22 < eps2:
return [_nearest_on_segment(a1, la, b1, 0.0)[0], b1]
var ab := b1 - a1
var l12 := -la.dot(lb)
var det_l := l11 * l22 - l12 * l12
if absf(det_l) < MY_EPSILON:
return _parallel(a1, a2, la, b1, b2, lb)
var ra := la.dot(ab)
# 出货代码是 +dot(Lb, AB)(旁注写的是负号)——两段都在动时 t 会反号,1:1 保留。
var rb := lb.dot(ab)
var t := (l11 * rb - ra * l12) / det_l
var s := (ra - l12 * t) / l11
if _out_of_range(s) or _out_of_range(t):
return _adjust(a1, la, b1, lb, s, t)
return [a1 + s * la, b1 + t * lb]
# DetectCollisionDynamicZCylinderVSDynamicZCylinder:两个扫掠球都压到 z=0(高度不参与),
# 各自 AABB(按自身半径外扩)不相交直接 false,否则比较两段最近点距离与 r1+r2。
static func detect_z_cylinder(c1_last: Vector3, c1_pos: Vector3, r1: float,
c2_last: Vector3, c2_pos: Vector3, r2: float) -> bool:
c1_last.z = 0.0
c1_pos.z = 0.0
c2_last.z = 0.0
c2_pos.z = 0.0
var r := r1 + r2
var mi1 := Vector3(minf(c1_last.x, c1_pos.x), minf(c1_last.y, c1_pos.y), minf(c1_last.z, c1_pos.z)) - Vector3.ONE * r1
var mi2 := Vector3(maxf(c1_last.x, c1_pos.x), maxf(c1_last.y, c1_pos.y), maxf(c1_last.z, c1_pos.z)) + Vector3.ONE * r1
var mi3 := Vector3(minf(c2_last.x, c2_pos.x), minf(c2_last.y, c2_pos.y), minf(c2_last.z, c2_pos.z)) - Vector3.ONE * r2
var mi4 := Vector3(maxf(c2_last.x, c2_pos.x), maxf(c2_last.y, c2_pos.y), maxf(c2_last.z, c2_pos.z)) + Vector3.ONE * r2
if mi4.x < mi1.x or mi2.x < mi3.x:
return false
if mi4.y < mi1.y or mi2.y < mi3.y:
return false
if mi4.z < mi1.z or mi2.z < mi3.z:
return false
var o := intersect_line_segments(c1_last, c1_pos, c2_last, c2_pos)
return (Vector3(o[0]) - Vector3(o[1])).length_squared() <= r * r
# IS_HUGE_RACEInstanceBase.cpp):出货表只有 2493。
static func is_huge_race(race: int) -> bool:
return race == HUGE_RACE_VNUM
# 解析 .msm 的防御球:[{radius, pos: Vector3(cm, 模型本地), bone}]。bone 非空 = isAttaching
# 挂骨骼(本端按模型本地坐标近似,seam)。缺文件 -> []。按路径缓存。
static func parse_msm_defending(path: String) -> Array:
if _msm_cache.has(path):
return _msm_cache[path]
var out := []
if FileAccess.file_exists(path):
var f := FileAccess.open(path, FileAccess.READ)
var ctype := -1
var bone := ""
var attaching := false
var radius := 0.0
while f and not f.eof_reached():
var parts := f.get_line().strip_edges().replace("\t", " ").split(" ", false)
if parts.is_empty():
continue
var key := String(parts[0]).to_lower()
if key == "group" and parts.size() > 1 and String(parts[1]).to_lower().begins_with("attachingdata"):
ctype = -1
bone = ""
attaching = false
elif key == "collisiontype" and parts.size() > 1:
ctype = int(parts[1])
elif key == "isattaching" and parts.size() > 1:
attaching = int(parts[1]) != 0
elif key == "attachingbonename" and parts.size() > 1:
bone = String(parts[1]).trim_prefix("\"").trim_suffix("\"")
elif key == "radius" and parts.size() > 1:
radius = float(parts[1])
elif key == "position" and parts.size() > 3 and ctype == COLLISION_TYPE_DEFENDING:
out.append({"radius": radius,
"pos": Vector3(float(parts[1]), float(parts[2]), float(parts[3])),
"bone": bone if attaching else ""})
_msm_cache[path] = out
return out