- 连击类型由 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
128 lines
4.6 KiB
GDScript
128 lines
4.6 KiB
GDScript
# HitReaction —— CLIENT-GAP §3.7 受击方视图(PlayerView / MobView)共用的受击状态机。
|
||
# __HitGood / __HitGreate / __HitStone(ActorInstanceBattle.cpp)的分支判定、
|
||
# __Shake + ShakeProcess(世界矩阵平移 ±rand()%10 cm)、CGraphicThingInstance::InsertDelay、
|
||
# InterceptOnceMotion + PushOnceMotion + PushLoopMotion 的动作链。
|
||
# 视图只提供「按状态名绑定一段一次性动作」的 Callable(bind(step) -> bool,资源缺失 = false,
|
||
# 等同 GetMotionKey 失败时 InterceptOnceMotion / PushOnceMotion 返回 FALSE)。
|
||
extends RefCounted
|
||
|
||
const SHAKE_TIME := 0.1 # __Shake(100):m_dwShakeTime = now + 100 ms
|
||
const SHAKE_RAND_CM := 10 # ShakeProcess:rand() % 10
|
||
const LOOP_STATES := ["wait", "walk", "run"]
|
||
const DAMAGE_STATES := ["damage", "damage_back"] # IsDamage 类
|
||
const KNOCKDOWN_STATES := ["knockdown", "knockdown_back"] # IsKnockDown:DAMAGE_FLYING(_BACK)
|
||
const STANDUP_STATES := ["standup", "standup_back"] # __IsStandUpMotion:STAND_UP(_BACK)
|
||
# CActorInstance::isLock:NORMAL_ATTACK / COMBO_ATTACK_* / SPECIAL_* / 钓鱼 / 表情跳舞等
|
||
const LOCK_STATES := ["attack", "combo", "skill", "__motion", "emotion", "dig",
|
||
"fishing", "fishing_react", "fishing_catch", "fishing_fail"]
|
||
const SKILL_STATES := ["skill"] # IsUsingSkill:SKILL 段 / SPECIAL_1..6
|
||
|
||
var delay_left := 0.0
|
||
var speed := 1.0 # 当前动作的 fSpeedRatio(InsertDelay 结束后恢复)
|
||
var shake_left := 0.0
|
||
var queue: Array = [] # 剩余的 PushOnceMotion 步
|
||
var resume := "wait" # 链尾 PushLoopMotion 的循环动作
|
||
var active := false # 受击动作链正在播
|
||
var real_dead := false # m_isRealDead:晕眩击倒后不起身
|
||
var _shaking := false
|
||
var _shake_base := Vector3.ZERO
|
||
|
||
static func is_reaction(state: String) -> bool:
|
||
return state in DAMAGE_STATES or state in KNOCKDOWN_STATES or state in STANDUP_STATES
|
||
|
||
# __HitGood:返回候选动作链(按顺序尝试,首步绑定成功的那条生效)。
|
||
func good(state: String, scalar: float, stunned: bool) -> Array:
|
||
if state in KNOCKDOWN_STATES:
|
||
return []
|
||
if stunned:
|
||
return [] # IsStun() -> Die():死亡动作由服务器驱动(seam)
|
||
shake()
|
||
if state in LOCK_STATES:
|
||
return []
|
||
if scalar < 0.0:
|
||
return [["damage"]]
|
||
return [["damage_back"], ["damage"]]
|
||
|
||
# __HitGreate
|
||
func greate(state: String, scalar: float, stunned: bool) -> Array:
|
||
if state in KNOCKDOWN_STATES or state in STANDUP_STATES:
|
||
return []
|
||
shake()
|
||
if state in SKILL_STATES:
|
||
return []
|
||
if stunned:
|
||
if scalar < 0.0:
|
||
return [["knockdown"]]
|
||
return [["knockdown_back"], ["knockdown"]]
|
||
if scalar < 0.0:
|
||
return [["knockdown", "standup"]]
|
||
return [["knockdown_back", "standup_back"], ["knockdown", "standup"]]
|
||
|
||
# __HitStone
|
||
func stone(stunned: bool) -> void:
|
||
if not stunned:
|
||
shake()
|
||
|
||
func shake() -> void:
|
||
shake_left = SHAKE_TIME
|
||
|
||
func insert_delay(d: float, anim: Object) -> void:
|
||
if d <= 0.0:
|
||
return
|
||
delay_left = d
|
||
if anim:
|
||
anim.set("time_scale", 0.0)
|
||
|
||
# 绑定新动作时的 time_scale:InsertDelay 期间保持冻结。
|
||
func scale_for(ratio: float) -> float:
|
||
speed = ratio
|
||
return 0.0 if delay_left > 0.0 else ratio
|
||
|
||
# 首步绑定成功 -> 记下链尾要回的循环动作,剩余步入队。
|
||
func start(chains: Array, current: String, bind: Callable, dead_after := false) -> bool:
|
||
for chain: Array in chains:
|
||
if chain.is_empty() or not bool(bind.call(String(chain[0]))):
|
||
continue
|
||
if not is_reaction(current):
|
||
resume = current if current in LOOP_STATES else "wait"
|
||
queue = chain.slice(1)
|
||
active = true
|
||
real_dead = dead_after
|
||
return true
|
||
return false
|
||
|
||
# 一段受击动作播完:绑下一步(缺资源的步跳过);返回 false = 链结束。
|
||
func advance(bind: Callable) -> bool:
|
||
while not queue.is_empty():
|
||
if bool(bind.call(String(queue.pop_front()))):
|
||
return true
|
||
active = false
|
||
return false
|
||
|
||
func cancel() -> void:
|
||
active = false
|
||
queue.clear()
|
||
real_dead = false
|
||
|
||
func tick(dt: float, anim: Object, model: Node3D) -> void:
|
||
if delay_left > 0.0:
|
||
delay_left -= dt
|
||
if delay_left <= 0.0 and anim:
|
||
anim.set("time_scale", speed)
|
||
if shake_left > 0.0:
|
||
shake_left -= dt
|
||
if model == null:
|
||
return
|
||
if shake_left > 0.0:
|
||
if not _shaking:
|
||
_shaking = true
|
||
_shake_base = model.position
|
||
model.position = _shake_base + Vector3(_rand_cm(), _rand_cm(), _rand_cm()) / 100.0
|
||
elif _shaking:
|
||
_shaking = false
|
||
model.position = _shake_base
|
||
|
||
static func _rand_cm() -> float:
|
||
var v := float(randi() % SHAKE_RAND_CM)
|
||
return v if randi() % 2 == 0 else -v
|