From a9b1ba1b226b583da3102e6117486c6810f824a4 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Wed, 9 Sep 2026 15:30:19 +0800 Subject: [PATCH] =?UTF-8?q?docs(02-01):=20=E8=B5=B7=E8=8D=89=E5=AF=BB?= =?UTF-8?q?=E8=B7=AF=E7=B3=BB=E7=BB=9F=E6=8E=A5=E5=8F=A3=E8=8D=89=E6=A1=88?= =?UTF-8?q?=E4=B8=8E=E5=86=B3=E7=AD=96=E9=97=AE=E9=A2=98=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 02-INTERFACE-DRAFT.md:IPathfindingSystem 签名草案(D-01…D-06 接口面) - D-08 OnTouch 派发方案 A/B 并列(推荐 A,附 P2 结论) - mover.ts:651 坐标回写缺陷逐字调查与 go/no-go 问题 - 打断时序两选项、图方向性两选项、边界/损失值守卫要求 --- .../02-pathfinding/02-INTERFACE-DRAFT.md | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 .planning/phases/02-pathfinding/02-INTERFACE-DRAFT.md diff --git a/.planning/phases/02-pathfinding/02-INTERFACE-DRAFT.md b/.planning/phases/02-pathfinding/02-INTERFACE-DRAFT.md new file mode 100644 index 0000000..0705d0f --- /dev/null +++ b/.planning/phases/02-pathfinding/02-INTERFACE-DRAFT.md @@ -0,0 +1,261 @@ +# 寻路系统接口草案(DRAFT 待拍板) + +> **状态:DRAFT 待拍板** —— 本文档全部命名、签名、文件归属均为草案,未经用户拍板(D-07)不得实现。 +> 依据:02-CONTEXT.md 决策 D-01…D-11、02-RESEARCH.md、02-PATTERNS.md;绑定面接口逐字引用既有代码,未做任何修改。 +> 草案产出:2026-09-09(02-01 Task 1)。用户可直接编辑本文档,执行者将逐字应用修改并记录至文末「拍板记录」。 + +## 0. 范围声明(D-11) + +本阶段**只交付数据端移动入口**:寻路系统提供供渲染端后续调用的寻路 / 移动 API,全部逻辑可在 Node 环境独立验证。渲染端点击接线(点击拾取、不可达格点击反馈等 UI 行为)属 **Phase 4 渲染适配**,本阶段不做任何接线(D-11)。 + +## 1. 接口签名草案 + +> 以下全部类型与命名均为 **DRAFT 待拍板**。绑定面直接引用既有接口 +> (`IObjectMovable`、`IMapLayer`、`ITileLocator`、`ObjectMoveStep`、`EventTrigger`),不重定义。 +> 命名 / 注释 / 类型遵循 dev.md:接口 `I` 前缀大驼峰、函数类型单独 `type`、 +> 对象类型单独 `interface`、jsDoc 中文注释、接口方法间空行。 + +```typescript +import { ITileLocator } from '@motajs/common'; +import { IHeroMoveTopHandler, IMapLayer, IMapState } from '@user/data-base'; +import { FaceDirection, IObjectMovable } from '@user/data-common'; + +/** + * 寻路损失函数类型(DRAFT 待拍板) + * 计算从一格移动至相邻一格的损失,默认实现为每格损失 1(D-01) + * @param from 移动起点坐标 + * @param to 移动终点坐标 + * @returns 非负有限损失值,非法值按守卫规则处理(见第 7 节) + */ +type IPathfindingCostFunction = (from: ITileLocator, to: ITileLocator) => number; + +/** + * 通行性谓词函数类型(DRAFT 待拍板) + * 判定在指定楼层从一格向指定方向移动一格是否可通行, + * 语义须与 DefaultHeroMoveTopImpl.canPass 一致(事件层永远参与判定, + * 其余层仅当 pass.onlyEvents 为真时参与)。L2 不 import L3, + * 该谓词由 L3 接线时注入,L2 只持有此函数类型槽位 + * @param handler 通行性检查对象,与 IHeroMoveTopImpl.canPass 入参一致 + */ +type IPathfindingPassPredicate = (handler: IHeroMoveTopHandler) => boolean; + +/** + * 瞬移回退策略函数类型(DRAFT 待拍板) + * 由该函数决策瞬移是否回退为逐步寻路(D-05) + * @param path 完整路径坐标序列,含起点 + * @param arrivals 瞬移逐步兑现时每一步会到达的位置 + * @returns `true` 表示需要回退为逐步寻路 + */ +type IPathfindingFallbackPolicy = ( + path: Readonly, + arrivals: Readonly +) => boolean; + +export interface IPathfindingSystem { + /** + * 绑定寻路所用的地图状态对象(DRAFT 待拍板) + * 用于按楼层 id 获取楼层与事件层;未绑定时寻路告警并返回空路径 + * @param maps 地图状态对象,传入 `null` 解绑 + */ + useMapState(maps: IMapState | null): void; + + /** + * 绑定构建有向图所用的地图图层(DRAFT 待拍板,D-03) + * 通常绑定事件层;图仅包含从当前位置可达的位置(D-02) + * @param layer 地图图层对象,传入 `null` 解绑 + */ + useMapLayer(layer: IMapLayer | null): void; + + /** + * 绑定寻路移动对象(DRAFT 待拍板,D-03) + * 可绑定勇士位置或任意 `IObjectMovable`(如动态图块、跟随者) + * @param movable 移动对象,传入 `null` 解绑 + */ + useMovable(movable: IObjectMovable | null): void; + + /** + * 注入自定义损失函数(DRAFT 待拍板,D-01) + * 未注入时使用默认实现:每格损失 1 + * @param cost 损失函数,传入 `null` 恢复默认 + */ + useCostFunction(cost: IPathfindingCostFunction | null): void; + + /** + * 注入通行性谓词(DRAFT 待拍板) + * 未注入时使用 PassBit 掩码默认判定(不含多层 onlyEvents 语义); + * 推荐由 L3 注入与 DefaultHeroMoveTopImpl.canPass 同源的谓词, + * 保证图边判定与逐步移动判定单一事实源 + * @param predicate 通行性谓词,传入 `null` 恢复默认 + */ + usePassPredicate(predicate: IPathfindingPassPredicate | null): void; + + /** + * 注入瞬移回退策略(DRAFT 待拍板,D-05) + * 未注入时使用默认实现:路径上存在事件即回退为逐步寻路 + * (事件可能改变状态,瞬移会跳过副作用) + * @param policy 回退策略函数,传入 `null` 恢复默认 + */ + useFallbackPolicy(policy: IPathfindingFallbackPolicy | null): void; + + /** + * 仅获取从当前位置至目标位置的最小损失路径(DRAFT 待拍板,D-06) + * 不产生任何移动 + * @param target 目标坐标 + * @returns 路径坐标序列(含起点与终点); + * 不可达或输入非法时为空数组(D-08 情况 2) + */ + getPath(target: ITileLocator): Readonly; + + /** + * 逐步寻路至目标位置(DRAFT 待拍板,D-04 / D-09) + * 复用现有 hero mover 逐步执行,每步走 enter/leave/hit 钩子, + * 途经事件自然触发;有向图上逐步搜索即自然避障(D-02) + * @param target 目标坐标 + * @returns 移动控制器;无法寻路、无路径或已有移动进行中时返回 `null` + */ + moveTo(target: ITileLocator): Readonly | null; + + /** + * 瞬移至目标位置(DRAFT 待拍板,D-04) + * 瞬移前经回退策略判定,判定需要回退则自动退为逐步寻路 + * @param target 目标坐标 + * @returns 移动控制器;无法寻路、无路径或已有移动进行中时返回 `null` + */ + teleportTo(target: ITileLocator): Readonly | null; + + /** + * 打断当前自动寻路(DRAFT 待拍板,D-10) + * 新的方向输入或新的寻路调用可随时打断并接管; + * 兑现时序见草案第 5 节两个选项,待用户拍板 + */ + interrupt(): Promise; +} +``` + +**语义补充说明(同样待拍板):** + +- **不可达目标双语义(D-08)**:目标本身为 no-pass 类图块且四周有可达相邻格 → 移动至该相邻格、勇士面朝目标、触发目标 OnTouch(派发方案见第 3 节);其他情况 → 忽略本次寻路移动,`getPath` 返回空数组、`moveTo`/`teleportTo` 返回 `null`。 +- **有向图(D-02)**:图仅包含从当前位置可到达的位置;每条边 = 从 A 向方向 d 走一步,需 A 的 `outPass` 含 d 位且 B 的 `inPass` 含 opposite(d) 位(掩码语义见 02-RESEARCH Pattern 2),单向通行由掩码不对称天然产生。 +- **逐步执行(D-09)**:路径翻译为 `ObjectMoveType.Dir` 步骤队列,复用 `HeroMover` + `DefaultHeroMoveTopImpl`(Phase 1 source-aware 事件链);`ObjectMoveStep` 直接引用既有类型,不重定义。 +- **打断入口(D-10)**:`interrupt()` 暴露给玩家输入接管;寻路系统内部同时持有当前 `IMoverController` 引用以识别"移动已自然终止"(`HeroMover.onStepEnd` 在 CannotMove/Stop/Hit 时自行 stop)。 +- **图构建时机**:默认每次寻路动态构建、不缓存(数据端状态可变——敌人/门/道具),此为 AI 自主裁量项(CONTEXT「agent's Discretion」),如用户有缓存需求请在拍板时说明。 + +## 2. 文件归属提案(待拍板,可调整) + +依据 02-RESEARCH 分层论证(L2 禁 import L3,防循环依赖;`IMapLayer` 在 L1、`IObjectMovable` 在 L0,L2 可同时引用二者): + +| 层级 | 文件 | 内容 | +| --- | --- | --- | +| L2 `packages-user/data-system/src/pathfinding/` | `types.ts` | `IPathfindingSystem`、损失 / 回退 / 谓词函数类型 | +| | `graph.ts` | `IMapLayer` → 有向图构建(含边界守卫,见第 7 节) | +| | `system.ts` | 寻路系统实现(绑定 / 最小损失搜索 / 移动方式决策) | +| | `index.ts` | barrel 导出 | +| L3 `packages-user/data-state/src/pathfinding/` | `heroPathfinding.ts` | hero 接线:注入默认通行性谓词与回退策略、OnTouch 派发执行、打断接管 | +| 测试 | `packages-user/data-system/src/pathfinding/*.test.ts`、`packages-user/data-state/src/heroPathfinding.test.ts` | 覆盖 PATH-01 各行为(Wave 0 计划内补齐) | +| 配置 | `packages/common/src/logger.json` | 集中登记新日志码(error ≥65、warn ≥173) | + +修改面:`data-system/src/index.ts` 追加 pathfinding barrel 导出;`data-state/src/index.ts` 追加寻路接线导出;`data-state/src/core.ts` 在 `useTopImplementation` 接线点之后追加寻路系统初始化。**用户可调整以上归属**(例如将默认回退策略实现放在 L2 或 L3),拍板时注明即可。 + +## 3. D-08 OnTouch 派发两方案(并列,待拍板) + +目标场景:目标格本身是 no-pass 类图块且四周存在可达相邻格(D-08 情况 1)——移动到相邻格后面朝目标,并触发目标位置的 OnTouch 触发器(走 Phase 1 事件链路)。 + +### 方案 A:到达相邻格后直接派发 OnTouch(推荐) + +- **流程**:寻路至相邻格 → 勇士面朝目标(`ObjectMoveType.Face` 步或 `mover.setFaceDir`)→ 构造 `IGameEventInvocation`(`trigger = EventTrigger.OnTouch`、`heroLocator = 相邻格`、`triggerLocator = 目标格`)→ 调 `executor.execute`。 +- **env 构造**:逐字复刻 `moverImpl.ts:206-222` `commonTrigger` 形态——收集目标格点事件(`event.getPointEvent`)与静态 / 动态图块事件(`tileEvent().get()`),按 priority 降序排序,逐个构造 `IBlockEventEnv` 后一次 `execute` 调用。 +- **优点**:语义直给,OnTouch 与「触碰」语义精确对应;不依赖 mover 撞击路径;`inPass=0` 的真 no-pass 格同样能触发 OnTouch。 +- **缺点**:绕开 mover 链路,派发时序由寻路系统自行负责(需保证在移动完全结束后派发)。 + +### 方案 B:追加朝向目标的撞击步,复用 hit 链 + +- **流程**:路径终点后追加一步朝向目标的 `Dir` 步 → mover 链路 `canPass=false` → `HeroMoveCode.CannotMove` → `topImpl.cannotEnter()`。 +- **P2 结论(对方案 B 不生效的原因)**:撞击触发(`Hit` → `topImpl.hit()` → OnTouch 派发)只在 `canPass=true` 且 `eventPass=false` 的格上发生(`data-base/hero/mover.ts:183-190` 判定顺序 + `moverImpl.ts:127-141` `shouldHit`);`inPass=0` 的真 no-pass 格走 `CannotMove → cannotEnter()`,而 `cannotEnter` 当前为空实现(`moverImpl.ts:259-263`「新事件触发器没有无法进入的对应项,保留空实现以满足移动接口」)→ **方案 B 对 D-08 的目标场景(no-pass 目标格)不生效**。 +- **若坚持选 B**:须同时修改 `cannotEnter` 语义(新增 OnTouch 派发或等价行为),影响面扩至 L0/L3 移动链路,且「走入可通行格」与「触碰 no-pass 格」的触发条件需重新对齐。 + +**推荐:方案 A**。由用户拍板(D-08)。 + +## 4. P1 缺陷调查:mover.ts:651 坐标回写条件(`&&` 疑为 `||`) + +**逐字引用**(`packages-user/data-common/src/common/mover.ts:648-654`,条件位于 **651 行**): + +```typescript +const loc = await this.onStepEnd(code, step, this.tile, controller); +const before: ITileLocator = { x: this.tile.x, y: this.tile.y }; +const curr: ITileLocator = { x: loc.x, y: loc.y }; +if (this.tile.x !== loc.x && this.tile.y !== loc.y) { // ← 651 行:&& 疑为 || + this.tile.setPos(loc.x, loc.y); +} +``` + +**推理链**: + +1. 逐步寻路(D-09)产生的每一步都是 `ObjectMoveType.Dir` 正交步,只改变 x 或 y 其中一轴。 +2. 正交步后 `loc` 仅一轴与 `this.tile` 不同 → `this.tile.x !== loc.x && this.tile.y !== loc.y` **恒为 false**。 +3. `setPos` 不被调用 → `this.tile.x/y` 永不更新(渲染端现走 legacy mover,新 mover 尚无多步消费者,故此缺陷未被现有功能暴露)。 +4. 多步寻路自第二步起,以**陈旧原点**计算 nextLoc(每步的移动结果基于上一步回写失败后的旧坐标),整条路径走崩。 +5. `onStepSettled` 收到的 `before`/`curr` 与实际位置不符,leave/enter 事件派发坐标错误。 + +**修复提案**:651 行 `&&` → `||`(任一轴变化即回写坐标)。斜向步与传送步双轴均变化,`||` 下行为与现状一致;正交步新增回写,属缺陷修复而非行为变更(回归用例已铺设于 `mover.test.ts`,skip 状态,修复后翻绿)。 + +**go/no-go 问题**: + +- **go**——确认 `&&` 为手误,授权 02-02 将该行改为 `||` 并翻绿回归用例; +- **no-go**——若该条件属有意设计(如「仅双轴同时变化才回写」),请用户给出**替代坐标回写语义**;否则逐步寻路无法成立(每多走一步,位置误差累积一格)。 + +## 5. 打断时序两选项(D-10,待拍板) + +**既有机制约束(逐字)**: + +- `mover.start()` 在移动中返回 `null`(`mover.ts:669-670` `if (this.moving) return null;`)→ 打断接管必须先 stop 旧移动再 start 新移动。 +- `IMoverController.stop()` 在**当前步完成后**才兑现(`mover.ts:695-698`,置 `shouldStop` 并返回 `onEnd`)。 +- `HeroMover.onStepEnd` 在 CannotMove/Stop/Hit 时自行 `controller.stop()`,且该处不能 await(`data-base/hero/mover.ts:223-243` 注释明示会卡死)。 + +### 选项 1:`stop()` 后 await 兑现,再查新位置起新寻路(推荐) + +- **流程**:持有当前 controller → `controller.stop()` → `await` 兑现 → 读取移动对象最新位置 → 以新位置起算新寻路 → `start`。 +- **优点**:新寻路必然以兑现后的真实坐标起算,无竞态、无双移动并存、无回调重入问题。 +- **缺点**:新寻路启动最多延迟一个步时长;await 期间到达的新输入需合并或丢弃(按「最新意图优先」处理)。 + +### 选项 2:`onEnd` 回调驱动 + +- **流程**:`controller.stop()` 不 await → `controller.onEnd.then(() => 起新寻路)`。 +- **优点**:响应更快,不阻塞调用方。 +- **缺点**:需自行处理回调竞态——多次接管时旧回调作废、回调执行期间再次打断的重入、回调与自然终止(CannotMove/Stop/Hit)的重复触发,实现复杂度显著更高。 + +**推荐:选项 1**(时序正确性优先;一步时延在魔塔节奏下可接受)。待拍板。 + +## 6. 图方向性选项(待拍板) + +### 选项 1:仅 4 正交向(推荐) + +- 与 `PassBit` 四位掩码(Up/Right/Down/Left,`store/types.ts:27-36`)完全一致,边判定无需新掩码语义。 +- `DefaultHeroMoveTopImpl.canPass` 对四个斜向**直接放行不做掩码判定**(`moverImpl.ts:67-75`)→ 若建斜向边,图边语义与移动链路判定语义冲突(斜向 Dir 步 `canPass` 直接放行等于斜向穿墙)。 +- 逐步寻路每步翻译为 `Dir` 步,4 向与既有 `Dir` 步语义完全一致,无需改动移动链路。 +- **缺点**:斜向相邻目标点路径更长(曼哈顿折线)。 + +### 选项 2:含 8 向 + +- 斜向直达,路径更短,视觉上更自然。 +- **代价**:需为斜向定义 `outPass`/`inPass` 掩码语义(`PassBit` 仅 4 位,需扩展枚举与图块数据结构);须修改 `canPass` 对斜向的直接放行行为,影响 L0/L1/L3 多处判定语义与既有存档数据;影响面大,建议不纳入本阶段。 + +**推荐:仅 4 正交向**。待拍板。 + +## 7. 边界守卫与损失值守卫要求(威胁缓解 T-02-01 / T-02-02) + +落点为本草案声明的接口行为,实现于 02-02/02-03: + +- **图构建与搜索入口(T-02-01)**:`IMapLayer.inMap` 边界守卫 + `isNil` 判空(楼层 id、图层、事件层);非法输入(越界坐标、缺失楼层、畸形掩码)→ `logger.warn(新数字码)` 后返回空路径,不抛异常、不死循环。 +- **损失值守卫(T-02-02)**:自定义损失函数返回非有限数(NaN/Infinity)或负数 → `logger.warn(新数字码)` 并按默认损失 1 处理,保证 Dijkstra 非负权不变式。 +- 新日志码在 `packages/common/src/logger.json` 集中登记:**新 error 码从 65 起、新 warn 码从 173 起**(本阶段一次性登记,避免撞码)。 + +## 拍板记录(待用户填写) + +> 用户逐项给出结论(含「按草案原样」),或直接编辑上文草案后回复 approved;执行者将逐字应用修改并在此记录。 + +1. **接口签名清单**(主接口命名、方法签名、注入槽位命名、损失 / 回退 / 谓词函数类型签名):待拍板 +2. **文件归属层**(L2/L3 文件放置提案或调整):待拍板 +3. **P1 修复 go/no-go**(mover.ts:651 条件是否确认为缺陷并授权改为 `||`;若否,请给出替代坐标回写语义):待拍板 +4. **D-08 OnTouch 派发**(方案 A 直派 executor / 方案 B 撞击步):待拍板 +5. **打断时序**(选项 1 stop 后 await 兑现 / 选项 2 onEnd 回调驱动):待拍板 +6. **图方向性**(仅 4 正交向 / 含 8 向):待拍板