docs(09-4): redesign plan — status ownership into statusBar + per-field hooks/TODO

This commit is contained in:
unanmed 2026-10-09 17:55:24 +08:00
parent 3f2e3b1083
commit f18d577b77
2 changed files with 212 additions and 138 deletions

View File

@ -498,7 +498,7 @@ Plans:
**Wave 4** *(blocked on Wave 3 completion)*
- [ ] 09-04-PLAN.md — 任务 1·D-12:响应式勇士属性对象(命名待用户批准;`IHeroAttr` 不可用)+ 接入 `main.tsx`
- [ ] 09-04-PLAN.md — 任务 1·D-12(定向重设计):把 `leftStatus` / `rightStatus` 迁入 `statusBar.tsx` 由状态栏组件自持 + 属性 9 字段与 `lv` 走 `hero.attribute` 钩子、`floor` 走 `hero.location` 钩子、无钩子字段 `// TODO` + 清理 `main.tsx`
## Progress

View File

@ -6,66 +6,85 @@ wave: 4
depends_on:
- 09-03
files_modified:
# ---- 新建响应式勇士属性对象(命名待用户批准)----
- packages-user/client-modules/src/ui/heroAttr.ts
- packages-user/client-modules/src/ui/statusBar.tsx
- packages-user/client-modules/src/ui/main.tsx
- packages-user/client-modules/src/ui/index.ts
- .planning/phases/09-minimum-browser-runnable/09-04-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 52000
raw_tokens: 52000
tokens: 47000
raw_tokens: 47000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,且 D-04 / D-12 明确命名须先获批)**:Task 0 必须先汇报「要解决的问题(`main.tsx` 内联的 hero 属性读取(9 处 `props.state.hero.attribute.getFinalAttribute`)与 `attributeHook` 订阅未封装为独立响应式对象)」「锁定裁定 D-12 原文要点」「命名候选与冲突说明(**`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr` 及 `IHeroAttribute` 一族重名,不得沿用**)」「拟实现的对象契约(接口 / 类 / 工厂、字段集、订阅与卸载、非属性字段归属)」「范围与硬边界」「验证方式」,然后必须**停下等待**用户明确回复「可以执行」四个字**并给出命名批准**;未收到命名批准前不得创建任何以该名字命名的文件 / 接口 / 类,也不得执行本计划任何一步、不得派发执行子代理。"
- "**D-12:单独实现一个响应式勇士属性对象**:新增文件 `packages-user/client-modules/src/ui/<APPROVED_NAME>.ts`(文件名与接口 / 类名取用户批准者;规划默认占位与注册的路径为 `heroAttr.ts` —— **仅在用户批准该名后方可沿用**);该对象:① 以 `reactive` 暴露指定勇士属性字段;② 经 `props.state.hero.attribute.addHook({ onUpdateAttribute })` 监听属性变化并据此更新;③ 提供卸载(`addHook` 返回的 `IHookController.unload()`),在组件 `onUnmounted` 时调用;④ 遵循 `dev.md:100`(先 `interface` 声明再 `class implements`)。"
- "**命名纪律(D-04 / D-12 / AGENTS.md)**:**不得**使用 `IHeroAttr` 作为新对象名(与 `@user/data-common` 的属性字典类型 `IHeroAttr`(`data-base/src/types.ts:8,21` 泛型实参)冲突);**不得**复用 / 覆写 `IHeroAttribute` / `IReadonlyHeroAttribute` 一族语义;除用户批准的新命名外**零命名变更**。"
- "**订阅与读值证据(只读基准,不改)**:`IHeroAttributeHooks.onUpdateAttribute?<K extends keyof THero>(name, value)`(`data-base/src/hero/types.ts:95-102`);`IReadonlyHeroAttribute<THero> extends IHookable`(`:104-106`);`getFinalAttribute<K extends keyof THero>(name)`(`:117`);`IHeroState.attribute: IReadonlyHeroAttribute<THero>`(`:921-928`)。"
- "**接线:`main.tsx` 改用新对象**:`main.tsx` 的 9 处 hero 属性读取与 `attributeHook` 订阅改由新响应式对象承担(对象在组件 setup 内创建、`onUnmounted` 卸载);`main.tsx` 中**非属性字段**(钥匙 / `core.itemCount`、`flags` 派生的 poison/weak/curse、`core.status.floorId`、`core.getLvName`、`core.getNextLvUpNeed`、`replayStatus`、`locked`)**保持原读取不动**(属 legacy / 其它来源,不在 D-12 范围);4 个阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)与所有 `core.*`、legacy 只报告项**保持不动**。"
- "**范围零外溢 + 无运行时门禁**:验证为**静态**(新文件在场 / 接口与类 / `addHook` 订阅 / `unload` 卸载 / `main.tsx` 已接线 / `src/ui` 零 `client`);**不得**以「TS 诊断数」「`pnpm check:type`」「`pnpm build`」为门禁。`client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、legacy(D-14)、用户并发改动(D-56)零触碰。"
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03)**:Task 0 必须先汇报「要解决的问题(`leftStatus` / `rightStatus` 现由 `main.tsx` 内联持有并经 `status={...}` 下传)」「D-12(本次定向重设计)/ D-13 / D-14 原文要点」「**逐字段「数据来源 → 对应钩子 → 结论」映射表(本计划核心调查产出)**」「`main.tsx` 清理面与保留面」「`replay` / `hideStatus` 的去向裁定」「范围与硬边界」「验证方式」「是否存在需用户批准的新命名」,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-12(定向重设计版):把 `leftStatus` / `rightStatus` 从 `main.tsx` 挪进 `statusBar.tsx`,由状态栏组件自身持有,不再由外部传入**:`LeftStatusBar` 在自身 setup 内创建并持有 `leftStatus: ILeftHeroStatus`;`RightStatusBar` 在自身 setup 内创建并持有 `rightStatus: IRightHeroStatus`;未导出的 `StatusBarProps<T>` 去除 `status` 成员(随之去除未使用的泛型形参),`statusBarProps.props` 去除 `'status'`,`main.tsx` 去除 `status={leftStatus}` / `status={rightStatus}` 传参。`ILeftHeroStatus` / `IRightHeroStatus` 仍定义并导出在 `statusBar.tsx`,**不新增任何新接口命名**(尤其**不得**引入名为 `IHeroAttr` 的任何对象——该名与既有 `@user/data-common` 的 `IHeroAttr` 属性字典冲突)。"
- "**D-12 属性字段一律经属性钩子更新**:`leftStatus` 的 9 个属性字段(`hp` / `hpmax` / `mana` / `manamax` / `atk` / `def` / `mdef` / `money` / `exp`)由 `p.state.hero.attribute.addHook({ onUpdateAttribute })` 驱动刷新(内部经 `p.state.hero.attribute.getFinalAttribute(name)` 取值),组件 `onUnmounted` 时调用该钩子返回控制器的 `unload()` 卸载。"
- "**D-12 非属性字段逐字段诚实处置(调查结论,不得臆造钩子)**:"
- "① **`lv`(等级名称)**:其底层 `level` 是 `IHeroAttr`(`@user/data-common`,`types.ts:29`)的属性,属性钩子 `onUpdateAttribute` 可覆盖 ⇒ **接线**(值仍经 legacy `core.getLvName(p.state.hero.attribute.getFinalAttribute('level'))` 格式化,保留原显示语义)。"
- "② **`floor`(楼层)**:`p.state.hero.location` 实现 `IHookable<IHeroLocationHooks>`,含 `onSetFloor?(map)` 钩子 ⇒ **接线**(以 `p.state.hero.location.addHook({ onSetFloor })` 为触发;值仍读 legacy `core.status.floorId`,以保持 `FloorIds` 字面量联合类型与既有取值不变——`location.floorId` 为 `string | undefined`,直赋会引入类型错配,登记为报告项、不引 `as`)。"
- "③ **`up`(距升级所需经验)**:原值来源为 legacy `core.getNextLvUpNeed()`,新数据端(`p.state.hero.attribute`)无对应访问器、无对应钩子 ⇒ **留空 + `// TODO:`**。"
- "④ **`yellowKey` / `blueKey` / `redKey` / `greenKey` / `pickaxe` / `bomb` / `centerFly`(道具/钥匙数量)**:`p.state.hero.items`(`IHeroItems`,`data-base/src/hero/types.ts:611`)**未实现 `IHookable`**,无道具数量变更钩子 ⇒ **留空 + `// TODO:`**。"
- "⑤ **`poison` / `weak` / `curse`(毒/衰/咒)与 `rightStatus.exampleHard`**:来源为 `p.state.flags`(`IFlagSystem`,`data-base/src/flag/types.ts:47`)**未实现 `IHookable`**(D-13:flags 尚无监听系统)⇒ **留空 + `// TODO:`(沿用既有注释文本 `// TODO: flags 更新未接(当前无监听系统)`)**。"
- "⑥ **`replay`(录像状态,`ILeftHeroStatus.replay`)**:`p.state.replaySystem`(`IReplaySystem`)仅提供沙箱级钩子(`onCreateSandbox` / `onRecordCommand`,及 `IReplaySandbox` 的 `onStartReplay` / `onStopReplay` / `onPauseReplay` / `onResumeReplay` / `onSpeedSet` / `onStep`),**无「聚合录像状态」单一钩子**,且原值来源为 legacy `core.status.replay` ⇒ 默认 **留空 + `// TODO:`(并注明可用的沙箱级钩子)**,于 Task 0 交用户裁定是否本期接线。"
- "**`main.tsx` 清理面(D-12 修订)」**:移除 `replayStatus` / `leftStatus` / `rightStatus` 定义、`updateStatus`、`updateDataFallback`、死引用 `const locked = ref(false)`(`grep` 确认**只写不读**、零消费点)、`attributeHook` 及其 `onUnmounted`、两处 `status={...}` 传参、以及因移除而不再使用的 import。"
- "**`main.tsx` 保留面(不得破坏)」**:`hideStatus`(`ref`)及其在渲染树中的全部消费点、地图渲染(`mainMapRenderer` / `mainMapExtension` / `layerState={state.maps}` / `renderMapMisc` / `mapMiscSprite`)、交互监听(`clickMap` / `downMap` / `moveMap`)、`mainUIController` 区块、`using.onExcitedFunc`、`MainSceneUI` 导出、以及所有仍需要的 legacy `core.*` 读取。"
- "**裁定去向(读码得出,不臆测)」**:`replayStatus` 即 `ILeftHeroStatus.replay` ⇒ 随 `leftStatus` 一并迁入 `LeftStatusBar`(作为 `reactive` 的 `ReplayingStatus` 零值对象,供 `<MixedToolbar status={s.replay}>` 使用);`locked` 与 `updateDataFallback` 为死代码(`locked` 从未被消费)⇒ 一并清理(不构成行为变更);`hideStatus` 为 `main.tsx` 主场景关注点 ⇒ **保留在 `main.tsx`**,其「刷新触发」因 `attributeHook` 迁出且 flags 无监听(D-13)而失效,默认改为在 setup 内**读取一次** flag 初值并加 `// TODO:`(详见 Task 0 待确认项)。"
- "**范围零外溢 + 无运行时门禁**:验证一律为**静态**(文件内容 / 符号 / `addHook` / `unload` / `TODO` / 零 `client` / 零范围外改动);**不得**以「TS 诊断数」「`pnpm check:type`」「`pnpm build`」为门禁。`packages/**`、`src/**`、legacy `core.*`(D-14 只报告)、用户并发改动(D-56)零触碰。"
artifacts:
- path: packages-user/client-modules/src/ui/heroAttr.ts
provides: "D-12 的响应式勇士属性对象(文件名 / 接口名 / 类名待用户批准;默认占位 `heroAttr.ts`):`interface` + `class implements`,`reactive` 属性字段 + `props.state.hero.attribute.addHook({ onUpdateAttribute })` 订阅 + `unload` 卸载契约"
- path: packages-user/client-modules/src/ui/statusBar.tsx
provides: "D-12(定向重设计版)的状态归属方:`LeftStatusBar` 自持 `leftStatus`、`RightStatusBar` 自持 `rightStatus`;属性 9 字段与 `lv` 经 `p.state.hero.attribute.addHook({ onUpdateAttribute })` 刷新、`floor` 经 `p.state.hero.location.addHook({ onSetFloor })` 触发、其余非属性字段以 `// TODO:` 标注缺失钩子;`onUnmounted` 卸载全部钩子;未导出的 `StatusBarProps` 去 `status` 成员与泛型。"
- path: packages-user/client-modules/src/ui/main.tsx
provides: "接线后的主场景 UI:hero 属性读取与订阅委托给新响应式对象;非属性字段 / 阻断项 / legacy 保持不动"
- path: packages-user/client-modules/src/ui/index.ts
provides: "新增新响应式对象的同目录 `export * from './<APPROVED_NAME>'`(D-69 合规)"
provides: "清理后的主场景 UI:不再持有 / 传入 `leftStatus` / `rightStatus` / `replayStatus` / `locked` / `attributeHook` / `updateStatus` / `updateDataFallback`;保留 `hideStatus`、地图渲染、交互监听、`mainUIController` 区块。"
key_links:
- "`packages-user/client-modules/src/ui/main.tsx`(组件 setup)↔ 新响应式对象(`createXxx` / `new Xxx`)↔ `props.state.hero.attribute.addHook` —— D-12 的订阅链"
- "`packages-user/client-modules/src/ui/main.tsx:148-150`(既有 `onUnmounted(() => attributeHook.unload())`)↔ 新对象的卸载契约 —— 生命周期清理范式(`09-PATTERNS.md` Shared Patterns / 生命周期清理)"
- "`packages-user/data-base/src/hero/types.ts:95-117,921-928`(`onUpdateAttribute` / `getFinalAttribute` / `IHeroState.attribute`)↔ 新对象读值 / 监听 —— 数据端接口证据"
- "`packages-user/client-modules/src/ui/statusBar.tsx`(`LeftStatusBar` setup)↔ `p.state.hero.attribute.addHook({ onUpdateAttribute })` ↔ `getFinalAttribute(name)` —— D-12 属性刷新链"
- "`packages-user/client-modules/src/ui/statusBar.tsx`(`LeftStatusBar` setup)↔ `p.state.hero.location.addHook({ onSetFloor })` ↔ `core.status.floorId` —— D-12 floor 触发链"
- "`packages-user/client-modules/src/ui/statusBar.tsx`(`onUnmounted`)↔ `attributeHook.unload()` / `locationHook.unload()` —— 生命周期清理(`09-PATTERNS.md` Shared Patterns / 生命周期清理)"
- "`packages-user/data-base/src/hero/types.ts:95-117,288-332,921-932`(`IHeroAttributeHooks.onUpdateAttribute` / `IHeroLocationHooks.onSetFloor` / `IHeroState.attribute` + `location`)—— 钩子存在性证据"
- "`packages-user/data-base/src/flag/types.ts:47`(`IFlagSystem` 非 `IHookable`)与 `packages-user/data-base/src/hero/types.ts:611-641`(`IHeroItems` 非 `IHookable`)—— 无钩子(TODO)证据"
assumptions:
- "**命名待批准(D-04 / D-12;阻断本计划):** 规划日无法确定新对象名。**`IHeroAttr` 因与 `@user/data-common` 的属性字典类型 `IHeroAttr` 重名而不可用**。执行者必须在 Task 0 取得用户的明确批准后再创建文件 / 命名;未获批准前不得臆造名字。"
- "**对象形态待确认:** 规划默认 = `interface` + `class implements`(`dev.md:100`),由 `main.tsx` 在 setup 内实例化并 `onUnmounted` 卸载;若用户偏好工厂函数 / composable,按 Task 0 确认结果调整。因为 `reactive` 与 `onUnmounted` 依赖组件实例,对象须在组件 setup 内创建。"
- "**字段集待确认:** 默认覆盖 `main.tsx` 现经 `getFinalAttribute` 读取的 9 个属性(`atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`);非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)**不在本对象内**。若用户要求并入更多字段,按 Task 0 确认结果调整。"
- "**`main.tsx` 的 `updateDataFallback()`(`locked.value = core.status.lockControl`)不在 D-12 范围:** 其来源为 legacy `core.status`、非勇士属性;默认保持在 `main.tsx`(若原订阅点被新对象取代,须确保 `updateDataFallback` 仍有触发路径或按 Task 0 确认后的方案处理)。**不得**因封装新对象而丢失既有更新行为。"
- "**执行日行号可能漂移:** 引用锚点为 2026-10-09 规划日;执行时按符号文本定位并在 SUMMARY 记录差异。"
- "**无新接口命名(D-04)**:本计划不新增接口 / 类 / 方法 / 字段名;`ILeftHeroStatus` / `IRightHeroStatus` / `LeftStatusBar` / `RightStatusBar` 沿用;仅**移除**未导出接口 `StatusBarProps<T>` 的 `status: T` 成员。因成员移除后泛型形参 `T` 变为未使用,默认一并去除泛型(`StatusBarProps<T>` → `StatusBarProps`)——此为**接口形状简化、非新命名**,仍于 Task 0 呈报用户。"
- "**组件 setup 内新增的局部标识符(非公共/受保护/私有成员)**:`refreshAttribute`(属性刷新函数)与 `locationHook`(floor 位置钩子控制器)为本计划新引入的组件内 `const`(函数局部,非类成员/导出符号);`leftStatus` / `rightStatus` / `s` / `attributeHook` 沿用 `main.tsx` 既有命名。如用户认定组件内局部命名亦须先批准,在 Task 0 一并确认(默认视为无需单独批准)。"
- "**非属性字段的初值 / 更新为 TODO(如实留空)**:`up` / 道具数量 / debuff / `replay` / `exampleHard` 因无对应钩子而**不写更新代码**,保持对象初值并在就近处加 `// TODO:`。此为指令明确要求(「没有相应的钩子,先留空并标明 TODO」),非功能简化。"
- "**`floor` 值来源保持 legacy(类型安全)**:`core.status.floorId` 类型为 `FloorIds`(`src/types/declaration/status.d.ts:663` / `map.d.ts:497`,字符串字面量联合),而 `p.state.hero.location.floorId` 为 `string | undefined`;直赋会引入类型错配。故 floor 的**触发**接新钩子、**取值**保持 legacy,避免 `as` 断言(`dev.md:96`)与类型错误。"
- "**`lv` 的 hook 归属**:`lv` 显示的是「等级名称」,其底层 `level` 是 `IHeroAttr` 属性(`data-common/src/types.ts:29`),`onUpdateAttribute('level')` 会触发,故其对应钩子即属性钩子;显示串仍由 legacy `core.getLvName()`(`core.d.ts:801`)生成(`statusBar.tsx` 本就依赖 `core.*`,符合 D-14 只报告不改)。"
- "**执行日行号可能漂移**:引用锚点为 2026-10-09 规划日;执行时按符号文本定位并在 SUMMARY 记录差异。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字**并给出命名批准**之前执行本计划的任何任务、或创建任何以未批准名字命名的文件 / 接口 / 类、或派发任何执行子代理。"
- "**不得擅自使用 `IHeroAttr` 作为新对象名**(与既有属性字典类型冲突);不得复用 / 覆写 `IHeroAttribute` / `IReadonlyHeroAttribute` 语义;除用户批准的新命名外零命名变更。"
- "本计划**只做 D-12**:不得做 D-11 之外的接口改造、不得改 UI 形态 / 视觉(D-14)、不得移植 `@motajs/legacy-ui`、不得新增 flags 监听(D-13)。"
- "不得触碰 `files_modified` 之外的任何生产文件;尤其不得改 `main.tsx` 的非属性字段 / 阻断项 / legacy 读取,不得改 `src/ui/{load,title,controller,background,func,types,save,statistics,viewmap,statusBar,toolbar}.tsx/ts`。"
- "不得修改任何既有 jsDoc 与注释(AGENTS.md);新增注释遵循 `dev.md:72-86`;不得引入 `import type`(dev.md:54);不得使用 `?.`(dev.md:107);类的成员显式声明类型(dev.md:91);CRLF。"
- "不得删除或改名任何模块级单例;不得修复任何 legacy 报错(D-14);不得把门禁建立在「TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
- "本计划**只做 D-12(定向重设计版:状态归属迁移 + 逐字段钩子/TODO)**:不得改 UI 形态 / 视觉(D-14)、不得移植 `@motajs/legacy-ui`、不得新增 flags 监听(D-13)、不得修复任何 legacy 报错(D-14)。"
- "**不得臆造任何接口 / 钩子 / 命名**:不得为「无钩子」字段凭空造监听;不得新增公共 / 受保护 / 私有成员命名;不得引入新接口(尤其不得引入名为 `IHeroAttr` 的任何对象——该名与既有 `@user/data-common` 属性字典类型及 `@user/data-state` 的 `IHeroAttr` 冲突)。"
- "不得触碰 `files_modified` 之外的任何生产文件;尤其不得改 `src/ui/{load,title,controller,background,func,types,save,statistics,viewmap,toolbar}.tsx/ts`、不得改 `src/render/**`、不得改数据端(`packages-user/data-*`)与 `packages/**`。"
- "不得修改任何既有 jsDoc 与注释(AGENTS.md)——既有注释文本仅「随逻辑迁移时逐字重用」(如 `// TODO: flags 更新未接(当前无监听系统)`),不得改写;被删除逻辑内随附的既有注释随其逻辑一并移除(属删除代码,非改写注释)。新增注释遵循 `dev.md:78-86`;不得引入 `import type`(`dev.md:54`);不得使用 `?.`(`dev.md:107`);CRLF;移除后不得遗留未使用 import(`eslint` 零报错,`dev.md:106`)。"
- "不得删除或改名任何模块级单例(`mainUIController` / `using` / `state`);不得改动 `main.tsx` 的地图渲染 / 交互监听;不得把门禁建立在「TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
- "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
---
<objective>
Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应式勇士属性对象,监听勇士属性变化并据此更新指定属性(命名待用户批准)**。
Phase 9 · 任务 1 · 第 4 个计划(**用户定向重设计版**)—— **D-12(定向重设计):把 `leftStatus` / `rightStatus` 挪入 `statusBar.tsx` 由其自身持有,字段更新改走对应钩子;无对应钩子者留空并标 `TODO`**。
**要解决的问题:**
`main.tsx` 目前在组件内内联读取 9 处勇士属性(`props.state.hero.attribute.getFinalAttribute(...)`)并以内联 `attributeHook = props.state.hero.attribute.addHook({ onUpdateAttribute })` 订阅更新;用户要求把这部分**单独实现为一个响应式对象**(D-12),以便复用与隔离。**命名待用户批准**——`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr` 重名,不得沿用。
当前 `main.tsx` 内联持有 `leftStatus`(`ILeftHeroStatus`)/ `rightStatus`(`IHeroRightStatus`)响应式对象,并以 `attributeHook`(`p.state.hero.attribute.addHook`)驱动 `updateStatus()` 刷新,再把二者经 `status={...}` 传给 `<LeftStatusBar>` / `<RightStatusBar>`。用户要求:**把状态挪进 `statusBar.tsx`,由状态栏组件自身持有、不再外部传入;其内部各字段的更新改用「对应的钩子」实现,没有对应钩子的字段先留空并标明 `TODO`。**
**锁定裁定:** D-12 = 单独实现一个 `IHeroAttr` 响应式对象,监听勇士属性变化并据此更新(命名待批准;注意与既有 `IHeroAttribute` 一族区分)。D-14 = 纯适配,不改形态、不移植 legacy。
**锁定裁定:** D-12 = 勇士属性经响应式对象 / 钩子更新(本次重设计为「状态归属迁移 + 逐字段钩子化」);D-13 = flags 无监听系统,只标 `TODO`;D-14 = 纯适配,不改形态 / 不移植 legacy;D-03 = 执行前须获用户「可以执行」;D-04 = 命名须先获批。
**大致解决方案(本计划 = 09-04):** Task 1(tracer)= 新建响应式对象文件(`interface` + `class implements`,`reactive` 属性字段 + `addHook` 订阅 + `unload` 卸载);Task 2 = 接入 `main.tsx`(替换 9 处属性读取与 `attributeHook`,保留非属性字段与阻断项)+ 桶导出 + 门禁。
**核心调查产出(逐字段「数据来源 → 对应钩子 → 结论」):**
**API coverage:** `detected:false`。**`No external API integration: 本对象仅订阅数据端 `IHeroAttribute` 钩子并更新 Vue `reactive` 字段,不涉及任何外部 API。`**
| 字段 | 数据来源(新 `p.state`) | 对应钩子 | 结论 |
|---|---|---|---|
| `hp` `hpmax` `mana` `manamax` `atk` `def` `mdef` `money` `exp` | `hero.attribute`(`IReadonlyHeroAttribute`) | `hero.attribute.addHook({ onUpdateAttribute })` | ✅ 接线 |
| `lv` | `hero.attribute`(`level` 属性) | 同上属性钩子(`onUpdateAttribute` 覆盖 `level`) | ✅ 接线(值经 legacy `core.getLvName()` 格式化) |
| `floor` | `hero.location`(`IHeroLocation` 实现 `IHookable<IHeroLocationHooks>`) | `hero.location.addHook({ onSetFloor })` | ✅ 接线(触发用新钩子;值保持 legacy `core.status.floorId` 以保 `FloorIds` 类型) |
| `up` | legacy `core.getNextLvUpNeed()` | 无(新端无访问器) | ⛔ 留空 + `TODO` |
| `yellowKey` `blueKey` `redKey` `greenKey` `pickaxe` `bomb` `centerFly` | `hero.items`(`IHeroItems` 非 `IHookable`) | 无 | ⛔ 留空 + `TODO` |
| `poison` `weak` `curse` | `flags`(`IFlagSystem` 非 `IHookable`,D-13) | 无 | ⛔ 留空 + `TODO` |
| `replay` | `replaySystem`(仅沙箱级钩子,无聚合钩子) | 无对应聚合钩子 | ⛔ 留空 + `TODO`(Task 0 裁定是否接线) |
| `rightStatus.exampleHard` | `flags`(非 `IHookable`,D-13) | 无 | ⛔ 留空 + `TODO` |
**大致解决方案(本计划 = 09-04):** Task 0(`blocking-human`)= 汇报上述设计并等待「可以执行」;Task 1(tracer)= 改 `statusBar.tsx`(状态栏自持 `leftStatus` / `rightStatus`;属性钩子 + `floor` 位置钩子;无钩子字段 `// TODO:`;`onUnmounted` 卸载;`StatusBarProps` 去 `status`);Task 2 = 清理 `main.tsx`(移除迁出的定义 / 传参 / `attributeHook` / `updateStatus` / `updateDataFallback` / 死引用 `locked`,保留 `hideStatus` / 地图渲染 / 交互监听)+ 门禁。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅做渲染端 UI 组件的状态归属迁移与数据端钩子订阅,不涉及任何外部 API。`**
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@ -86,145 +105,183 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「D-12 响应式勇士属性对象」设计并**请求命名批准**(`IHeroAttr` 不可用),等待用户回复「可以执行」</name>
<precondition>`09-03` 已闭环:`main.tsx` 已使用 `props.state.hero.attribute.getFinalAttribute`(9 处)与 `props.state.hero.attribute.addHook`;`src/ui/` 内零 `client`。</precondition>
<name>Task 0: 预执行汇报关卡 —— 汇报「D-12 定向重设计:状态归宿与逐字段钩子/TODO」设计,等待用户回复「可以执行」</name>
<precondition>`09-03` 已闭环:`main.tsx` 已用 `props.state`(非 `client`);`src/ui/**` 零 `client`;`statusBar.tsx` 的 `LeftStatusBar` / `RightStatusBar` 仍从 `p.status` 读取外部传入的状态对象。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-12 原文;D-04 命名须先获批;D-07)
- .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(§7「新建 `IHeroAttr` 响应式对象」+ `No Analog Found` 表 + Shared Patterns「生命周期清理」)
- .planning/phases/09-minimum-browser-runnable/09-04-PLAN.md(本计划全文)
- packages-user/client-modules/src/ui/main.tsx(`:66-93` reactive 状态;`:96-133` `updateStatus` 的 9 处 getFinalAttribute;`:135-138` `updateDataFallback`;`:141-150` `attributeHook` + `onUnmounted`)
- packages-user/data-base/src/hero/types.ts:95-129,921-934(`IHeroAttributeHooks` / `IReadonlyHeroAttribute` / `IHeroState.attribute`)
- packages-user/data-base/src/types.ts:8,21(既有 `IHeroAttr` 属性字典类型——命名冲突证据)
- dev.md(命名规则 / 类须 interface 先行再 implements / 成员显式类型 / 不写 getter-setter / CRLF)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-12 原文;D-13;D-14;D-03 / D-04)
- .planning/phases/09-minimum-browser-runnable/09-04-PLAN.md(本计划全文,含逐字段映射表)
- packages-user/client-modules/src/ui/main.tsx(`:61-100` 状态定义;`:103-146` `updateStatus`;`:148-151` `updateDataFallback`;`:153-163` `attributeHook` + `onUnmounted`;`:241-246` / `:288-293` `status={...}` 传参)
- packages-user/client-modules/src/ui/statusBar.tsx(`:17-104` `ILeftHeroStatus` / `IRightHeroStatus` / `StatusBarProps` / `statusBarProps`;`:106-131` `LeftStatusBar` 内 `s = p.status`;`:461-487` `RightStatusBar` 内 `p.status.exampleHard`)
- packages-user/data-base/src/hero/types.ts:95-117(`IHeroAttributeHooks.onUpdateAttribute` / `getFinalAttribute`)、:288-332(`IHeroLocationHooks.onSetFloor` / `IHeroLocation.floorId`)、:611-641(`IHeroItems` 非 `IHookable`)、:921-932(`IHeroState.attribute` / `location`)
- packages-user/data-base/src/flag/types.ts:47(`IFlagSystem` 非 `IHookable`)
- packages-user/data-common/src/types.ts:25-48(`IHeroAttr` 属性字典,含 `level`——命名冲突源)、:63-64(`replaySystem`)
- packages-user/data-common/src/replay/types.ts:80-112,342-360(`IReplaySandboxHooks` / `IReplaySystemHooks`——沙箱级钩子,无聚合钩子)
- dev.md(命名规则 / 类须 interface 先行 / 成员显式类型 / 不写 getter-setter / 无 `?.` / CRLF)
</read_first>
<action>
**停下并向用户汇报,等待明确回复并取得命名批准后才继续;汇报本身只读。**
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** `main.tsx` 内联读取 9 处勇士属性并内联订阅 `attributeHook`;D-12 要求把这部分封装为**单独一个响应式对象**。
① **要解决的问题:** `main.tsx` 内联持有 `leftStatus` / `rightStatus` 并以 `attributeHook` 驱动 `updateStatus()` 刷新、经 `status={...}` 下传;用户要求把状态挪进 `statusBar.tsx` 由组件自持,字段更新改用对应钩子,无钩子者留空标 `TODO`。
② **锁定裁定:** **D-12**(单独实现响应式 `IHeroAttr` 对象,监听勇士属性变化并据此更新;命名待批准);**D-14**(纯适配)。
② **锁定裁定原文要点:** **D-12**(勇士属性经响应式对象 / 钩子更新;本次重设计 = 状态归宿迁移 + 逐字段钩子化);**D-13**(flags 无监听系统,仅标 `TODO`);**D-14**(纯适配,不改形态 / 不移植 legacy)。**D-03 / D-04**(执行前须获批;命名须先获批)。
③ **命名冲突(须用户拍板):** **`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr`(`data-base/src/types.ts:8,21` 泛型实参)重名,本计划不得沿用 `IHeroAttr`。** 请用户在 Task 0 给出新对象名(文件名 + 接口名 + 类名;可先不指定由 AI 提候选再批准)。规划默认占位 = 文件 `heroAttr.ts`、接口 `IHeroAttrState`、类 `HeroAttrState`(**仅为占位,未获批不得使用**)。
③ **逐字段「数据来源 → 对应钩子 → 结论」映射表(须用户确认):** 原样呈报 `<objective>` 中的 8 行表(属性 9 字段 ✅ 属性钩子;`lv` ✅ 属性钩子;`floor` ✅ 位置钩子;`up` / 道具数量 / debuff / `replay` / `exampleHard` ⛔ TODO,并说明缺失原因与证据出处)。
④ **拟实现契约(须用户确认):**
- 形态:`interface` 先声明、`class implements`(`dev.md:100`);在组件 setup 内创建(`reactive` 与 `onUnmounted` 依赖组件实例)。
- 读值:经构造参数接收 `props.state`(`IClientBase`),内部用 `props.state.hero.attribute.getFinalAttribute(name)` 初始化 / 刷新字段。
- 监听:`props.state.hero.attribute.addHook({ onUpdateAttribute })`(`data-base/src/hero/types.ts:95-102`);提供 `unload()`(透传 `IHookController.unload()`)供 `onUnmounted` 调用。
- 字段集:默认覆盖 `atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`(现 9 处读取);**非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)不并入**。
- `updateDataFallback()`(`locked = core.status.lockControl`)来源为 legacy `core.status`、非属性 ⇒ 默认保留在 `main.tsx`,不并入对象。
- `LeftStatusBar` 在 setup 内 `reactive(...)` 创建并持有 `leftStatus`;`RightStatusBar` 同法持有 `rightStatus`;原 `const s = p.status` 改为绑定自持对象(`const s = leftStatus`)。
- 属性刷新:`const attributeHook = p.state.hero.attribute.addHook({ onUpdateAttribute: () => refreshAttribute() })`,`refreshAttribute` 用 `p.state.hero.attribute.getFinalAttribute(name)` 逐字段赋值 9 属性,并刷新 `s.lv = core.getLvName(p.state.hero.attribute.getFinalAttribute('level'))`。
- `floor`:`const locationHook = p.state.hero.location.addHook({ onSetFloor: map => { if (map) s.floor = core.status.floorId; } })`(触发用新钩子、值保持 legacy 以保类型)。
- 无钩子字段:不写更新代码,就近加 `// TODO:`(debuff 沿用既有文本 `// TODO: flags 更新未接(当前无监听系统)`)。
- `onUnmounted(() => { attributeHook.unload(); locationHook.unload(); })`。
- 未导出的 `StatusBarProps` 去 `status: T` ⇒ 因 `T` 随即未使用,默认一并去泛型(`StatusBarProps<T>` → `StatusBarProps`);`statusBarProps.props` 去 `'status'`;保留 `'state'`(`RightStatusBar` 仍接收 `state`,为未来接入保留)。
- 组件初值:`leftStatus` / `rightStatus` 采用与 `main.tsx` 原字面量一致的初值(数值 0 / `floor: 'MT0'` / `lv: ''` / `replay` = 一个 `reactive` 的 `ReplayingStatus` 零值对象 `{ replaying: false, playing: false, speed: 1, played: 0, total: 0 }`)。
⑤ **范围与硬边界:** 改动面 = 新文件 + `main.tsx` 接线 + 桶导出。**零触碰** = 非属性字段 / 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)/ legacy(D-14)/ `client-base/src/material/**`(D-42 / D-48)/ `packages/**` / `src/**` / 用户并发改动(D-56)。**无依赖变更、不执行 `pnpm i`。**
⑤ **`main.tsx` 清理面与保留面:** 清理 = `leftStatus` / `rightStatus` / `replayStatus` 定义 + `updateStatus` + `updateDataFallback` + `locked`(读码确认**只写不读**的死引用)+ `attributeHook` 及其 `onUnmounted` + 两处 `status={...}` 传参 + 因移除而未使用的 import;保留 = `hideStatus` 及其全部消费点 + 地图渲染 + 交互监听 + `mainUIController` 区块 + `using.onExcitedFunc` + `MainSceneUI` 导出 + 仍需要的 `core.*`。
⑥ **验证方式:** 静态——新文件在场(含 `interface` + `class implements` + `reactive` + `addHook` + `unload`)、`main.tsx` 已接线且非属性字段 / 阻断项保持、`src/ui` 零 `client`。**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作门禁。
⑥ **待用户裁定的开放项:**
- **`replay` 是否本期接线**:默认 ⛔ TODO(`replaySystem` 仅沙箱级钩子、无聚合钩子,且原值为 legacy `core.status.replay`);若用户要求接线,须另行界定沙箱生命周期编排范围。
- **`hideStatus` 的刷新触发**:`attributeHook` 迁出且 flags 无监听(D-13)后,其唯一旧触发路径消失。默认 = 在 setup 内**读取一次** `p.state.flags.getFieldValueDefaults('hideStatusBar', false)` 作为初值 + `// TODO:`(保留稳态显示);备选 = 保持 `ref(false)` 完全静态。请用户确认取默认或备选。
- **`floor` 取值来源**:默认保持 legacy `core.status.floorId`(类型安全);备选 = 改 `hero.location.floorId`(需处理 `string` → `FloorIds` 类型错配,登记报告项)。
⑦ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者/子代理必须停下等待用户明确回复「可以执行」四个字**并给出命名批准**;未收到命名批准前不得创建任何以该名字命名的文件 / 接口 / 类,也不得执行任何一步、不得派发执行子代理。回复其它内容视为未通过。
⑦ **命名:** 本计划**不新增任何公共 / 受保护 / 私有成员命名、不新增导出接口**;仅移除未导出接口 `StatusBarProps` 的 `status` 成员(及随之未使用的泛型形参)。组件 setup 内新增局部 `const`(`refreshAttribute` / `locationHook`),请用户知悉(默认视为无需单独批准)。**不引入名为 `IHeroAttr` 的任何对象**(与 `@user/data-common` / `@user/data-state` 冲突)。
⑧ **若用户给出修订意见**(含否决形态 / 调整字段集 / 指定不同命名):退出本计划、修订后重新汇报。
⑧ **范围与硬边界:** 改动面 = `statusBar.tsx` + `main.tsx`;`src/ui/index.ts` **无需改动**(`ILeftHeroStatus` / `IRightHeroStatus` / 两组件仍由 `statusBar.tsx` 导出)。**零触碰** = 其它 `src/ui/*`、`src/render/**`、数据端、`packages/**`、legacy `core.*`(D-14)、用户并发改动(D-56)。**无依赖变更、不执行 `pnpm i`。**
⑨ **验证方式:** 静态——`statusBar.tsx` 自持状态 + 含 `addHook` / `onUpdateAttribute` / `getFinalAttribute` / `onSetFloor` / `unload` / `onUnmounted` / `TODO`、且 `StatusBarProps` 无 `status` 与泛型、无 `p.status` 残留;`main.tsx` 已清理且保留 `hideStatus` / 地图 / 交互;`src/ui/**` 零 `client`。**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作门禁。
⑩ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字;回复其它内容视为未通过。若用户给出修订意见(否决处置 / 调整钩子映射 / 指定 `replay` 或 `hideStatus` 方案),退出本计划、修订后重新汇报。
</action>
<decision>是否按本计划(新建响应式勇士属性对象[文件名 / 接口名 / 类名取用户批准者]、`reactive` 字段 + `props.state.hero.attribute.addHook({onUpdateAttribute})` 订阅 + `unload` 卸载;接入 `main.tsx` 替换 9 处属性读取与 `attributeHook`,非属性字段 / 阻断项 / legacy 保持不动;桶导出;无依赖变更、无运行时门禁)开始执行;**并批准该新对象的具体命名**</decision>
<decision>是否按本计划开始执行:`leftStatus` / `rightStatus` 迁入 `statusBar.tsx` 由状态栏组件自持(`main.tsx` 去除传参);属性 9 字段与 `lv` 经 `hero.attribute` 钩子刷新、`floor` 经 `hero.location` 钩子触发、其余非属性字段留空标 `TODO`;清理 `main.tsx`(含死引用 `locked` / `updateDataFallback`)并保留 `hideStatus` / 地图 / 交互;`replay` 与 `hideStatus` 取默认处置;无新接口命名、无依赖变更、无运行时门禁</decision>
<context>
AGENTS.md 与 D-04 / D-12 均要求命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心未知项 = **命名**(`IHeroAttr` 重名不可用)与对象形态 / 字段集,必须让用户拍板后才能落地。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心内容 = **逐字段钩子调查结论 + 非属性字段的「留空 + TODO」边界 + `replay` / `hideStatus` 处置**,须用户拍板后方可落地。本计划无新的接口命名未知项(与旧版 09-04 被否决的 `IHeroAttr` 命名冲突方案不同),关卡焦点转为「逐字段处置 + 开放项裁定」。
</context>
<options>
<option id="execute-with-name">
<name>可以执行 + 批准命名(用户给出)</name>
<pros>直接落地 D-12</pros>
<option id="execute-all">
<name>可以执行(采纳全部默认处置)</name>
<pros>直接落地 D-12 重设计;属性 / floor / lv 钩子化,其余 TODO</pros>
<cons>无</cons>
</option>
<option id="execute-default-name">
<name>可以执行 + 采用默认占位命名</name>
<pros>无需另行取名</pros>
<cons>须用户明确同意 `heroAttr.ts` / `IHeroAttrState` / `HeroAttrState` 之一</cons>
<option id="execute-with-wire-replay">
<name>可以执行 + 本期一并接线 `replay`</name>
<pros>录像状态栏恢复联动</pros>
<cons>须额外界定沙箱级钩子编排范围(超出本次「迁移」体量)</cons>
</option>
<option id="execute-alt-hide">
<name>可以执行 + `hideStatus` 取备选(保持静态)或指定其它方案</name>
<pros>严格控制行为面</pros>
<cons>需在汇报中明确所选方案</cons>
</option>
<option id="revise">
<name>修订形态 / 字段集 / 接线方式后再执行</name>
<name>修订处置 / 钩子映射后重新汇报</name>
<pros>避免带着错误契约落地</pros>
<cons>修订 PLAN.md 后重新汇报</cons>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字并给出命名批准(修订意见请按 action ⑧ 处理)</resume-signal>
<resume-signal>回复「可以执行」四个字(如需调整 `replay` / `hideStatus` / `floor` 处置请在回复中指明;修订意见请按 action ⑩ 处理)</resume-signal>
<verify>
<human-check>用户在对话中明确回复「可以执行」四个字且给出了新对象的命名批准(人工核对用户回复原文;失败语义写在 action ⑦ 与 acceptance_criteria)</human-check>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/main.tsx','utf8').replace(/\r\n/g,'\n');const s=fs.readFileSync('packages-user/client-modules/src/ui/statusBar.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(!/const leftStatus/.test(t))bad.push('pre-state: leftStatus missing in main.tsx');if(!/status=\{leftStatus\}/.test(t))bad.push('pre-state: status={leftStatus} missing');if(!/\battributeHook\b/.test(t))bad.push('pre-state: attributeHook missing');if(/const leftStatus/.test(s))bad.push('post-state: leftStatus already in statusBar.tsx');if(!/p\.status/.test(s))bad.push('pre-state: p.status missing in statusBar.tsx');if(!/interface StatusBarProps</.test(s))bad.push('pre-state: StatusBarProps generic missing');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (status still owned by main.tsx)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(尚未汇报即已有迁移痕迹,或汇报基线不成立)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 `replay` / `hideStatus` / `floor` 的非默认处置,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 用户已明确批准新对象的具体命名(文件名 / 接口名 / 类名至少其一),且**不是** `IHeroAttr`
- 汇报内容涵盖:要解决的问题、D-12 / D-14 原文要点、命名冲突说明(`IHeroAttr` 不可用)、拟实现契约(形态 / 读值 / 监听 / 卸载 / 字段集 / 非属性字段归属)、范围与硬边界、验证方式
- 汇报中已原样告知**非属性字段与 `updateDataFallback` 不并入对象**、阻断项保持不动
- 汇报中已原样告知**无运行时要求**(不设 `check:type` / `build` / TS 诊断数类门禁)
- 汇报中已原样告知**关卡性质**(必须停下等待命名批准,不因触发方式或 `yolo` / `auto_advance` 自动放行)
- 汇报内容涵盖:要解决的问题、D-12 / D-13 / D-14 要点、**逐字段「数据来源 → 钩子 → 结论」映射表(8 行,明确区分 ✅ 接线与 ⛔ TODO,并注明证据出处)**、拟实现契约(自持 / 属性钩子 / floor 钩子 / 无钩子字段留空 / `onUnmounted` 卸载 / `StatusBarProps` 去 `status`)、`main.tsx` 清理面与保留面、范围与硬边界、验证方式
- 汇报中已原样告知:`replay` 默认 ⛔ TODO、`hideStatus` 默认「读一次初值 + TODO」、`floor` 值保持 legacy(均可请用户改选)
- 汇报中已原样告知:**不新增接口命名、不引入 `IHeroAttr`、无依赖变更、无运行时门禁(不设 `check:type` / `build` / TS 诊断数门禁)**
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
<done>用户明确回复「可以执行」并批准命名;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="tracer">
<name>Task 1(tracer):端到端创建响应式勇士属性对象 —— 新建 `src/ui/<APPROVED_NAME>.ts`(`interface` + `class implements`:`reactive` 属性字段 + `addHook` 订阅 + `unload` 卸载),并加桶导出</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字**并批准命名**(未收到命名批准则不得创建任何文件)</precondition>
<files>packages-user/client-modules/src/ui/heroAttr.ts, packages-user/client-modules/src/ui/index.ts</files>
<name>Task 1(tracer):把状态归属迁入 `statusBar.tsx` —— `LeftStatusBar` / `RightStatusBar` 自持状态 + 属性/floor 钩子 + 无钩子字段 `// TODO:` + `onUnmounted` 卸载 + `StatusBarProps` 去 `status`</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户指定 `replay` / `hideStatus` / `floor` 非默认处置,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/statusBar.tsx</files>
<read_first>
- packages-user/client-modules/src/ui/main.tsx:66-93,96-150(内联 `leftStatus` reactive 字段集 + `getFinalAttribute` 读取 + `attributeHook` 订阅 + `onUnmounted` 卸载——本对象要封装的既有逻辑)
- packages-user/data-base/src/hero/types.ts:95-129(`IHeroAttributeHooks.onUpdateAttribute` / `IReadonlyHeroAttribute extends IHookable` / `getFinalAttribute` / `markDirty`)
- packages-user/data-base/src/types.ts:8,21(既有 `IHeroAttr` 属性字典类型——命名不可沿用)
- packages-user/client-base/src/types.ts:21(`IClientBase extends ICoreState`,其 `hero.attribute` 可供读值)
- .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(§7 内联参照 + 约束;`No Analog Found`)
- dev.md(类须 interface 先行再 implements;成员显式类型;不写 getter/setter;引入置首;CRLF)
- packages-user/client-modules/src/ui/statusBar.tsx(整文件:`:17-64` `ILeftHeroStatus`;`:66-69` `IRightHeroStatus`;`:96-104` `StatusBarProps<T>` 与 `statusBarProps`;`:106-131` `LeftStatusBar` 内 `s = p.status`;`:139-148,176-274,282-305,313-347` `s.*` 读取;`:450-454` `<MixedToolbar status={s.replay}>`;`:461-487` `RightStatusBar` 内 `p.status.exampleHard`)
- packages-user/client-modules/src/ui/main.tsx:66-100,103-146(原 `leftStatus` / `rightStatus` / `replayStatus` 初值与 `updateStatus` 的逐字段来源——迁移基准)
- packages-user/data-base/src/hero/types.ts:95-117(`IHeroAttributeHooks.onUpdateAttribute` / `getFinalAttribute`)、:288-332(`IHeroLocationHooks.onSetFloor` / `IHeroLocation.floorId`)
- packages-user/data-common/src/types.ts:25-48(`IHeroAttr` 属性字典,`level` 为属性名——`getFinalAttribute('level')` 的类型依据)
- packages-user/client-modules/src/ui/toolbar.tsx:150-170(`ReplayingStatus` 定义与 `replayingProps`——`replay` 字段类型)
- dev.md(类须 interface 先行;成员显式类型;不改既有注释;无 `?.`;CRLF)
</read_first>
<action>
(1) **新建文件:** 在 `packages-user/client-modules/src/ui/` 新建文件,文件名取 **Task 0 用户批准的名字**(规划默认占位 = `heroAttr.ts`;仅在用户批准后可沿用;未获批准**停下询问**)。文件内先 `export interface <接口名>` 声明契约,再 `export class <类名> implements <接口名>`(`dev.md:100`),类成员显式声明类型(`dev.md:91`)。本任务门禁据「`src/ui/` 内除 `index.ts` / `types.ts` / `func.ts` 外的唯一 `.ts` 文件」**自动定位**该文件,故批准的基名不影响门禁匹配(不得再新增其它 `.ts` 文件)。
(1) **未导出 `StatusBarProps` 去 `status`:** 将 `interface StatusBarProps<T> extends DefaultProps, IUIPropsBase { loc: ElementLocator; status: T; hidden: boolean }` 改为不带泛型、不含 `status` 的 `interface StatusBarProps extends DefaultProps, IUIPropsBase { loc: ElementLocator; hidden: boolean }`;`statusBarProps` 的 `props` 数组由 `['loc', 'status', 'hidden', 'state']` 改为 `['loc', 'hidden', 'state']`,其类型 `SetupComponentOptions<StatusBarProps>`;`LeftStatusBar` / `RightStatusBar` 的 `defineComponent<...>` 泛型由 `StatusBarProps<ILeftHeroStatus>` / `StatusBarProps<IRightHeroStatus>` 改为 `StatusBarProps`(组件名与 `ILeftHeroStatus` / `IRightHeroStatus` 定义 / 导出**不变**)。
(2) **对象契约:** 构造器接收主对象(类型 `IClientBase`,自 `@user/client-base` 引入);以 `reactive`(自 `vue` 引入)暴露 9 个属性字段(`atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`,命名沿用 `ILeftHeroStatus` 同款);提供刷新方法(用 `this.state.hero.attribute.getFinalAttribute(name)` 逐字段赋值)与订阅:`this.hook = this.state.hero.attribute.addHook({ onUpdateAttribute: () => this.update() })`。提供 `unload()` 方法透传 `this.hook.unload()`。**不得**使用 `?.`;**不得**写 getter / setter(`dev.md:113`)。**不得**并入非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)。
(2) **`LeftStatusBar` 自持 `leftStatus` 并接钩子:**
- 在 setup 内 `const leftStatus: ILeftHeroStatus = reactive({ ... })`,字段与初值**照搬** `main.tsx:73-97` 原字面量(`hp`/`hpmax`/`mana`/`manamax`/`atk`/`def`/`mdef`/`money`/`exp` = 0;`up` = 0;`yellowKey`/`blueKey`/`redKey`/`greenKey`/`pickaxe`/`bomb`/`centerFly` = 0;`poison`/`weak`/`curse` = false;`floor` = `'MT0'`;`lv` = `''`;`replay` = 一个 `reactive` 的 `ReplayingStatus` 零值对象 `{ replaying: false, playing: false, speed: 1, played: 0, total: 0 }`)。
- 将原 `const s = p.status;` 改为绑定自持对象(`const s = leftStatus;`),其余 `s.*` 读取与 JSX 使用**不动**(`<MixedToolbar status={s.replay} state={p.state} />` 保持)。
- 属性刷新:定义 `const refreshAttribute = () => { s.atk = p.state.hero.attribute.getFinalAttribute('atk'); ...(9 字段,照搬 main.tsx:110-121); s.lv = core.getLvName(p.state.hero.attribute.getFinalAttribute('level')); }`;`const attributeHook = p.state.hero.attribute.addHook({ onUpdateAttribute: () => refreshAttribute() });`。
- `floor`:`const locationHook = p.state.hero.location.addHook({ onSetFloor: map => { if (map) s.floor = core.status.floorId; } });`(触发接新钩子;值保持 legacy `core.status.floorId`,保 `FloorIds` 类型与行为;仅在楼层存在时更新)。
- 无钩子字段留空 + `// TODO:`(就近放置,均为**新增**注释,遵循 `dev.md:78`;以下字段**不写任何更新 / 订阅代码**):
- `up`:`// TODO: 缺少「距升级所需经验」钩子(legacy core.getNextLvUpNeed 无新对应)`
- 道具/钥匙数量(`yellowKey`…`centerFly`):`// TODO: 缺少道具数量变更钩子(IHeroItems 未实现 IHookable)`
- `poison`/`weak`/`curse`:`// TODO: flags 更新未接(当前无监听系统)`(**沿用既有注释文本,逐字不改**)
- `replay`:`// TODO: 缺少聚合录像状态钩子(replaySystem 仅提供沙箱级钩子 onCreateSandbox/onStartReplay 等);原值来源 legacy core.status.replay`
(若 Task 0 用户指定 `replay` 接线,则据其指定另行落实沙箱钩子编排。)
- 卸载:`onUnmounted(() => { attributeHook.unload(); locationHook.unload(); });`(`onUnmounted` 自 `vue` 引入)。
(3) **桶导出:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './<APPROVED_NAME>';`(同目录,D-69 合规)。
(3) **`RightStatusBar` 自持 `rightStatus`:** 在 setup 内 `const rightStatus: IRightHeroStatus = reactive({ exampleHard: 0 });`;将 `p.status.exampleHard` 改为 `rightStatus.exampleHard`。`exampleHard` 来源为 flags(`IFlagSystem` 非 `IHookable`,D-13)⇒ **留空 + `// TODO: flags 更新未接(当前无监听系统)`**,保持初值 `0`。(若 Task 0 用户就 `exampleHard` 另有指定,据其执行。)
(4) **CRLF 与收口:** 新文件须为 CRLF;本任务只允许新建该文件并改 `index.ts`。
(4) **import 收口:** 新增所需 import(`reactive`、`onUnmounted` 自 `vue`);移除因改动而未使用的 import;`import` 置首、无空行 / 注释(`dev.md:55`);**不得修改任何既有 jsDoc 与注释**(既有 `// TODO: flags 更新未接(当前无监听系统)` 若随字段迁移,则在目标位置**逐字重用**)。
(5) **CRLF 与收口:** 文件须为 CRLF;本任务只允许改 `statusBar.tsx`。
</action>
<reversibility rating="reversible">新增一个包内实现层文件 + 一行桶导出;`git revert` 可整体回退;未改任何既有公共契约。</reversibility>
<reversibility rating="reversible">组件内状态归属迁移 + 订阅;`git revert` 可整体回退;未改导出公共契约(未导出接口的内部成员移除与泛型简化)。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const dir='packages-user/client-modules/src/ui';const known=new Set(['index.ts','types.ts','func.ts']);const cand=fs.readdirSync(dir).filter(f=>f.endsWith('.ts')&&!known.has(f));if(cand.length!==1){console.error('EXPECTED EXACTLY ONE NEW .ts FILE (the approved hero-attr object), GOT :: '+JSON.stringify(cand));process.exit(1)}const target=dir+'/'+cand[0];const t=fs.readFileSync(target,'utf8').replace(/\r\n/g,'\n');const bad=[];if(!/export interface \w+/.test(t))bad.push('interface missing');if(!/export class \w+ implements \w+/.test(t))bad.push('class implements missing');if(!/reactive\(/.test(t))bad.push('reactive missing');if(!/addHook\(/.test(t))bad.push('addHook missing');if(!/onUpdateAttribute/.test(t))bad.push('onUpdateAttribute missing');if(!/unload\(\)/.test(t))bad.push('unload missing');if(!/getFinalAttribute/.test(t))bad.push('getFinalAttribute missing');if(/\bIHeroAttr\b/.test(t))bad.push('ILlegal name IHeroAttr used');if(bad.length>0){console.error('HERO ATTR FILE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hero-attr object file ('+cand[0]+'): interface+class+reactive+addHook+unload')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "EXPECTED EXACTLY ONE NEW .ts FILE" / "HERO ATTR FILE FAILED" / "ILlegal name IHeroAttr"(未找到唯一新 `.ts` 文件(用户批准名)、缺 interface/class/reactive/addHook/unload/getFinalAttribute,或误用 `IHeroAttr`)</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/statusBar.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(!/export interface ILeftHeroStatus/.test(t))bad.push('ILeftHeroStatus export lost');if(!/export interface IRightHeroStatus/.test(t))bad.push('IRightHeroStatus export lost');if(!/const leftStatus[^=]*=\s*reactive\(/.test(t))bad.push('leftStatus not owned locally');if(!/const rightStatus[^=]*=\s*reactive\(/.test(t))bad.push('rightStatus not owned locally');if(!/hero\.attribute\.addHook\(/.test(t))bad.push('attribute addHook missing');if(!/onUpdateAttribute/.test(t))bad.push('onUpdateAttribute missing');if(!/getFinalAttribute/.test(t))bad.push('getFinalAttribute missing');if(!/hero\.location\.addHook\(/.test(t))bad.push('location addHook missing');if(!/onSetFloor/.test(t))bad.push('onSetFloor missing');if(!/onUnmounted\(/.test(t))bad.push('onUnmounted missing');if(!/\.unload\(\)/.test(t))bad.push('unload missing');if(!/TODO:/.test(t))bad.push('TODO markers missing');if(/interface StatusBarProps</.test(t))bad.push('StatusBarProps still generic');if(/status\s*:\s*[A-Za-z]/.test(t))bad.push('StatusBarProps still has status member');if(/props:\s*\[[^\]]*'status'/.test(t))bad.push('statusBarProps still requires status prop');if(/p\.status\b/.test(t))bad.push('p.status still referenced');if(bad.length>0){console.error('STATUSBAR FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK statusBar owns status + hooks(attribute/location) + unload + TODOs; status prop removed')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "STATUSBAR FAILED"(未自持状态 / 缺属性或位置钩子 / 缺 `onUnmounted` 卸载 / 缺 `TODO` / `StatusBarProps` 仍含泛型或 `status` / `statusBarProps` 仍要求 `status` / 仍引用 `p.status` / 导出接口丢失)</fails_when>
</verify>
<acceptance_criteria>
- 新文件在场(文件名 = 用户批准者;规划默认 `heroAttr.ts`)
- 含 `export interface` + `export class ... implements`(`dev.md:100`)
- 含 `reactive(`、`addHook(`、`onUpdateAttribute`、`unload()`、`getFinalAttribute`
- **未**出现 `IHeroAttr` 作为新对象名
- `src/ui/index.ts` 含对应同目录 `export *`
- `LeftStatusBar` 自持 `leftStatus`、`RightStatusBar` 自持 `rightStatus`(`reactive`,初值照搬原 `main.tsx`)
- 属性 9 字段经 `hero.attribute.addHook({ onUpdateAttribute })` 刷新;`lv` 经同一属性钩子刷新(`getFinalAttribute('level')` + `core.getLvName`)
- `floor` 经 `hero.location.addHook({ onSetFloor })` 触发(值保持 legacy `core.status.floorId`)
- `up` / 道具数量 / debuff / `replay` / `exampleHard` **无更新代码**,就近 `// TODO:`(debuff 沿用既有注释文本)
- `onUnmounted` 调用 `attributeHook.unload()` 与 `locationHook.unload()`
- `StatusBarProps` 无 `status`(及未使用泛型);`statusBarProps.props` 无 `'status'`;组件内无 `p.status` 残留
- `ILeftHeroStatus` / `IRightHeroStatus` 仍定义并导出;未新增任何接口命名;CRLF
</acceptance_criteria>
<done>响应式勇士属性对象文件创建完成(禁名未用、契约齐备),桶导出就位</done>
<done>`statusBar.tsx` 完成状态归属迁移:状态栏组件自持状态、属性/floor 走对应钩子、`lv` 经属性钩子、无钩子字段留空标 `TODO`、钩子随卸载清理、`status` 传参契约移除</done>
</task>
<task type="auto">
<name>Task 2:接入 `main.tsx`(用新对象替换 9 处属性读取与 `attributeHook` 订阅;保留非属性字段 / `updateDataFallback` / 阻断项 / legacy)+ 门禁</name>
<precondition>Task 1 已完成(新对象文件在场且桶导出就位)</precondition>
<name>Task 2:清理 `main.tsx`(移除迁出的状态 / 传参 / `attributeHook` / `updateStatus` / `updateDataFallback` / 死引用 `locked`;保留 `hideStatus` / 地图渲染 / 交互监听)+ 门禁</name>
<precondition>Task 1 已完成(`statusBar.tsx` 自持状态且 `StatusBarProps` 不再含 `status`)。</precondition>
<files>packages-user/client-modules/src/ui/main.tsx</files>
<read_first>
- packages-user/client-modules/src/ui/main.tsx(整文件;`:35` `const { mainMapRenderer, mainMapExtension } = props.state;`;`:66-90` `leftStatus` 字段集;`:96-133` `updateStatus`(9 处 `props.state.hero.attribute.getFinalAttribute`、keys、debuff、floor、lv、up、replay、hard);`:135-138` `updateDataFallback`;`:141-150` 新订阅与 `onUnmounted`;`:228-232` `<LeftStatusBar status={leftStatus} ...>`)
- packages-user/client-modules/src/ui/&lt;APPROVED_NAME&gt;.ts(Task 1 的产物,文件名取 Task 0 用户批准者;规划默认占位 `heroAttr.ts`;确认构造签名与字段名)
- dev.md(不改既有注释;CRLF;无 `?.`)
- packages-user/client-modules/src/ui/main.tsx(整文件:`:1-32` import;`:61-62` `hideStatus` / `locked`;`:66-100` `replayStatus` / `leftStatus` / `rightStatus`;`:103-146` `updateStatus`;`:148-151` `updateDataFallback`;`:153-163` `attributeHook` + `onUnmounted`;`:233-325` 渲染树,`:241-246` / `:288-293` `<LeftStatusBar>` / `<RightStatusBar>`)
- packages-user/client-modules/src/ui/statusBar.tsx(Task 1 产物:确认 `LeftStatusBar` / `RightStatusBar` 已不接受 `status` prop)
- dev.md(不改既有注释;移除未使用 import;无 `?.`;CRLF)
</read_first>
<action>
(1) **接线:** 在 `main.tsx` 顶部从 `'./<APPROVED_NAME>'` 直连导入新响应式对象(接口 / 类名取用户批准者;**必须直连该文件**,不得经桶导入——本任务门禁据该文件名自动定位并断言 `main.tsx` 含 `from './<APPROVED_NAME>'`)。在组件 setup 内用该对象承载勇士属性:创建该对象(传入 `props.state`),以它的属性字段替换 `leftStatus` 中对应的 9 个属性字段读取(`leftStatus.atk/hp/hpmax/mana/manamax/def/mdef/money/exp` 改由新对象提供);把原 `attributeHook = props.state.hero.attribute.addHook({...})` 的订阅交给新对象(新对象内部持有钩子),`onUnmounted` 改为调用新对象的 `unload()`。**不得**改变 `<LeftStatusBar status={...}>` 对外的形态(D-14)。
(1) **移除迁出物:** 删除 `replayStatus`、`leftStatus`、`rightStatus` 定义(`:66-100`);删除 `updateStatus`(`:103-146`);删除 `updateDataFallback`(`:148-151`)与死引用 `const locked = ref(false)`(`:62`,读码确认其**只写不读**、零消费点);删除 `attributeHook` 与为其而设的 `onUnmounted`(`:153-163`)。
(2) **保留不动:** `leftStatus` 的非属性字段(`up` / `yellowKey` / `blueKey` / `redKey` / `greenKey` / `pickaxe` / `bomb` / `centerFly` / `poison` / `weak` / `curse` / `floor` / `lv` / `replay`)与其来源(`core.itemCount` / `flags` / `core.status`);`updateDataFallback()` 与 `locked`;`rightStatus`;阻断项(`state`(data-state) 含 `layerState={state.maps}`、`using`、`mainUIController`);所有 `core.*` 与 legacy 导入。**不得**因接线而丢失任一既有更新触发路径(若原 `attributeHook` 同时驱动属性更新与 `updateDataFallback`,须保证二者仍有触发路径)。
(2) **移除 `status` 传参:** `<LeftStatusBar>`(`:241-246`)删除 `status={leftStatus}`(保留 `loc` / `hidden` / `state`);`<RightStatusBar>`(`:288-293`)删除 `status={rightStatus}`(保留 `loc` / `hidden` / `state`)。
(3) **CRLF 与收口:** 文件须为 CRLF;本任务只允许改 `main.tsx`。
(3) **`hideStatus` 处置(默认方案):** 保留 `hideStatus`(`ref`)及其在渲染树中的**全部**消费点(`:244,291,303,310,320`,逐字不动)。因 `attributeHook` 迁出且 flags 无监听(D-13),其旧刷新路径消失 ⇒ 将 `const hideStatus = ref(false)` 改为在 setup 内读取一次 flag 初值:`const hideStatus = ref(p.state.flags.getFieldValueDefaults('hideStatusBar', false));` 并就近加 `// TODO: flags 更新未接(当前无监听系统)`。**若 Task 0 用户指定备选(保持 `ref(false)` 完全静态)**,则据其指定执行并同样标注 TODO。
(4) **保留面(不得破坏):** `props.state.mainMapRenderer` / `mainMapExtension`(`:41-42`)、`mainTextboxProps`、`mapMiscSprite` / `renderMapMisc` / `using.onExcitedFunc`(`:167-202`)、交互监听 `clickMap` / `downMap` / `moveMap`(`:204-231`)、渲染树中的 `map-render`(`layerState={state.maps}`,含既有 `@ts-expect-error`)、`Textbox` / `Tip` / `custom`、`mainUIController` 区块(`:294-300`)、`g-line` / `g-rect` 与 `hideStatus` 相关绘制、`MainSceneUI` 导出。**所有 `core.*` legacy 读取保持**。
(5) **import 收口:** 移除因移除代码而不再使用的 import——`reactive`、`onUnmounted`(自 `vue`)、`ILeftHeroStatus` / `IRightHeroStatus`(保留下方仍用的 `LeftStatusBar` / `RightStatusBar`)、`ReplayingStatus`(自 `./toolbar`);保留 `ref`、`Font` / `IActionEvent` / `MotaOffscreenCanvas2D` / `CustomRenderItem`、`Textbox` / `TextboxProps` / `Tip`、`GameUI` / `SetupComponentOptions`、`shared` 常量、`state`(`@user/data-state`,用于 `state.maps`)、`mainUIController`、`isNil`、`using`、`IUIPropsBase` 等仍用者。**仅当确认不再使用才移除**;不得留下未使用 import(`dev.md:106`)。`import` 置首、无空行 / 注释(`dev.md:55`)。
(6) **不改既有注释(删除代码内的注释随代码移除,非改写);CRLF;** 本任务只允许改 `main.tsx`。
</action>
<reversibility rating="reversible">组件内接线调整;`git revert` 可整体回退;未改公共契约。</reversibility>
<reversibility rating="reversible">组件内清理;`git revert` 可整体回退;未改导出公共契约。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const dir='packages-user/client-modules/src/ui';const known=new Set(['index.ts','types.ts','func.ts']);const cand=fs.readdirSync(dir).filter(f=>f.endsWith('.ts')&&!known.has(f));const t=fs.readFileSync(dir+'/main.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(cand.length!==1)bad.push('cannot locate the new hero-attr .ts file');else{const base=cand[0].replace(/\.ts$/,'');if(t.indexOf(\"'./\"+base+\"'\")<0)bad.push('new object not imported in main.tsx from ./'+base)}if(!/\.unload\(\)/.test(t))bad.push('unload() not called');if(t.indexOf('updateDataFallback')<0)bad.push('updateDataFallback lost');if(t.indexOf('layerState={state.maps}')<0)bad.push('data-state layerState wrongly changed');if(t.indexOf('using.onExcitedFunc')<0)bad.push('using singleton wrongly changed');if(t.indexOf('mainUIController')<0)bad.push('mainUIController wrongly changed');if(t.indexOf('leftStatus.')<0)bad.push('leftStatus non-attr fields lost');if(bad.length>0){console.error('WIRING FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hero-attr object wired, non-attr/blocked items preserved')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "WIRING FAILED"(新对象未接线 / 未 `unload` / 丢失 `updateDataFallback` / 误改阻断项 / 丢失非属性字段)</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/main.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(/status=\{leftStatus\}/.test(t))bad.push('leftStatus prop remains');if(/status=\{rightStatus\}/.test(t))bad.push('rightStatus prop remains');if(/\battributeHook\b/.test(t))bad.push('attributeHook remains');if(/updateStatus/.test(t))bad.push('updateStatus remains');if(/updateDataFallback/.test(t))bad.push('updateDataFallback remains');if(/const leftStatus/.test(t))bad.push('leftStatus def remains');if(/const rightStatus/.test(t))bad.push('rightStatus def remains');if(/const replayStatus/.test(t))bad.push('replayStatus def remains');if(/const locked/.test(t))bad.push('locked def remains');if(!/<LeftStatusBar/.test(t))bad.push('LeftStatusBar usage lost');if(!/<RightStatusBar/.test(t))bad.push('RightStatusBar usage lost');if(!/hideStatus/.test(t))bad.push('hideStatus lost');if(!/layerState=\{state\.maps\}/.test(t))bad.push('layerState={state.maps} changed');if(!/mainUIController/.test(t))bad.push('mainUIController changed');if(!/using\.onExcitedFunc/.test(t))bad.push('using.onExcitedFunc changed');if(!/renderMapMisc/.test(t))bad.push('renderMapMisc lost');if(!/MainSceneUI/.test(t))bad.push('MainSceneUI export lost');if(bad.length>0){console.error('MAIN CLEANUP FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK main.tsx cleaned; hideStatus/map/interaction preserved')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MAIN CLEANUP FAILED"(迁出物 / 死引用未移除,或误伤 `hideStatus` / 地图渲染 / 交互 / `MainSceneUI`)</fails_when>
<automated>node -e "const fs=require('fs');const path=require('path');const dir='packages-user/client-modules/src/ui';const bad=[];for(const f of fs.readdirSync(dir).filter(f=>/\.(ts|tsx)$/.test(f))){const t=fs.readFileSync(path.join(dir,f),'utf8');if(/from '\.\.\/(?:\.\.\/)?core'/.test(t)||/\bclient\.[a-zA-Z]/.test(t))bad.push(f)}if(bad.length>0){console.error('CLIENT REMAINS :: '+bad.join(','));process.exit(1)}console.log('OK src/ui still client-free')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CLIENT REMAINS"(接线使 `src/ui` 重新出现 `client` 单例用法)</fails_when>
<fails_when>non-zero exit, 或 stderr 含 "CLIENT REMAINS"(`src/ui` 出现 `client` 单例用法)</fails_when>
</verify>
<acceptance_criteria>
- `main.tsx` 引用并使用新响应式对象;`onUnmounted` 调用其 `unload()`
- 9 个勇士属性读取改由新对象提供;非属性字段(keys / debuff / floor / lv / up / replay)与 `updateDataFallback` / `locked` 保持
- 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)与 legacy / `core.*` 保持不动
- `src/ui/**` 仍零 `client`
- 无计划外文件被改
- `main.tsx` 不再含 `leftStatus` / `rightStatus` / `replayStatus` / `locked` / `attributeHook` / `updateStatus` / `updateDataFallback` 定义与用法
- `<LeftStatusBar>` / `<RightStatusBar>` 不再接收 `status`,仍接收 `loc` / `hidden` / `state`
- `hideStatus` 及其全部渲染消费点保留(默认读一次 flag 初值 + `// TODO:`)
- 地图渲染(`mainMapRenderer` / `mainMapExtension` / `layerState={state.maps}` / `renderMapMisc`)、交互监听、`mainUIController` 区块、`using.onExcitedFunc`、`MainSceneUI` 导出保持
- 无未使用 import;`src/ui/**` 仍零 `client`;CRLF
</acceptance_criteria>
<done>D-12 完成:响应式勇士属性对象独立实现并接入 `main.tsx`,属性更新经该对象驱动,非属性字段 / 阻断项 / legacy 零改动</done>
<done>D-12(定向重设计)完成:状态归宿迁入 `statusBar.tsx`、字段更新走对应钩子(无钩子者 TODO),`main.tsx` 清理且 `hideStatus` / 地图 / 交互零破坏</done>
</task>
</tasks>
@ -234,47 +291,64 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
| Boundary | Description |
|----------|-------------|
| 命名 / 接口边界 | 新对象命名须用户批准;不得与既有 `IHeroAttr` / `IHeroAttribute` 冲突 |
| 组件 ↔ 数据端钩子 | 新对象经 `props.state.hero.attribute.addHook` 订阅数据端属性钩子 |
| 生命周期边界 | 订阅须在 `onUnmounted` 卸载,避免泄漏 |
| 组件 ↔ 数据端钩子 | `LeftStatusBar` 经 `p.state.hero.attribute` / `hero.location` 订阅数据端钩子 |
| 生命周期边界 | 订阅钩子须在 `onUnmounted` 卸载,避免泄漏 / 重复订阅 |
| 状态归属边界 | 状态对象迁入组件后,不得残留外部传入契约(`status` prop)或 `p.status` 引用 |
| 范围边界 | 清理 `main.tsx` 时须保留 `hideStatus` / 地图渲染 / 交互 / `MainSceneUI` |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-04-01 | Elevation of Privilege | 未获批命名 / 复用 `IHeroAttr` | high | mitigate | Task 0 强制命名批准、明列 `IHeroAttr` 不可用;Task 1 门禁断言 `IHeroAttr` 未出现 |
| T-09-04-02 | Denial of Service | 订阅未卸载导致泄漏 / 重复订阅 | medium | mitigate | `unload()` 契约 + `onUnmounted` 调用;门禁断言 `unload()` 调用在场 |
| T-09-04-03 | Tampering | 接线丢失既有更新行为(`updateDataFallback` / 非属性字段) | medium | mitigate | Task 2 门禁断言 `updateDataFallback` / `leftStatus` 非属性字段 / 阻断项保持 |
| T-09-04-04 | Tampering | 范围外文件 / 阻断项 / legacy 被改 | medium | mitigate | 白名单 + prohibitions + 任务门禁 + `git status` 核对 |
| T-09-04-05 | Repudiation | 未验证即提交 | medium | mitigate | 每任务静态门禁 + SUMMARY;验证未通过不得提交 |
| T-09-04-01 | Denial of Service | 属性 / 位置钩子未卸载导致泄漏 | medium | mitigate | `onUnmounted` 调用 `attributeHook.unload()` 与 `locationHook.unload()`;Task 1 门禁断言 `onUnmounted` + `unload()` 在场 |
| T-09-04-02 | Tampering | 误把「无钩子」字段伪造钩子 / 臆造接口 | high | mitigate | 逐字段映射表;只接 `onUpdateAttribute` / `onSetFloor` 两个实证钩子;无钩子者仅标 `// TODO:`;prohibitions 禁臆造 |
| T-09-04-03 | Tampering | 迁移残留 `p.status` 旧契约 | medium | mitigate | Task 1 门禁断言 `StatusBarProps` 去 `status`、`statusBarProps` 去 `'status'`、无 `p.status` 残留 |
| T-09-04-04 | Tampering | 清理 `main.tsx` 时误伤 `hideStatus` / 地图 / 交互 / `MainSceneUI` | medium | mitigate | Task 2 门禁逐项断言保留面在场、迁出物不在场 |
| T-09-04-05 | Elevation of Privilege | 未经批准即执行 / 引入未批准命名 | high | mitigate | Task 0 `blocking-human`;本计划零新增接口命名(仅移除 `status` 成员与未用泛型);prohibitions 禁 `IHeroAttr` |
| T-09-04-06 | Repudiation | 未验证即提交 | medium | mitigate | 每任务静态门禁 + SUMMARY;验证未通过不得提交 |
| T-09-04-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装、不执行 `pnpm i` |
| T-09-04-04b | Tampering | 范围外文件 / legacy `core.*` / 用户并发改动被改 | medium | mitigate | `files_modified` 白名单 + prohibitions + Task 2 零 `client` 门禁 + `git status` 核对(D-56) |
</threat_model>
<verification>
- **文件门禁**:新对象文件在场(含 `interface` + `class implements` + `reactive` + `addHook` + `onUnmounted`/`unload`),且未使用禁用名 `IHeroAttr`。
- **接线门禁**:`main.tsx` 使用新对象、调用 `unload()`;非属性字段 / `updateDataFallback` / 阻断项 / legacy 保持。
- **零 `client` 门禁**:`src/ui/**` 无 `client` 用法。
- **范围门禁** 与 **CRLF 门禁** + **人工复核**(结论写入 SUMMARY)。
- **Task 1(`statusBar.tsx`)**:自持 `leftStatus` / `rightStatus`;含 `hero.attribute.addHook` + `onUpdateAttribute` + `getFinalAttribute`、`hero.location.addHook` + `onSetFloor`、`onUnmounted` + `unload()`、`TODO:`;`StatusBarProps` 无泛型 / 无 `status`;`statusBarProps.props` 无 `'status'`;无 `p.status` 残留;`ILeftHeroStatus` / `IRightHeroStatus` 仍导出。
- **Task 2(`main.tsx`)**:迁出物与死引用 `locked` / `updateDataFallback` / `updateStatus` 不在场;`<LeftStatusBar>` / `<RightStatusBar>` 保留且无 `status` 传参;`hideStatus` / `layerState={state.maps}` / `mainUIController` / `using.onExcitedFunc` / `renderMapMisc` / `MainSceneUI` 在场。
- **零 `client` 门禁**:`src/ui/**` 无 `client` 单例用法。
- **范围门禁** 与 **CRLF 门禁** + **人工复核**(结论写入 `09-04-SUMMARY.md`)。
- **无运行时门禁**:全程不使用 `pnpm check:type` / `pnpm build` / TS 诊断数。
</verification>
<success_criteria>
- 响应式勇士属性对象按用户批准命名独立实现,订阅与卸载契约齐备。
- `main.tsx` 的勇士属性读取与更新改由该对象驱动,无行为丢失。
- 非属性字段 / 阻断项 / legacy 零改动;零范围外改动;无运行时门禁。
- `leftStatus` / `rightStatus` 由 `statusBar.tsx` 的组件自身持有,`main.tsx` 不再传入。
- 属性 9 字段经 `hero.attribute.onUpdateAttribute` 钩子刷新;`floor` 经 `hero.location.onSetFloor` 触发;`lv` 经属性钩子;无钩子字段(`up` / 道具数量 / debuff / `replay` / `exampleHard`)如实留空并标 `// TODO:`;钩子随 `onUnmounted` 卸载。
- `main.tsx` 清理完毕(含死引用 `locked` / `updateDataFallback` / `updateStatus`),`hideStatus` / 地图渲染 / 交互监听 / `MainSceneUI` 零破坏。
- 零新增接口命名、零范围外改动、零 `client`、无运行时门禁。
</success_criteria>
## Artifacts this phase produces
**新增文件与符号(命名待用户批准;默认占位仅供参考):**
**修改的文件与符号(无新增接口命名):**
| 符号 / 路径 | 类型 | 说明 |
|---|---|---|
| `packages-user/client-modules/src/ui/heroAttr.ts`(文件名待批准) | 文件(新) | D-12 的响应式勇士属性对象 |
| `<接口名>`(待批准;默认占位 `IHeroAttrState`) | 接口(新) | 对象契约:属性字段 + 更新方法 + `unload()` |
| `<类名>`(待批准;默认占位 `HeroAttrState`) | 类(新,`implements <接口名>`) | `reactive` 字段 + `state.hero.attribute.addHook({ onUpdateAttribute })` 订阅 + `unload()` |
| `packages-user/client-modules/src/ui/index.ts`(改) | barrel | 新增 `export * from './<APPROVED_NAME>';` |
| `packages-user/client-modules/src/ui/statusBar.tsx`(改) | 组件 / 契约 | `LeftStatusBar` / `RightStatusBar` 自持 `leftStatus` / `rightStatus`;属性钩子 + `floor` 位置钩子 + 无钩子字段 `// TODO:`;未导出 `StatusBarProps` 移除 `status` 成员(及未使用泛型) |
| `packages-user/client-modules/src/ui/main.tsx`(改) | 组件 | 移除迁出的状态 / 传参 / `attributeHook` / `updateStatus` / `updateDataFallback` / 死引用 `locked`;保留 `hideStatus` / 地图渲染 / 交互监听 |
| `ILeftHeroStatus` / `IRightHeroStatus`(沿用,位于 `statusBar.tsx`) | 接口 | 状态栏状态契约,**不改名、不迁移导出位置** |
| `LeftStatusBar` / `RightStatusBar`(沿用) | 组件 | 状态自持方;对外不再接收 `status` |
**禁用名:** `IHeroAttr`(与 `@user/data-common` 属性字典类型冲突)——不得使用。
**无新增接口命名:** 本计划不新增任何公共 / 受保护 / 私有成员命名,不新增导出接口;不引入 `IHeroAttr`。组件 setup 内新增的局部 `const`(`refreshAttribute` / `locationHook`)为函数局部标识符(非成员 / 非导出),已列入 Task 0 呈报用户。`src/ui/index.ts` 无需改动(导出位置不变)。
## Source Coverage Audit
| 来源 | 项 | 处置 |
|------|----|------|
| GOAL | Phase 9(ROADMAP:481)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·D-12 定向重设计子切片:状态栏渲染数据联动) |
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
| RESEARCH | 无 RESEARCH.md(本阶段为增量派发,无独立研究产物) | N/A |
| CONTEXT | D-12(勇士属性钩子化,本次重设计为「状态归属迁移 + 逐字段钩子/TODO」) | COVERED(Task 1 / Task 2) |
| CONTEXT | D-13(flags 无监听,仅标 TODO) | COVERED(poison / weak / curse / exampleHard / hideStatus 的 `// TODO:`) |
| CONTEXT | D-14(纯适配,不改形态 / 不移植 legacy) | COVERED(未改视觉 / 未移植 legacy;legacy `core.*` 只读不改) |
| CONTEXT | Deferred Ideas(其余杂项对齐由用户后续增量派发) | DEFERRED(未纳入任何任务) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-04-SUMMARY.md` when done