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
+125
View File
@@ -0,0 +1,125 @@
# net_world_push_test —— CLIENT-GAP §3.7 受击击退的场景侧:
# __PushCircle + IncreaseExternalForce -> net_world.push_victim
# CActorInstance::PhysicsProcess(每帧 AddMovement(GetX/YMovement)-> 节点偏移
# GetBlendingPositionblending 中 = 当前位置 + LastPosition-> OnHit 的 CG_SYNC_POSITION 坐标
# godot --headless --path project --script net_world_push_test.gd
extends SceneTree
const NetWorld = preload("res://net_world.gd")
const PhysicsPush = preload("res://physics_push.gd")
class FakeClient extends Node:
signal entity_spawned(entity: Dictionary)
signal entity_despawned(vid: int)
signal entity_moved(vid: int)
signal entity_main_set(vid: int)
signal entity_info(vid: int, entity: Dictionary)
signal chat(type: int, vid: int, text: String)
signal vitals_changed(vid: int)
signal entity_dead(vid: int)
signal damage(vid: int, amount: int, flag: int)
var ents := {}
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
func get_entities() -> Array: return ents.values()
func get_pvp_relations() -> Array: return []
func get_duel() -> Dictionary: return {}
func add(vid: int, cm: Vector2, extra := {}) -> Dictionary:
var d := {"vid": vid, "name": "e%d" % vid, "is_main": false,
"func": 0, "moving": false, "angle_deg": 0.0,
"hp": 100, "max_hp": 100, "dead": false, "race": 0,
"affect_flags": 0, "parts": [0, 0, 0, 0]}
d.merge(extra, true)
ents[vid] = d
place(vid, cm)
return d
func place(vid: int, cm: Vector2) -> void:
ents[vid]["pos"] = Vector3(cm.x * 0.01, 0.0, -cm.y * 0.01)
ents[vid]["pos_cm"] = Vector3(cm.x, cm.y, 0.0)
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
var _done := false
func _init() -> void:
await _run()
_ck(_done, "test body ran to completion (a SCRIPT ERROR aborted _run)")
if _fail == 0:
print("PASS: net_world_push_test (push_victim / PhysicsProcess offset / GetBlendingPosition)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _off(v: Vector2) -> Vector3:
return Vector3(v.x / 100.0, 0.0, -v.y / 100.0)
func _run() -> void:
var mount := Node3D.new()
get_root().add_child(mount)
var fc := FakeClient.new()
get_root().add_child(fc)
var nw: Node = NetWorld.new()
get_root().add_child(nw)
nw.setup(fc, mount)
fc.add(1, Vector2.ZERO, {"is_main": true})
fc.entity_main_set.emit(1)
fc.entity_spawned.emit(fc.add(2, Vector2(300, 0)))
await process_frame
nw._process(0.02)
var n: Node3D = nw.node_for(2)
_ck(n != null, "victim node spawned")
if n == null:
return
_ck(not nw.is_pushing(2), "no push -> not pushing")
_ck(nw.blending_position(2).is_equal_approx(n.global_position), "not blending -> GetBlendingPosition = current position")
# 攻击者在西边:victim - attacker = actor-world (+1, 0)
nw.push_victim(2, Vector2(1, 0), 3.0)
var ref := PhysicsPush.new()
ref.set_direction(Vector2(1, 0))
ref.increase_external_force(3.0)
var last := ref.get_last_position()
_ck(last.x > 0.0, "reference push has a positive displacement")
_ck(nw.is_pushing(2), "push_victim -> is_pushing")
_ck(nw.blending_position(2).is_equal_approx(n.global_position + _off(last)),
"blending -> GetBlendingPosition = current + LastPosition, got %s" % nw.blending_position(2))
var acc := Vector2.ZERO
for i in 60:
nw._process(0.02)
ref.update(0.02)
acc += ref.get_movement()
var want: Vector3 = nw._grounded(fc.ents[2]["pos"]) + _off(acc)
_ck(acc.x > 0.0 and n.position.is_equal_approx(want),
"PhysicsProcess: node follows the accumulated ease-out movement, got %s want %s" % [n.position, want])
# blending 中服务器位置变化:偏移叠在新位置上(位移还在进行)
fc.place(2, Vector2(300, 50))
nw._process(0.02)
ref.update(0.02)
acc += ref.get_movement()
want = nw._grounded(fc.ents[2]["pos"]) + _off(acc)
_ck(n.position.is_equal_approx(want), "server move while blending -> offset kept on top, got %s want %s" % [n.position, want])
for i in 100:
nw._process(0.02)
ref.update(0.02)
acc += ref.get_movement()
_ck(not nw.is_pushing(2), "blend over -> not pushing")
want = nw._grounded(fc.ents[2]["pos"]) + _off(acc)
_ck(n.position.is_equal_approx(want), "blend over -> actor stays where the push left it, got %s want %s" % [n.position, want])
# blend 结束后服务器位置再变:以服务器位置为准(丢弃本地击退偏移)
fc.place(2, Vector2(320, 50))
nw._process(0.02)
_ck(n.position.is_equal_approx(nw._grounded(fc.ents[2]["pos"])), "server position after the blend supersedes the push offset")
nw.push_victim(2, Vector2(0, 1), 3.0)
fc.entity_despawned.emit(2)
_ck(not nw.is_pushing(2) and not nw._push.has(2), "despawn drops the push state")
_done = true