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
+57
View File
@@ -27,6 +27,7 @@ const PickShow = preload("res://pick_show.gd")
const NameTailLayout = preload("res://name_tail_layout.gd")
const TextMetrics = preload("res://text_metrics.gd")
const ScreenHpBar = preload("res://screen_hp_bar.gd")
const PhysicsPush = preload("res://physics_push.gd")
signal entity_added(node: Node3D, vid: int)
signal entity_removed(vid: int)
@@ -93,6 +94,8 @@ var follow_lerp := 14.0
var snap_dist := 6.0
var _by_vid := {} # vid:int -> Node3D
# §3.7 受击击退:vid -> {obj: PhysicsPush(CPhysicsObject), off: Vector2 累计位移 cm, base: 推开时的 e.pos}
var _push := {}
var _main_vid := 0
var _local_vid := 0 # 由 net_play 设:这个 vid 由本地 player 代表,不生成节点
var _local_node: Node3D # 本地玩家的真模型(不挂在 _mount 下)
@@ -836,6 +839,7 @@ func _on_despawn(vid: int) -> void:
_duel_opponents.erase(vid)
_fading.erase(vid)
_chat_tails.erase(vid)
_push.erase(vid)
_remove_hp_bar(vid)
var n: Node3D = _by_vid.get(vid, null)
if n:
@@ -1279,6 +1283,8 @@ func _process(dt: float) -> void:
# SNetworkActorData::UpdatePosition 式线性插值),这里只跟随 e.pos,不再
# 叠第二层 follow_lerp——否则会把 C++ 的到达时机拖慢、和动作事件脱节。
var want := _grounded(e.get("pos", n.position))
if _push.has(vid):
want = _pushed_position(vid, e, dt, want)
n.position = want
# 朝向:Metin2 heading(度) -> Godot yaw。经验换算,真机再校准。
var yaw := MapCoord.heading_to_yaw(float(e.get("angle_deg", 0.0)))
@@ -1318,10 +1324,61 @@ func _apply_anim(n: Node3D, f: int, moving: bool, walk_mode := 1, dead := false)
func _grounded(p: Variant) -> Vector3:
# p 是 M2Client 的网络帧 pos —— 先转到 Metin2World 本地帧,再贴地。
var v: Vector3 = MapCoord.to_world(p) if p is Vector3 else Vector3.ZERO
return _ground_world(v)
func _ground_world(v: Vector3) -> Vector3:
if world and world.has_method("sample_height"):
v.y = float(world.call("sample_height", v.x, v.z))
return v
# §3.7 __PushCircle + IncreaseExternalForceActorInstanceBattle.cpp / PhysicsObject.cpp):
# dir = normalize(受击方 - 攻击方)Metin2 actor 平面帧(cm)。seam:无 IPhysicsWorld 地形碰撞回调。
func push_victim(vid: int, dir: Vector2, force: float) -> void:
if client == null or force <= 0.0 or not _by_vid.has(vid):
return
var e: Dictionary = client.get_entity(vid)
if e.is_empty():
return
var pe: Dictionary = _push.get(vid, {})
if pe.is_empty():
pe = {"obj": PhysicsPush.new(), "off": Vector2.ZERO, "base": e.get("pos")}
_push[vid] = pe
pe.obj.set_direction(dir)
pe.obj.increase_external_force(force)
# CActorInstance::IsPushing -> m_PhysicsObject.isBlending()
func is_pushing(vid: int) -> bool:
return _push.has(vid) and bool(_push[vid].obj.is_blending())
# CActorInstance::GetBlendingPositionblending 中 = 当前位置 + LastPosition(击退终点),否则当前位置。
func blending_position(vid: int) -> Vector3:
var n: Node3D = _by_vid.get(vid, null)
if n == null:
return Vector3.ZERO
if not is_pushing(vid):
return n.global_position
var last: Vector2 = _push[vid].obj.get_last_position()
return n.global_position + Vector3(last.x / 100.0, 0.0, -last.y / 100.0)
# CActorInstance::PhysicsProcess:每帧 m_PhysicsObject.Update + AddMovement(GetX/YMovement)。
# 位移叠在服务器位置上;blend 结束后服务器位置一变,以服务器为准丢弃本地偏移(seam)。
func _pushed_position(vid: int, e: Dictionary, dt: float, want: Vector3) -> Vector3:
var pe: Dictionary = _push[vid]
var obj = pe.obj
var pos: Variant = e.get("pos")
if not (pos is Vector3):
return want
if pos != pe.base:
if not obj.is_blending():
_push.erase(vid)
return want
pe.base = pos
if obj.is_blending():
obj.update(dt)
pe.off += obj.get_movement()
var off: Vector2 = pe.off
return _ground_world(MapCoord.to_world(pos) + Vector3(off.x / 100.0, 0.0, -off.y / 100.0))
func _make_placeholder(d: Dictionary) -> Node3D:
var root := Node3D.new()
var mesh := MeshInstance3D.new()