Files
mtgodot-poc/audit/contracts/movement.keyboard-motion.md
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

156 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# movement.keyboard-motion
## Scope
比较键盘输入、WASD 快速切换、移动请求、方向/速度计算、动作选择和停止清理。
## Reference call chain
- `UserInterface/PythonPlayerInputKeyboard.cpp`
- `UserInterface/PythonPlayerInput.cpp`
- `UserInterface/InstanceBaseMovement.cpp`
- `UserInterface/InstanceBaseMotion.cpp`
- `GameLib/ActorInstanceMotion.cpp`
- `GameLib/ActorInstanceMotionEvent.cpp`
## Current call chain
- `project/player_controller.gd`
- `project/net_play.gd`
- `project/net_world.gd`
- `project/test_wasd_steering_parity.gd`
- `project/test_no_auto_move_regression.gd`
- `project/player_motion_test.gd`
- `project/keyboard_motion_timeline_test.gd`
## Audit findings (2026-09-20)
40250 的键盘路径是事件驱动的:`NEW_SetSingleDIKKeyState` → `NEW_SetSingleDirKeyState` 更新四个方向位 → `NEW_SetMultiDirKeyState`。无方向立即 `NEW_Stop`;有方向按 `Up > Down`、`Left > Right` 计算八方向角,再调用 `NEW_MoveToDirection`。`NEW_MoveToDirection` 依次执行取消点地预约、主角存在/私店/锁定/移动技能/同步门、设置 `m_isGoing = FALSE`、设置 advancing rotation、启动 walk/run motion,并把当前位置向量加 300 像素目标。
当前端已按事件维护方向位,W/S 优先级、释放切换、失焦、进场残留和直立旋转均有回归证据;本轮新增 `keyboard_motion_timeline_test.gd`,实际驱动 `InputEventKey` 和 `_process`,覆盖“加载期按键不继承 → W+S 优先 → 释放 W 暴露 S → 释放最后方向停止”的时序。
仍存在明确的实现层差异,不能升级为 STATIC_VERIFIED:
- 40250 在按键事件中设置固定 300 像素 `Dst`,随后由 `.msa`/`CActorInstance::AccumulationMovement` 按动作根运动和 `m_fMovSpd` 推进;当前端在 `_process` 中按米制连续速度推进,模型可用时读取 `get_move_motion_speeds`,否则回退 `SPEED_WALK/SPEED_RUN`。方向角可一致,但目标距离、速度、到达时刻和阻挡后的状态不同。
- 40250 的 `CInstanceBase::Update → AccumulationMovement → Transform` 才会触发 `OnMove/OnMoving/OnWaiting/OnStop`;当前端 `moved` 和 `anim_state` 来自每帧预测位置,`NetPlay` 再按 0/300/100ms 节流发 `FUNC_MOVE/FUNC_WAIT`。已有节流测试,但没有证明每个动作资源停止/切换事件与网络状态事件一一对应。
- 40250 `NEW_MoveToDirection` 的 `__IsSyncing`、`isLock`、`IsUsingMovingSkill` 分支是“忽略移动但可能保留原动作”的同步门;当前端 `frozen`/`locked`/`moving_skill` 在 `_process` 早返回,`_goto` 被门挡时只对点地目标建立预约,键盘方向本身不建立预约。被击退、情绪、技能锁和解锁恢复的顺序尚未全部对齐。
- 40250 的相机旋转可在 `NEW_MoveToDirection` 内先改变相机再叠加角色方向;当前端读取 `camera.heading()` 后直接构造前后左右向量,没有实现同一套相机旋转副作用。
- 当前端用 Euler 分量强制保持 `rotation.x/z = 0`,解决了 W/S 快切翻跟头;这是有效的 Godot 平台适配,但还缺“角色根节点/骨骼动画旋转不被姿态资源覆盖”的真实模型测试。
因此现有测试证明的是输入回归和网络节流的已覆盖路径,不是完整 40250 移动/动作/资源链等价。
## Baseline tests
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`netplay_test.gd` 和 `keyboard_motion_timeline_test.gd` 当前通过,证明已覆盖的回归路径没有复现自动移动、W/S 粘键、翻滚和 0/300/100ms 网络节流问题;这不替代完整 40250 分支等价审计。
## Deep audit round 2026-09-20
本轮重新对照 `NEW_SetSingleDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement`、`RotationProcess` 和当前 `PlayerController`,并重跑输入与动作测试:
- 当前 `active` 切换、W/S 优先级、KeyUp 全局接收、失焦清键、`rotation.x/z=0` 和跨 `±PI` 的单向转向均有可重复测试;这解释了此前 W/S 快切翻跟头已经被当前平台适配压住。
- 40250 方向键移动把目标设置为当前位置前方固定 300 像素,再由动作根运动和 `m_fMovSpd` 推进;当前端仍用 `_process` 的米制连续速度推进,并在模型缺失时使用固定速度 fallback。因此“方向一样”不代表到达时刻、动作结束事件和服务端 MOVE/WAIT 边沿一致。
- 当前 `frozen/locked/moving_skill` 在 `_process` 中早返回或只转向;40250 的同步、私店、受击、情绪、移动技能和 `NEW_Stop` 分支会分别决定是否清 `m_isGoing`、是否保留动作及何时恢复。本轮没有端到端证明这些门在解锁后与 40250 同一帧恢复。
- 当前摄像机方向只参与构造移动向量,没有复刻 40250 `NEW_MoveToDirection` 内相机旋转与角色 advancing rotation 的完整副作用;真实角色模型测试 `player_motion_test.gd` 虽打印 PASS,但本轮退出码为 139 并报告 6 个 ObjectDB 泄漏,不能作为干净证据。
- `test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`keyboard_motion_timeline_test.gd` 本轮退出码 0,覆盖的输入回归通过;因此只保留“输入问题已回归通过”,不把根运动、资源事件、锁门和退出生命周期标记为等价。
结论:WASD 输入状态和翻转回归已稳定,但 40250 的固定目标/根运动、动作事件源、锁门恢复、相机副作用和真实模型退出安全仍有差异,合同保持 `PARTIAL`。
### Active UserInterface actor-transform review
- `InstanceBaseEvent.cpp` 将 `CInstanceBase` 的 event handler 直接绑定到 `CActorInstance`;`InstanceBaseTransform.cpp` 提供 cm 像素位置、rotation/blend direction、LookAt fly target/destination 和当前/advancing rotation 访问。
- `PythonPlayerEventHandler.h` 明确消费 `OnSyncing/Waiting/Moving/Move/Stop/Warp/ClearAffects/SetAffect/ResetAffect/Attack/UseSkill/Hit`,并在 bow fly handler 中把 `OnSetFlyTarget` 与动作帧 `OnShoot` 转成网络副作用。当前端 `PlayerController`/`NetPlay` 有移动/技能信号,但未证明远端/本地都按同一 event handler 顺序消费,自动移动和快速 WASD 仍需状态时序 oracle。
- 本轮完成 InstanceBase/PlayerEventHandler 静态核对,键盘合同继续为 `PARTIAL`。
## Implementation round 2026-09-20T19:03Z
本轮将键盘方向目标按 `InstanceBaseMovement.cpp::NEW_MoveToDirection` 的固定距离
规则收敛:方向事件现在记录当前角色位置加 300 像素(当前 Godot 单位为 3.0m)的
`_direction_dst`,后续帧只向该目标推进,目标到达后停止,不再每帧从当前位置延长
路径;移动技能仍保留只转向不平移的分支。死亡、受击/锁定门和方向释放会清理该目标。
新增的时间线断言验证了固定目标、到达停止、W/S 优先级、KeyUp 暴露、最后方向释放和
进入游戏不继承输入;`keyboard_motion_timeline_test.gd`、`player_move_test.gd`、
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd` 均退出码 0。
这只修复了目标创建/消费语义,不宣称完整根运动等价:当前端仍由 Godot `_process`
按速度推进,尚未接入 40250 `.msa`/`AccumulationMovement` 的真实动作事件、相机副作用、
同步/情绪/受击解锁恢复和完整 MOVE/WAIT 时间线,合同继续为 `PARTIAL`。
## Equivalence matrix
| 项目 | 状态 | 备注 |
|---|---|---|
| Preconditions | PARTIAL | active/失焦/按键残留、锁门、私店、情绪和锁门 held WASD 已覆盖;同步、受击、死亡和传送门未全覆盖 |
| Branch structure | PARTIAL | W/S、A/D、八方向和释放顺序已覆盖;相机旋转与所有禁止移动分支未闭合 |
| Algorithms/formulas | PARTIAL | 方向优先级和归一化一致;固定 300 像素 Dst 与根运动被连续速度替代 |
| State transition order | PARTIAL | 输入状态、锁门/移动技能恢复、PlayerController、NetPlay 入口已定位;资源事件与网络事件的完整顺序未证实 |
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;米/厘米、移动速度和目标距离不同 |
| Timing/event sources | PARTIAL | 当前是 `_process` 预测 + signal;参考是 Actor motion event 驱动 |
| Resource/data sources | PARTIAL | 当前模型速度可从动作资源读取但有 fallback;`.msa` event/AccumulationMovement 尚未一一对照 |
| Protocol side effects | PARTIAL | `FUNC_MOVE/FUNC_WAIT` 发送和节流有测试;阻止/重复/相机副作用仍未闭合 |
| Interruption/failure/cleanup | PARTIAL | stop、失焦、换图、锁定/移动技能恢复已覆盖;受击/情绪/传送和完整解锁矩阵仍需测试 |
## Remaining
- 用真实 `camera.heading()`、模型动作速度和网络 fake 记录同一方向序列的输入事件、移动目标、停止请求、动画状态和包时间线。
- 补充死亡/传送/受击以及私店、情绪、移动技能和普通锁定的多门叠加顺序;各单门 held WASD 恢复已覆盖基础路径。
- 继续对照 `.msa` 的 motion event、`AccumulationMovement` 根运动和当前 `get_move_motion_speeds` fallback,决定是否需要根运动适配层。
- 核对相机旋转副作用与角色方向叠加,以及真实角色模型在 W/S 快切时根节点/骨骼姿态不翻转。
### Audit round 2026-09-21
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd` 和
`test_no_auto_move_regression.gd`,三项均通过;W/S 优先级、KeyUp 暴露被按住方向、最后
方向释放停止、进入游戏不继承加载期按键、角色 X/Z 姿态归零、跨 `±PI` 转向和技能结束后
不自动恢复移动均有回归证据。
沿 `ActorInstanceBattle.cpp::ComboAttack`、`ActorInstanceMotion.cpp::AccumulationMovement`
和 `PythonPlayerEventHandler::OnMove/OnMoving/OnWaiting/OnStop` 再次核对后,没有把这些
输入测试升级为完整等价:当前端仍用连续速度预测替代固定 300 像素 Dst + 根运动,动作资源
事件与移动网络事件也不是同一来源;同步/受击/情绪/移动技能解锁恢复、相机旋转副作用和
真实模型根节点测试仍是下一轮差异项。合同保持 `PARTIAL`,本轮未改动实现。
### Implementation fix round 2026-09-21T11:45Z
本轮按 40250 `CPythonPlayer::Update` 在 `m_isDirKey` 下每帧重试
`NEW_SetMultiDirKeyState(m_isLeft,m_isRight,m_isUp,m_isDown)` 的语义修复了当前端的
锁门恢复差异:方向键仍按住时,`frozen/locked` 不再丢弃方向状态;移动技能接管移动时
会清除旧的平移目标但保留 held WASD,技能结束或锁定解除后的首个可移动更新重新创建
固定 300cm 目标。方向释放、死亡和停止仍会清除 pending 状态,避免旧目标在技能结束后
莫名续走。
`keyboard_motion_timeline_test.gd` 新增锁定恢复和移动技能恢复断言;与
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、
`player_motion_test.gd` 一起退出码均为 0。真实模型测试同时改为清理 native model/animation
节点并清空测试用的 GR2/MSA 缓存,确认本轮测试不再出现 ObjectDB 泄漏和 exit 139。
这轮只闭合了 held WASD 在锁门/移动技能边沿的状态恢复,不宣称完整移动等价:当前端仍
用 Godot 连续速度预测替代 `.msa`/`AccumulationMovement` 根运动,`OnMove/OnMoving/`
`OnWaiting/OnStop` 事件源、同步/受击/传送边界、相机副作用和完整网络时间线
仍保持 `PARTIAL`。
### Implementation fix round 2026-09-21T12:05Z
本轮补齐了 40250 `__CanMove` 的两个此前缺失的主角状态门:
`CPythonPlayer::IsOpenPrivateShop()` 与 `__IsProcessingEmotion()`。当前端新增
`PlayerController.set_private_shop_open()` / `set_processing_emotion()`;门开启时会
清理点地预约和旧方向目标,但保留 WASD 的方向位,和 `NEW_SetSingleDirKeyState`
保留 `m_isDirKey`、`CPythonPlayer::Update` 解锁后重新调用
`NEW_SetMultiDirKeyState` 的行为一致。`NetPlay._on_entity_info()` 只对主角读取
`shop_sign` / `acting_emotion`,因此查看或打开其他角色的商店不会锁住本地输入。
`keyboard_motion_timeline_test.gd` 新增摆摊中、情绪处理中“无平移且保留按键”,以及
状态解除后第一帧恢复移动的断言;本轮 targeted tests 通过。私店/情绪两个移动门已闭合,
但根运动、动作事件源、同步/受击/传送边界、相机副作用和完整网络时间线仍保持
`PARTIAL`。
## Audit round 2026-09-21T04:45Z — keyboard/root-motion boundary
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd`、
`test_no_auto_move_regression.gd` 和 `player_motion_test.gd`,全部退出码为 0:
- 进入游戏不继承加载期按键、W/S 优先与释放切换、最后方向停止、技能结束不续走、快速 WASD 不翻跟头、跨 `±PI` 单向转向和真实模型动作装配均保持通过。
- 继续对照 `NEW_SetMultiDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement` 和 `OnMove/OnMoving/OnWaiting/OnStop` 后,确认当前端仍是 Godot `_process` 连续速度预测,不能用输入测试替代 40250 `.msa` 根运动/动作事件源证据。
结论:本轮未修改实现;输入回归保持稳定,合同继续 `PARTIAL`。下一轮需要用真实动作资源和 fake network timeline 对拍固定 300cm 目标、根运动、停止事件和 MOVE/WAIT 包边沿。