- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
205 lines
16 KiB
Markdown
205 lines
16 KiB
Markdown
# skill.cast-effect-timing
|
||
|
||
## Scope
|
||
|
||
比较技能等级、目标选择、施法前置、动作事件、伤害/状态结果以及粒子和命中特效的时序。
|
||
|
||
## Reference call chain
|
||
|
||
- `UserInterface/PythonSkill.cpp`
|
||
- `UserInterface/PythonPlayerSkill.cpp`
|
||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||
- `UserInterface/InstanceBaseEffect.cpp`
|
||
- `EffectLib/EffectInstance.cpp`
|
||
- `EffectLib/ParticleInstance.cpp`
|
||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||
- `UserInterface/InstanceBase.cpp`
|
||
|
||
## Current call chain
|
||
|
||
- `project/player_skill.gd`
|
||
- `project/skill_table.gd`
|
||
- `project/net_play.gd`
|
||
- `project/net_world.gd`
|
||
- `project/fx/skill_fx.gd`
|
||
- `project/fx/effect_player.gd`
|
||
- `extension/src/net/entity_store.cpp`
|
||
- `extension/tests/net_state_queue_test.cpp`
|
||
- `project/combat_fx_test.gd`
|
||
- `project/skill_test.gd`
|
||
|
||
## Initial finding
|
||
|
||
当前存在技能表、技能上下文和特效测试,但这些测试主要证明局部数据/材质行为,尚未证明完整 40250 施法链路。技能动作事件、挂点、目标范围、冷却、服务端结果和特效停止/重播仍需要逐分支对照。
|
||
|
||
## Current finding
|
||
|
||
`combat_fx_test.gd` 的失败是测试选择器使用本地化文本造成的,并非技能业务失败;现已改为按 40250 的控制节点名定位并通过。更重要的是,实体状态机此前没有复刻 40250 的 `eFunc & FUNC_SKILL` 位判断,移动中的技能会在到达目标时回到 WAIT;现已补齐远近两种分支并加入 C++ 回归测试。
|
||
|
||
本轮继续追到视觉动作层后,发现还有一个具体的参数丢失:`project/net_world.gd::_apply_anim` 只从 `FUNC_SKILL` 取 `motion_idx` 并调用 `PlayerView.play_skill_motion(motion_name)`,没有把 40250 `NEW_UseSkill`/到达逻辑中的 `uArg & 0x0f` 动作循环次数和 `(uArg >> 4)` 移动技能标志传入视图;`PlayerView.play_skill_motion` 的 `grade` 也没有来自网络状态的真实来源。这样只能证明“技能状态会进入”,不能证明每个技能的动作循环、移动技能动作和结束时机与 40250 一致。
|
||
|
||
同时,当前 `SkillFx`/`NetPlay` 仍有固定施法锁定和特效时长路径;40250 的关键帧、挂点、颜色/混合和动作事件来自 MSA/MSE 与 `ActorInstanceMotionEvent`。这些资源驱动事件尚未建立逐技能时间线,因此本合同保持 `PARTIAL`。
|
||
|
||
## Deep audit round 2026-09-20
|
||
|
||
本轮沿 `CInstanceBase::NEW_UseSkill`、`CActorInstance::ProcessMotionEvent*`、本地施法入口和远端 `FUNC_SKILL` 路径复核,并重新执行技能/战斗/特效回归:
|
||
|
||
- 40250 `NEW_UseSkill` 明确接收 `uMotLoopCount` 和 `isMovingSkill`,调用 `InterceptOnceMotion`、`__OnUseSkill`,再设置动作循环次数;当前 `NetPlay.on_use_skill()` 只把 `motion_skill` 和 `arg` 上行,`NetWorld._apply_anim()` 只取 `FUNC_SKILL` 的 motion index,调用远端 `play_skill_motion(mname)` 时没有把 `uArg & 0x0f` 循环次数、`uArg >> 4` 移动技能标志或技能 grade 传入。
|
||
- 当前 `PlayerView.play_skill_motion()` 支持按 grade 选择 `.msa` 后缀,但正式远端网络动作路径没有 grade 来源;本地 `GameScene._on_skill_cast_started()` 能传 grade 并从 MSA 读取 duration/cancel_enable,两个路径的输入并不统一。
|
||
- 当前 `SkillFx.spawn_skill()` 只在 `skill_fx_test.gd` 中被直接调用,正式 `GameScene`/`NetPlay` 没有生产调用;正式本地技能特效主要依赖 MSA motion event 和 `fx.spawn_motion`,因此技能表中的主特效/子特效映射没有被证明与 40250 的技能触发链一致。
|
||
- 40250 的 `MOTION_EVENT_TYPE_FLY`、`EFFECT_TO_TARGET`、`SPECIAL_ATTACKING` 和 `SOUND` 都由动作事件驱动,带有攻击窗口、挂点、跟随目标、碰撞球和一次性/循环生命周期;当前 `effect_playback_test` 只验证通用 MSE 播放/停止/重播,`combat_fx_test` 只验证战斗门和桩,不是逐技能 MSA→MSE→命中结果时间线。
|
||
- 本轮 `skill_test.gd`、`combat_fx_test.gd`、`effect_playback_test.gd` 和 `net_state_queue_test` 均退出码 0。它们证明现有表格、技能 UI、战斗状态机、通用特效播放和 `FUNC_SKILL` 队列不回归,但不能把动作循环、移动技能、grade、挂点/颜色/混合、飞行/命中事件和服务端结果判定视为 40250 等价。
|
||
|
||
本轮没有修改实现;技能基础校验和部分本地动作链可用,但正式技能特效接线、远端动作参数和资源驱动事件仍未闭合,合同保持 `PARTIAL`。
|
||
|
||
## Active MSA motion ownership review
|
||
|
||
本轮继续对照 `CInstanceBase::NEW_UseSkill`、`CActorInstance::SetMotionLoopCount` 和
|
||
`CActorInstance::MotionEventProcess` 核对施法锁定的结束源。此前本地入口虽然已经绑定真实
|
||
MSA 并支持 LoopData,但 `NetPlay` 仍会无条件按 clip duration 清除 `_using_skill`,因此
|
||
当循环片段或动作尾部晚于名义 duration 时会提前解锁,和 40250 的动作所有权不一致。
|
||
|
||
现已修复:`GameScene._on_skill_cast_started()` 将“技能 MSA 是否成功绑定”传入
|
||
`NetPlay.start_skill_cast()`;有效动作设置为 motion-driven,施法门只在
|
||
`PlayerView` 的动作尾部发出 `motion_bound("wait")` 时清除;缺失 MSA 或 headless 测试仍使用
|
||
duration 作为兼容兜底。`is_lock()` 与 `_process()` 两条计时路径均遵守该门,死亡/重置/网络
|
||
状态失效也会清除 motion-driven 状态,避免下一次施法继承旧门。
|
||
|
||
新增回归覆盖“名义时长已过但 MSA 动作未结束仍保持锁定”和“动作尾事件后解锁”。
|
||
`target_effect_test`、`test_no_auto_move_regression`、`netplay_test`、`skill_test`、
|
||
`combat_fx_test`、`effect_playback_test`、`test_skill_state_arg_parity`、
|
||
`net_world_remote_action_test`、`player_motion_test` 及既有战斗/技能循环测试均通过。
|
||
|
||
### Active GameLib skill-event review
|
||
|
||
- `GameLib/ActorInstanceEvent.cpp` 明确把 `OnAttack`、`OnUseSkill`、`OnHit` 和 affect 清理交给 actor event handler;`OnUseSkill` 的 `uLoopCount` 与 moving-skill bit 组合后才传给上层,不能用固定施法延迟完全替代。
|
||
- `GameLib/ActorInstance.h` 的 motion queue、`IsUsingMovingSkill`、`CanCancelSkill`、`MotionEventProcess`、`ProcessMotionEventEffectToTargetEvent`、`ProcessMotionEventSpecialAttacking` 和 `ProcessMotionEventFly` 构成技能时序的消费者;当前 `GameScene` 主要处理本地主角 signal,远端/怪物的同一事件分支没有完整接线。
|
||
- 本轮完成 GameLib actor skill/event 核对;现有 `skill_test`/`effect_playback_test` 仍只是局部资源与 helper 证据,技能合同继续为 `PARTIAL`。
|
||
|
||
## Equivalence matrix
|
||
|
||
| 项目 | 状态 | 备注 |
|
||
|---|---|---|
|
||
| Preconditions | PARTIAL | 等级、职业、目标、冷却已有本地校验;真实服务端权限仍待验证 |
|
||
| Branch structure | PARTIAL | 单体/范围/辅助及失败分支已有代码映射;中断仍需端到端记录 |
|
||
| Algorithms/formulas | PARTIAL | 范围/目标选择已映射,技能公式仍由服务端权威 |
|
||
| State transition order | VERIFIED | `FUNC_SKILL|motion` 远近分支与到达后动作已按 40250 修复并测试 |
|
||
| Constants/units | UNASSESSED | 范围、持续时间、速度和坐标 |
|
||
| Timing/event sources | PARTIAL | 有效本地 MSA 的施法门现在由 `motion_bound("wait")` 动作尾释放,缺失动作才使用兼容计时器;逐技能 MSA 关键帧和特效事件仍未完整接线 |
|
||
| Resource/data sources | UNASSESSED | skilldesc、skilltable、MSA、MSE 和挂点 |
|
||
| Protocol side effects | PARTIAL | 技能状态上行、动作状态接收已覆盖;实际结果包仍需服务端验证 |
|
||
| Interruption/failure/cleanup | PARTIAL | 远端不可取消技能/表情会阻止后续 TCP 状态,动作尾、节点剔除和换图会清门;可取消技能、死亡中断、重复施法和特效回收仍未全闭合 |
|
||
|
||
## Remaining
|
||
|
||
- 逐技能类型建立 40250 → Godot 分支映射。
|
||
- 用至少一个真实攻击技能和一个辅助技能完成时序测试。
|
||
- 远端 `FUNC_SKILL` 的动作循环次数、移动技能标志和等级动作索引已完成传递;本地有效 MSA
|
||
的施法锁定已改为动作尾驱动;逐技能
|
||
MSA 事件到 MSE 的挂点、颜色/混合和服务端结果仍需继续验证。
|
||
- 继续补齐未实现的颜色、混合、挂点和动作事件语义。
|
||
|
||
## Active UserInterface skill-data review
|
||
|
||
- `PythonSkill.h` 还负责从 skill table/desc 注册技能、计算 cooldown/SP/continuation/loop/target/duration 和 motion index;这些值是 `PythonPlayerSkill`、quickslot 与 `ActorInstanceEvent` 的输入,不能由固定 UI 延迟代替。
|
||
- 本轮确认技能数据桥已被 active Visual Studio 工程引用,但当前端动作事件链仍没有证明使用同一套资源字段驱动循环次数、移动技能标志、grade、飞行/命中/特殊攻击挂点和 cleanup;合同保持 `PARTIAL`。
|
||
|
||
## Implementation fix round 2026-09-21T12:30Z
|
||
|
||
本轮按 40250 `CActorInstance::__OnUseSkill` 的确切打包规则闭合技能动作参数丢失:
|
||
`uArg = uLoopCount | (isMovingSkill ? 1 << 4 : 0)`。`SkillTable` 现在从 `.msk`
|
||
的 `MotionLoopCountFormula` 按 `SkillPoint` 计算低 4 位,并把 `MOVING_SKILL` 属性编码到
|
||
第 4 位;快捷栏和本地 `GameScene` 都使用同一份值,`M2Client.cast_skill` 不再固定发送
|
||
零参数。
|
||
|
||
远端链路新增 `Entity.func_arg`,TCP state queue 在“走到目标后施法”与“近距离立即施法”
|
||
两条分支都保留该参数;`NetWorld._apply_anim` 再解出循环次数和移动技能标志传给
|
||
`PlayerView.play_skill_motion`。新增 `test_skill_state_arg_parity.gd` 与原生
|
||
`net_state_queue_test` 断言了位编码、近距离分支和远距离到达分支;技能表/快捷栏、场景装配、
|
||
战斗特效、通用播放及全量 23 项 CTest 均通过。
|
||
|
||
这一轮修复的是动作参数和状态链,不宣称已经实现 `SetMotionLoopCount` 的真实循环片段、
|
||
远端 grade 的权威来源,或完整 MSA→MSE→命中结果时间线;挂点、颜色/混合、飞行/特殊攻击
|
||
事件、服务端结果和中断清理继续保持 `PARTIAL`。
|
||
|
||
## Implementation fix round 2026-09-21 — remote motion gates
|
||
|
||
按 40250 `__CanProcessNetworkStatePacket` / `__IsEnableTCPProcess` 补齐远端动作期间的
|
||
网络状态门:不可取消技能、活动表情和 TCP 状态禁用会保持后续 `GC_MOVE` 在队列中;技能或
|
||
表情的真实一次性动作通过 `motion_bound("wait")` 尾事件释放,而不是使用固定计时器。`FUNC_EMOTION`
|
||
的 `uArg` 也会尝试进入实际 `CRaceMotionData` action ID,出生时携带动作状态的实体会立即绑定
|
||
初始动作。
|
||
|
||
`net_state_queue_test`、`net_world_remote_action_test`、`net_world_vis_test`、`netplay_test`、
|
||
`player_motion_test`、`test_skill_state_arg_parity` 和完整 `ctest`(23/23)通过。技能可取消
|
||
性的权威 MSA 数据、grade、逐技能关键帧/特效事件、双人表情目标和完整死亡/断线清理仍为
|
||
`PARTIAL`。
|
||
|
||
## Implementation fix round 2026-09-22 — MSA LoopData / SetMotionLoopCount
|
||
|
||
按 40250 `CInstanceBase::NEW_UseSkill`、`CActorInstance::SetMotionLoopCount` 和
|
||
`CurrentMotionProcess` 的实际语义补齐技能动作循环:`Metin2AnimPlayer` 现在读取当前
|
||
`.msa` 的 `LoopData`,保留动作的 lead-in/tail,只在 `LoopStartTime..LoopEndTime` 片段
|
||
内回绕;正数 `uMotLoopCount` 在动作绑定后覆盖资源默认次数,`0` 使用资源中的
|
||
`MotionLoopCount`,`-1` 保持无限片段循环。循环边界会重复派发该段动作事件,最终尾部才发出
|
||
`playback_finished`,因此 `motion_bound("wait")`/技能网络门不会提前释放。
|
||
|
||
`PlayerView.play_skill_motion()` 已把本地与远端统一传入的 loop count 交给原生播放器。
|
||
新增 `skill_motion_loop_test.gd` 覆盖 PlayerView 参数传递、真实 `yeonsa.msa` 的资源默认值、
|
||
覆盖为 2 时的一次片段回绕、计数递减、尾部结束和切换普通动作后的清理;技能表、状态参数、
|
||
远端动作、特效播放和 PlayerView 回归均通过。
|
||
|
||
该轮闭合了 `SetMotionLoopCount`/MSA 片段时序差异,但技能 grade 的权威来源、逐技能 MSA
|
||
事件到 MSE 的挂点/颜色/混合、飞行/特殊攻击、服务端结果和完整中断清理仍保持 `PARTIAL`。
|
||
|
||
## Implementation fix round 2026-09-21T03:46Z — skill grade motion index
|
||
|
||
本轮先在 `project/skill_test.gd` 加入等级动作回归,修复前复现 1 项失败:技能表没有
|
||
等级动作索引入口。对照 40250 `PythonSkill.cpp::GetSkillMotionIndex`、
|
||
`SKILL_EFFECT_COUNT=4`、`SKILL_GRADEGAP=25` 以及当前资源
|
||
`assets/root/constinfo.py` 的 `USE_SKILL_EFFECT_UPGRADE_ENABLE=1` 后完成修复:
|
||
|
||
- `SkillTable::motion_idx_for_grade` 按 `base + grade * 25` 计算 M/G/P 动作索引,
|
||
超出该技能 `grade_count` 回退基础索引;新增 `motion_name_by_idx` / `motion_grade_by_idx`
|
||
让远端收到 26/51/76 等等级动作索引时能恢复同一技能和对应 `_2/_3/_4.msa`。
|
||
- `Quickbar` 普通技能快捷栏和技能窗口直施路径在 `cast_skill` 前读取当前 master grade,
|
||
不再只发送基础 motion index;公会技能仍按 40250 `UseGuildSkill` 使用基础索引。
|
||
- `NetWorld::_apply_anim` 从远端 FUNC_SKILL 的等级动作索引反解 grade,再传给
|
||
`PlayerView.play_skill_motion`;循环次数和 moving-skill bit 保持原有编码。
|
||
|
||
修复后通过:`skill_test.gd`、`net_world_remote_action_test.gd`、
|
||
`test_skill_state_arg_parity.gd`、`skill_motion_loop_test.gd`、`skill_fx_test.gd`、
|
||
`combat_fx_test.gd`。本轮只闭合了等级动作索引及其本地/远端映射,不宣称逐技能 MSA
|
||
事件、MSE 挂点/颜色/混合、飞行/特殊攻击、服务端结果和完整中断清理已完全等价,合同继续为
|
||
`PARTIAL`。
|
||
|
||
## Implementation fix round 2026-09-21 — remote SPECIAL_ATTACKING consumer
|
||
|
||
本轮按 40250 `ActorInstanceMotionEvent.cpp::ProcessMotionEventSpecialAttacking`、
|
||
`InstanceBaseBattle.cpp::AttackProcess` 和
|
||
`ActorInstanceCollisionDetection.cpp::__SplashAttackProcess` 继续核对技能事件消费者。
|
||
参考端的 `SPECIAL_ATTACKING` 不是只有本地主角才能创建的事件:每个可见
|
||
`CActorInstance` 都会建立自己的 `m_kSplashArea`,随后由 `AttackProcess` 遍历目标,执行
|
||
1000cm broad-phase、`ATTACK_TYPE_SNIPE` 目标限制、碰撞球检测、`HittedObjectSet` 去重和
|
||
`iHitLimitCount` 上限,最后调用 `__ProcessDataAttackSuccess`;只有本地事件处理器才把
|
||
`isEnableHitProcess` 转成 `CG_ATTACK`。
|
||
|
||
修复前 `GameScene._on_remote_motion_event()` 只处理远端特效/飞行/隐藏事件,远端 type 4
|
||
事件没有消费者;`NetPlay` 只有本地主角的 `start_motion_splash()` 和
|
||
`_splash_attack_process()`。现在:
|
||
|
||
- `NetWorld._apply_anim()` 保存当前远端 `FUNC_SKILL` 的 motion index,供事件恢复 `uSkill`;
|
||
- `GameScene._on_remote_motion_event()` 将 type 4 送入 `NetPlay.start_remote_motion_splash()`;
|
||
- `NetPlay._remote_splash_attack_process()` 使用与本地相同的 `MotionSplash` 几何、持续时间、
|
||
去重、命中上限和攻击可达性判定;
|
||
- 远端命中复用 `__ProcessDataAttackSuccess` 对应的硬直、无敌时间、受击动作、抖动和击退
|
||
分支,但把 `InsertDelay` 施加在远端攻击者,且禁止远端命中伪造本地 `CG_ATTACK`。
|
||
|
||
新增 `remote_motion_splash_test.gd` 覆盖创建、命中反应、攻击者硬直、无上行攻击包、同 VID
|
||
去重和 `DuringTime` 到期清理。该回归与 `net_world_remote_action_test.gd`、
|
||
`net_world_motion_event_test.gd`、`motion_splash_test.gd`、`combat_fx_test.gd`、
|
||
`skill_test.gd` 均通过。
|
||
|
||
本轮只闭合了远端 `SPECIAL_ATTACKING` 的本地消费者;真实服务端伤害/经验结果、完整技能
|
||
MSA→MSE 挂点/颜色/混合、远端资源矩阵、所有中断/断线清理仍保持 `PARTIAL`。
|