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:
+299
-38
@@ -19,7 +19,9 @@ class FakeClient extends Node:
|
||||
signal affect_added(affect: Dictionary)
|
||||
signal affect_removed(type: int)
|
||||
signal phase_changed(phase: String)
|
||||
signal combo_changed(enabled: bool)
|
||||
var main := 1000
|
||||
var skills := []
|
||||
var ents := {1000: {"vid": 1000, "hp": 100, "dead": false}, 2000: {"vid": 2000, "hp": 50, "dead": false, "pos": Vector3(1, 0, 0)}}
|
||||
var attacks := []
|
||||
var moves := []
|
||||
@@ -40,10 +42,38 @@ class FakeClient extends Node:
|
||||
func move(a, b, c, d, e) -> bool: moves.append([a, b, c, d, e]); return true
|
||||
func sync_positions(arr) -> bool: synced.append(arr); return true
|
||||
func say(t, s) -> bool: said.append([t, s]); return true
|
||||
func get_skills() -> Array: return skills
|
||||
|
||||
class FakeNetWorld extends Node:
|
||||
var nodes := {}
|
||||
var pushes := [] # [vid, dir(actor-world xy), force]
|
||||
var pushing := {}
|
||||
func node_for(vid: int) -> Node3D: return nodes.get(vid, null)
|
||||
func push_victim(vid: int, dir: Vector2, force: float) -> void:
|
||||
pushes.append([vid, dir, force])
|
||||
pushing[vid] = true
|
||||
func is_pushing(vid: int) -> bool: return bool(pushing.get(vid, false))
|
||||
func blending_position(vid: int) -> Vector3: return nodes[vid].global_position + Vector3(0.1, 0, 0)
|
||||
|
||||
# 受击方视图桩:.msm 防御球 + InsertDelay + __Hit* 反应
|
||||
class FakeVictim extends Node3D:
|
||||
var spheres := [{"radius": 45.0, "pos": Vector3(0, 0, 100), "bone": ""}]
|
||||
var delays := []
|
||||
var reactions := []
|
||||
func get_defending_spheres() -> Array: return spheres
|
||||
func insert_delay(t: float) -> void: delays.append(t)
|
||||
func hit_good(scalar: float, _stunned: bool) -> void: reactions.append(["good", scalar])
|
||||
func hit_greate(scalar: float, _stunned: bool) -> void: reactions.append(["greate", scalar])
|
||||
func hit_stone(_stunned: bool) -> void: reactions.append(["stone", 0.0])
|
||||
|
||||
class FakeFx extends Node:
|
||||
var spawned := [] # [name, global_pos, node]
|
||||
func spawn_at(n: String, parent: Node3D, pos: Vector3, _one_shot := true) -> Node3D:
|
||||
var e := Node3D.new()
|
||||
parent.add_child(e)
|
||||
e.global_position = pos
|
||||
spawned.append([n, pos, e])
|
||||
return e
|
||||
|
||||
class FakeProto extends Node:
|
||||
var items := {}
|
||||
@@ -61,11 +91,18 @@ class FakeAnim extends Node:
|
||||
func get_motion_data() -> Dictionary: return md
|
||||
|
||||
class FakeView extends Node3D:
|
||||
signal motion_bound(state: String)
|
||||
var states := []
|
||||
var attack_motions := [] # [motion_mode, motion_index, speed_ratio]
|
||||
var delays := []
|
||||
var anim := FakeAnim.new()
|
||||
func _init() -> void:
|
||||
add_child(anim)
|
||||
func set_anim_state(s): states.append(s)
|
||||
func play_attack_motion(mode: int, index: int, speed: float) -> void:
|
||||
attack_motions.append([mode, index, speed])
|
||||
motion_bound.emit("attack")
|
||||
func insert_delay(t: float) -> void: delays.append(t)
|
||||
|
||||
class FakeHud extends Node:
|
||||
var affects := []
|
||||
@@ -77,6 +114,19 @@ func _ck(c: bool, m: String) -> void:
|
||||
_fail += 1
|
||||
printerr("FAIL: " + m)
|
||||
|
||||
# 命中小节:每个场景前清掉命中表 / 无敌时间 / 防御球上一帧缓存 / 硬直与各桩记录
|
||||
func _reset_hit(np: Node, fc: Node, nw: Node, fx: Node) -> void:
|
||||
np._hit_dedup.clear()
|
||||
np._victim_invisible_until.clear()
|
||||
np._def_last.clear()
|
||||
np._delay = 0.0
|
||||
np._victim_flush.clear()
|
||||
fc.attacks.clear()
|
||||
fc.synced.clear()
|
||||
nw.pushes.clear()
|
||||
nw.pushing.clear()
|
||||
fx.spawned.clear()
|
||||
|
||||
func _init() -> void:
|
||||
await _run()
|
||||
if _fail == 0:
|
||||
@@ -156,8 +206,49 @@ func _run() -> void:
|
||||
_ck(is_equal_approx(np._atk_speed_factor, 1.5), "attack_speed 150 -> factor 1.5")
|
||||
_ck(is_equal_approx(np._current_attack_period(), np.DEFAULT_ATTACK_PERIOD / 1.5),
|
||||
"attack speed scales the resolved period")
|
||||
# CInstanceBase::SetAttackSpeed:uAtkSpd > 1100 -> 0;否则 /100,无上下限
|
||||
np._apply_attack_speed(400)
|
||||
_ck(is_equal_approx(np._atk_speed_factor, 4.0), "attack_speed 400 -> factor 4.0 (no clamp)")
|
||||
np._apply_attack_speed(1200)
|
||||
_ck(np._atk_speed_factor == 0.0, "attack_speed > 1100 -> factor 0")
|
||||
_ck(is_finite(np._current_attack_period()), "factor 0 -> attack period stays finite")
|
||||
np._apply_attack_speed(100)
|
||||
|
||||
# --- CGraphicThingInstance::InsertDelay / UpdateTime:本地时间 ---
|
||||
np._local_time = 5.0
|
||||
np._delay = 0.05
|
||||
np._advance_local_time(0.02)
|
||||
_ck(is_equal_approx(np._local_time, 5.0) and is_equal_approx(np._delay, 0.03),
|
||||
"delay > elapsed -> local time frozen, delay shrinks")
|
||||
np._advance_local_time(0.05)
|
||||
_ck(is_equal_approx(np._local_time, 5.02) and np._delay == 0.0,
|
||||
"delay < elapsed -> local time advances by the remainder")
|
||||
np._apply_attack_speed(200)
|
||||
np._motion_start_t = np._local_time - 0.1
|
||||
_ck(is_equal_approx(np._attacking_elapsed(), 0.2),
|
||||
"GetAttackingElapsedTime = (local - start) * fSpeedRatio")
|
||||
np._apply_attack_speed(100)
|
||||
var lt0: float = np._local_time
|
||||
np._process(0.016)
|
||||
_ck(np._local_time > lt0, "_process advances the local time")
|
||||
|
||||
# --- SetComboSkillFlag(PythonPlayerSkill.cpp):GC 连击开关 -> SetComboType(MIN(lv,2)) ---
|
||||
fc.skills = [{"id": 122, "level": 3, "master": 0}]
|
||||
fc.combo_changed.emit(true)
|
||||
_ck(np._combo_type == 2, "combo on + skill 122 lv3 -> combo type MIN(3,2)=2, got %d" % np._combo_type)
|
||||
fc.combo_changed.emit(false)
|
||||
_ck(np._combo_type == 0, "combo off -> combo type 0")
|
||||
fc.skills = [{"id": 122, "level": 1, "master": 0}]
|
||||
fc.combo_changed.emit(true)
|
||||
_ck(np._combo_type == 1, "combo on + skill 122 lv1 -> combo type 1")
|
||||
fc.skills = [{"id": 122, "level": 0, "master": 0}]
|
||||
fc.combo_changed.emit(false)
|
||||
_ck(np._combo_type == 1, "skill 122 level 0 -> return before SetComboType")
|
||||
fc.skills = []
|
||||
fc.combo_changed.emit(false)
|
||||
_ck(np._combo_type == 1, "no skill 122 slot -> return before SetComboType")
|
||||
np._combo_type = 0
|
||||
|
||||
# --- §3.5 连击状态机:InputComboAttackCommand / __RunNextCombo / ComboProcess ---
|
||||
# 合成一张 4 段的 1H type1 段表(段号 14..17),并给 player_view 一个可控 .msa。
|
||||
var K: int = (np.MOTION_MODE_GENERAL << 16) | 0
|
||||
@@ -181,101 +272,271 @@ func _run() -> void:
|
||||
_ck(fc.attacks.size() == 1 and fc.attacks[0][0] == 0, "CG_ATTACK bType is skill 0, not combo seg")
|
||||
_ck(fc.moves.size() >= 1 and fc.moves.back()[0] == np.FUNC_COMBO and fc.moves.back()[1] == 14,
|
||||
"FUNC_COMBO carries seg no. 14 (NAME_COMBO_ATTACK_1)")
|
||||
_ck(view.attack_motions.size() == 1 and view.attack_motions[0] == [np.MOTION_MODE_GENERAL, 14, 1.0],
|
||||
"swing binds the seg motion (mode, index 14, speed ratio 1.0), got %s" % [view.attack_motions])
|
||||
|
||||
# 立刻再输入(elapsed ~0 < pre_input_time):既不推进也不置 pre-input
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 1 and not np._is_pre_input, "input before InputStartTime -> ignored")
|
||||
|
||||
# elapsed 落在 [start, next):置 m_isPreInput,不推进
|
||||
np._combo_started_t = np._now() - 0.15
|
||||
# elapsed 落在 [start, next):置 m_isPreInput,不推进(GetAttackingElapsedTime 基于本地时间)
|
||||
np._motion_start_t = np._local_time - 0.15
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 1 and np._is_pre_input, "input in [start,next) -> pre-input latched")
|
||||
|
||||
# ComboProcess:elapsed 过了 NextComboTime -> 触发挂起的 pre-input -> 段 2
|
||||
np._combo_started_t = np._now() - 0.25
|
||||
np._motion_start_t = np._local_time - 0.25
|
||||
np._combo_process()
|
||||
_ck(np._combo_index == 2 and not np._is_pre_input, "ComboProcess fires pre-input -> combo index 2")
|
||||
|
||||
# elapsed 过 NextComboTime 直接输入 -> 立即推进(段 3、段 4)
|
||||
np._combo_started_t = np._now() - 0.30
|
||||
np._motion_start_t = np._local_time - 0.30
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 3, "input past NextComboTime -> advance to 3")
|
||||
np._combo_started_t = np._now() - 0.30
|
||||
np._motion_start_t = np._local_time - 0.30
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 4, "advance to last seg 4")
|
||||
|
||||
# 段 4 是最后一段:__OnEndCombo(非骑乘不复位),再输入越界不推进
|
||||
np._combo_started_t = np._now() - 0.30
|
||||
np._motion_start_t = np._local_time - 0.30
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 4, "past last seg -> no further advance (non-horse)")
|
||||
|
||||
# 动作回到 Wait(elapsed > duration)-> __ClearCombo
|
||||
np._combo_started_t = np._now() - 1.5
|
||||
# 参考端 ComboProcess 没有「超过整段时长就清连击」:elapsed > duration 段号保持
|
||||
np._motion_start_t = np._local_time - 1.5
|
||||
np._combo_process()
|
||||
_ck(np._combo_index == 0 and not np._is_pre_input, "motion back to Wait -> __ClearCombo")
|
||||
_ck(np._combo_index == 4, "ComboProcess has no duration timeout -> combo index kept, got %d" % np._combo_index)
|
||||
# __SetMotion 尾:新绑定的动作没有 ComboInputData(回 Wait)-> m_dwcurComboIndex = 0
|
||||
view.anim.md = {"duration": 2.0}
|
||||
view.motion_bound.emit("wait")
|
||||
_ck(np._combo_index == 0, "bound motion without ComboInputData -> combo index 0")
|
||||
# __SetMotion 尾:绑定带 MotionAttackData 的动作 -> m_HitDataMap.clear()
|
||||
np._hit_dedup = {0: {2000: 1.0}}
|
||||
view.anim.md = {"has_attacking_data": true, "has_combo_input": true, "duration": 1.0}
|
||||
view.motion_bound.emit("attack")
|
||||
_ck(np._hit_dedup.is_empty(), "bound attacking motion -> hit data map cleared")
|
||||
|
||||
# 没有段表(缺资源)-> 退化为单段普攻(段号 13)
|
||||
# 没有段表(缺资源)-> 退化为单段普攻(段号 13);攻速系数原样传给动作播放
|
||||
np._combo_tables = {}
|
||||
fc.moves.clear()
|
||||
np._apply_attack_speed(150)
|
||||
np._do_attack_swing(pcn, {})
|
||||
_ck(np._combo_index == 0 and fc.moves.back()[0] == np.FUNC_COMBO \
|
||||
and fc.moves.back()[1] == np.NAME_NORMAL_ATTACK,
|
||||
"no combo table -> single NORMAL_ATTACK swing")
|
||||
_ck(view.attack_motions.back() == [np.MOTION_MODE_GENERAL, np.NAME_NORMAL_ATTACK, 1.5],
|
||||
"attack motion plays at fSpeedRatio 1.5, got %s" % [view.attack_motions.back()])
|
||||
np._apply_attack_speed(100)
|
||||
|
||||
# --- §3.5 修改 4:命中窗几何判定 -> OnHit + FlushVictimList ---
|
||||
# --- __NormalAttackProcess 的 m_HitDataMap:新窗 insert;旧窗 map::insert 不覆盖 + 每窗上限 ---
|
||||
np._hit_dedup.clear()
|
||||
np._hit_motion_type = np.MOTION_TYPE_SKILL
|
||||
np._hit_limit_count = 2
|
||||
np._hit_invisible_time = 0.5
|
||||
np._local_time = 10.0
|
||||
_ck(np._register_hit(0, 1) and is_equal_approx(float(np._hit_dedup[0][1]), 10.5),
|
||||
"first hit of a window -> insert local + fInvisibleTime, process")
|
||||
np._local_time = 11.0
|
||||
_ck(np._register_hit(0, 1) and is_equal_approx(float(np._hit_dedup[0][1]), 10.5),
|
||||
"victim already in the window -> map::insert keeps the old time, count 1 <= limit")
|
||||
_ck(np._register_hit(0, 2), "second victim -> count 2 <= iHitLimitCount 2")
|
||||
_ck(not np._register_hit(0, 3), "third victim -> count 3 > iHitLimitCount -> FALSE")
|
||||
np._hit_limit_count = 0
|
||||
np._hit_dedup.clear()
|
||||
_ck(np._register_hit(0, 1), "SKILL: first hit of a window skips the limit check")
|
||||
_ck(not np._register_hit(0, 1), "SKILL iHitLimitCount 0 -> next hit in that window 1 > 0 -> FALSE")
|
||||
np._hit_motion_type = np.MOTION_TYPE_COMBO
|
||||
for i in 16:
|
||||
np._register_hit(1, 100 + i)
|
||||
_ck(not np._register_hit(1, 200), "COMBO/NORMAL: 17th victim in one window -> > 16 -> FALSE")
|
||||
np._hit_dedup.clear()
|
||||
|
||||
# --- §3.5 修改 4 / §3.7:__NormalAttackProcess 扫掠球 vs .msm 防御球 -> __ProcessDataAttackSuccess ---
|
||||
# SceneTree 脚本里 add_child 到 root 的节点要等一帧才真正 is_inside_tree(),
|
||||
# 几何判定读 global_position,所以这一小节先 await 一帧。
|
||||
var nw := FakeNetWorld.new(); get_root().add_child(nw)
|
||||
np.net_world = nw
|
||||
var vnode := Node3D.new(); get_root().add_child(vnode)
|
||||
var fx := FakeFx.new(); get_root().add_child(fx)
|
||||
var fx_parent := Node3D.new(); get_root().add_child(fx_parent)
|
||||
np.fx = fx
|
||||
np.fx_parent = fx_parent
|
||||
var vnode := FakeVictim.new(); get_root().add_child(vnode)
|
||||
nw.nodes[2000] = vnode
|
||||
fc.ents[2000] = {"vid": 2000, "ch_type": 2, "hp": 50, "dead": false, "knock_down": true}
|
||||
fc.ents[2000] = {"vid": 2000, "ch_type": 2, "race": 101, "hp": 50, "dead": false, "owner_vid": 1000}
|
||||
fc.ents[1000]["dead"] = false
|
||||
await process_frame
|
||||
vnode.global_position = Vector3(0, 0, 1.0) # 攻击者正前方 1 m(yaw 0 -> 前向 +Z)
|
||||
vnode.global_position = Vector3(0, 0, 1.0) # 攻击者正前方 1 m(yaw 0:模型 -Y = Godot +Z)
|
||||
vnode.rotation.y = 0.0
|
||||
pcn.global_position = Vector3.ZERO
|
||||
pcn.rotation.y = 0.0
|
||||
# 一个采样:t=0.1 时刀尖球从模型本地 (-60,-80) 横扫到 (60,-80),高 1 m
|
||||
view.anim.md = {
|
||||
"has_attacking_data": true,
|
||||
"hit_windows": [{
|
||||
"start_time": 0.0, "end_time": 0.5, "bone": "equip_right_hand",
|
||||
"weapon_length": 120.0, "samples": [],
|
||||
"start_time": 0.0, "end_time": 0.5, "bone": "equip_right_hand", "weapon_length": 120.0,
|
||||
"samples": [{"time": 0.1, "last_pos": Vector3(-60, -80, 100), "pos": Vector3(60, -80, 100)}],
|
||||
}],
|
||||
"motion_type": np.MOTION_TYPE_COMBO, "hit_limit_count": 0, "invisible_time": 0.1,
|
||||
"next_combo": 0.2,
|
||||
"motion_type": np.MOTION_TYPE_COMBO, "hitting_type": np.HIT_TYPE_GOOD, "hit_limit_count": 0,
|
||||
"invisible_time": 0.1, "stiffen_time": 0.05, "external_force": 3.0, "next_combo": 0.2,
|
||||
}
|
||||
np._combo_tables = {}
|
||||
fc.attacks.clear()
|
||||
fc.synced.clear()
|
||||
np._target_vid = 0
|
||||
view.delays.clear()
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
np._do_attack_swing(pcn, {}) # _emit_swing:有命中窗 -> CG_ATTACK 不在挥击时发
|
||||
_ck(fc.attacks.size() == 0, "hit windows present -> CG_ATTACK deferred off the swing")
|
||||
_ck(np._hit_windows.size() == 1, "swing cached the .msa hit window")
|
||||
np._swing_start_t = np._now() - 0.1 # 推进到命中窗内
|
||||
np._process(0.016)
|
||||
_ck(fc.attacks.size() == 1 and fc.attacks[0] == [0, 2000],
|
||||
"in-window + in front arc -> OnHit CG_ATTACK(skill 0, vid 2000)")
|
||||
_ck(fc.synced.size() == 1 and fc.synced[0][0]["vid"] == 2000,
|
||||
"pushed victim -> frame-end CG_SYNC_POSITION")
|
||||
np._swing_start_t = np._now() - 0.2
|
||||
np._process(0.016)
|
||||
# 采样时刻 0.1 不在 [motiontime - elapsed, motiontime] 内 -> 不判定
|
||||
np._motion_start_t = np._local_time - 0.2
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "no .msa sample inside [t-dt, t] -> no hit")
|
||||
np._motion_start_t = np._local_time - 0.105
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks == [[0, 2000]], "sweep sphere crosses the defending sphere -> OnHit CG_ATTACK(0, 2000), got %s" % [fc.attacks])
|
||||
_ck(np._target_vid == 2000, "OnHit -> SetTarget(victim)")
|
||||
_ck(is_equal_approx(np._delay, 0.05) and view.delays == [0.05], "attacker InsertDelay(fStiffenTime), got %s" % [view.delays])
|
||||
_ck(vnode.delays == [0.05], "victim InsertDelay(fStiffenTime), got %s" % [vnode.delays])
|
||||
_ck(float(np._victim_invisible_until.get(2000, 0.0)) > np._now(), "victim m_fInvisibleTime = now + fInvisibleTime")
|
||||
_ck(vnode.reactions == [["good", 1.0]], "HIT_TYPE_GOOD -> __HitGood(scalar cos(0) = 1), got %s" % [vnode.reactions])
|
||||
_ck(fx.spawned.size() == 1 and fx.spawned[0][0] == np.EFFECT_HIT \
|
||||
and Vector3(fx.spawned[0][1]).is_equal_approx(vnode.global_position),
|
||||
"blow_1_low.mse spawned at the victim position, got %s" % [fx.spawned])
|
||||
if fx.spawned.size() == 1:
|
||||
_ck(absf(absf((fx.spawned[0][2] as Node3D).rotation.y) - PI) < 0.01, "hit effect yaw = atan2(-dx, -dz) = ±π")
|
||||
_ck(nw.pushes.size() == 1 and nw.pushes[0][0] == 2000 and Vector2(nw.pushes[0][1]).is_equal_approx(Vector2(0, -1)) \
|
||||
and is_equal_approx(float(nw.pushes[0][2]), 3.0),
|
||||
"__PushCircle + IncreaseExternalForce(3) along victim - attacker (actor-world (0,-1)), got %s" % [nw.pushes])
|
||||
np._flush_victim_list()
|
||||
var bxy := MapCoord.to_server_cm(vnode.global_position + Vector3(0.1, 0, 0))
|
||||
_ck(fc.synced.size() == 1 and fc.synced[0][0]["vid"] == 2000 and fc.synced[0][0]["x"] == int(bxy.x) \
|
||||
and fc.synced[0][0]["y"] == int(bxy.y),
|
||||
"pushing victim -> frame-end CG_SYNC_POSITION at its blending position, got %s" % [fc.synced])
|
||||
# COMBO:同窗同目标只一次(先把受击方无敌时间去掉,单独验 m_HitDataMap)
|
||||
np._victim_invisible_until.clear()
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "COMBO motion_type -> same window/victim hits once only")
|
||||
vnode.global_position = Vector3(0, 0, -1.0) # 转到身后
|
||||
# AttackingProcess:rVictim.__isInvisible() -> FALSE
|
||||
np._hit_dedup.clear()
|
||||
np._swing_start_t = np._now() - 0.2
|
||||
np._process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "victim behind attacker -> outside front arc -> no hit")
|
||||
np._def_last.clear()
|
||||
np._victim_invisible_until[2000] = np._now() + 5.0
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "victim still inside m_fInvisibleTime -> no hit")
|
||||
|
||||
# __CanPushDestActor:owner 不是攻击者 / owner 超 3 秒 -> 不推;晕眩 -> 推
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
fc.ents[2000]["owner_vid"] = 0
|
||||
np._attack_process(0.016)
|
||||
np._flush_victim_list()
|
||||
_ck(fc.attacks.size() == 1 and nw.pushes.is_empty() and fc.synced.is_empty(),
|
||||
"victim not owned by the attacker -> hit, no push, no CG_SYNC_POSITION")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
fc.ents[2000]["owner_vid"] = 1000
|
||||
np._owner_seen[2000] = [1000, np._now() - 3.5]
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1 and nw.pushes.is_empty(), "owner time > 3 s -> no push")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
fc.ents[2000]["stunned"] = true
|
||||
np._attack_process(0.016)
|
||||
_ck(nw.pushes.size() == 1, "stunned victim -> always pushable")
|
||||
fc.ents[2000]["stunned"] = false
|
||||
np._owner_seen.erase(2000)
|
||||
|
||||
# HIT_TYPE_NONE:__ProcessDataAttackSuccess 直接 return(不硬直、不发包);GREAT -> __HitGreate
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
view.delays.clear()
|
||||
vnode.delays.clear()
|
||||
vnode.reactions.clear()
|
||||
np._hit_type = np.HIT_TYPE_NONE
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty() and view.delays.is_empty() and vnode.delays.is_empty() and fx.spawned.is_empty(),
|
||||
"HIT_TYPE_NONE -> no delay / effect / OnHit")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
np._hit_type = np.HIT_TYPE_GREAT
|
||||
np._attack_process(0.016)
|
||||
_ck(vnode.reactions.size() == 1 and vnode.reactions[0][0] == "greate", "HIT_TYPE_GREAT -> __HitGreate, got %s" % [vnode.reactions])
|
||||
np._hit_type = np.HIT_TYPE_GOOD
|
||||
|
||||
# 距离怪癖:v3Distance = (dX, dZ, dZ) —— 平面 Y 不参与,高度差算两次
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.global_position = Vector3(3.5, 0, 1.0)
|
||||
vnode.spheres = [{"radius": 45.0, "pos": Vector3(-350, 0, 100), "bone": ""}]
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "|dX| 350 >= 300 -> distance reject although the spheres touch")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.global_position = Vector3(0, 0, 3.5)
|
||||
vnode.spheres = [{"radius": 45.0, "pos": Vector3(0, 270, 100), "bone": ""}]
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "|dY| 350 is not part of the distance check -> hit")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.global_position = Vector3(0, 2.2, 1.0)
|
||||
vnode.spheres = [{"radius": 45.0, "pos": Vector3(0, 0, 100), "bone": ""}]
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "height 2.2 m -> 2 * 220² >= 300² -> reject")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.global_position = Vector3(0, 2.0, 1.0)
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "height 2.0 m -> 2 * 200² < 300², Z cylinder ignores height -> hit")
|
||||
|
||||
# IS_HUGE_RACE:500 cm 距离、不推、特效在 (攻击者 + 防御球) * 0.5
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
fc.ents[2000]["race"] = 2493
|
||||
vnode.global_position = Vector3(4.0, 0, 1.0)
|
||||
vnode.spheres = [{"radius": 45.0, "pos": Vector3(-400, 0, 100), "bone": ""}]
|
||||
np._attack_process(0.016)
|
||||
np._flush_victim_list()
|
||||
_ck(fc.attacks.size() == 1 and nw.pushes.is_empty() and fc.synced.is_empty(),
|
||||
"IS_HUGE_RACE: 400 cm < 500 -> hit, no push, no sync")
|
||||
_ck(fx.spawned.size() == 1 and Vector3(fx.spawned[0][1]).is_equal_approx(Vector3(0, 0.5, 0.5)),
|
||||
"IS_HUGE_RACE: effect at (attacker + defending sphere) * 0.5, got %s" % [fx.spawned])
|
||||
fc.ents[2000]["race"] = 101
|
||||
|
||||
# 防御球随受击方朝向旋转;__HitGood 的 scalar = cos(攻击者朝向 - 受击方朝向)
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.reactions.clear()
|
||||
vnode.global_position = Vector3(0, 0, 1.6)
|
||||
vnode.spheres = [{"radius": 25.0, "pos": Vector3(0, -60, 100), "bone": ""}]
|
||||
vnode.rotation.y = 0.0
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "victim yaw 0 -> defending sphere 220 cm ahead -> miss")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.rotation.y = PI
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "victim yaw π -> sphere swings to 100 cm -> hit")
|
||||
_ck(vnode.reactions.size() == 1 and is_equal_approx(float(vnode.reactions[0][1]), -1.0),
|
||||
"__HitGood scalar = cos(0 - π) = -1, got %s" % [vnode.reactions])
|
||||
vnode.rotation.y = 0.0
|
||||
|
||||
# 身后:扫掠球够不到
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
vnode.global_position = Vector3(0, 0, -1.0)
|
||||
vnode.spheres = [{"radius": 45.0, "pos": Vector3(0, 0, 100), "bone": ""}]
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "victim behind the attacker -> sweep misses")
|
||||
|
||||
# 占位胶囊(无 .msm):退化防御球
|
||||
var ph := Node3D.new(); get_root().add_child(ph)
|
||||
await process_frame
|
||||
ph.global_position = Vector3(0, 0, 1.0)
|
||||
nw.nodes[2000] = ph
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "node without defending spheres -> fallback sphere -> hit")
|
||||
nw.nodes[2000] = vnode
|
||||
vnode.global_position = Vector3(0, 0, 1.0)
|
||||
np._swing_start_t = np._now() - 2.0 # 动作越过所有命中窗
|
||||
np._process(0.016)
|
||||
|
||||
np._motion_start_t = np._local_time - 2.0 # 动作越过所有命中窗
|
||||
np._attack_process(0.016)
|
||||
_ck(np._hit_windows.is_empty(), "motion past all windows -> hit windows cleared")
|
||||
# 目标已死:挥击命中窗内也不发 CG_ATTACK(死亡目标不是有效命中)
|
||||
fc.ents[2000]["dead"] = true
|
||||
np._do_attack_swing(pcn, {})
|
||||
np._hit_dedup.clear()
|
||||
np._swing_start_t = np._now() - 0.1
|
||||
np._process(0.016)
|
||||
_ck(fc.attacks.size() == 1, "dead victim inside the hit window -> no CG_ATTACK")
|
||||
_reset_hit(np, fc, nw, fx)
|
||||
np._motion_start_t = np._local_time - 0.105
|
||||
np._attack_process(0.016)
|
||||
_ck(fc.attacks.is_empty(), "dead victim inside the hit window -> no CG_ATTACK")
|
||||
fc.ents[2000]["dead"] = false
|
||||
np.net_world = null
|
||||
np.fx = null
|
||||
|
||||
# --- §3.5 修改 6:武器种类 -> combo_motion_mode(RefreshState 的 SetMotionMode 分支)---
|
||||
# 纯映射表:CItemData 类型/子类型 + 骑乘/变身 -> CRaceMotionData::EMode
|
||||
|
||||
Reference in New Issue
Block a user