From f99ad0cbe99fcd22b22e2bddcc119fe7e2a091a7 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sat, 19 Sep 2026 12:56:01 +0800 Subject: [PATCH] docs(04): create phase plan 04-04 (hero render correction, D-24..D-28) --- .planning/ROADMAP.md | 6 +- .../phases/04-render-adaptation/04-04-PLAN.md | 496 ++++++++++++++++++ 2 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/04-render-adaptation/04-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2637785..656df71 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -181,7 +181,7 @@ Plans: 3. 移动端(窄屏)布局下,同一场景正常显示且可操作 4. 数据端与渲染端保持双端分离——数据端无 DOM,仍可在 Node 环境跑回放验证 -**Plans**: 3/3 plans executed(第一步·只读对账已完成;第二步·勇士移动接口探索(只读)已完成;第三步·勇士本体适配(渲染端被动重接)已规划,待执行 — 04-CONTEXT D-01/D-02/D-13/D-17/D-18/D-19/D-20/D-21/D-22;其余渲染适配与移动端/桌面端双布局待后续增量另行规划) +**Plans**: 4 plans(第一步·只读对账已完成;第二步·勇士移动接口探索(只读)已完成;第三步·勇士本体适配已执行;第四步·勇士渲染修正(04-03 人工审查反馈 D-24..D-28)已规划,待执行 — 04-CONTEXT D-01/D-02/D-13/D-17/D-18/D-19/D-20/D-21/D-22/D-24..D-28;其余渲染适配与移动端/桌面端双布局待后续增量另行规划) Plans: **Wave 1** @@ -196,6 +196,10 @@ Plans: - [x] 04-03-PLAN.md — 勇士本体适配(渲染端被动重接):`render/map/extension/{hero,types,manager}.ts` 绑定 `IHeroLocation` / `IHeroMover`,改接现有钩子 `IHeroLocationHooks` / `IObjectMoverHooks`(D-21),按 D-20 裁剪 `IMapHeroRenderer` 的外部驱动成员;排除贴图/不透明度(D-18)、跟随者(D-22)、移动控制(D-19)、`core.*`(D-12)与裸函数(D-11),不新增数据端接口 +**Wave 4** *(blocked on Wave 3 completion)* + +- [ ] 04-04-PLAN.md — 勇士渲染修正(04-03 人工审查反馈 D-24..D-28):`render/map/extension/{hero,manager}.ts` 注入 `IFaceHandler` 并替换三个弃用 helper(D-24)、拆分 `MapHeroLocationHook` / `MapHeroMoverHook` 且 `onSetPos` 无条件(D-25)、补 `AnimDir`(D-26)、改 `@motajs/animate` 的 `ExcitationCurve2D`(D-27)、保留存量 `state` 用法(D-28);不改 `types.ts`、不新增数据端接口,排除 D-18/D-19/D-22 + **UI hint**: yes ### Phase 5: Legacy 移植 diff --git a/.planning/phases/04-render-adaptation/04-04-PLAN.md b/.planning/phases/04-render-adaptation/04-04-PLAN.md new file mode 100644 index 0000000..c51390c --- /dev/null +++ b/.planning/phases/04-render-adaptation/04-04-PLAN.md @@ -0,0 +1,496 @@ +--- +phase: 04-render-adaptation +plan: 4 +type: execute +wave: 4 +depends_on: + - 04-01 + - 04-02 + - 04-03 +files_modified: + - packages-user/client-modules/src/render/map/extension/hero.ts + - packages-user/client-modules/src/render/map/extension/manager.ts +autonomous: false +requirements: + - REND-01 + - REND-02 +estimate: + tokens: 73000 + raw_tokens: 73000 + tasks: 4 + confidence: low +must_haves: + truths: + - "本计划的**第一个 block 点**是 Task 0 的 `blocking-human` 汇报关卡:先向用户汇报要解决的问题(04-03 人工审查反馈 D-24..D-28)、大致解决方案(含逐调用替换映射表)、**拟新增 / 删除 / 改写的成员命名清单**(两个钩子类名 + `IFaceHandler` 构造器参数名 + `onSetPos` 归属 + D-18/D-22 多余方法归属)、范围边界与禁令、验证方式与运行时不可得的诚实降级,然后必须**停下等待**用户明确回复「可以执行」四个字;收到该回复前不得执行任何任务、不得派发任何执行子代理。无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`yolo`、`workflow.auto_advance`)如何,均不得自动放行(AGENTS.md)" + - "**D-24 落地**:`hero.ts` 中不再出现已弃用工具 `getFaceMovement` / `degradeFace` / `nextFaceDirection`(词边界判定),改为 `IFaceHandler` 的 `movement(dir)` / `degrade(dir)` / `next(dir)`;`MapHeroRenderer` 构造器新增 `IFaceHandler` 参数,并在 `manager.ts` 的 `new MapHeroRenderer(...)` 处由 `state.mover.faceHandler` 注入(`IObjectMover.faceHandler`,`data-common/src/common/mover.ts:251`),不使用全局 `state` 取朝向处理器(D-23)" + - "**D-24 的 `degradeFace(dir, FaceDirection.Down)` 回退细节已显式处理**(`hero.ts:357` 的 `addFollower`):`faceHandler.degrade(dir)` 之后当结果为 `FaceDirection.Unknown` 时显式回退为 `FaceDirection.Down`(`IFaceHandler.degrade` 自身对 Unknown 返回 Unknown),与旧 helper 的 `unknown` 参数逐字等价;该处理已在 Task 0 汇报" + - "**D-25 落地**:`MapHeroHook` 拆为**两个内部类**(`MapHeroLocationHook` 实现 `Partial`,`MapHeroMoverHook` 实现 `Partial>`),各自用 `hero.addHook(...)` / `hero.mover.addHook(...)` 注册到自己的 hookable 并各持一个 `IHookController` 字段(沿用既有 `locationController` / `moverController`);`onSetPos` **只在 `MapHeroLocationHook` 上实现一次**(`mover.setPos` 会先 `tile.setPos`,location 触发面是超集,见 `mover.ts:522-527`),因此同一次定位不会触发两次;`onSetPos` **无条件** `this.hero.setPosition(x, y)`,**不存在** `mover.moving` 守卫" + - "**D-26 落地**:`MapHeroMoverHook.onStepEnd` 的 `ObjectMoveType.AnimDir` 分支不再跳过——除开头既有的 `mover.currAnimDir` 设置外,该分支按本步的 `step.dir`(`IObjectMoveAnimDir.dir: ObjectAnimDirection`,`mover.ts:103-108`)调用 `setHeroAnimateDirection`" + - "**D-27 落地(范围仅限 hero.ts)**:`hero.ts` 不再出现 `mutate-animate`(词边界判定),改为从 `@motajs/animate` 引入 `ExcitationCurve2D`(`packages/animate/src/types.ts:12`,barrel `packages/animate/src/index.ts:4`),`generateJumpFn` 返回类型由 `TimingFn<2>` 改为 `ExcitationCurve2D`;二者结构等价(均为 `(progress: number) => [number, number]`,见 `node_modules/mutate-animate/index.d.ts:12-14`),故 `IMovingBlock.moveRelative(curve: TimingFn<2>, ...)`(`render/map/moving.ts:162-166`)仍可接收;**`moving.ts` / `maps/types.ts` / 其它文件的存量 `mutate-animate` 使用一律不动**" + - "**D-28 落地(存量 `state` 保留)**:`hero.ts` 的 `import { state } from '@user/data-state';` 与其 `state.roleFace.getFaceOf(...)` 三处调用(`hero.ts:235` / `:358` / `:417`)**逐字保留**,本计划不删除、不替换、不据此阻塞;`state` 仍被引用即视为符合 D-28" + - "**D-18 / D-22 成员逐字保留**:`setImage` / `setAlpha` / `setFollowerAlpha` / `addFollower` / `removeFollower` / `removeAllFollowers` 与钩子方法 `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` / `onAddFollower` / `onRemoveFollower` / `onRemoveAllFollowers` 均不被删除、不被改写语义(贴图 / 不透明度按 D-18 待用户先改数据端;跟随者按 D-22 暂缓)" + - "**契约面零变更**:`types.ts` **不被修改**(`IMapHeroRenderer` / `IMapExtensionManager` 的成员集合沿用 04-03 收口结果,本计划不再动它);无任何新增公共接口成员;D-24 的新参数只出现在 `MapHeroRenderer` 构造器(不在任何接口里)" + - "**范围外零改动**:`extension/types.ts`、`action/move.ts` 与全部移动控制 API(D-19)、`core.*` / legacy(D-12)、`getHeroStatusOn` 类裸函数(D-11)、任何数据端文件与 `packages/**` 均未被本计划改动;`mutate-animate` 的其余存量使用(D-27 范围外)一动不动" + - "**验证可运行且不臆造通过**:改动前先记录 `pnpm exec vue-tsc --noEmit` 的 `client-modules` 基线错误集(当日实际值,逐行原样入 SUMMARY);改动后该错误集必须与基线**完全一致**(不多不少),且三个在范围文件命中 0 条类型错误;`pnpm exec eslint` 对两个被改文件 0 错误;静态门禁(弃用 helper、`mutate-animate`、`IFaceHandler` 注入、两钩子类分别注册、`onSetPos` 无 `moving` 守卫、AnimDir 已处理、D-18/D-22 保留、D-28 保留)、CRLF 门禁、基线感知范围门禁与并发改动基线门禁全部转绿。渲染运行时无测试设施、且 `MapExtensionManager.addHero` 当前**未被接线**(`client.ts:148-152` 已注释),故本计划的实质验证为「编译 + 契约 + 静态」三类机器门禁,另附人工 UAT 代码复核;**不得**以任何无法运行的门禁冒充通过" + - "用户并发的未提交改动(以 Task 1 起始 `git status --porcelain` 当日实测为准,排除 `packages-user/client-modules/src/render/map/extension/` 与 `.planning/phases/04-render-adaptation/` 两个前缀)在 Task 1 起始记录基线(porcelain 条目 + 各文件内容 sha256 摘要),Task 3 复验逐条与内容未变;**不回滚 / 不暂存 / 不提交 / 不修改**它们。注意:04-03 SUMMARY 记录的旧基线(`client-base/src/load/*` 等)已在 `4e64953 chore: 内部接口改名` 落库,本计划**不沿用旧列表**,一律以 Task 1 当日实测为准" + - "REND-01 / REND-02 在本 run 后仍保持 Pending:本计划只落地「04-03 勇士渲染修正」一个子切片,未实施其余渲染适配与移动端 / 桌面端双布局(见 assumptions 的两条 FLAGGED ASSUMPTION)" + artifacts: + - path: packages-user/client-modules/src/render/map/extension/hero.ts + provides: "`MapHeroRenderer` 持有注入的 `IFaceHandler` 并以 `movement` / `degrade` / `next` 取代三个弃用 helper;`MapHeroHook` 拆为 `MapHeroLocationHook` / `MapHeroMoverHook` 两个内部类分别注册;`onSetPos` 无条件设置且无 `moving` 守卫;`onStepEnd` 的 AnimDir 分支按 `step.dir` 设置动画方向;`mutate-animate` 改为 `@motajs/animate` 的 `ExcitationCurve2D`;D-18 / D-22 / D-28 成员逐字保留" + - path: packages-user/client-modules/src/render/map/extension/manager.ts + provides: "`addHero` 把 `state.mover.faceHandler` 作为第四个实参注入 `new MapHeroRenderer(this.renderer, layer, state, state.mover.faceHandler)`;无其它改动" + key_links: + - "`manager.ts` 的 `addHero(state: IHeroLocation, layer)` → `new MapHeroRenderer(this.renderer, layer, state, state.mover.faceHandler)` ↔ `hero.ts` 构造器 `readonly faceHandler: IFaceHandler`(`packages/common/src/utils/types.ts:152`)" + - "`IObjectMover.faceHandler: IFaceHandler`(`data-common/src/common/mover.ts:251`)↔ `IHeroMover extends IObjectMover`(`data-base/src/hero/types.ts:399`)↔ `IHeroLocation.mover: IHeroMover`(`data-base/src/hero/types.ts:303-314`)" + - "`IFaceHandler.movement(dir): IFaceDescriptor {x, y}`(`common/src/utils/types.ts:164`)/ `degrade(dir): T`(`:158`,Unknown→Unknown)/ `next(dir, anticlockwise?)`(`:186`,默认顺时针)↔ `hero.ts` 原来对三个弃用 helper 的 7 处调用(`:132` / `:202` / `:230` / `:357` / `:364` / `:365` / `:453`)" + - "`IHeroLocationHooks.onSetPos(x, y)`(`data-base/src/hero/types.ts:283`)由 `HeroLocation.setPos` 触发(`data-base/src/hero/location.ts:59-63`);`IObjectMoverHooks.onSetPos(x, y, tile, mover)`(`mover.ts:216`)由 `ObjectMover.setPos` 触发(`mover.ts:522-527`,其中先 `this.tile.setPos(...)` → 亦触发 location 钩子)——两接口共享 `onSetPos` 是本计划拆分钩子类的原因" + - "`IObjectMoverHooks.onStepEnd` 触发时序(`mover.ts:659-687`):`onStepStart`(坐标未变)→ 内部 `onStepEnd` 计算 → `tile.setPos(loc)`(触发 location `onSetPos`)→ `onStepSettled` → `onStepEnd` 钩子(坐标已更新)" + - "`ObjectMoveType.AnimDir` 步形状 `IObjectMoveAnimDir { type, dir: ObjectAnimDirection }`(`mover.ts:103-108`);`prepareStep` 对 AnimDir 写 `mover.currAnimDir = step.dir`(`mover.ts:513-515`)" + - "`generateJumpFn(dx, dy): ExcitationCurve2D`(`@motajs/animate`)↔ `IMovingBlock.moveRelative(curve: TimingFn<2>, time)`(`render/map/moving.ts:162-166`):`ExcitationCurve2D = (progress: number) => [number, number]`(`packages/animate/src/types.ts:12`)与 `TimingFn<2> = (input: number) => [number, number]`(`node_modules/mutate-animate/index.d.ts:12-14`)结构等价" + - "`pnpm exec vue-tsc --noEmit` 的 `client-modules` 错误集基线(Task 1 当日实测,逐行入 SUMMARY)↔ Task 1 / 2 / 3 门禁的一致断言" + assumptions: + - "**FLAGGED ASSUMPTION(specless fallback)— REND-01 edge unresolved:** 未提供 REND-01(渲染端适配新数据层接口)的验收边界;本计划**不发明**阈值,只交付「04-03 勇士渲染修正」这一个子切片,验收判据取自 04-CONTEXT 的 D-24..D-28 与 `04-HERO-MOVER-INTERFACE.md` 的 `#04-02-R-01..12`。用户须在推进其余渲染适配前补齐 REND-01 的边界判据。" + - "**FLAGGED ASSUMPTION(specless fallback)— REND-02 edge unresolved:** 未提供 REND-02(移动端与桌面端双布局)的验收边界;本计划不涉及双布局实现,也不发明其验收阈值。" + - "**执行前必须复核(未确定项,不猜):** Task 1 步骤 (0a) 必须在动手前打开当日 HEAD 的基准文件逐条核对:① `IFaceHandler` 七成员、`movement` 返回 `IFaceDescriptor`、`degrade` 对 Unknown 返回 Unknown;② `IObjectMover.faceHandler` 存在且类型为 `IFaceHandler`;③ `IHeroLocation.mover` 为 `IHeroMover` 且 `HeroLocation` 由 `IFaceHandler` 构造 `HeroMover`;④ 三个弃用 helper 仍导出且签名如记录(「待替换物尚在」的核对,不是要求保留它们);⑤ `@motajs/animate` 导出 `ExcitationCurve2D`、`mutate-animate` 的 `TimingFn<2>` 与 `MovingBlock.moveRelative` 的形参形态;⑥ `IObjectMoveAnimDir.dir`、`prepareStep` 对 AnimDir 的处理、`moveProgress` 的钩子时序。任一不同即**停下汇报并修订本计划**,不得按记忆改代码。" + - "**`IFaceHandler` 语义等价性(Task 0 陈述、Task 1 编译佐证):** 弃用 helper 面向八方向全集,而 `hero.mover` 注入的处理器是有限方向组(`docs/dev/hero/hero-mover.md:74` 记载为 `Dir4FaceHandler`,`data-common/src/common/faceManager.ts:169`)。替换后:① `degrade` 与 `degradeFace(dir)`(unknown 默认 Unknown)对 Unknown 同返回 Unknown;② `movement` / `next` 会先 `degrade`,对落在本组内的正交朝向与旧 helper 逐字一致;③ `nextFaceDirection(dir, false, false)`(默认 face8=false)对**对角输入**与 `IFaceHandler.next`(先 degrade 再 90°)不同,但 `hero.ts:453` 的输入 `this.heroEntity.direction` 只可能来自 `getCurrentFaceDirection()` / `turn(...)` / `degrade(...)`,均为本组朝向,故实际不触发差异;若执行当日实测处理器为 `Dir8FaceHandler`,须在 SUMMARY 记录并复核 `next` / `movement` 差异。" + - "**运行时 UAT 不可得(诚实降级):** `MapExtensionManager.addHero` 目前无任何调用点(`client.ts:148-152` 被注释),故勇士渲染拓展在运行时并未启用;本计划不接线 `client.ts`。因此本计划的运行结果无法在渲染环境观察,验收依赖「scoped 类型门禁 + 契约 / 静态门禁 + 人工代码复核」,运行时渲染 UAT 顺延到启用接线的增量。" + - "**数据端实现可能被用户并发修改(D-05):** 接口签名视为稳定,实现可变;本计划只读引用数据端接口,不修改任何数据端文件。若执行当日数据端接口签名与规划基线不一致,按 Task 1 步骤 (0a) 停下汇报,不按旧形状适配。" + - "**成员命名需用户同意:** 本计划引入 / 删除的渲染端成员名(两个钩子类 `MapHeroLocationHook` / `MapHeroMoverHook`、`MapHeroRenderer` 的新构造器参数 / 字段 `faceHandler`、被删除的类名 `MapHeroHook`,以及 `onSetPos` 与 D-18/D-22 多余方法的归属)须在 Task 0 汇报中获得用户同意后才可落地(AGENTS.md「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈」);用户未同意即改名为用户指定的写法,或停下修订计划。" + prohibitions: + - "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理(AGENTS.md 的第一个 block 点约束;不因触发方式或 `yolo` / `auto_advance` 模式而放宽)。" + - "不得修改任何数据端文件(`packages-user/data-common` / `data-base` / `data-system` / `data-state`),也不得新增数据端公共接口;本计划只从数据端 import 现有类型并读取 `state.mover.faceHandler`。" + - "不得改动 `packages-user/client-modules/src/render/map/extension/types.ts`(本计划的构造器参数不在任何接口里,无需改契约面);若编译链强制要求触碰它,必须停下汇报并修订本计划(不得自行扩面)。" + - "不得改动 `packages-user/client-modules/src/action/move.ts` 及任何移动控制 API(D-19:`HeroKeyMover` / `IMoveController` 队列 / `oneStep` 由用户手动处理;`#04-02-G-02` 不在本计划)。" + - "不得删除 / 替换 / 阻塞 `hero.ts` 的存量 `state` 使用与 `import { state } from '@user/data-state'`(D-28)。" + - "不得改写贴图 / 不透明度相关代码的语义:`setImage` / `setAlpha` / `hero.image`(贴图别名读取)/ `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 一律保留(D-18)。" + - "不得适配跟随者:`IHeroFollower` / `IHeroFollowersController` / 跟随者钩子与渲染方法(`addFollower` / `removeFollower` / `removeAllFollowers` / `setFollowerAlpha` / `onAddFollower` / `onRemoveFollower` / `onRemoveAllFollowers`)保持原样(D-22 暂缓)。" + - "不得把 `mutate-animate` → `@motajs/animate` 的迁移扩散到 `hero.ts` 之外(D-27 范围;`moving.ts` / `maps/types.ts` / `components/**` / `legacy-ui` 等的存量使用一律不动)。" + - "不得引入 `core.*` / `client.*` / legacy 全局适配(D-12);不得处理 `getHeroStatusOn` 类裸函数(D-11)。" + - "不得新增任何渲染端公共接口成员(`IMapHeroRenderer` / `IMapExtensionManager` 的成员集合只减不增,且本计划不改 `types.ts`);不得让渲染端出现允许外部驱动渲染状态的新方法。" + - "不得修改任何既有 jsDoc 注释的内容;被 D-25 拆分 / 重命名所涉及的方法与其配套 jsDoc 属「移动 / 删除代码」而非「改写注释」,须在 Task 0 汇报中经用户确认。" + - "不得改动 `files_modified` 之外的源码;不得回滚 / 暂存 / 提交用户并发的未提交改动。" + - "不得新增依赖、不得修改 `package.json` / `pnpm-lock.yaml`(`@motajs/animate` 已是 `client-modules` 的 workspace 依赖,`packages-user/client-modules/package.json:4`);出现安装需求即暂停并要求用户确认包合法性。" + - "不得以「无测试设施」为由弱化验证:每条 `` 必须有 ``;确实无法运行的门禁必须显式声明为不可得并降级为静态 + 人工,不得伪造绿。" +--- + + +Phase 4 第四个增量 —— **勇士渲染修正:04-03 人工审查反馈落地**。按用户对 04-03 产出的裁定 D-24..D-28,修正 `render/map/extension/hero.ts`(以及注入所需的 `.../manager.ts`)里的五处问题。**本计划是代码改动计划**,改动面严格限定为两个文件。 + +**要解决的问题(五条,全部来自用户对 04-03 的人工审查):** +1. **D-24 仍在用弃用工具**:`hero.ts` 使用 `@user/data-common` 的已弃用 `getFaceMovement` / `degradeFace` / `nextFaceDirection`(import `:2`/`:4`/`:7`,使用 `:132`/`:202`/`:230`/`:357`/`:364`/`:365`/`:453`);数据端已提供 `IFaceHandler`(`packages/common/src/utils/types.ts:152`)。 +2. **D-25 钩子重复调用 + 陈旧守卫**:`IHeroLocationHooks` 与 `IObjectMoverHooks` 共享 `onSetPos`,单个 `MapHeroHook` 同时注册到两个 hookable 会重复触发;且 `onSetPos` 带 `if (mover.moving) return` 守卫,使渲染在移动中不跟随数据端终点位置(渲染不再客观描述数据端状态)。 +3. **D-26 AnimDir 未设**:`onStepEnd` 的 `ObjectMoveType.AnimDir` 分支是 no-op,未按步的 `dir` 设置动画播放方向。 +4. **D-27 用了 `mutate-animate`**:`hero.ts:19` import `TimingFn`,`generateJumpFn`(`:255`)返回 `TimingFn<2>`。 +5. **D-28 存量 `state`**:`hero.ts` 的 `state.roleFace.getFaceOf(...)`(`:235`/`:358`/`:417`)属存量单例使用——D-23 只约束新增 / 后续代码;本计划**保留**该用法,只记录、不处理。 + +**大致解决方案(逐调用替换映射表):** + +| 旧调用(hero.ts) | 替换 | 说明 | +|---|---|---| +| `degradeFace(dir)` `:132` / `:202` | `this.faceHandler.degrade(dir)` | 旧 helper 默认 `unknown = Unknown`,与 `IFaceHandler.degrade` 对 Unknown 返回 Unknown 一致 | +| `degradeFace(dir, FaceDirection.Down)` `:357` | `this.faceHandler.degrade(dir)`,随后 `=== FaceDirection.Unknown ? FaceDirection.Down : 结果` | `IFaceHandler.degrade` 无 `unknown` 形参,Down 回退须显式保留(行为逐字等价) | +| `getFaceMovement(dir)` `:230` / `:364` / `:365` | `this.faceHandler.movement(dir)` | 返回 `IFaceDescriptor { x, y }`,解构用法不变 | +| `nextFaceDirection(dir)` `:453` | `this.faceHandler.next(dir)` | 默认顺时针;旧 helper 的 `face8=false` / `anticlockwise=false` 默认与此一致,`face8` 在本处未被使用 | + +- **D-24 注入**:`MapHeroRenderer` 构造器新增 `IFaceHandler` 参数(拟名 `faceHandler`);`manager.ts` 的 `new MapHeroRenderer(...)` 由 `state.mover.faceHandler` 注入(`IObjectMover.faceHandler`,`data-common/src/common/mover.ts:251`;`state` 是 `addHero` 的 `IHeroLocation` 形参,不是全局单例,符合 D-23)。 +- **D-25 拆分**:把 `MapHeroHook` 拆成 `MapHeroLocationHook`(`Partial`;实现 `onSetPos` **且只实现一次**,另承载 D-18/D-22 的非接口多余方法)与 `MapHeroMoverHook`(`Partial>`;实现 `onMoveStart` / `onMoveEnd` / `onStepEnd` / `onSetFaceDir`,**不**实现 `onSetPos`);`hero.addHook(...)` / `hero.mover.addHook(...)` 分别注册,各持 `locationController` / `moverController` 字段;`onSetPos(x, y)` 无条件 `this.hero.setPosition(x, y)`(删除 `mover.moving` 守卫)。 +- **D-26**:`onStepEnd` 的 `case ObjectMoveType.AnimDir:` 改为 `this.hero.setHeroAnimateDirection(step.dir); break;`(开头既有的 `mover.currAnimDir` 设置保留)。 +- **D-27**:`hero.ts` 的 `import { TimingFn } from 'mutate-animate';` 改为 `import { ExcitationCurve2D } from '@motajs/animate';`,`generateJumpFn` 返回类型改为 `ExcitationCurve2D`。 +- **D-28**:`import { state } from '@user/data-state';` 与三处 `state.roleFace.getFaceOf(...)` 一字不动。 +- **契约面**:`types.ts` **不改**(新参数只在构造器里,不进任何接口);无新增公共接口成员。 +- **范围硬边界**:贴图 / 不透明度(D-18)、跟随者(D-22)、`action/move.ts` 与移动控制(D-19)、`core.*`(D-12)、裸函数(D-11)、D-27 的其余存量迁移一律不动。 + +**执行前置关卡(AGENTS.md,第一个 block 点):** 本计划第一个执行步骤是 Task 0 —— 先向用户汇报「要解决的问题 / 大致解决方案(含逐调用替换映射表)/ 拟新增·删除·改写的成员命名清单 / `onSetPos` 与 D-18·D-22 多余方法的归属 / Down 回退处理 / 范围边界与禁令 / 验证方式的诚实降级」,然后**停下等待**用户明确回复「可以执行」四个字。收到该回复前,任何任务都不得执行,也不得派发执行子代理;无论触发方式或配置模式如何均不得自动放行。故本计划 `autonomous: false`。 + +**显式延后(本计划不涉及、不实施):** `client.ts` 的 `addHero` 接线启用、贴图与不透明度来源(D-18)、跟随者位置 / 动画归属(D-22)、移动控制(D-19)、D-27 范围外的 `mutate-animate` 存量迁移、其余 ① 错配与 ③ 多余旧路径、移动端 / 桌面端双布局(REND-02)。 + +Purpose: 04-03 让勇士渲染在编译与契约上对齐了数据端,但仍留着「弃用 helper、钩子重复触发 + 陈旧守卫、AnimDir no-op、`mutate-animate` 依赖」四处实质问题。本计划把它们一次修平,使后续渲染适配与双布局建立在一个语义正确、依赖一致的地基上。 + +Output: `hero.ts` / `manager.ts` 的修正改动(原子提交)+ 会话内可复跑的 scoped 类型门禁、静态门禁(D-24/D-25/D-26/D-27)、D-18/D-22 与 D-28 保留门禁、并发改动基线门禁与 CRLF 门禁结果 + `.planning/phases/04-render-adaptation/04-04-SUMMARY.md`。Task 0 为只读汇报关卡,不写任何文件。 + +## Source Audit(多源覆盖审计) + +| SOURCE | ID | Feature/Requirement | Plan | Status | Notes | +|--------|----|---------------------|------|--------|-------| +| GOAL | — | Phase 4:「渲染端通过新数据层接口驱动,并同时支持移动端与桌面端布局」 | 04-04 | COVERED(增量·勇士渲染修正子切片) | 本 run 只落地 04-03 人工审查反馈 D-24..D-28;其余渲染适配与双布局属后续增量(REND-02 见 assumptions) | +| REQ | REND-01 | 渲染端适配新数据层接口 | 04-04 | COVERED(子切片;需求保持 Pending) | D-24 把勇士渲染从弃用 helper 改到 `IFaceHandler`(新数据层接口),D-25/D-26 修正钩子语义;验收边界未提供 → 见 assumptions 的 FLAGGED ASSUMPTION | +| REQ | REND-02 | 渲染端同时支持移动端与桌面端布局 | 04-04 | COVERED(不涉及) | 本 run 范围不含双布局;REND-02 验收边界未提供 → 见 assumptions 的 FLAGGED ASSUMPTION | +| RESEARCH | — | (本阶段无 RESEARCH.md) | — | N/A | Phase 4 未产出 RESEARCH.md;等价输入为 04-01 / 04-02 账本与 04-03 产出 | +| CONTEXT | D-24 | 弃用 helper 全改 `IFaceHandler`,构造器注入 | 04-04 | COVERED | Task 1(tracer):注入 + 7 处替换 + Down 回退显式保留 | +| CONTEXT | D-25 | 钩子实现拆两类,`onSetPos` 直接设置 | 04-04 | COVERED | Task 2:`MapHeroLocationHook` / `MapHeroMoverHook` 分别注册;`onSetPos` 无条件 | +| CONTEXT | D-26 | 补上 `AnimDir` 设置 | 04-04 | COVERED | Task 2:AnimDir 分支按 `step.dir` 设置动画方向 | +| CONTEXT | D-27 | 不用 `mutate-animate`,改 `@motajs/animate`(仅 hero.ts) | 04-04 | COVERED | Task 3:`TimingFn<2>` → `ExcitationCurve2D`;其余存量使用不动 | +| CONTEXT | D-28 | 存量 `state` 使用暂不处理 | 04-04 | COVERED(保留) | `state.roleFace.getFaceOf` 三处与 import 逐字保留;设保留门禁 | +| CONTEXT | D-18 | `HeroRendering` 暂不处理(贴图 / 不透明度) | 04-04 | COVERED(排除 + 保留门禁) | 贴图 / 不透明度成员逐字保留;不重设来源 | +| CONTEXT | D-19 | 移动控制由用户手动修改 | 04-04 | COVERED(排除) | `action/move.ts` 与移动控制 API 不在 `files_modified` | +| CONTEXT | D-20 | 勇士渲染完全被动 | 04-04 | COVERED(沿用) | 沿用 04-03 的被动契约;本计划不新增任何公共接口成员 | +| CONTEXT | D-21 | 走现有 `IObjectMoverHooks` / `IHeroLocationHooks`,不新增数据端接口 | 04-04 | COVERED(沿用) | 两个钩子类仍只注册到现有 hookable,不新增数据端接口 | +| CONTEXT | D-22 | 跟随者暂缓 | 04-04 | COVERED(排除 + 保留门禁) | 跟随者族成员与钩子方法逐字保留 | +| CONTEXT | D-23 | 不使用 `client` / `state` 全局单例(仅约束新增 / 后续) | 04-04 | COVERED | 新增代码只经构造器参数取 `faceHandler`,不取全局单例;存量 `state` 由 D-28 保留 | +| CONTEXT | D-05 | 以接口为准,不受数据端实现变动影响 | 04-04 | COVERED | Task 1 步骤 (0a) 复核;签名若变则停下汇报 | +| CONTEXT | D-11 | `getHeroStatusOn` 类裸函数多为待删除 | 04-04 | COVERED(边界) | 非勇士本体;prohibitions 明列不处理 | +| CONTEXT | D-12 | `core.*` 属旧引擎适配,归第五阶段 | 04-04 | COVERED(排除) | prohibitions 明列不得引入 | +| CONTEXT | D-01..D-17 | 阶段 4 增量方式 / 对账 / 探索 / 聚焦 | 04-01 / 04-02 / 04-03 | COVERED(前序计划) | 由前序计划落地;本计划是其审查修正 | +| CONTEXT | D-13 | 缺失接口待改时逐步确认 | 04-04 | COVERED(边界) | 本计划不做新的缺失接口裁定 | +| CONTEXT | D-14..D-16 | hook / loading / 存档 UI / 结构化重构 | 后续阶段 | DEFERRED(CONTEXT 未纳入本阶段) | 非本增量范围 | + +## Estimate Basis(估算依据) + +`estimate-calibration` 返回 `factor: 1`、`sample_count: 0`、`confidence: low`(项目无历史样本),故 `tokens == raw_tokens`,并如实标注 `confidence: low`。按「执行者实际触及量」估算(`estimate.tasks: 4` = Task 0 汇报关卡 + 三个工作任务):Task 0 只读汇报并等待用户确认(约 4k,不写文件);Task 1(tracer)起始复核数据端现行接口 + 注入 + 7 处替换 + scoped 类型门禁(约 28k,含 `hero.ts` 552 行与数据端基准的定向读取);Task 2 拆钩子类 + AnimDir + 门禁(约 22k);Task 3 D-27 替换 + 全部门禁 + 人工 UAT 复核 + SUMMARY(约 15k);固定开销约 4k;合计约 73k。 + + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@.planning/phases/04-render-adaptation/04-CONTEXT.md +@.planning/phases/04-render-adaptation/04-03-PLAN.md +@.planning/phases/04-render-adaptation/04-03-SUMMARY.md +@.planning/phases/04-render-adaptation/04-HERO-MOVER-INTERFACE.md +@.planning/phases/04-render-adaptation/04-PATTERNS.md +@AGENTS.md +@dev.md + + + + + + Task 0: 预执行汇报关卡 —— 汇报 D-24..D-28 的解决方案、逐调用替换映射表、成员命名清单与 `onSetPos` / D-18·D-22 方法归属,等待用户回复「可以执行」 + 用户对 04-03 的人工审查裁定 D-24..D-28 已写入 `.planning/phases/04-render-adaptation/04-CONTEXT.md`(以文件内实际存在 D-24..D-28 为准) + (只读汇报,不修改任何文件) + + - AGENTS.md(「任何计划的执行,其第一个 block 点必须是向我汇报计划内容并等待我确认」;只有明确回复「可以执行」四个字之后才允许开始执行;「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈,我同意后才可以使用」) + - .planning/phases/04-render-adaptation/04-CONTEXT.md(D-24 / D-25 / D-26 / D-27 / D-28 裁定原文;D-18 / D-19 / D-20 / D-21 / D-22 / D-23 边界) + - .planning/phases/04-render-adaptation/04-03-PLAN.md 与 04-03-SUMMARY.md(04-03 的产出与命名基线:`locationController` / `moverController` 已获批;本计划是其后修正) + - .planning/phases/04-render-adaptation/04-04-PLAN.md(本计划全文:objective 的映射表 / tasks / 禁令 / 门禁) + - packages-user/client-modules/src/render/map/extension/hero.ts(现状 import `:1-21`、`addHeroMoving` `:110-140`、`setImage` `:199-206`、`moveEntity` `:225-248`、`generateJumpFn` `:255`、`addFollower` `:351-386`、`turn` `:451-460`、`MapHeroHook` `:468-552`) + - packages-user/client-modules/src/render/map/extension/manager.ts:24-32(`addHero` 的 `new MapHeroRenderer(this.renderer, layer, state)`) + - packages/common/src/utils/types.ts:145-197(`IFaceDescriptor` / `IFaceHandler` 七成员) + - packages-user/data-common/src/common/utils.ts:1-64(三个弃用 helper 的现行签名,尤以 `degradeFace` 的 `unknown` 默认为 Unknown、`nextFaceDirection` 的 `face8=false` 默认) + - packages-user/data-common/src/common/mover.ts:103-108,216,235-251,513-527,646-687(`IObjectMoveAnimDir` / `IObjectMoverHooks` / `IObjectMover.faceHandler` / `ObjectMover.setPos` / `moveProgress` 时序) + - packages-user/data-base/src/hero/types.ts:277-321,399-417(`IHeroLocationHooks` / `IHeroLocation` / `IHeroMover`) + - packages/animate/src/types.ts:7-12 与 node_modules/mutate-animate/index.d.ts:12-14(`ExcitationCurve2D` vs `TimingFn<2>`) + + + **停下并向用户汇报,等待明确回复后才继续;汇报本身只读,不写任何文件、不改任何生产代码、不派发子代理。** + + ① **要解决的问题(来自用户对 04-03 的人工审查,五条)**: + - D-24:`hero.ts` 仍在用已弃用 helper `getFaceMovement` / `degradeFace` / `nextFaceDirection`(import `:2`/`:4`/`:7`,使用 `:132`/`:202`/`:230`/`:357`/`:364`/`:365`/`:453`),数据端现行接口是 `IFaceHandler`。 + - D-25:`IHeroLocationHooks` 与 `IObjectMoverHooks` 共享 `onSetPos`,单个 `MapHeroHook` 同注册到两个 hookable 会**重复触发**;且 `onSetPos` 的 `if (mover.moving) return` 守卫让渲染在移动中不跟随数据端终点位置(违背「渲染客观描述数据端」)。 + - D-26:`onStepEnd` 的 `ObjectMoveType.AnimDir` 分支是 no-op,不按步的 `dir` 设置动画播放方向。 + - D-27:`hero.ts:19` import `mutate-animate` 的 `TimingFn`,`generateJumpFn`(`:255`)返回 `TimingFn<2>`。 + - D-28:`hero.ts` 的 `state.roleFace.getFaceOf(...)`(`:235`/`:358`/`:417`)是存量单例使用——本计划**保留**(D-23 只约束新增代码)。 + + ② **大致解决方案(两个文件)+ 逐调用替换映射表(须逐行汇报)**: + - `manager.ts`:`new MapHeroRenderer(this.renderer, layer, state, state.mover.faceHandler)`(第 4 实参为新增)。 + - `hero.ts`:构造器新增 `IFaceHandler` 参数并保存为字段;映射表: + `degradeFace(dir)` `:132`/`:202` → `faceHandler.degrade(dir)`; + `degradeFace(dir, FaceDirection.Down)` `:357` → `faceHandler.degrade(dir)` 后 `=== FaceDirection.Unknown ? FaceDirection.Down : 结果`(Down 回退显式保留,行为逐字等价); + `getFaceMovement(dir)` `:230`/`:364`/`:365` → `faceHandler.movement(dir)`(`IFaceDescriptor { x, y }`); + `nextFaceDirection(dir)` `:453` → `faceHandler.next(dir)`(默认顺时针;本处未用 `face8`);随后删除三个弃用 import。 + 说明语义等价性:弃用 helper 面向八方向全集,注入的处理器是有限方向组(`docs/dev/hero/hero-mover.md:74` 记载为 `Dir4FaceHandler`);对 Unknown 二者同返回 Unknown,对落在本组内的正交朝向逐字一致;`nextFaceDirection(..., face8=false)` 仅在对角输入上与 `IFaceHandler.next`(先 degrade 再 90°)不同,而 `:453` 的输入恒为本组朝向,故不触发差异。 + - D-25:把 `MapHeroHook` 拆成 `MapHeroLocationHook`(`Partial`)与 `MapHeroMoverHook`(`Partial>`);前者实现 `onSetPos(x, y)`(无条件 `this.hero.setPosition(x, y)`,**无 `moving` 守卫**)并承载 D-18/D-22 的非接口多余方法;后者实现 `onMoveStart` / `onMoveEnd` / `onStepEnd` / `onSetFaceDir`,**刻意不实现 `onSetPos`**(location 触发面是超集:`mover.setPos` 先 `tile.setPos`,见 `mover.ts:522-527`),从而同一次定位只触发一次;构造体内分别 `hero.addHook(new MapHeroLocationHook(this))` / `hero.mover.addHook(new MapHeroMoverHook(this))` 并各自 `load()`,`destroy()` 各自 `unload()`。 + - D-26:`MapHeroMoverHook.onStepEnd` 的 AnimDir 分支改为 `this.hero.setHeroAnimateDirection(step.dir); break;`(开头既有的 `mover.currAnimDir` 设置保留)。 + - D-27:`import { ExcitationCurve2D } from '@motajs/animate';` 取代 `import { TimingFn } from 'mutate-animate';`,`generateJumpFn` 返回类型改 `ExcitationCurve2D`;二者结构等价(`(progress: number) => [number, number]`),`moveRelative(curve: TimingFn<2>, ...)` 仍可接收;仅限 `hero.ts`。 + - D-28:`import { state } from '@user/data-state';` 与三处 `roleFace.getFaceOf` 一字不动。 + + ③ **成员命名清单(须用户同意,AGENTS.md 命名规则)**: + - **新增**内部类 `MapHeroLocationHook`(实现 `Partial`)与 `MapHeroMoverHook`(实现 `Partial>`);**删除**内部类名 `MapHeroHook`(其逻辑分散到上述两类)。 + - **新增** `MapHeroRenderer` 构造器参数 / 只读字段 `faceHandler`(类型 `IFaceHandler`);备选名请用户当场指定(如 `face` / `directionHandler`,但 `face` 与数据端语义易混,推荐 `faceHandler`,与 `IObjectMover.faceHandler` 同名)。 + - **重新使用**(04-03 已获批,本计划不变)`locationController` / `moverController` 两个 `IHookController` 字段。 + - **方法归属**:`onSetPos` 归 `MapHeroLocationHook` 且只出现一次;`onMoveStart` / `onMoveEnd` / `onStepEnd` / `onSetFaceDir` 归 `MapHeroMoverHook`;D-18/D-22 的多余方法 `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` / `onAddFollower` / `onRemoveFollower` / `onRemoveAllFollowers` 归 `MapHeroLocationHook`(它们不属于任何现有钩子接口,也不注册到任何数据端对象,仅逐字保留)——请用户确认该归属,或指定其它归属(如全部放 `MapHeroMoverHook`,或维持原类另立第三容器)。 + - 名字均为**内部类 / 内部字段**,不进任何公共接口;`IMapHeroRenderer` / `IMapExtensionManager` 成员集合不变。 + + ④ **Down 回退处理(D-24 唯一需裁定的细节)**:`:357` 的 `degradeFace(last.nextDirection, FaceDirection.Down)` 要求 Unknown 回退为 Down,而 `IFaceHandler.degrade` 对 Unknown 返回 Unknown。本计划主张在调用点显式写 `degraded === FaceDirection.Unknown ? FaceDirection.Down : degraded`(最贴近旧语义、不引入新接口)。请用户确认,或指定其它处理(如抽一个渲染端私有小函数)。 + + ⑤ **范围边界与禁令**:只改 `hero.ts` 与 `manager.ts`;**不改 `types.ts`**(新参数只在构造器里,契约面无需变动);不新增数据端公共接口、不改任何数据端文件;不动 `action/move.ts` 与移动控制(D-19);不动贴图 / 不透明度(D-18)、跟随者(D-22)、`core.*`(D-12)、裸函数(D-11);D-27 不扩散到 `hero.ts` 之外;不删除存量 `state`(D-28);不接线 `client.ts`;不回滚 / 不暂存 / 不提交用户并发未提交改动。 + + ⑥ **验证方式与诚实降级(须原样告知)**:渲染端无测试设施,且 `MapExtensionManager.addHero` 当前无调用点(`client.ts:148-152` 被注释),故运行时渲染无法观察。本计划的运行结果 = **scoped 类型门禁**(`pnpm exec vue-tsc --noEmit` 三个在范围文件 0 条,且 `client-modules` 错误集与当日记录基线完全一致)+ **静态门禁**(弃用 helper 清零、`IFaceHandler` 注入、两钩子类分别注册、`onSetPos` 无守卫、AnimDir 已处理、`mutate-animate` 清零且 `@motajs/animate` 就位)+ **D-18/D-22 与 D-28 保留门禁** + **eslint / CRLF 门禁** + **基线感知范围门禁 + 并发改动基线门禁**;另附**人工代码复核**。**不冒充任何无法运行的门禁。** + + ⑦ **关卡性质(必须原样告知用户)**:无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `yolo`、`workflow.auto_advance`)如何,**执行者/子代理都必须在此停下并等待**用户明确回复「可以执行」四个字;未收到这四个字前不得执行任何任务、不得派发执行子代理,也不得以「自动化模式」为由自动放行。 + + ⑧ **失败语义(等同硬门禁,非自动命令)**:用户未回复「可以执行」即继续,或回复的是其它任何内容(含修订意见、沉默、`yolo` / `auto_advance` 模式下的自动推进、或子代理/执行器的自行放行),一律视为**未通过本关卡**:必须立即停止执行,不派发任何执行子代理,保持只读、不写任何文件,直到用户在对话中明确回复「可以执行」四个字后方可继续。 + + ⑨ **若用户给出修订意见**(含不同意命名 / 不同意方法归属 / 不同意 Down 回退):退出本计划、修订 `04-04-PLAN.md` 后重新汇报,不得自行改道执行。 + + 是否按本计划(两文件勇士渲染修正:D-24 注入 `IFaceHandler` 并替换弃用 helper、D-25 拆分钩子类且 `onSetPos` 无条件、D-26 补 AnimDir、D-27 换 `@motajs/animate`、D-28 保留存量 `state`)开始执行;并确认 ③ 的成员命名与归属、④ 的 Down 回退处理 + + AGENTS.md 要求任何计划的第一个 block 点必须是向用户汇报计划内容并等待用户确认,且任何新增 / 改写的成员命名必须先经用户同意。本计划改的是渲染端勇士本体的钩子接线与内部命名,属于「接口 / 命名设计归用户」的边界;`.planning/config.json` 为 `mode: yolo`,因此该关卡必须显式声明为 `blocking-human` 并置 `autonomous: false`。`onSetPos` 的归属与 D-18/D-22 多余方法的归属若不先裁决,执行者会在「避免重复触发」与「逐字保留」之间自行猜测。 + + + + + + + 回复「可以执行」四个字(如需改名 / 改归属 / 改回退处理,请在回复中一并给出),或给出修订意见 + + 用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;本关卡为人工关卡,无可运行的 `` 命令,故不携带 ``;失败语义完整写在 Task 0 的 action ⑧ 与 acceptance_criteria 中) + + + - 用户回复中明确包含「可以执行」四个字 + - 汇报内容涵盖:要解决的问题(D-24..D-28 五条)、逐调用替换映射表(旧 helper 调用行 → `IFaceHandler` 方法 → 语义等价说明)、成员命名清单(新增 `MapHeroLocationHook` / `MapHeroMoverHook` / `faceHandler`;删除 `MapHeroHook`;方法归属)、Down 回退处理、范围边界与禁令、验证方式与运行时不可得的诚实降级 + - 汇报中已原样告知关卡性质:执行者/子代理必须停下等待,不因触发方式或 yolo / auto 模式自动放行 + + 用户明确回复「可以执行」(如需改名 / 改归属 / 改回退处理,已在回复中给出);在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理 + + + + Task 1(tracer):D-24 端到端最小切片 —— `manager.ts` 注入 `state.mover.faceHandler` → `MapHeroRenderer` 构造器保存 → 7 处弃用 helper 全部改走 `IFaceHandler` + packages-user/client-modules/src/render/map/extension/manager.ts, packages-user/client-modules/src/render/map/extension/hero.ts, .planning/phases/04-render-adaptation/04-04-SUMMARY.md + + - AGENTS.md(命名规则 / 注释规则 / 第一个 block 点)与 dev.md(双端分离;注释规范;换行 CRLF) + - packages-user/client-modules/src/render/map/extension/hero.ts(整文件 552 行:import `:1-21`;`addHeroMoving` `:110-140`;`setImage` `:199-206`;`moveEntity` `:225-248`;`generateJumpFn` `:255`;`addFollower` `:351-386`;`turn` `:451-460`;`MapHeroHook` `:468-552`) + - packages-user/client-modules/src/render/map/extension/manager.ts(`addHero` `:24-32`) + - packages/common/src/utils/types.ts:145-197(`IFaceDescriptor` / `IFaceHandler`:`degrade` / `movement` / `move` / `opposite` / `next` / `mapDirection` / `mapMovement`) + - packages-user/data-common/src/common/utils.ts:1-64(三个弃用 helper 的现行签名与语义,作为替换等价性的对照) + - packages-user/data-common/src/common/mover.ts:103-108,166-232,235-251,513-527,646-687(`IObjectMoveAnimDir` / 钩子成员 / `IObjectMover.faceHandler` / `setPos` / `moveProgress` 时序,供 Task 2 参照,本任务只确认 `faceHandler` 来源) + - packages-user/data-base/src/hero/types.ts:277-321,399-417(`IHeroLocationHooks` / `IHeroLocation` / `IHeroMover`) + - packages-user/data-base/src/hero/location.ts:33-45(`HeroLocation` 以 `IFaceHandler` 构造 `HeroMover`) + - packages-user/data-common/src/common/faceManager.ts:169-202(`Dir4FaceHandler` 的 `degrade` / `movement` / `next` 实现,核对等价性) + - packages-user/client-modules/src/action/move.ts:1-6(**只读**:确认 D-19 范围外,不得改动) + + + (0) **执行前核对与基线(只读前置,必须先做)**: + (0a) **数据端现行接口复核(消费未确定项,不猜)**:打开当日 HEAD 的基准文件逐条确认,并把结论(每条一行:结论 + `file:line`)写入 `04-04-SUMMARY.md` 的「执行前核对」段:① `IFaceHandler` 七成员(`degrade` / `movement` / `move` / `opposite` / `next` / `mapDirection` / `mapMovement`)存在,`movement(dir)` 返回 `IFaceDescriptor { readonly x; readonly y }`,`degrade` 对 Unknown 返回 Unknown(`packages/common/src/utils/types.ts:152-197`);② `IObjectMover.faceHandler: IFaceHandler` 存在(`data-common/src/common/mover.ts:251`);③ `IHeroMover extends IObjectMover`(`data-base/src/hero/types.ts:399`)且 `IHeroLocation.mover` 类型为 `IHeroMover`(`:303-314`);④ `HeroLocation` 由传入的 `IFaceHandler` 构造 `HeroMover`(`data-base/src/hero/location.ts:33-45`);⑤ 三个弃用 helper 仍导出且签名如记录(`data-common/src/common/utils.ts:8` `getFaceMovement`、`:37` `degradeFace`、`:64` `nextFaceDirection`)——这是「待替换物尚在」的核对;⑥ `hero.ts` 当前 7 处调用行号仍为 `:132`/`:202`/`:230`/`:357`/`:364`/`:365`/`:453`,import 行仍为 `:2`/`:4`/`:7`。**任一不同:立即停止,不写任何代码,向用户汇报差异并修订本计划。** + (0b) **并发改动基线**:执行 `node -e "const fs=require('fs'),c=require('crypto'),{execSync}=require('child_process');const keep=l=>!/packages-user\/client-modules\/src\/render\/map\/extension\//.test(l)&&!/\.planning\/phases\/04-render-adaptation\//.test(l);const out=execSync('git status --porcelain',{encoding:'utf8'}).replace(/\s+$/,'');const cur=(out?out.split(/\r?\n/).map(s=>s.replace(/\s+$/,'')):[]).filter(keep);const digest=c.createHash('sha256').update(cur.slice().sort().map(l=>{let h='MISSING';try{h=c.createHash('sha256').update(fs.readFileSync(l.slice(3))).digest('hex')}catch(e){}return l+' '+h}).join('\n')).digest('hex');console.log(cur.join('\n'));console.log('基线摘要 sha256='+digest)"`,把输出**原样**写入 SUMMARY,格式固定为: + ``` + + + + 基线摘要 sha256=<64hex> + ``` + **不得沿用 04-03 SUMMARY 的旧列表**(那批改动已随 `4e64953 chore: 内部接口改名` 落库);一律以本次实测为准。**不得回滚 / 暂存 / 提交 / 修改**其中任何条目。 + (0c) **类型门禁基线**:执行 `pnpm exec vue-tsc --noEmit`(整仓会以非 0 退出属预期)并把 `client-modules` 的命中行**归一化**(每行形如 `<相对 client-modules/src 的路径>(line,col): error TSxxxx`)后原样写入 SUMMARY,格式固定为: + ``` + + <归一化后的 client-modules 命中行,逐行> + + ``` + 同时确认三个在范围文件(`render/map/extension/{hero,types,manager}.ts`)**当前 0 条诊断**。**若当日基线与规划时记录不同,以当日实测为准记入 SUMMARY**(说明用户并发改动改变了基线),**不得静默沿用旧值**。 + (0d) 建立 `04-04-SUMMARY.md` 骨架(含上面三段 + 后续任务要填的空节),保持 CRLF。 + (1) **`manager.ts`(注入层)**:把 `addHero` 中的 `new MapHeroRenderer(this.renderer, layer, state)` 改为 `new MapHeroRenderer(this.renderer, layer, state, state.mover.faceHandler)`。不改 `addHero` 的签名 / jsDoc、不改 `heroMap` / `removeHero` / `addDoor` / `removeDoor` / `addText` / `removeText` / `destroy`,不新增 import。 + (2) **`hero.ts` import(D-24 部分)**:从 `@user/data-common` 的 import 中删除 `degradeFace` / `getFaceMovement` / `nextFaceDirection` 三个名字(其余 `FaceDirection` / `IObjectMover` / `IObjectMoverHooks` / `ObjectAnimDirection` / `ObjectMoveStep` / `ObjectMoveType` 本任务全部保留——都有既有使用点);把 `@motajs/common` 的 `import { IHookController, logger }` 扩为 `import { IFaceHandler, IHookController, logger }`。**本任务不要引入本任务用不到的名字**(eslint `@typescript-eslint/no-unused-vars` 为 error 且 `args: 'all'`,未用 import 直接报错;`--fix` 不会删除未使用 import)。 + (3) **`MapHeroRenderer` 构造器(D-24 注入)**:在 `readonly hero: IHeroLocation` 之后新增第 4 个参数 `readonly faceHandler: IFaceHandler`(字段名以 Task 0 用户确认为准)。不改构造体既有钩子注册逻辑(本任务**保留**单一 `MapHeroHook` 现状,Task 2 再拆)。 + (4) **7 处弃用 helper 替换(逐处对齐,行号为改动前锚点)**: + (4a) `:132` `degradeFace(hero.getCurrentFaceDirection())` → `this.faceHandler.degrade(hero.getCurrentFaceDirection())`(`addHeroMoving`)。 + (4b) `:202` `degradeFace(this.hero.getCurrentFaceDirection())` → `this.faceHandler.degrade(this.hero.getCurrentFaceDirection())`(`setImage`)。 + (4c) `:230` `const { x: dx, y: dy } = getFaceMovement(direction);` → `const { x: dx, y: dy } = this.faceHandler.movement(direction);`(`moveEntity`)。 + (4d) `:357` `const nowFace = degradeFace(last.nextDirection, FaceDirection.Down);` → 先 `const degraded = this.faceHandler.degrade(last.nextDirection);`,再 `const nowFace = degraded === FaceDirection.Unknown ? FaceDirection.Down : degraded;`(`addFollower`;**Down 回退显式保留**,行为与旧 helper 的 `unknown` 形参逐字等价)。 + (4e) `:364` `const { x: dxn, y: dyn } = getFaceMovement(last.nextDirection);` → `this.faceHandler.movement(last.nextDirection)`(变量名与后续用法不变)。 + (4f) `:365` `const { x: dx, y: dy } = getFaceMovement(last.direction);` → `this.faceHandler.movement(last.direction)`。 + (4g) `:453` `nextFaceDirection(this.heroEntity.direction)` → `this.faceHandler.next(this.heroEntity.direction)`(`turn`;默认顺时针,与旧 helper 的 `face8=false` / `anticlockwise=false` 默认一致)。 + **不得**改动这些方法里与其无关的分支 / 注释 / 变量名;**不得**改动 `:235`/`:358`/`:417` 的 `state.roleFace.getFaceOf(...)`(D-28 保留)。 + (5) **提交**:本任务改动是整体修正的中间态,**不单独提交**;Task 3 完成全部改动后再一次性原子提交(见 Task 3)。若执行环境强制每任务提交,提交信息用 `fix(04-04): inject IFaceHandler into hero renderer`,并与后续任务的提交在本次执行内连续完成。 + (6) 全程保持文件 CRLF;改完执行 `pnpm exec eslint --fix` 对这两个文件,再 `pnpm exec eslint` 必须 0 错误。 + + 本任务只把渲染端内部朝向计算从弃用 helper 换到注入的 `IFaceHandler`,并把新参数加在 `MapHeroRenderer` 构造器(不在任何接口里);不改变公共契约面,回退只需还原两个文件,故为 `reversible`。 + + node -e "const fs=require('fs'),{execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const norm=l=>{const m=l.match(/client-modules[\\/]src[\\/](.+)$/);return m?m[1].replace(/\s+$/,''):null};const cur=lines.map(norm).filter(Boolean).sort();const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const bm=t.match(/([\s\S]*?)/);if(!bm){console.error('NO TYPE BASELINE');process.exit(1)}const base=bm[1].split(/\r?\n/).map(s=>s.trim()).filter(Boolean).sort();const extra=cur.filter(x=>base.indexOf(x)<0);const missing=base.filter(x=>cur.indexOf(x)<0);if(extra.length||missing.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK in-scope type errors 0; client-modules baseline '+cur.length)" + non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS"(三个在范围文件仍有类型错误)/ "CLIENT-MODULES ERROR SET CHANGED"(渲染端类型错误集相对基线增减)/ "NO TYPE BASELINE"(SUMMARY 未按固定格式记录基线) + node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';const src=['hero.ts','types.ts','manager.ts'].map(f=>fs.readFileSync(S+f,'utf8')).join('\n');const hasTok=(s,t)=>new RegExp('(?hasTok(src,t));if(hit.length){console.error('DEPRECATED FACE HELPERS REMAIN '+hit.join(','));process.exit(1)}const miss=['IFaceHandler','faceHandler'].filter(t=>!src.includes(t));if(miss.length){console.error('FACE HANDLER INJECTION MISSING '+miss.join(','));process.exit(1)}const m=fs.readFileSync(S+'manager.ts','utf8');if(!/\.mover\.faceHandler/.test(m)){console.error('MANAGER NOT PASSING mover.faceHandler');process.exit(1)}const h=fs.readFileSync(S+'hero.ts','utf8');if(!/getCurrentFaceDirection\(\)/.test(h)){console.error('FACE DIRECTION READ LOST');process.exit(1)}console.log('OK D24 face handler injection (deprecated helpers 0; faceHandler wired)')" + non-zero exit, 或 stderr 含 "DEPRECATED FACE HELPERS REMAIN"(三个弃用 helper 名仍以**独立 token**(词边界)出现在三个在范围文件)/ "FACE HANDLER INJECTION MISSING"(`IFaceHandler` 或 `faceHandler` 未出现)/ "MANAGER NOT PASSING mover.faceHandler"(注入点未接)/ "FACE DIRECTION READ LOST"(朝向读取口径丢失) + pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts packages-user/client-modules/src/render/map/extension/manager.ts + non-zero exit 或输出含 "error"(ESLint / Prettier 违规,含未使用 import / 未使用变量) + node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';for(const f of ['hero.ts','manager.ts']){const s=fs.readFileSync(S+f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF LINE ENDINGS '+f);process.exit(1)}}console.log('OK CRLF')" + non-zero exit, 或 stderr 含 "NON-CRLF LINE ENDINGS"(出现孤立 LF,违反 dev.md「换行使用 CRLF 格式」) + + + - `manager.ts` 的 `addHero` 以 `state.mover.faceHandler` 作为 `new MapHeroRenderer(...)` 的第四个实参;`addHero` 签名与 jsDoc 未变 + - `hero.ts` 构造器新增 `IFaceHandler` 参数(`faceHandler`),且 `@motajs/common` 的 import 含 `IFaceHandler` + - 弃用 helper 的 7 处调用(`:132`/`:202`/`:230`/`:357`/`:364`/`:365`/`:453` 对应位置)全部改走 `this.faceHandler.degrade` / `.movement` / `.next`;`:357` 的 Unknown→Down 回退显式保留;三个弃用名字不再出现在三个在范围文件的任何位置(含 import) + - `:235`/`:358`/`:417` 的 `state.roleFace.getFaceOf(...)` 与 `import { state } from '@user/data-state'` 一字未动(D-28) + - 本任务**未**触碰 `MapHeroHook` / 钩子注册 / 贴图 / 跟随者 / `types.ts`;未提交任何改动 + - 四条 `` 全部转绿:scoped 类型门禁(三个文件 0 条、`client-modules` 错误集 == SUMMARY 记录的 `TYPE-BASELINE`)、D-24 注入门禁、eslint 0 错误、CRLF 门禁 + + `state.mover.faceHandler` 经 `manager.ts` 注入到 `MapHeroRenderer`,7 处弃用 helper 全部改走 `IFaceHandler`(含 Down 回退显式保留),D-28 的 `state` 用法与 D-18/D-22 成员未动,四条机器门禁转绿 + + + + Task 2:D-25 钩子拆两类(各自注册、各自 controller、`onSetPos` 无条件且只出现一次)+ D-26 补 AnimDir 设置 + packages-user/client-modules/src/render/map/extension/hero.ts + + - packages-user/client-modules/src/render/map/extension/hero.ts(Task 1 产出的现状全文;重点构造器 `:77-106`、`destroy` `:462-465`、`MapHeroHook` `:468-552`) + - packages-user/data-base/src/hero/types.ts:277-300(`IHeroLocationHooks`:`onSetPos(x, y)` / `onSetFloor(map)`) + - packages-user/data-common/src/common/mover.ts:166-232(`IObjectMoverHooks` 七成员与返回类型:`onMoveStart` / `onMoveEnd` / `onStepEnd` 为 `Promise`;`onSetPos` / `onSetFaceDir` / `onSetMoveDir` 为 `void`) + - packages-user/data-common/src/common/mover.ts:103-108,513-527,646-687(`IObjectMoveAnimDir { type, dir }`;`prepareStep` 对 AnimDir 写 `currAnimDir`;`moveProgress` 时序中 `mover.setPos` 会先 `tile.setPos` → 触发 location 钩子) + - packages-user/data-base/src/hero/location.ts:59-63(`HeroLocation.setPos` 触发 `IHeroLocationHooks.onSetPos`) + - .planning/phases/04-render-adaptation/04-CONTEXT.md(D-18 / D-21 / D-22 / D-25 / D-26) + - .planning/phases/04-render-adaptation/04-03-PLAN.md:282-290(04-03 对 `onStepEnd` 分派与 D-18/D-22 保留方法的既有约定,本任务在其上叠加 D-26) + + + (1) **D-25 拆分 `MapHeroHook` 为两个内部类**(类名以 Task 0 用户确认为准;`MapHeroHook` 删除): + (1a) `class MapHeroLocationHook implements Partial`:`constructor(readonly hero: MapHeroRenderer) {}`;实现 `onSetPos(x: number, y: number): void { this.hero.setPosition(x, y); }`——**无条件设置,不得出现 `moving` 守卫**(旧守卫 `if (this.hero.hero.mover.moving) return;` 整行删除)。`onSetFloor` 仍不实现(既有约定)。该类同时**逐字承载** D-18/D-22 的非接口多余方法:`onSetImage(image: ImageIds)` / `onSetAlpha(alpha: number)` / `onSetFollowerAlpha(identifier: string, alpha: number)` / `onAddFollower(follower: number, identifier: string)` / `onRemoveFollower(identifier: string, animate: boolean)` / `onRemoveAllFollowers()`——方法体、参数名、注释与 jsDoc **一字不改**(它们不属于任何现有钩子接口,也不注册到任何数据端对象)。 + (1b) `class MapHeroMoverHook implements Partial>`:`constructor(readonly hero: MapHeroRenderer) {}`;逐字搬运 / 保留移动族钩子:`async onMoveStart(): Promise { this.hero.startMove(); }`、`async onMoveEnd(): Promise { await this.hero.waitMoveEnd(); }`、`async onStepEnd(_code, step, tile, mover): Promise`(内部分派逻辑见 (2))、`onSetFaceDir(dir: FaceDirection): void { this.hero.turn(dir); }`。**刻意不实现 `onSetPos`**(`onSetPos` 只出现在 location 类一次;`mover.setPos` 会先 `tile.setPos` 触发 location 钩子,location 触发面是超集,从而同一次定位不会重复触发 `setPosition`)。`onStepStart` / `onSetMoveDir` 仍不实现(既有约定,理由照 04-03)。`onStepEnd` 的未使用首参命名保持 `_code`(eslint `args: 'all'` + `argsIgnorePattern: '^_'`)。 + (2) **D-26 补 AnimDir**:在 `MapHeroMoverHook.onStepEnd` 中,保留开头既有的 `this.hero.setHeroAnimateDirection(mover.currAnimDir);`,并把 `case ObjectMoveType.AnimDir:` 由 `break;`(no-op)改为 `this.hero.setHeroAnimateDirection(step.dir); break;`(`step` 在 `case ObjectMoveType.AnimDir` 下被收窄为 `IObjectMoveAnimDir`,`step.dir: ObjectAnimDirection`)。`ObjectMoveType.Speed` 分支保持 `break;`(无渲染动作)。 + (3) **构造器注册(拆成两次、各自 `load()`)**:把原来「一个 `hook` 实例注册到 `hero` 与 `hero.mover`」改为:`this.locationController = hero.addHook(new MapHeroLocationHook(this)); this.locationController.load(); this.moverController = hero.mover.addHook(new MapHeroMoverHook(this)); this.moverController.load();`。`locationController` / `moverController` 字段声明不变;`destroy()`(`locationController.unload(); moverController.unload();`)不变。 + (4) **不得改动的事**:不改 `MapHeroRenderer` 的动画算法(`tick` / `moveEntity` / `jumpEntity` / `generateJumpFn`)、不改 `addHeroMoving` 的贴图分支结构、不改 `setImage` / `setAlpha` / 跟随者族实现体、不改 `:235`/`:358`/`:417` 的 `state.roleFace.getFaceOf(...)`(D-28)、不接线 `client.ts`、不动 `action/move.ts`。既有注释 / jsDoc 一字不改(新增注释除外)。 + (5) **提交**:不单独提交(Task 3 整体原子提交)。若执行环境强制每任务提交,提交信息用 `fix(04-04): split hero hooks and handle AnimDir`。 + (6) 全程保持文件 CRLF;改完执行 `pnpm exec eslint --fix`,再 `pnpm exec eslint` 必须 0 错误。 + + + node -e "const fs=require('fs');const h=fs.readFileSync('packages-user/client-modules/src/render/map/extension/hero.ts','utf8');const hasTok=(s,t)=>new RegExp('(?!h.includes(t));if(miss.length){console.error('SPLIT HOOKS MISSING '+miss.join(','));process.exit(1)}const pos=h.indexOf('onSetPos(');if(pos<0){console.error('NO onSetPos');process.exit(1)}const posBody=h.slice(pos,h.indexOf('\n }',pos)+6);if(hasTok(posBody,'moving')){console.error('onSetPos STILL GUARDED BY moving');process.exit(1)}if(!/setPosition\(/.test(posBody)){console.error('onSetPos DOES NOT SET POSITION');process.exit(1)}const se=h.indexOf('async onStepEnd(');if(se<0){console.error('NO onStepEnd');process.exit(1)}const seBody=h.slice(se,h.indexOf('\n }',se)+6);if(!seBody.includes('ObjectMoveType.AnimDir')){console.error('ANIMDIR CASE MISSING');process.exit(1)}if(!/setHeroAnimateDirection\(step\.dir\)/.test(seBody)){console.error('ANIMDIR NOT APPLIED');process.exit(1)}console.log('OK D25 split hooks / D26 AnimDir')" + non-zero exit, 或 stderr 含 "OLD SINGLE HOOK CLASS REMAINS"(`MapHeroHook` 仍以独立 token 存在)/ "SPLIT HOOKS MISSING"(两个钩子类 / 两次 `addHook` / 两个 controller 未齐)/ "NO onSetPos" / "onSetPos STILL GUARDED BY moving"(`onSetPos` 方法体内仍出现 `moving`,守卫未删)/ "onSetPos DOES NOT SET POSITION"(未直接设置位置)/ "ANIMDIR CASE MISSING" / "ANIMDIR NOT APPLIED"(AnimDir 分支未按 `step.dir` 设置动画方向) + node -e "const fs=require('fs');const h=fs.readFileSync('packages-user/client-modules/src/render/map/extension/hero.ts','utf8');const need=['onSetImage(','onSetAlpha(','onSetFollowerAlpha(','onAddFollower(','onRemoveFollower(','onRemoveAllFollowers(','setImage(','setAlpha(','setFollowerAlpha(','addFollower(','removeFollower(','removeAllFollowers('];const miss=need.filter(x=>!h.includes(x));if(miss.length){console.error('D18 D22 MEMBERS REMOVED FROM hero.ts '+miss.join(' , '));process.exit(1)}const legacy=['onSetPosition','onStartMove','onMoveHero','onEndMove','onJumpHero','onTurnHero'];const lHit=legacy.filter(x=>h.includes(x));if(lHit.length){console.error('LEGACY HOOK NAMES REMAIN '+lHit.join(','));process.exit(1)}console.log('OK D18 D22 members preserved')" + non-zero exit, 或 stderr 含 "D18 D22 MEMBERS REMOVED FROM hero.ts"(贴图 / 不透明度 / 跟随者成员被误删,违反 D-18 / D-22)/ "LEGACY HOOK NAMES REMAIN"(04-03 已清除的旧钩子名回潮) + node -e "const fs=require('fs'),{execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const norm=l=>{const m=l.match(/client-modules[\\/]src[\\/](.+)$/);return m?m[1].replace(/\s+$/,''):null};const cur=lines.map(norm).filter(Boolean).sort();const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const bm=t.match(/([\s\S]*?)/);if(!bm){console.error('NO TYPE BASELINE');process.exit(1)}const base=bm[1].split(/\r?\n/).map(s=>s.trim()).filter(Boolean).sort();const extra=cur.filter(x=>base.indexOf(x)<0);const missing=base.filter(x=>cur.indexOf(x)<0);if(extra.length||missing.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK in-scope type errors 0; client-modules baseline '+cur.length)" + non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS" / "CLIENT-MODULES ERROR SET CHANGED" / "NO TYPE BASELINE"(拆分钩子类或 AnimDir 分支引入新诊断) + pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts + non-zero exit 或输出含 "error"(含未使用变量 / 未使用 import / Prettier 违规) + node -e "const fs=require('fs');const s=fs.readFileSync('packages-user/client-modules/src/render/map/extension/hero.ts','utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF LINE ENDINGS hero.ts');process.exit(1)}console.log('OK CRLF')" + non-zero exit, 或 stderr 含 "NON-CRLF LINE ENDINGS"(出现孤立 LF) + + + - `MapHeroHook` 已不存在;存在 `MapHeroLocationHook`(`Partial`)与 `MapHeroMoverHook`(`Partial>`)两个内部类 + - 构造器分别 `hero.addHook(new MapHeroLocationHook(this))` 与 `hero.mover.addHook(new MapHeroMoverHook(this))`,两个 controller 各自 `load()`;`destroy()` 各自 `unload()` + - `onSetPos(x, y)` 只在 `MapHeroLocationHook` 上出现一次,方法体为直接的 `this.hero.setPosition(x, y)`,**不含 `moving` 守卫**;`MapHeroMoverHook` 不含 `onSetPos` + - `MapHeroMoverHook.onStepEnd` 保留开头 `mover.currAnimDir` 设置,且 `ObjectMoveType.AnimDir` 分支调用 `this.hero.setHeroAnimateDirection(step.dir)` + - D-18/D-22 成员(`onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` / 跟随者三方法,以及 `setImage` / `setAlpha` / `setFollowerAlpha` / 跟随者三实现)逐字保留;04-03 已清除的旧钩子名未回潮 + - `:235`/`:358`/`:417` 的 `state.roleFace.getFaceOf(...)` 未动(D-28) + - 五条 `` 全部转绿(拆分类门禁、D-18/D-22 保留门禁、scoped 类型门禁、eslint、CRLF) + + 两个钩子类各自注册、`onSetPos` 无条件且只出现一次、AnimDir 分支按 `step.dir` 设置动画方向,D-18/D-22/D-28 保留,五条机器门禁转绿 + + + + Task 3:D-27 换 `@motajs/animate` + 原子提交 + 终验门禁(范围 / 并发基线 / 静态 / 类型 / eslint / CRLF)+ 人工复核 + `04-04-SUMMARY.md` 定稿 + packages-user/client-modules/src/render/map/extension/hero.ts, .planning/phases/04-render-adaptation/04-04-SUMMARY.md + + - packages-user/client-modules/src/render/map/extension/hero.ts(Task 1/2 产出的现状;`import { TimingFn } from 'mutate-animate';` 与 `generateJumpFn` 及其返回类型) + - packages/animate/src/types.ts:7-12 与 packages/animate/src/index.ts:1-5(`ExcitationCurve2D` 与其 barrel 导出) + - node_modules/mutate-animate/index.d.ts:8-31(`TimingFn` = `(input: number) => [number, number]`(N=2);`PathFn = TimingFn<2>`) + - packages-user/client-modules/src/render/map/moving.ts:162-187(`moveRelative(curve: TimingFn<2>, time, timing?)` 的形参,确认 `ExcitationCurve2D` 可赋值) + - .planning/phases/04-render-adaptation/04-04-SUMMARY.md(Task 1 写入的执行前核对 / 并发基线 / 类型基线三段) + - .planning/phases/04-render-adaptation/04-04-PLAN.md(本任务的全部门禁定义与成功标准) + - .planning/phases/04-render-adaptation/04-CONTEXT.md(D-24..D-28 的裁定原文,用于 SUMMARY 的决策段) + + + (1) **D-27(仅 hero.ts)**:把 `import { TimingFn } from 'mutate-animate';` 改为 `import { ExcitationCurve2D } from '@motajs/animate';`(`@motajs/animate` 已是 `client-modules` 的 workspace 依赖,零依赖变更);把 `generateJumpFn(dx: number, dy: number): TimingFn<2>` 的返回类型改为 `ExcitationCurve2D`。函数体与返回值 `[x, y]` 一字不改(返回类型标注会让元组字面量被上下文类型化为 `[number, number]`)。确认 `hero.ts` 内再无 `TimingFn` / `mutate-animate` 引用;**不得**改动 `moving.ts` / `maps/types.ts` / 其它文件的存量 `mutate-animate` 使用(D-27 范围)。 + (2) **原子提交**:`git add` 仅 `packages-user/client-modules/src/render/map/extension/hero.ts` 与 `packages-user/client-modules/src/render/map/extension/manager.ts`,提交信息 `fix(04-04): align hero renderer with IFaceHandler and split hooks`(或执行环境约定的等价信息);**不得**把用户的并发未提交改动纳入提交,也不得提交 `types.ts`(本计划不改它)。 + (3) **范围门禁(基线感知,先跑,防止越界改动)**:把当前 `git status --porcelain` 的**生产路径**(`^packages(-user)?/`)与 SUMMARY 中 `CONCURRENT-BASELINE` 段记录的基线条目做差集;只有**基线之外**、且不属于 `hero.ts` / `manager.ts` 的生产改动才算越界。特别断言 `extension/types.ts`(本计划不改)、`action/move.ts`(D-19)、`client-base/**`、`data-common/**`、`data-base/**`、`data-system/**`、`data-state/**`、`packages/**` 均无基线之外的新改动。若出现基线之外的第三个生产文件改动:停止、汇报、修订计划(不得自行扩面)。 + (4) **并发改动基线门禁**:用与 Task 1 步骤 (0b) 完全相同的固定命令重算(排除 `packages-user/client-modules/src/render/map/extension/` 与 `.planning/phases/04-render-adaptation/` 两个前缀),断言 porcelain 条目集合与内容摘要 `sha256` 均与 SUMMARY 的 `CONCURRENT-BASELINE` 段记录一致。 + (5) **复跑全部机器门禁**:scoped 类型门禁(三个在范围文件 0 条 + `client-modules` 错误集 == `TYPE-BASELINE`)、静态门禁 A(D-24/D-25/D-26/D-28)、静态门禁 B(D-27 + D-18/D-22 保留 + `IMapHeroRenderer` 契约保持)、`pnpm exec eslint` 对 hero.ts / manager.ts 0 错误、CRLF 门禁。 + (6) **人工复核(写进 SUMMARY 的「人工复核」段,逐条给结论)**:① D-24 的 7 处逐条对位(旧 helper 行 → 新 `faceHandler` 调用)与语义等价性结论(含当日实测注入的处理器是 `Dir4FaceHandler` 还是 `Dir8FaceHandler`);② `:357` 的 Unknown→Down 回退是否与旧 helper 语义一致;③ D-25:两个钩子类是否分别注册到 `hero` / `hero.mover`,`onSetPos` 是否只出现一次且无条件,并说明「`mover.setPos` → `tile.setPos`(location 钩子)→ `onSetPos` 一次」为何不会再重复触发;④ D-26:AnimDir 分支是否按 `step.dir` 设置,`Speed` 是否仍无渲染动作;⑤ D-27:`hero.ts` 是否已无 `mutate-animate`,`ExcitationCurve2D` 是否被 `moveRelative(curve: TimingFn<2>)` 的形参接受(以编译结果为准);⑥ D-28:`state.roleFace.getFaceOf` 三处与 `import { state } from '@user/data-state'` 是否未动;⑦ D-18/D-22 成员是否逐字保留(对照门禁输出);⑧ 是否新增任何公共接口成员(应为零)且 `types.ts` 是否未改;⑨ 运行时 UAT 不可得的理由已写明(`addHero` 未被接线,`client.ts:148-152` 仍为注释)。 + (7) **SUMMARY 定稿**:写 `.planning/phases/04-render-adaptation/04-04-SUMMARY.md`,含:改动两文件的清单与职责;D-24..D-28 的逐条处置对照表(含 7 处替换映射与 Down 回退处理);执行前核对六条结论(带 `file:line`);执行前基线(`CONCURRENT-BASELINE` 段 + `基线摘要 sha256`)与类型门禁基线(`TYPE-BASELINE` 段,逐行原样);全部机器门禁的实际输出摘要;人工复核九条;未实现 / 显式延后项清单(`client.ts` 接线、D-18 贴图来源、D-22 跟随者、D-19 移动控制、D-27 其余存量迁移、REND-02 双布局、其余 ① / ③ 项);以及 REND-01 / REND-02 仍为 Pending 与运行时 UAT 不可得的明确声明。全文 CRLF。 + (8) **提交 SUMMARY**:`docs(04-04): record execution state`,仅含 `04-04-SUMMARY.md`。 + (9) **不做的事**:不接线 `client.ts`、不改 `action/move.ts`、不改 `types.ts`、不新增测试文件(渲染端无测试设施)、不改任何数据端文件、不修任何 ③ 多余旧路径或其它 ① 错配、不迁移 `hero.ts` 之外的 `mutate-animate`。 + + + node -e "const fs=require('fs'),{execSync}=require('child_process');let out;try{out=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('GIT STATUS FAILED '+e.message);process.exit(1)}const allow=['packages-user/client-modules/src/render/map/extension/hero.ts','packages-user/client-modules/src/render/map/extension/manager.ts'];const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const bm=t.match(/([\s\S]*?)/);if(!bm){console.error('NO CONCURRENT BASELINE');process.exit(1)}const base=bm[1].split(/\r?\n/).map(s=>s.replace(/\s+$/,'')).filter(Boolean).map(s=>s.slice(3).replace(/^\"|\"$/g,''));const lines=out.split(/\r?\n/).filter(l=>l.trim().length>0);const prod=lines.map(l=>l.slice(3).replace(/^\"|\"$/g,'')).filter(p=>/^packages(-user)?\//.test(p));const extra=prod.filter(p=>allow.indexOf(p)<0&&base.indexOf(p)<0);if(extra.length){console.error('OUT-OF-SCOPE FILE MODIFIED '+extra.join(' ; '));process.exit(1)}console.log('OK scope (baseline-aware): out-of-scope extra 0')" + non-zero exit, 或 stderr 含 "OUT-OF-SCOPE FILE MODIFIED"(出现 `files_modified` 之外、且不在并发基线内的生产文件改动,含 `extension/types.ts`、D-19 的 `action/move.ts`、任何数据端文件或 `packages/**` 的新改动;基线内的用户并发改动不触发)/ "GIT STATUS FAILED" / "NO CONCURRENT BASELINE" + node -e "const fs=require('fs'),c=require('crypto'),{execSync}=require('child_process');const keep=l=>!/packages-user\/client-modules\/src\/render\/map\/extension\//.test(l)&&!/\.planning\/phases\/04-render-adaptation\//.test(l);const out=execSync('git status --porcelain',{encoding:'utf8'}).replace(/\s+$/,'');const cur=(out?out.split(/\r?\n/).map(s=>s.replace(/\s+$/,'')):[]).filter(keep);const digest=c.createHash('sha256').update(cur.slice().sort().map(l=>{let h='MISSING';try{h=c.createHash('sha256').update(fs.readFileSync(l.slice(3))).digest('hex')}catch(e){}return l+' '+h}).join('\n')).digest('hex');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const bm=t.match(/([\s\S]*?)/);if(!bm){console.error('NO CONCURRENT BASELINE');process.exit(1)}const rec=bm[1].split(/\r?\n/).map(s=>s.replace(/\s+$/,'')).filter(Boolean).filter(keep);const dm=t.match(/基线摘要[^\r\n]*?sha256=([0-9a-f]{64})/);if(!dm){console.error('NO BASELINE DIGEST');process.exit(1)}const extra=cur.filter(l=>rec.indexOf(l)<0);const gone=rec.filter(l=>cur.indexOf(l)<0);if(extra.length||gone.length){console.error('BASELINE ENTRY SET CHANGED extra=['+extra.join(' ; ')+'] missing=['+gone.join(' ; ')+']');process.exit(1)}if(dm[1]!==digest){console.error('BASELINE DIGEST CHANGED '+dm[1]+' vs '+digest);process.exit(1)}console.log('OK user concurrent edits unchanged entries '+cur.length)" + non-zero exit, 或 stderr 含 "NO BASELINE DIGEST" / "BASELINE ENTRY SET CHANGED" / "BASELINE DIGEST CHANGED"(用户并发的未提交改动被本计划回滚 / 暂存 / 提交 / 修改) + node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';const src=['hero.ts','types.ts','manager.ts'].map(f=>fs.readFileSync(S+f,'utf8')).join('\n');const hasTok=(s,t)=>new RegExp('(?hasTok(src,t));if(hit.length){console.error('FORBIDDEN TOKENS REMAIN '+hit.join(','));process.exit(1)}const need=['IFaceHandler','faceHandler','MapHeroLocationHook','MapHeroMoverHook','hero.addHook','hero.mover.addHook','locationController','moverController','setHeroAnimateDirection(step.dir)'];const miss=need.filter(t=>!src.includes(t));if(miss.length){console.error('REQUIRED BINDINGS MISSING '+miss.join(','));process.exit(1)}const h=fs.readFileSync(S+'hero.ts','utf8');const pos=h.indexOf('onSetPos(');if(pos<0){console.error('NO onSetPos');process.exit(1)}const posBody=h.slice(pos,h.indexOf('\n }',pos)+6);if(hasTok(posBody,'moving')||!/setPosition\(/.test(posBody)){console.error('D25 onSetPos WRONG');process.exit(1)}const gf=(h.match(/state\.roleFace\.getFaceOf/g)||[]).length;if(gf<3||!h.includes(\"from '@user/data-state'\")){console.error('D28 STATE USAGE LOST getFaceOf='+gf);process.exit(1)}console.log('OK final gates D24 D25 D26 D28')" + non-zero exit, 或 stderr 含 "FORBIDDEN TOKENS REMAIN"(弃用 helper / 旧单一钩子类名仍以独立 token 出现)/ "REQUIRED BINDINGS MISSING" / "D25 onSetPos WRONG"(`onSetPos` 仍有守卫或未直接设置位置)/ "D28 STATE USAGE LOST"(存量 `state.roleFace` 用法被误删) + node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';const h=fs.readFileSync(S+'hero.ts','utf8');const hasTok=(s,t)=>new RegExp('(?!h.includes(x));if(miss.length){console.error('D18 D22 MEMBERS REMOVED '+miss.join(','));process.exit(1)}const t=fs.readFileSync(S+'types.ts','utf8');const i=t.indexOf('export interface IMapHeroRenderer');const j=t.indexOf('export interface ',i+1);const seg=t.slice(i,j<0?t.length:j);const d18=['setImage(image: ITexture): void;','addFollower(image: number, id: string): void;','removeFollower(follower: string, animate: boolean): Promise;','removeAllFollowers(): void;','setAlpha(alpha: number): void;','setFollowerAlpha(identifier: string, alpha: number): void;','destroy(): void;'];const d18Miss=d18.filter(x=>!seg.includes(x));if(d18Miss.length){console.error('D18 D22 CONTRACT MEMBERS MISSING '+d18Miss.join(' , '));process.exit(1)}console.log('OK final D27 + D18 D22 preservation')" + non-zero exit, 或 stderr 含 "MUTATE-ANIMATE REMAINS IN hero.ts"(D-27 未落地)/ "ANIMATE IMPORT MISSING" / "D18 D22 MEMBERS REMOVED"(hero.ts 的 D-18/D-22 成员被误删)/ "D18 D22 CONTRACT MEMBERS MISSING"(`IMapHeroRenderer` 契约被误改) + node -e "const fs=require('fs'),{execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const norm=l=>{const m=l.match(/client-modules[\\/]src[\\/](.+)$/);return m?m[1].replace(/\s+$/,''):null};const cur=lines.map(norm).filter(Boolean).sort();const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const bm=t.match(/([\s\S]*?)/);if(!bm){console.error('NO TYPE BASELINE');process.exit(1)}const base=bm[1].split(/\r?\n/).map(s=>s.trim()).filter(Boolean).sort();const extra=cur.filter(x=>base.indexOf(x)<0);const missing=base.filter(x=>cur.indexOf(x)<0);if(extra.length||missing.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK final type gate: in-scope 0; client-modules baseline '+cur.length)" + non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS" / "CLIENT-MODULES ERROR SET CHANGED" / "NO TYPE BASELINE"(终验类型门禁未过) + pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts packages-user/client-modules/src/render/map/extension/manager.ts + non-zero exit 或输出含 "error" + node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';for(const f of ['hero.ts','manager.ts']){const s=fs.readFileSync(S+f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF LINE ENDINGS '+f+' lone-lf='+(lf-crlf));process.exit(1)}}console.log('OK CRLF source files')" + non-zero exit, 或 stderr 含 "NON-CRLF LINE ENDINGS"(两个源文件出现孤立 LF,违反 dev.md「换行使用 CRLF 格式」/ `.prettierrc` 的 `endOfLine: crlf`;SUMMARY 不在本机器门禁范围——04-03-SUMMARY 实测为混合行尾,故不对工具写出的 markdown 施加此硬门禁,改由人工复核遵循仓库 markdown 惯例) + node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-04-SUMMARY.md','utf8');const need=['执行前核对','CONCURRENT-BASELINE:START','TYPE-BASELINE:START','人工复核','REND-01','REND-02','Pending','addHero','client.ts:148','@motajs/animate','IFaceHandler','MapHeroLocationHook','MapHeroMoverHook','AnimDir'];const miss=need.filter(x=>!t.includes(x));if(miss.length){console.error('SUMMARY MISSING SECTIONS '+miss.join(','));process.exit(1)}console.log('OK summary completeness')" + non-zero exit, 或 stderr 含 "SUMMARY MISSING SECTIONS"(SUMMARY 缺少执行前核对 / 基线段 / 人工复核 / 需求 Pending 声明 / 运行时不可得说明 / D-24..D-27 关键项) + + + - `hero.ts` 不再出现 `mutate-animate`(独立 token),`generateJumpFn` 返回 `ExcitationCurve2D`(`@motajs/animate`),且 `moving.ts` 等范围外文件未被改动(D-27) + - `hero.ts` / `manager.ts` 已一个原子提交入库(`fix(04-04)`),提交未包含 `types.ts`、未包含用户并发改动 + - 范围门禁(基线感知)通过:除两个在范围文件与基线内的并发用户改动外,无其它生产文件改动(`extension/types.ts`、`action/move.ts`、任何数据端包、`client-base`、`packages` 均无基线之外的新改动) + - 并发改动基线门禁通过:porcelain 条目集合与内容 `sha256` 均与 SUMMARY 记录一致 + - 静态门禁 A(D-24/D-25/D-26/D-28)、静态门禁 B(D-27 + D-18/D-22 保留 + `IMapHeroRenderer` 契约)、scoped 类型门禁(三文件 0 条、`client-modules` 错误集 == `TYPE-BASELINE`)、eslint 0 错误、CRLF 门禁全部转绿 + - `04-04-SUMMARY.md` 含执行前核对六条(带 `file:line`)、`CONCURRENT-BASELINE` 段 + `基线摘要 sha256`、`TYPE-BASELINE` 段、D-24..D-28 处置对照、门禁输出摘要、人工复核九条、未实现 / 延后项清单、REND-01 / REND-02 仍 Pending 与运行时 UAT 不可得的明确说明;并以 `docs(04-04)` 提交(仅含该 SUMMARY) + + D-27 落地并原子提交;七类机器门禁全绿、用户并发改动零变化、范围无越界、SUMMARY 定稿并提交;用户可据此继续裁决后续渲染适配增量(贴图 D-18、跟随者 D-22、接线启用、双布局 REND-02) + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 渲染端契约面 → 下游调用方 | `IMapHeroRenderer` / `IMapExtensionManager` 是 `client-base` / `client-modules` 其它模块与用户代码消费的公共契约;本计划声称不改契约面,误改 `types.ts` 或误删 D-18/D-22 成员会直接扩大破坏面 | +| 数据端接口 → 渲染端绑定 | 渲染端只读 import `data-common` / `data-base` 的类型并读取 `state.mover.faceHandler`;本计划不改数据端,接口一旦被用户并发改动,适配即按错误形状落地 | +| 数据端钩子回调 → 渲染状态 | 现有钩子是唯一允许推进渲染状态的入口(D-20 / D-21);两个钩子类若归错 hookable 或 `onSetPos` 重复实现,会造成重复驱动 / 状态偏差 | +| 执行者/子代理 → 执行前置关卡 | AGENTS.md 要求第一个 block 点先汇报并等待用户回复「可以执行」;`yolo` / `auto_advance` 或子代理派发都不是放行理由(Task 0 为 `blocking-human`、`autonomous: false`) | +| 执行者 → 仓库工作树 | 用户会并发手工重构数据端与加载系统;本计划只允许改两个渲染端文件,且不得触碰用户的未提交改动 | +| 未接线状态 → 验收结论 | `addHero` 无调用点,运行时渲染不可观察;若不显式声明,「编译通过」会被误读为「勇士渲染已可用」 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-04-04-01 | Tampering | `IMapHeroRenderer` / `IMapExtensionManager` 公共契约(`types.ts`) | high | mitigate | `types.ts` 不在 `files_modified`;Task 1 的构造器参数不进任何接口;Task 3 的 D-18/D-22 契约门禁断言 `IMapHeroRenderer` 的 7 个保留成员逐字仍在、范围门禁断言 `types.ts` 无改动;prohibitions 明列不得新增接口成员 | +| T-04-04-02 | Repudiation | 钩子拆分 / 命名 / 映射的可追溯性 | medium | mitigate | 逐调用替换映射表与成员命名清单在 Task 0 汇报并经用户确认;Task 2 / Task 3 静态门禁断言旧单一钩子类名不复现、两类分别注册、`onSetPos` 只出现一次且无守卫、AnimDir 已处理;映射与归属写入 SUMMARY 人工复核段 | +| T-04-04-03 | Elevation of Privilege | 执行前置关卡(AGENTS.md 要求的第一处 block 点) | high | mitigate | Task 0 为 `checkpoint:decision` + `gate="blocking-human"` 且本计划 `autonomous: false`;Task 0 的 action ⑦/⑧ 与 acceptance_criteria 明示未收到「可以执行」四个字即必须停止、不派发执行子代理 | +| T-04-04-04 | Tampering | 用户并发的未提交改动 | high | mitigate | Task 1 步骤 (0b) 记录 porcelain 条目 + 各文件内容 `sha256` 摘要(写入 SUMMARY 的 `CONCURRENT-BASELINE` 段);Task 3 用同一固定命令复算并断言集合与摘要未变;prohibitions 明确不得回滚 / 暂存 / 提交 / 修改 | +| T-04-04-05 | Spoofing | 「编译通过」被当作「运行时可用」 | medium | mitigate | 运行时 UAT 不可得(`addHero` 未被接线)作为 truths/assumptions 明写;SUMMARY 必须含运行时不可得说明(Task 3 门禁断言 `addHero` 与 `client.ts:148` 字样);严禁以不可运行的门禁冒充通过 | +| T-04-04-06 | Elevation of Privilege | 范围蔓延(D-27 范围外的 `mutate-animate`、D-28 误删 `state`、D-18/D-19/D-22、`types.ts`) | medium | mitigate | 范围门禁(基线感知)只放行两个文件;D-18/D-22 保留门禁与 D-28 保留门禁;prohibitions 逐条列明;需要第三个文件时强制停下汇报并修订计划 | +| T-04-04-07 | Information Disclosure | 改动引用的接口名、文件行号与类型签名 | low | accept | 改动只含渲染端源码引用,不含凭据 / 密钥 / 用户数据;无需额外缓解 | +| T-04-04-SC | Tampering | npm / pnpm 依赖安装 | high | accept | 本计划零依赖变更、不修改 `package.json` / `pnpm-lock.yaml`(`@motajs/animate` 已是 `client-modules` 的 workspace 依赖),Package Legitimacy Gate 不触发;若执行中出现安装需求,立即停止并汇报 | + + + + + + + + + + +0. 执行前置关卡(Task 0,AGENTS.md 第一个 block 点):向用户汇报「要解决的问题(D-24..D-28)/ 大致解决方案(含逐调用替换映射表)/ 成员命名清单与方法归属 / Down 回退处理 / 范围边界 / 验证方式的诚实降级」后**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,任何触发方式与 `yolo` / `auto_advance` 模式均不得自动放行(人工核对:用户回复原文)。 +1. 执行前核对与基线(Task 1 步骤 0):六条数据端现行接口复核结论(带 `file:line`)写入 SUMMARY;并发改动基线(`CONCURRENT-BASELINE` 段 + `基线摘要 sha256`)与类型门禁基线(`TYPE-BASELINE` 段,逐行原样)写入 SUMMARY;任一与规划不一致即停止并修订计划;**不得沿用 04-03 SUMMARY 的旧并发列表**(已随 `4e64953` 落库)。 +2. scoped 类型门禁(Task 1 / 2 / 3):`pnpm exec vue-tsc --noEmit` 过滤到 `render/map/extension/{hero,types,manager}.ts` 必须 0 条诊断;`client-modules` 错误集必须与 SUMMARY 的 `TYPE-BASELINE` 完全一致(集合比较,顺序无关)。整仓其余既有诊断不属本计划,不得以「整仓非 0 退出」判定失败,也不得漏判范围文件的新诊断。 +3. 静态门禁 A(Task 1 / 2 / 3):三个在范围文件不出现**独立 token**(词边界)`getFaceMovement` / `degradeFace` / `nextFaceDirection` / `MapHeroHook`;必须出现 `IFaceHandler` / `faceHandler` / `MapHeroLocationHook` / `MapHeroMoverHook` / `hero.addHook` / `hero.mover.addHook` / `locationController` / `moverController` / `setHeroAnimateDirection(step.dir)`;`onSetPos` 方法体内不含 `moving` 且含 `setPosition(`;`hero.ts` 含 `state.roleFace.getFaceOf`(≥3)与 `from '@user/data-state'`。 +4. 静态门禁 B(Task 3):`hero.ts` 不含 `mutate-animate`(独立 token)且含 `from '@motajs/animate'` 与 `ExcitationCurve2D`;`hero.ts` 的 D-18/D-22 钩子与实现方法逐字保留;`IMapHeroRenderer` 的 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy` 逐字保留。 +5. D-18 / D-22 / D-28 保留门禁(Task 2 / 3):贴图 / 不透明度 / 跟随者成员不被删除或改写;存量 `state.roleFace.getFaceOf` 三处与 `import { state } from '@user/data-state'` 未被移除(这是**正向**门禁:D-28 要求保留)。 +6. 并发改动基线门禁(Task 3):排除两个在范围文件与 `.planning/phases/04-render-adaptation/` 后,porcelain 条目集合与内容 `sha256` 与 SUMMARY 的 `CONCURRENT-BASELINE` 段记录一致(用户改动未被回滚 / 暂存 / 提交 / 修改)。 +7. 范围门禁(Task 3,基线感知):将当前 `git status --porcelain` 的生产路径(`^packages(-user)?/`)与 `CONCURRENT-BASELINE` 段做差集;只有基线之外、且不属于 `hero.ts` / `manager.ts` 的生产改动才判越界;`extension/types.ts`、`action/move.ts`、数据端各包、`client-base`、`packages` 均无基线之外的新改动。 +8. ESLint / CRLF 门禁(Task 1 / 2 / 3):`pnpm exec eslint` 对 `hero.ts` / `manager.ts` 0 错误;两个源文件无孤立 LF(`.prettierrc` 的 `endOfLine: crlf`)。SUMMARY 不施加该硬门禁(04-03-SUMMARY 实测为混合行尾),改由人工复核遵循仓库 markdown 惯例。 +9. 人工复核(Task 3):D-24 的 7 处替换逐条对位且语义等价(含注入处理器实际方向组的记录);Down 回退与旧语义一致;两钩子类分别注册且 `onSetPos` 只出现一次、无条件;AnimDir 按 `step.dir` 设置且 `Speed` 仍 no-op;D-27 仅在 `hero.ts` 生效且 `ExcitationCurve2D` 被编译接受;D-28 未动;D-18/D-22 逐字保留;无新增公共接口成员且 `types.ts` 未改;运行时 UAT 不可得的理由已写明。 +10. 提交:Task 3 的 `fix(04-04)` 仅含 `hero.ts` / `manager.ts`;`docs(04-04)` 仅含 SUMMARY;用户并发改动未被纳入任何提交。 + + + +- **执行前置关卡(AGENTS.md):** 本计划第一个 block 点是 Task 0 —— 向用户汇报计划内容、逐调用替换映射表、成员命名清单与 `onSetPos` / D-18·D-22 方法归属、Down 回退处理后停下等待用户明确回复「可以执行」四个字;未收到前不执行任何任务、不派发执行子代理,任何触发方式与 `yolo` / `auto_advance` 模式均不得自动放行。 +- **D-24:** 勇士渲染已绑定注入的 `IFaceHandler`(`manager.ts` 传入 `state.mover.faceHandler`),三个弃用 helper(含 import)在三个在范围文件内以独立 token 判定彻底消失;7 处调用逐条改走 `degrade` / `movement` / `next`,`:357` 的 Unknown→Down 回退显式保留。 +- **D-25:** `MapHeroHook` 已拆为 `MapHeroLocationHook` / `MapHeroMoverHook` 两个内部类,各自注册到 `hero` / `hero.mover` 并各持一个 `IHookController`;`onSetPos` 只出现一次、无条件直接设置位置、无 `moving` 守卫,不再重复触发。 +- **D-26:** `onStepEnd` 的 `ObjectMoveType.AnimDir` 分支按 `step.dir` 设置动画播放方向(且在既有 `mover.currAnimDir` 处理之外)。 +- **D-27:** `hero.ts` 不再使用 `mutate-animate`,改用 `@motajs/animate` 的 `ExcitationCurve2D`;范围外文件的存量使用未被动。 +- **D-28:** 存量 `state.roleFace.getFaceOf` 三处与 `from '@user/data-state'` 逐字保留。 +- **契约面与范围:** `types.ts` 未改、无新增公共接口成员;D-18 / D-22 成员逐字保留;`action/move.ts`、数据端各包、`client.ts` 接线、`core.*` 均未被触碰;用户并发的未提交改动未被回滚 / 暂存 / 提交 / 修改。 +- **验证诚实且可运行:** scoped 类型门禁(三文件 0 条、`client-modules` 错误集 == 基线)、静态门禁 A/B、D-18/D-22 与 D-28 保留门禁、eslint / CRLF 门禁、范围门禁(基线感知)、并发改动基线门禁全部转绿且有实际输出;运行时渲染 UAT 不可得的原因(`addHero` 未被接线)在摘要中明写,未以任何不可运行的门禁冒充通过。 +- REND-01 / REND-02 保持 Pending(两条 FLAGGED ASSUMPTION),用户可据此继续裁决后续增量(贴图 D-18、跟随者 D-22、接线启用、双布局 REND-02、D-27 其余存量迁移、其余 ① / ③ 项)。 + +## Artifacts this phase produces + +| 类型 | 路径 | 内容 | +|------|------|------| +| 生产源码(修改) | `packages-user/client-modules/src/render/map/extension/hero.ts` | `MapHeroRenderer` 持有注入的 `IFaceHandler` 并以 `degrade` / `movement` / `next` 取代三个弃用 helper(含 `:357` 的 Unknown→Down 显式回退);`MapHeroHook` 拆为 `MapHeroLocationHook` / `MapHeroMoverHook` 两个内部类分别注册;`onSetPos` 无条件且无 `moving` 守卫;`onStepEnd` 的 AnimDir 分支按 `step.dir` 设置动画方向;`mutate-animate` → `@motajs/animate` 的 `ExcitationCurve2D`;D-18 / D-22 / D-28 成员逐字保留 | +| 生产源码(修改) | `packages-user/client-modules/src/render/map/extension/manager.ts` | `addHero` 把 `state.mover.faceHandler` 作为第四个实参注入 `new MapHeroRenderer(...)`;其余一字不改 | +| 摘要 | `.planning/phases/04-render-adaptation/04-04-SUMMARY.md` | 执行前核对六条(带 `file:line`)、`CONCURRENT-BASELINE` 段 + `基线摘要 sha256`、`TYPE-BASELINE` 段、D-24..D-28 处置对照与 7 处替换映射、门禁输出摘要、人工复核九条、未实现项清单、REND-01 / REND-02 Pending 声明与运行时 UAT 不可得说明 | + + +Create `.planning/phases/04-render-adaptation/04-04-SUMMARY.md` when done + +