# 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`。