template/.planning/phases/02-pathfinding/02-RESEARCH.md

42 KiB
Raw Blame History

Phase 2: 寻路系统 (Pathfinding) - Research

Researched: 2026-09-09 Domain: 数据端寻路系统 — 有向图最小损失寻路 + 逐步/瞬移移动驱动(mota-ts 引擎, pnpm monorepo, TypeScript ESNext) Confidence: HIGH(代码接缝全部本会话实地读取;外部依赖为零)

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: 寻路目标是两点之间损失最小的路径;系统允许调用方自定义损失函数,默认实现为每格损失 1
  • D-02: 算法流程:(1) 将地图转换为有向图——仅包含从当前位置可以到达的位置,由于存在单向通行位置,图必须是有向的;(2) 对图中每个图块用自定义损失函数计算损失;(3) 用算法找出损失最少的路径并返回
  • D-03: 寻路系统具备足够扩展性,允许绑定 IMapLayer(地图)与 IObjectMovable(移动对象)两类对象
  • D-04: 系统提供两种移动方式:逐步寻路至目标点;瞬移至目标点。瞬移前经回退策略判定,判定需要回退则自动退为逐步寻路
  • D-05: 提供回退决策接口:接收一个策略函数,函数入参为寻路路径及每步会到达的位置,由该函数决策是否回退移动方式。默认策略实现为"路径上存在事件即回退"(事件可能改变状态,瞬移会跳过副作用)
  • D-06: 提供仅获取寻路路径的方法(不移动,只返回路径)
  • D-07: 接口设计流程:AI 先按现有接口设计理念起草接口草案,经用户修改拍板后才进入实现——planner 必须内置该 review 关卡(接口经用户确认前不得开始实现任务)
  • D-08: 寻路至不可达位置时分两种情况:(1) 目标本身是 no-pass 类图块且四周有坐标可以到达——移动到该相邻格,触发目标位置的 OnTouch 触发器(走 Phase 1 事件链路),勇士面朝目标位置;(2) 其他情况——忽略本次寻路移动,路径为空数组
  • D-09: 逐步寻路的执行复用现有 hero mover(DefaultHeroMoveTopImpl),每步走 enter/leave/hit hooks,途经事件自然触发(与 Phase 1 source-aware 事件派发一致)
  • D-10: 自动寻路途中可被玩家输入打断:新的方向键输入或新的点击立即打断并接管(魔塔惯例)
  • D-11: 本阶段仅关注数据端系统;玩家点击交互是渲染端内容,PATH-02 的点击触发接线延迟到 Phase 4 渲染适配

the agent's Discretion

  • 图的构建时机与缓存策略(每次寻路动态构建 vs 缓存)——数据端状态可变(敌人/门/道具),默认建议动态构建
  • 具体算法选型(满足"最小损失 + 有向图"语义即可,AI 决定)
  • 接口命名、签名与文件归属层的细节——在接口草案中提出,最终由用户拍板

Deferred Ideas (OUT OF SCOPE)

  • 移动端点击地图触发寻路的渲染端接线(点击拾取、不可达格点击反馈等 UI 行为)——属 Phase 4 渲染适配

Project Constraints (from AGENTS.md → dev.md)

AGENTS.md 指向 dev.md(本会话已完整读取)。硬性规范,planner 任务必须遵守:

  • 模块原则:模块无顶层副作用;初始化用 createXxx() 工厂;不转发导出;禁循环引用;禁 import type(全部普通引入)
  • 类型规范:禁非必要 any;类成员显式类型;尽量不用 as,绝对禁止连续 as;对象类型单独开 interface;成员用接口类型而非类类型
  • 命名:接口 I 前缀 + 大驼峰;文件名小驼峰;私有成员不用下划线开头;不变常量全大写下划线分隔
  • 日志:错误/警告用 logger.error(数字码) / logger.warn(数字码),码集中注册于 packages/common/src/logger.json(error 已用 1–64,warn 已用 1–172 → 新 error 码从 65 起,新 warn 码从 173 起)[VERIFIED: packages/common/src/logger.json — 本会话以 node 脚本枚举全部键位确认]
  • 其他:禁字符串作键/标识符(用枚举);尽量不用 ?.(仅副作用调用与 Required 化两场景);不写 getter/setter;CRLF;jsDoc 中文注释 40–60 字符换行;测试文件每个 it 前必须有单行注释说明覆盖内容
  • 双端分离:数据端可在 Node 独立运行、可直接录像验证;数据端调用渲染端必须 Mota.r() 包裹(本阶段不应需要)

Phase Requirements

ID Description Research Support
PATH-01 引擎支持自动寻路 本文件给出完整实现接缝:图构建数据来源(IMapLayer 通行性掩码)、算法选型(Dijkstra 有向图)、移动执行复用(HeroMover + DefaultHeroMoveTopImpl)、瞬移/回退策略、不可达语义、打断机制
PATH-02 移动端点击地图触发自动寻路移动 本阶段仅交付数据端移动入口(供渲染端 Phase 4 调用的寻路/移动 API);点击拾取与接线为渲染端内容,本阶段只保证入口存在且可在 Node 下验证

Summary

本阶段在数据端(L0–L3 分层包内)新建一个寻路系统:把 IMapLayer 的通行性掩码转换成有向图(每条边 = 从 A 向方向 d 走一步,需 A 的 outPass 含 d 且 B 的 inPass 含 opposite(d)),在图上跑最小损失算法(推荐 Dijkstra,地图均为小网格,无需外部依赖),产出坐标路径。路径有两种消费方式:逐步执行(把路径翻译成 ObjectMoveType.Dir 步骤队列,复用 HeroMover + DefaultHeroMoveTopImpl 的 enter/leave/hit 事件链)与瞬移(mover.tp(),执行前经可注入的回退策略判定,默认"路径上有事件即回退为逐步")。不可达目标按 D-08 双语义处理。寻路过程可被打断(新寻路调用或显式 interrupt 入口),复用 IMoverController.stop()。

研究中最关键的发现:L0 基类 ObjectMover.moveProgress 中步后坐标回写条件疑似存在缺陷(&& 应为 ||,见 Common Pitfalls P1),正交单步移动后 setPos 永远不会被调用——当前渲染端尚走旧 legacy mover(client-modules 引用的 @user/data-state 的 HeroMover 在新数据端 barrel 中已无此导出),所以此缺陷未被现有功能暴露;而 D-09 逐步寻路是第一个串联多步 Dir 步骤的新消费者,会立刻踩中。planner 必须把"向用户确认并修复该 L0 缺陷"作为实现前置任务。

Primary recommendation: 接口草案先行(D-07 review 关卡置顶)→ 修复 L0 && 缺陷(用户确认后)→ 纯寻路核心(L2,可注入通行性谓词与回退策略,默认策略复用 DefaultHeroMoveTopImpl 的判定语义)→ L3 hero 接线与 vitest 全覆盖。

Architectural Responsibility Map

Capability Primary Tier Secondary Tier Rationale
有向图构建(通行性掩码 → 邻接) data-system (L2) — 纯数据变换,不存档,只读 L1 的 IMapLayer
最小损失算法 + 仅取路径 API (D-01/D-02/D-06) data-system (L2) — 纯算法,Node 可验证,无状态
回退策略接口与默认实现 (D-05) data-system (L2) data-state (L3) 策略函数注入到 L2 系统;默认策略读取路径上事件(L1 数据),瞬移回退执行在 L3
逐步寻路执行 (D-09) data-state (L3) data-common (L0) 复用 HeroMover(L0 队列机制)+ DefaultHeroMoveTopImpl(L3 判定与事件派发)
瞬移执行 (D-04) data-state (L3) data-common (L0) mover.tp() 步骤类型已存在(ObjectMoveType.Teleport)
不可达目标 OnTouch (D-08) data-state (L3) data-system (L2) 走 Phase 1 source-aware 派发链(IGameEventExecutor.execute),env 构造复用 moverImpl 模式
打断/接管 (D-10) data-common (L0) data-system (L2) IMoverController.stop() 已存在;寻路控制器封装"停旧起新"
渲染端点击接线 (PATH-02 UI) client-modules (渲染端) — Phase 4,本阶段只提供数据端入口

Standard Stack

Core

Library Version Purpose Why Standard
TypeScript (repo ESNext) 已有 实现语言 项目既有,无新依赖
Dijkstra(自写,~40 行) — 有向图最小损失搜索 非负自定义损失下保证最小损失;魔塔楼层 ≤ 数百格,O(V²) 简单实现足够 [ASSUMED:算法通识,未外部验证——但符合 D-01/D-02 语义,风险极低]
vitest 4.0.18(已装) 单元测试 项目既有测试栈,co-located *.test.ts [VERIFIED: 根目录 npx vitest --version → 4.0.18;既有 6 个测试文件]

Supporting

Library Version Purpose When to Use
lodash-es isNil 已有 判空(DefaultHeroMoveTopImpl 同款) 边界/楼层数据判空
@motajs/common logger 已有 数字错误码日志 新码 error≥65 / warn≥173

Alternatives Considered

Instead of Could Use Tradeoff
Dijkstra A*(曼哈顿启发) 大图更快,但引入启发函数设计复杂度;魔塔网格小,Dijkstra 实现更简单且严格满足"最小损失"语义。不采用
BFS — 仅当损失恒为 1 时正确;自定义损失函数下不保证最小损失。不采用
外部寻路库(如 pathfinding/astar-typescript) — 数据端包不引第三方运行时依赖(现有 data-* 包仅依赖 workspace 内部 + lodash-es),且需支持有向图与自定义损失。不采用

Installation: 无新依赖。不执行任何安装。

Package Legitimacy Audit

本阶段不安装任何外部包(纯内部代码 + 既有 vitest),无 legitimacy 检查对象。

Packages removed due to [SLOP] verdict: none Packages flagged as suspicious [SUS]: none

Architecture Patterns

System Architecture Diagram

渲染端(Phase 4) ──点击/方向输入──▶ 数据端寻路入口 (L3 wiring)
                                        │
                                        ▼
                        ┌───────────────────────────────┐
                        │  PathfindingSystem (L2)       │
                        │  - 绑定 IMapLayer             │
                        │  - 绑定 IObjectMovable        │
                        └───────────────────────────────┘
                              │               ▲
                 (1) 构建有向图│               │(4) 注入
                              ▼               │
   ┌──────────────────────────────────┐   ┌─────────────────────────┐
   │ IMapLayer.getLocationData(x,y)   │   │ 通行性谓词(可注入)        │
   │  .static.raw() → pass:           │   │ 默认复用 PassBit 掩码语义 │
   │    outPass/inPass (方向位掩码)     │   │ (=DefaultHeroMoveTopImpl │
   │    eventPass (撞击终止格)          │   │  .canPass 判定)          │
   └──────────────────────────────────┘   └─────────────────────────┘
                              │
                 (2) 最小损失搜索 (Dijkstra, 自定义损失函数, 默认每格 1)
                              ▼
                        路径: ITileLocator[]
                              │
              (3) 移动方式决策 ─┴─▶ D-06 仅返回路径(结束)
                              │
                    ┌─────────┴──────────┐
                    ▼                    ▼
             回退策略(可注入)          回退策略判定"需回退"
             = false → 瞬移            → 逐步寻路
                    │                    │
                    ▼                    ▼
            mover.tp(x,y)          逐步翻译为 ObjectMoveType.Dir 队列
                    │                    │
                    └─────────┬──────────┘
                              ▼
                  HeroMover.start() → IObjectMoverController
                              │
              每步: onStepStart(canPass/shouldHit判定)
                   → onStepEnd(hit→OnTouch派发/stop)
                   → onStepSettled(leave→enter 派发)
                              │
                              ▼
                 完成回调 / 被新输入打断(controller.stop)

不可达分支(D-08):目标 no-pass 且存在可达相邻格 → 路径截断至相邻格 + 面朝目标 + OnTouch 派发(走 IGameEventExecutor.execute,构造 IBlockEventEnv,trigger=EventTrigger.OnTouch);否则返回空数组且不移动。

packages-user/data-system/src/
└── pathfinding/
    ├── types.ts        # 接口定义(草案经用户拍板后落定)
    ├── graph.ts        # IMapLayer → 有向图构建
    ├── system.ts       # PathfindingSystem(绑定/搜索/移动方式决策)
    └── index.ts
packages-user/data-state/src/
└── pathfinding/
    └── heroPathfinding.ts   # L3 接线:绑定 hero mover + 默认通行性谓词(复用 DefaultHeroMoveTopImpl 语义)
packages-user/data-system/src/pathfinding/xxx.test.ts   # co-located 测试
packages-user/data-state/src/heroPathfinding.test.ts

(文件归属层是草案内容,最终由用户拍板——见 D-07;上表为研究建议。)

分层归属论证(防循环依赖) [VERIFIED: packages-user/data-state/package.json、data-system/package.json 依赖声明 + data-system/src/types.ts:6-11]:

  • data-common(L0) → 无依赖;data-base(L1) → L0;data-system(L2) → L0+L1;data-state(L3) → L0+L1+L2
  • IMapLayer 定义在 data-base(L1),IObjectMovable 定义在 data-common(L0)→ L2 可同时引用二者,寻路核心放 data-system 合法
  • DefaultHeroMoveTopImpl(通行性/事件判定)在 data-state(L3);L2 系统若直接 import 会反向依赖 L3 → 不可行。解法:L2 接受可注入谓词/策略函数(与项目既有 useTopImplementation / useSorter / useCalculator / useStore 注入惯例完全一致),L3 接线时注入

Pattern 1: 可注入策略(项目既有惯例)

What: 系统持有策略槽位,useXxx() 注入;不注入时使用内建默认或空实现。 When to use: 通行性谓词、回退策略(D-05)。 Example(既有代码,逐字):

// Source: packages-user/data-base/src/hero/mover.ts:72-74(同款惯例还有
// data-system/event/system.ts useStore、combat useCalculator/useReducer/useSorter)
useTopImplementation(impl: IHeroMoveTopImpl | null): void {
    this.topImpl = impl;
}

Pattern 2: 有向边判定(PassBit 掩码语义)

What: 从 A 向方向 d 的边成立 = A 可离开(outPass 含 d 位)且 B 可进入(inPass 含 opposite(d) 位)。单向通行天然由掩码不对称产生。 Example(既有判定逻辑,逐字摘自 DefaultHeroMoveTopImpl):

// Source: packages-user/data-state/src/hero/moverImpl.ts:85-104
const opposite = face.opposite(direction);
const leaveMask = this.directionToPassBit(direction);
const enterMask = this.directionToPassBit(opposite);

let canLeave = true;
let canEnter = true;

// 判断事件层
const curr = event.getLocationData(x, y);
const next = event.getLocationData(nx, ny);
const currRaw = curr?.static.raw();
const nextRaw = next?.static.raw();
if (currRaw) {
    canLeave = !!(leaveMask & currRaw.pass.outPass);
}
if (nextRaw) {
    canEnter = !!(enterMask & nextRaw.pass.inPass);
}

注意 canPass 还包含多层逻辑:事件层永远参与判定;其余层仅当该格 pass.onlyEvents 为真时参与(moverImpl.ts:107-122)。图构建必须完整复刻该语义,或(推荐)通过注入谓词直接复用同一实现。

Pattern 3: 移动队列与打断(L0 既有机制)

// Source: packages-user/data-common/src/common/mover.ts:142-164(接口节选,逐字)
export interface IMoverController {
    /** 本次移动是否已经全部完成 */
    done: boolean;
    /** 当本次移动结束时兑现 */
    onEnd: Promise<void>;

    /**
     * 向当前移动队列末尾追加步骤
     * @param steps 要追加的步骤列表
     */
    push(...steps: Readonly<ObjectMoveStep>[]): void;

    /**
     * 停止当前移动,在当前步骤完成后兑现
     */
    stop(): Promise<void>;
}
  • mover.start() 已在移动中时返回 null(mover.ts:669-670 if (this.moving) return null;)→ 打断接管必须先 stop 旧移动再 start 新移动 [VERIFIED]
  • stop() 在当前步完成后兑现 → "立即打断"语义 = 调 stop 后不 await 再开新寻路(新寻路从新位置起算会与正在完成的步冲突时,应 await 或在 onEnd 后启动;接口草案中定夺)

Anti-Patterns to Avoid

  • 在 L2 import L3(data-system → data-state):造成循环依赖,违反 dev.md;用注入解耦
  • 在步骤钩子里 await controller.onEnd:HeroMover.onStepEnd 既有注释明示会卡死("这里不能 await,因为其 Promise 会在当前步结束后兑现,如果 await 就会卡住",mover.ts data-base:227-228)
  • 把图缓存为长期结构:数据端状态可变(门/敌人/动态图块),CONTEXT 已建议默认动态构建
  • as 断言 / 字符串键 / 顶层副作用:dev.md 硬禁
  • 在数据端写 DOM/渲染逻辑:双端分离,Node 回放必须可跑

Don't Hand-Roll

Problem Don't Build Use Instead Why
逐步移动执行 新写移动循环 HeroMover(data-base)+ ObjectMover 队列(data-common) 队列/stop/hit 语义已完备,D-09 明锁复用
通行性判定 重写一套掩码逻辑 注入 DefaultHeroMoveTopImpl 同款谓词(或抽公共判定) 多层 onlyEvents 语义易漏
事件派发(OnTouch/OnEnter/OnLeave) 直接调 executor 之外的新链路 复用 IHeroMoveTopImpl.enter/leave/hit(内部走 commonTrigger → executor.execute) Phase 1 已验证 source-aware 顺序
朝向→坐标增量 手写 switch IFaceHandler.movement(dir) / opposite(dir) Dir4/Dir8 已注册于 FaceManager
朝向→PassBit 映射 新写映射 复用 moverImpl directionToPassBit 语义(或抽公共) 单一事实源
日志 console.* logger.warn/error(数字码) + logger.json 注册 项目规范

Key insight: 本阶段约 80% 的"难点"(移动、事件、打断)已由 Phase 1 与 L0 mover 完成;寻路系统的增量主要是图构建 + 最小损失搜索 + 方式决策三块纯逻辑。手写新移动循环是最大的返工风险。

Key Code Seams (verified this session)

以下离散值与接缝全部为本会话 Read 逐字读取,供 planner 直接引用:

PassBit(方向位掩码,有向图的边权基础) [VERIFIED: packages-user/data-common/src/store/types.ts:27-36]

export const enum PassBit {
    /** 上方向掩码 */
    Up = 0b0001,
    /** 右方向掩码 */
    Right = 0b0010,
    /** 下方向掩码 */
    Down = 0b0100,
    /** 左方向掩码 */
    Left = 0b1000
}

FaceDirection(含 4 斜向;斜向在 canPass 中直接放行) [VERIFIED: packages-user/data-common/src/common/types.ts:1-11]

export const enum FaceDirection {
    Unknown,
    Left,
    Up,
    Right,
    Down,
    LeftUp,
    RightUp,
    LeftDown,
    RightDown
}

HeroMoveCode(单步判定结果,逐步寻路复用) [VERIFIED: packages-user/data-base/src/hero/types.ts:278-287]

export const enum HeroMoveCode {
    /** 正常移动 */
    Step,
    /** 移动被停止 */
    Stop,
    /** 不能移动,并触发目标格撞击触发器 */
    Hit,
    /** 不能移动,并触发目标格无法进入触发器 */
    CannotMove
}

ObjectMoveType(寻路翻译目标步骤类型;逐步→Dir,瞬移→Teleport) [VERIFIED: packages-user/data-common/src/common/mover.ts:14-31]

export const enum ObjectMoveType {
    /** 绝对方向步,同步更新移动方向与朝向 */
    Dir,
    /** 绝对方向步,显式指定朝向 */
    DirFace,
    /** 速度步 */
    Speed,
    /** 纯转向步 */
    Face,
    /** 特殊步,如前进或后退 */
    Special,
    /** 动画方向步 */
    AnimDir,
    /** 传送步 */
    Teleport,
    /** 跳跃步 */
    Jump
}

EventTrigger.OnTouch(D-08 派发所用触发器) [VERIFIED: packages-user/data-common/src/event/types.ts:7-17] — OnTouch 枚举值为 1(None=0,OnEnter=2,OnLeave=3),注释明确"当玩家触碰指定图块时触发,如果直接走入则不触发"。

shouldHit / eventPass 语义 [VERIFIED: packages-user/data-state/src/hero/moverImpl.ts:127-141]

shouldHit(handler: IHeroMoveTopHandler): boolean {
    // …取 eventLayer.getLocationData(nx, ny)?.static.raw()
    if (!nextRaw) return false;
    return !nextRaw.eventPass;
}

配合 ITileRawData.eventPass 注释("当为 false 时,玩家会通过撞击触发图块的触发器,当为 true 时,玩家会通过走上去触发图块的触发器。类似于旧样板的 noPass")[VERIFIED: packages-user/data-common/src/store/types.ts:58-62]。执行顺序:HeroMover.onStepStart 先 canPass,通过后才 shouldHit(data-base/hero/mover.ts:183-190)→ 撞击触发(Hit→OnTouch)只在"可通行但 eventPass=false"的格上发生;inPass=0 的真 no-pass 格走 CannotMove → cannotEnter(),而 cannotEnter 当前为空实现(moverImpl.ts:259-263)。这是 D-08 的关键语义缺口(见 Pitfalls P2)。

IBlockEventEnv(D-08 直派 OnTouch 时需要构造的环境) [VERIFIED: packages-user/data-base/src/map/types.ts:40-62]

export interface IBlockEventEnv extends IDataCommonExtended {
    /** 事件类型 */
    readonly type: BlockEventType;
    /** 本次事件的触发器类型 */
    readonly trigger: EventTrigger;
    /** 触发事件时玩家的位置 */
    readonly heroLocator: Readonly<ITileLocator>;
    /** 触发事件时触发者的位置,有可能不存在 */
    readonly triggerLocator: Readonly<ITileLocator> | null;
    /** 触发事件的图块,有可能不存在 */
    readonly tile: IReadonlyTileBase | null;
    /** 触发事件的图层,有可能不存在 */
    readonly layer: IMapLayer | null;
    /** 触发事件的地图,有可能不存在 */
    readonly map: IGameMap | null;
}

export interface IGameEventInvocation {
    /** 事件在 `IGameEventStore` 中的 id */
    readonly id: string;
    /** 此次调用对应的真实来源环境 */
    readonly env: IBlockEventEnv;
}

BlockEventType 逐字值:CommonEvent=0, PointEvent=1, TileEvent=2 [VERIFIED: packages-user/data-base/src/map/types.ts:26-33]。

图构建数据入口 [VERIFIED: packages-user/data-base/src/map/types.ts:514-612(IMapLayer)、249-258(ILayerLocation)]

  • IMapLayer.getLocationData(x, y): ILayerLocation | null;ILayerLocation = { locator, tile, dynamics, static: IStaticTile }
  • IReadonlyTileBase.raw(): ITileRawData | null(含 pass.outPass/inPass/onlyEvents、eventPass、事件视图)
  • IMapLayer.inMap(x, y)、width/height、map.eventLayer、map.layerList、map.getLayerByAlias(alias)

IObjectMovable 绑定面 [VERIFIED: packages-user/data-common/src/common/mover.ts:47-64]

export interface IObjectMovable {
    /** 当前横坐标 */
    readonly x: number;
    /** 当前纵坐标 */
    readonly y: number;

    /**
     * 设置对象位置
     * @param x 横坐标
     * @param y 纵坐标
     */
    setPos(x: number, y: number): void;

    /**
     * 获取当前朝向
     */
    getCurrentFaceDirection(): FaceDirection;
}

IDynamicTile 同样 extends ITileBase, IObjectMovable 且带 mover: IObjectMover<IDynamicTile> [VERIFIED: data-base/map/types.ts:200-209] → D-03 绑定 IObjectMovable 可同时服务勇士与动态图块(如跟随者/NPC)。

L3 现有接线点(寻路系统照此接入 CoreState) [VERIFIED: packages-user/data-state/src/core.ts:235-238]

// 勇士顶层初始化
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);

D-08 面朝目标:mover.face(dir)(纯转向步,ObjectMoveType.Face)或 mover.setFaceDir(dir);IHeroState 另有 getLocation(): IFacedTileLocator [VERIFIED: data-base/hero/types.ts:835]。

Common Pitfalls

Pitfall 1(阻断级): L0 ObjectMover 步后坐标回写条件疑似错误(&& 应为 ||)

What goes wrong: moveProgress 中,单步结束后仅当 x 与 y 同时变化才回写坐标 [VERIFIED: packages-user/data-common/src/common/mover.ts:648-654]:

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) {
    this.tile.setPos(loc.x, loc.y);
}

正交步(上下左右,逐步寻路唯一会产生的步骤类型)只改变一轴 → 条件恒 false → setPos 不调用 → tile.x/y 永不更新。逐步寻路第 2 步起 createHandler 以陈旧原点计算 nextLoc,整条路径走崩;onStepSettled 的 leave/enter 事件坐标也与实际不符。 Why it happens: 疑为手误(意图应为"任一轴变化即回写",即 ||)。 How to avoid: planner 必须把"用户确认 + 修复该行 + L0 回归单测(正交/斜向/传送步的 setPos 回写)"列为实现前置任务(属 L0 共享代码 + D-07 用户拍板文化,不可静默修复)。 Warning signs: 单测里 hero 连走两步后坐标不动;事件 env 中 heroLocator 与 step 计划不一致。 为何现在才暴露: 渲染端现走 legacy mover——client-modules/src/action/move.ts import { HeroMover, IMoveController } from '@user/data-state',而 data-state barrel 已不导出这两个名字 [VERIFIED: data-state/src/index.ts 仅有 enemy/hero(moverImpl)/core/ins/shared/types;该 import 处于失效状态,与 STATE.md 记录的"仓库级类型门禁存在既有诊断"一致]。新 HeroMover(data-base)尚无消费者串联多步 Dir,故未踩中。

Pitfall 2: D-08 的 OnTouch 与现有 Hit/CannotMove 语义不衔接

What goes wrong: D-08 要求"移动到相邻格 + 触发目标 OnTouch"。但现有 mover 链路中:Hit(→topImpl.hit()→OnTouch 派发)只在 canPass=true 且 eventPass=false 时发生;inPass=0 的真 no-pass 格走 CannotMove → cannotEnter(),而 cannotEnter 是空实现("新事件触发器没有无法进入的对应项,保留空实现以满足移动接口")[VERIFIED: moverImpl.ts:259-263]。 How to avoid: 两种方案进接口草案由用户拍板:(a) 寻路系统在到达相邻格后直接构造 IGameEventInvocation(trigger=OnTouch, heroLocator=相邻格, triggerLocator=目标格)调 executor.execute——绕开 mover 撞击路径,语义直给;(b) 追加一个朝向目标的"撞击步"复用 hit 链——但真 no-pass 格会走 CannotMove 而非 Hit,方案 (b) 对 inPass=0 目标不生效。推荐 (a)。 Warning signs: 单测里撞墙 OnTouch 不触发。

Pitfall 3: eventPass=false 的格子是"终点格"而非"途经格"

What goes wrong: canPass 通过但 shouldHit 为真的格(eventPass=false),走进去会 Hit 并 stop——图构建时若把它当普通可通行节点,路径会"穿过"它而实际移动在半路停止。 How to avoid: 图构建时把此类格标记为终端节点:路径可以以它结尾,不允许作为中间节点。与 D-08 case (1) 的 no-pass 目标语义统一处理。

Pitfall 4: 单向通行的方向性遗漏

What goes wrong: 只查目标格 inPass 或只查当前格 outPass,会把单向门当双向。 How to avoid: 每条边同时验证 outPass(source) & dirBit 与 inPass(dest) & oppositeBit(Pattern 2 逐字语义);单测必须有"A→B 可行、B→A 不可行"的用例(D-02 明示存在单向通行位置)。

Pitfall 5: 打断竞态

What goes wrong: mover.start() 移动中返回 null;stop() 在当前步完成后才兑现。若"打断后立即 start"不处理时序,新移动可能被旧队列污染(start() 返回 null 静默失败)。 How to avoid: 寻路系统持有当前 controller 引用;接管流程 = clear() 新队列 + stop() 旧移动 → 等待兑现(或 onEnd.then)→ 查询新位置 → 计算并启动新路径。注意 HeroMover.onStepEnd 内部在 CannotMove/Stop/Hit 时也自行 controller.stop()——寻路控制器要能识别"移动已自然终止"。

Pitfall 6: 测试环境桩

What goes wrong: @motajs/common logger 模块加载时有 DOM 代码(document.createElement),由 main.replayChecking 守卫。 How to avoid: 沿用既有测试桩模式:vi.stubGlobal('main', { replayChecking: true }) + Map.prototype.getOrInsertComputed polyfill + 动态 import [VERIFIED: eventPath.test.ts:18-40、eventDispatch.test.ts:19-33 逐字同款]。

Pitfall 7: 日志码冲突

What goes wrong: logger.json 码是顺序分配的全局注册表;撞码会覆盖语义。 How to avoid: 新 warn 从 173、新 error 从 65 起 [VERIFIED: node 枚举 logger.json 全键];建议本阶段统一在一个 PR/任务里登记。

Code Examples

路径 → 移动步骤翻译(草案示意,命名待用户拍板)

// Source: 接缝组合自 packages-user/data-common/src/common/mover.ts (IObjectMover API)
// 伪代码——接口签名与命名须经 D-07 用户拍板后才实现
function translatePath(
    path: Readonly<ITileLocator[]>,
    face: IFaceHandler<FaceDirection>
): ObjectMoveStep[] {
    const steps: ObjectMoveStep[] = [];
    for (let i = 1; i < path.length; i++) {
        const dx = path[i].x - path[i - 1].x;
        const dy = path[i].y - path[i - 1].y;
        // face.mapMovement() 返回 [FaceDirection, IFaceDescriptor],
        // 找到 (x,y) 增量匹配的方向;每次一步保证 canPass 逐格判定
        steps.push({ type: ObjectMoveType.Dir, move: dirOf(dx, dy) });
    }
    return steps;
}
// 执行:mover.push(...steps); const c = mover.start();  // c 为 IMoverController | null

逐步寻路接线(L3,示意)

// Source: 模式复刻自 packages-user/data-state/src/core.ts:236-237 与
// packages-user/data-base/src/hero/mover.ts (HeroMover.onStepStart 判定顺序)
// hero.location.mover 已绑定 DefaultHeroMoveTopImpl:
//   step → canPass ? (shouldHit ? Hit : Step) : CannotMove
//   Hit   → topImpl.hit()  → OnTouch 派发 → stop
//   Step  → onStepSettled → leave → enter → 途经事件自然触发

D-08 直派 OnTouch(示意)

// Source: env 构造逐字复刻 packages-user/data-state/src/hero/moverImpl.ts:206-222 (commonTrigger)
const env: IBlockEventEnv = {
    state: handler.state,
    type: BlockEventType.PointEvent, // 或 TileEvent,按目标格事件来源
    trigger: EventTrigger.OnTouch,
    heroLocator: adjacentLoc,        // 相邻格(勇士实际所在)
    triggerLocator: targetLoc,       // 目标格
    tile: targetStaticTile,          // 目标为 no-pass 类图块时的 static 引用
    layer: eventLayer,
    map
};
await executor.execute<void>([{ id, env }], { custom: {} });

State of the Art

Old Approach Current Approach When Changed Impact
legacy 路线系统(core.status.route / stopAutomaticRoute / clearContinueAutomaticRoute,见 data-state/src/legacy/move.ts 对旧 core 的调用) 新数据端寻路系统(本阶段):有向图 + 可注入策略 + IMoverController.stop 打断 Phase 2(本阶段) Phase 5 LEGACY 移植时删除旧 route 逻辑;新系统不依赖 legacy
旧 mover(data-state/src/legacy/move.ts HeroMover extends ObjectMoverBase,渲染端 action 仍在引用) 新 HeroMover(data-base/hero/mover.ts,topImpl 驱动,Phase 1 事件化) Phase 1 渲染端 action 的旧 import 已失效,Phase 4 重接;本阶段不动渲染端

Deprecated/outdated:

  • 渲染端 client-modules/src/action/move.ts 对 @user/data-state 的 HeroMover/IMoveController import:barrel 已无此导出 [VERIFIED]——Phase 4 范畴,本阶段仅记录。

Assumptions Log

# Claim Section Risk if Wrong
A1 Dijkstra 满足 D-01/D-02 且网格规模下 O(V²) 足够(魔塔楼层典型 13×13) Standard Stack 极低;若超大地图需 A*,接口不变仅换内部算法
A2 D-08 OnTouch 采用"直派 executor"方案(而非复用 mover 撞击步)为推荐方案 Pitfalls P2 / Code Examples 中;语义由用户拍板定夺,草案阶段必须显式列出两方案
A3 mover.ts:651 && 为手误,意图为 ` `
A4 打断接管采用"stop 旧移动 → 等待兑现 → 起新寻路"时序 Architecture Patterns 低;接口草案中定夺是否 await
A5 寻路核心放 data-system(L2)、hero 接线放 data-state(L3) Project Structure 低;归属层是 D-07 草案内容,用户可改

Open Questions

解决机制(已制度化闭环):以下全部待决问题经 02-01 Task 3 的 checkpoint:human-verify(D-07 用户拍板关卡) 解决——AI 起草接口草案并列出各选项,用户逐项拍板,结论记录于 02-INTERFACE-DRAFT.md 末尾「拍板记录」节。未经拍板不得实现(D-07)。

  1. D-08 OnTouch 派发路径 — RESOLVED-BY: D-07 拍板关卡(02-01 Task 3;方案 A 直派 executor / 方案 B 撞击步已在草案并列,附 P2 结论「方案 B 对 inPass=0 目标不生效」)
    • What we know: mover 的 Hit 链对 inPass=0 真墙格不生效(cannotEnter 为空实现)
    • What's unclear: 用户期望的 OnTouch 派发语义(直派 executor vs 改 hit 语义)
    • Recommendation: 接口草案中两方案并列,用户拍板(D-07 关卡内解决)
  2. L0 && 缺陷修复 — RESOLVED-BY: D-07 拍板关卡(02-01 Task 1 铺设 skip 回归测试、Task 3 关卡内 go/no-go;02-02 Task 1 按拍板结果执行修复并翻绿)
    • What we know: 逐字引用与推理链完整(Pitfall 1)
    • What's unclear: 是否属有意设计;是否允许本阶段修 L0
    • Recommendation: planner 首个任务设 checkpoint:human-verify,用户确认后修 + 回归测试
  3. 打断时序 — RESOLVED-BY: D-07 拍板关卡(02-01 草案选项 1/2 并列、Task 3 拍板;02-03 Task 3 按拍板时序实现)——stop 后 await 兑现再起新寻路,还是 onEnd 回调驱动
  4. 斜向(Dir8)寻路 — RESOLVED-BY: D-07 拍板关卡(02-01 草案第 (6) 节并列 4 向/8 向、Task 3 拍板)——FaceDirection 含 4 斜向且 canPass 对斜向直接放行(moverImpl.ts:67-75),但 PassBit 只有 4 位。图是否含斜向边(默认建议:仅 4 正交向,与 PassBit 掩码语义一致)

Environment Availability

Dependency Required By Available Version Fallback
Node.js 数据端运行/测试 ✓ v22.18.0(满足 ^20 || >=22) —
pnpm 包管理 ✓ 10.15.0(满足 >=10) —
vitest 单元测试 ✓ 4.0.18 —
vue-tsc pnpm check:type ✓(script 存在) — 仓库级存在既有诊断(STATE.md 延迟项),只对计划内文件收口
eslint pnpm lint:user ✓(script 存在) — —
madge pnpm check:circular ✓(script 存在,入口 src/main.ts) — —

Missing dependencies with no fallback: 无 Missing dependencies with fallback: 无

Validation Architecture

Test Framework

Property Value
Framework vitest 4.0.18(根 vite.config.ts 无 test 配置段 → vitest 默认发现 co-located *.test.ts)
Config file none(沿用根 vite.config.ts alias:@user/* → packages-user/*/src)
Quick run command npx vitest run packages-user/data-system/src/pathfinding packages-user/data-state/src
Full suite command npx vitest run(注意:pnpm test 为 watch 模式,CI 用 vitest run)

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
PATH-01-a 有向图构建正确(单向边、掩码方向性) unit npx vitest run packages-user/data-system/src/pathfinding ❌ Wave 0
PATH-01-b 最小损失 + 自定义损失函数(默认每格 1) unit 同上 ❌ Wave 0
PATH-01-c D-06 仅取路径 API(不移动) unit 同上 ❌ Wave 0
PATH-01-d D-04/D-05 瞬移 + 回退策略(默认"路径有事件即回退";注入自定义策略) unit 同上 ❌ Wave 0
PATH-01-e D-09 逐步执行:复用 HeroMover+topImpl,途经 OnEnter/OnLeave/OnTouch 派发 unit(fixture 复刻 eventDispatch.test.ts 模式) 同上 ❌ Wave 0
PATH-01-f D-08 双语义:相邻格+OnTouch+面朝 / 空数组忽略 unit 同上 ❌ Wave 0
PATH-01-g D-10 打断接管(stop 旧移动、新寻路起算) unit 同上 ❌ Wave 0
PATH-02 数据端移动入口存在且 Node 可验证(渲染端接线 Phase 4) unit(入口冒烟) 同上 ❌ Wave 0
(前置)L0 修复回归 正交/斜向/传送步 setPos 回写 unit npx vitest run packages-user/data-common ❌ Wave 0

Sampling Rate

  • Per task commit: 该任务的 vitest 文件(quick run command)
  • Per wave merge: npx vitest run 全量 + pnpm check:circular
  • Phase gate: 全量测试绿 + lint:user 无新错后 /gsd-verify-work(含 D-07 接口草案人工拍板确认项)

Node 回放验证(双端分离约束)

  • 数据端测试全部无 DOM:stub main.replayChecking: true;Map.prototype.getOrInsertComputed polyfill(既有 6 个测试文件同款)[VERIFIED]
  • 成功标准 1/3(自动寻路、避障)可完全在 Node 下以 fixture 地图验证;成功标准 2(移动端点击)属 Phase 4,本阶段以"数据端入口冒烟测试"等效验证

Wave 0 Gaps

  • packages-user/data-system/src/pathfinding/*.test.ts — 覆盖 PATH-01-a..d、PATH-02
  • packages-user/data-state/src/heroPathfinding.test.ts — 覆盖 PATH-01-e..g(L3 接线 + 事件派发 + 打断)
  • packages-user/data-common L0 mover 回归测试 — Pitfall 1 修复的前置/伴随
  • 测试 fixture 工厂:MapState.fromRaw + TileStore.addTile + FaceManager.register(Dir8) 组合(直接照抄 eventDispatch.test.ts:105-146 既有模式,无需新框架)

Security Domain

security_enforcement 已启用(config 默认)。

Applicable ASVS Categories

ASVS Category Applies Standard Control
V2 Authentication no 数据端单机引擎,无身份体系
V3 Session Management no 无会话
V4 Access Control no 无多用户边界
V5 Input Validation yes 寻路入口对坐标/楼层 id 做边界校验(inMap、isNil floorId),非法输入走 logger 告警 + 空路径返回(沿用 moverImpl isNil 判空模式)
V6 Cryptography no 无加密需求

Known Threat Patterns for 数据端寻路

Pattern STRIDE Standard Mitigation
越界/负数坐标导致死循环或越界读 Tampering/DoS 图构建与搜索入口统一 inMap 守卫(IMapLayer 已提供)+ 目标格校验
自定义损失函数返回 NaN/负数破坏 Dijkstra 不变式 Tampering 损失值守卫(非有限数/负数 → logger 告警并按默认损失 1 或拒绝)
恶意超大地图/损失函数导致主线程阻塞 DoS 图为网格受 width×height 上限约束;可加迭代上限(可选,草案提出)

Sources

Primary (HIGH confidence)

  • 本会话逐字读取的仓库源文件(全部带行号引用,见文中 VERIFIED 标签):
    • packages-user/data-common/src/common/mover.ts(IObjectMovable / ObjectMoveType / IMoverController / ObjectMover.moveProgress)
    • packages-user/data-common/src/store/types.ts(PassBit / ITileRawData.pass / eventPass)
    • packages-user/data-common/src/common/types.ts(FaceDirection)、common/faceManager.ts(IFaceHandler)
    • packages-user/data-common/src/event/types.ts(EventTrigger)
    • packages-user/data-base/src/hero/types.ts(IHeroMoveTopImpl / HeroMoveCode / IHeroMover)
    • packages-user/data-base/src/hero/mover.ts(HeroMover 判定顺序)、hero/location.ts
    • packages-user/data-state/src/hero/moverImpl.ts(DefaultHeroMoveTopImpl / canPass / shouldHit / commonTrigger)
    • packages-user/data-base/src/map/mapLayer.ts、map/types.ts(IMapLayer / IGameMap / ILayerLocation / IBlockEventEnv)
    • packages-user/data-system/src/types.ts、event/types.ts、event/system.ts
    • packages-user/data-state/src/core.ts(CoreState 接线点)、index.ts、ins.ts
    • packages/common/src/logger.ts、logger.json(码位枚举)
    • dev.md、测试文件 ×3(eventPath / eventDispatch / mapLifecycle 模式)
  • 运行时探测:node --version、pnpm --version、npx vitest --version、logger.json 键位枚举脚本

Secondary (MEDIUM confidence)

  • 无(外部搜索提供方全部关闭且本阶段无外部依赖问题)

Tertiary (LOW confidence)

  • Dijkstra/网格算法通识(ASSUMED A1,风险极低)

Metadata

Confidence breakdown:

  • Standard stack: HIGH — 零新依赖,全部仓库内既有
  • Architecture: HIGH — 所有关键接缝逐字读取,分层依赖由 package.json 证实
  • Pitfalls: HIGH — P1 有逐字代码佐证;P2 语义链完整;P3–P7 均有源码依据

Research date: 2026-09-09 Valid until: 2026-10-09(纯仓库内研究,30 天内稳定;mover.ts 若被他人改动需复查 P1)