- 拍板记录节完整登记用户六项结论(接口签名 types.ts user-owned、文件归属、P1 go、D-08 触发语义、打断选项 1、4 向) - 各节 DRAFT/待拍板标注更新为已拍板状态
21 KiB
寻路系统接口草案(已拍板)
状态:已拍板(2026-09-09) —— 六项决策全部经用户拍板,结论逐字记录于文末「拍板记录」节;02-02/02-03 以本文档 + 拍板记录为执行依据(D-07)。 接口事实源:
packages-user/data-system/src/pathfinding/types.ts由用户亲自编写(user-owned)——02-02 与 02-03 不得创建或重写该文件,所有实现以用户编写的 types.ts 为准。 依据:02-CONTEXT.md 决策 D-01…D-11、02-RESEARCH.md、02-PATTERNS.md;绑定面接口逐字引用既有代码,未做任何修改。 草案产出:2026-09-09(02-01 Task 1)。拍板完成:2026-09-09(02-01 Task 3,用户逐项拍板)。
0. 范围声明(D-11)
本阶段只交付数据端移动入口:寻路系统提供供渲染端后续调用的寻路 / 移动 API,全部逻辑可在 Node 环境独立验证。渲染端点击接线(点击拾取、不可达格点击反馈等 UI 行为)属 Phase 4 渲染适配,本阶段不做任何接线(D-11)。
1. 接口签名草案
拍板结论:本节签名为草案参考;最终接口签名以用户亲自编写的
packages-user/data-system/src/pathfinding/types.ts为准(user-owned),02-02/02-03 不得创建或重写该文件。 以下类型与命名沿用草案形态,命名以用户 types.ts 实际内容为准。绑定面直接引用既有接口 (IObjectMovable、IMapLayer、ITileLocator、ObjectMoveStep、EventTrigger),不重定义。 命名 / 注释 / 类型遵循 dev.md:接口I前缀大驼峰、函数类型单独type、 对象类型单独interface、jsDoc 中文注释、接口方法间空行。
import { ITileLocator } from '@motajs/common';
import { IHeroMoveTopHandler, IMapLayer, IMapState } from '@user/data-base';
import { FaceDirection, IObjectMovable } from '@user/data-common';
/**
* 寻路损失函数类型(已拍板,签名以用户 types.ts 为准)
* 计算从一格移动至相邻一格的损失,默认实现为每格损失 1(D-01)
* @param from 移动起点坐标
* @param to 移动终点坐标
* @returns 非负有限损失值,非法值按守卫规则处理(见第 7 节)
*/
type IPathfindingCostFunction = (from: ITileLocator, to: ITileLocator) => number;
/**
* 通行性谓词函数类型(已拍板,签名以用户 types.ts 为准)
* 判定在指定楼层从一格向指定方向移动一格是否可通行,
* 语义须与 DefaultHeroMoveTopImpl.canPass 一致(事件层永远参与判定,
* 其余层仅当 pass.onlyEvents 为真时参与)。L2 不 import L3,
* 该谓词由 L3 接线时注入,L2 只持有此函数类型槽位
* @param handler 通行性检查对象,与 IHeroMoveTopImpl.canPass 入参一致
*/
type IPathfindingPassPredicate = (handler: IHeroMoveTopHandler) => boolean;
/**
* 瞬移回退策略函数类型(已拍板,签名以用户 types.ts 为准)
* 由该函数决策瞬移是否回退为逐步寻路(D-05)
* @param path 完整路径坐标序列,含起点
* @param arrivals 瞬移逐步兑现时每一步会到达的位置
* @returns `true` 表示需要回退为逐步寻路
*/
type IPathfindingFallbackPolicy = (
path: Readonly<ITileLocator[]>,
arrivals: Readonly<ITileLocator[]>
) => boolean;
export interface IPathfindingSystem {
/**
* 绑定寻路所用的地图状态对象(已拍板,签名以用户 types.ts 为准)
* 用于按楼层 id 获取楼层与事件层;未绑定时寻路告警并返回空路径
* @param maps 地图状态对象,传入 `null` 解绑
*/
useMapState(maps: IMapState | null): void;
/**
* 绑定构建有向图所用的地图图层(已拍板,签名以用户 types.ts 为准,D-03)
* 通常绑定事件层;图仅包含从当前位置可达的位置(D-02)
* @param layer 地图图层对象,传入 `null` 解绑
*/
useMapLayer(layer: IMapLayer | null): void;
/**
* 绑定寻路移动对象(已拍板,签名以用户 types.ts 为准,D-03)
* 可绑定勇士位置或任意 `IObjectMovable`(如动态图块、跟随者)
* @param movable 移动对象,传入 `null` 解绑
*/
useMovable(movable: IObjectMovable | null): void;
/**
* 注入自定义损失函数(已拍板,签名以用户 types.ts 为准,D-01)
* 未注入时使用默认实现:每格损失 1
* @param cost 损失函数,传入 `null` 恢复默认
*/
useCostFunction(cost: IPathfindingCostFunction | null): void;
/**
* 注入通行性谓词(已拍板,签名以用户 types.ts 为准)
* 未注入时使用 PassBit 掩码默认判定(不含多层 onlyEvents 语义);
* 推荐由 L3 注入与 DefaultHeroMoveTopImpl.canPass 同源的谓词,
* 保证图边判定与逐步移动判定单一事实源
* @param predicate 通行性谓词,传入 `null` 恢复默认
*/
usePassPredicate(predicate: IPathfindingPassPredicate | null): void;
/**
* 注入瞬移回退策略(已拍板,签名以用户 types.ts 为准,D-05)
* 未注入时使用默认实现:路径上存在事件即回退为逐步寻路
* (事件可能改变状态,瞬移会跳过副作用)
* @param policy 回退策略函数,传入 `null` 恢复默认
*/
useFallbackPolicy(policy: IPathfindingFallbackPolicy | null): void;
/**
* 仅获取从当前位置至目标位置的最小损失路径(已拍板,签名以用户 types.ts 为准,D-06)
* 不产生任何移动
* @param target 目标坐标
* @returns 路径坐标序列(含起点与终点);
* 不可达或输入非法时为空数组(D-08 情况 2)
*/
getPath(target: ITileLocator): Readonly<ITileLocator[]>;
/**
* 逐步寻路至目标位置(已拍板,签名以用户 types.ts 为准,D-04 / D-09)
* 复用现有 hero mover 逐步执行,每步走 enter/leave/hit 钩子,
* 途经事件自然触发;有向图上逐步搜索即自然避障(D-02)
* @param target 目标坐标
* @returns 移动控制器;无法寻路、无路径或已有移动进行中时返回 `null`
*/
moveTo(target: ITileLocator): Readonly<IMoverController> | null;
/**
* 瞬移至目标位置(已拍板,签名以用户 types.ts 为准,D-04)
* 瞬移前经回退策略判定,判定需要回退则自动退为逐步寻路
* @param target 目标坐标
* @returns 移动控制器;无法寻路、无路径或已有移动进行中时返回 `null`
*/
teleportTo(target: ITileLocator): Readonly<IMoverController> | null;
/**
* 打断当前自动寻路(已拍板,签名以用户 types.ts 为准,D-10)
* 新的方向输入或新的寻路调用可随时打断并接管;
* 兑现时序已拍板:选项 1(见第 5 节)
*/
interrupt(): Promise<void>;
}
语义补充说明(已拍板):
- 不可达目标双语义(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. 文件归属提案(已拍板:按草案原样,types.ts 除外)
拍板结论:文件归属按草案原样(L2
packages-user/data-system/src/pathfinding/的 graph/system/index + L3packages-user/data-state/src/pathfinding/heroPathfinding.ts);唯一例外:types.ts由用户提供(见拍板记录第 1 项),02-02/02-03 不得创建或重写。
依据 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 派发两方案(已拍板:触发语义由用户指定,见拍板记录第 4 项)
拍板结论(用户原话):"如果是由 CannotIn 或 CannotOut 导致无法从一格到另一格,无论目标是不是 no pass,都不应该触发 hit,只有 CannotIn 和 CannotOut 允许到达,且目标位置是 no pass 时才触发。" —— 即 hit/OnTouch 的触发条件 = 通行掩码(CannotIn/CannotOut)允许到达 且 目标位置为 no-pass;掩码导致的不可达一律不触发。实现机制(直接派发 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-222commonTrigger形态——收集目标格点事件(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-141shouldHit);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) → 已拍板:用户指定触发语义(掩码允许到达 + 目标 no-pass 才触发;掩码不可达一律不触发),实现机制须与该语义一致——方案 A 直派 OnTouch 与该语义一致,可按 A 落地;方案 B 的 hit 链(inPass=0 走 CannotMove)不得作为掩码不可达时的触发路径。
4. P1 缺陷调查:mover.ts:651 坐标回写条件(&& 疑为 ||)
拍板结论:go —— 用户确认
&&为缺陷,授权 02-02 将 651 行条件改为||语义,并翻绿 mover.test.ts 的 4 个回归用例(见拍板记录第 3 项)。
逐字引用(packages-user/data-common/src/common/mover.ts:648-654,条件位于 651 行):
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);
}
推理链:
- 逐步寻路(D-09)产生的每一步都是
ObjectMoveType.Dir正交步,只改变 x 或 y 其中一轴。 - 正交步后
loc仅一轴与this.tile不同 →this.tile.x !== loc.x && this.tile.y !== loc.y恒为 false。 setPos不被调用 →this.tile.x/y永不更新(渲染端现走 legacy mover,新 mover 尚无多步消费者,故此缺陷未被现有功能暴露)。- 多步寻路自第二步起,以陈旧原点计算 nextLoc(每步的移动结果基于上一步回写失败后的旧坐标),整条路径走崩。
onStepSettled收到的before/curr与实际位置不符,leave/enter 事件派发坐标错误。
修复提案:651 行 && → ||(任一轴变化即回写坐标)。斜向步与传送步双轴均变化,|| 下行为与现状一致;正交步新增回写,属缺陷修复而非行为变更(回归用例已铺设于 mover.test.ts,skip 状态,修复后翻绿)。
go/no-go 问题:
- go——确认
&&为手误,授权 02-02 将该行改为||并翻绿回归用例; - no-go——若该条件属有意设计(如「仅双轴同时变化才回写」),请用户给出替代坐标回写语义;否则逐步寻路无法成立(每多走一步,位置误差累积一格)。
5. 打断时序两选项(D-10,已拍板:选项 1)
拍板结论:选项 1 ——
stop()后 await 兑现,再查新位置起新寻路(无竞态,最多延迟一步)。
既有机制约束(逐字):
mover.start()在移动中返回null(mover.ts:669-670if (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(时序正确性优先;一步时延在魔塔节奏下可接受)。已拍板:选项 1。
6. 图方向性选项(已拍板:仅 4 正交向)
拍板结论:仅 4 正交向(与 PassBit 四位掩码一致;不含斜向)。
选项 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 正交向。已拍板:仅 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 起(本阶段一次性登记,避免撞码)。
拍板记录(2026-09-09,用户逐项拍板,原话忠实整理)
以下为用户对六项决策的逐项结论(原话整理,忠实记录);执行者已按结论同步标注上文各节。02-02/02-03 以本记录为执行依据。
- 接口签名清单(主接口命名、方法签名、注入槽位命名、损失 / 回退 / 谓词函数类型签名):用户将亲自编写
packages-user/data-system/src/pathfinding/types.ts—— 该文件是接口事实源(user-owned)。02-02 与 02-03 不得创建或重写该文件;所有实现以用户编写的 types.ts 为准。 - 文件归属层(L2/L3 文件放置提案或调整):按草案原样(L2
packages-user/data-system/src/pathfinding/的 graph/system/index + L3packages-user/data-state/src/pathfinding/heroPathfinding.ts),但 types.ts 由用户提供(见第 1 项)。 - P1 修复 go/no-go(mover.ts:651 条件是否确认为缺陷并授权改为
||):go —— 授权 02-02 将packages-user/data-common/src/common/mover.ts:651的条件this.tile.x !== loc.x && this.tile.y !== loc.y修复为||语义,并翻绿 mover.test.ts 的 4 个回归用例。 - D-08 OnTouch 派发(方案 A 直派 executor / 方案 B 撞击步)——用户原话:"如果是由 CannotIn 或 CannotOut 导致无法从一格到另一格,无论目标是不是 no pass,都不应该触发 hit,只有 CannotIn 和 CannotOut 允许到达,且目标位置是 no pass 时才触发。" —— 即:hit/OnTouch 的触发条件 = 通行掩码(CannotIn/CannotOut)允许到达 且 目标位置为 no-pass;掩码导致的不可达一律不触发。实现机制(直接派发 OnTouch 或撞击步)必须与该触发条件语义一致。
- 打断时序(选项 1 stop 后 await 兑现 / 选项 2 onEnd 回调驱动):选项 1 ——
stop()后 await 兑现,再查新位置起新寻路(无竞态,最多延迟一步)。 - 图方向性(仅 4 正交向 / 含 8 向):仅 4 正交向(与 PassBit 四位掩码一致;不含斜向)。