docs(09): revise book/equipbox/toolbox plans to consume new data-end interfaces

- book: wire base-attribute critical/damage via calculateEnemyCritical; rename calculateCritical->calculateViewCritical; @user/data-system dependency already declared (no package.json change)
- equipbox: single-equipment delta via compareEquip(uid,-1,slot)/(-1,uid,slot)
- toolbox: enumerate owned items via IHeroItems.iterateItems() (drop saveState workaround)
This commit is contained in:
unanmed 2026-10-10 19:12:37 +08:00
parent afc484465e
commit 337d28c1d7
4 changed files with 137 additions and 154 deletions

View File

@ -481,7 +481,7 @@ Plans:
**Goal**: 在已完成的数据端(可在 Node 环境独立运行/回放验证)之上完成渲染端框架,产出可在浏览器中运行并验证的最小版本
**Requirements**: TBD
**Depends on**: Phase 8
**Plans**: 5/8 plans executed (任务 1·UI 接口适配,D-10..D-14 已闭环;任务 2·迁移后 UI 修复,D-15..D-17 已闭环;任务 3·legacy UI 移植,book/equipbox/toolbox 已规划(09-06..09-08),shop 暂缓待 `ShopState`;本阶段增量派发,其余任务待用户再派发)
**Plans**: 5/8 plans executed (任务 1·UI 接口适配,D-10..D-14 已闭环;任务 2·迁移后 UI 修复,D-15..D-17 已闭环;任务 3·legacy UI 移植,book/equipbox/toolbox 已规划(09-06..09-08)并已按用户补齐的数据端接口修订——裸怪物临界 `calculateEnemyCritical`、与空装备对比 `compareEquip(-1)`、道具枚举 `iterateItems`、`@user/data-system` 依赖已声明;shop 暂缓待 `ShopState`;本阶段增量派发,其余任务待用户再派发)
Plans:
**Wave 1**

View File

@ -8,7 +8,6 @@ depends_on:
files_modified:
- packages-user/client-modules/src/ui/book.tsx
- packages-user/client-modules/src/ui/bookDetail.tsx
- packages-user/client-modules/package.json
- .planning/phases/09-minimum-browser-runnable/09-06-SUMMARY.md
autonomous: false
requirements: []
@ -22,11 +21,11 @@ must_haves:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-22)**:Task 0 必须先汇报「本计划 = 任务 3 · book 怪物手册(列表 `book.tsx` + 独立详情 `bookDetail.tsx`)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**逐一列出所有新增命名(文件名、组件名、`GameUI` 名、props 接口名)请求批准**,并**明确列出开放项(见下)与接口缺口**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-23(数据源 / 去重):** 手册展示**当前地图**怪物,**绑定当前 `EnemyContext`**(`props.state.enemyContext`,`IEnemyContext<IEnemyAttr, IHeroAttr>`);列表为**去重**后的怪物,**按怪物图块数字(`IReadonlyEnemy.code`)去重**;每个怪物的**基础数据取 `view.getBaseEnemy()`**(即不考虑光环的原始状态);依赖勇士属性的派生信息(伤害 / 临界 / 减伤 / 1 防)仍显示。"
- "**D-24(列表字段):** 列表项显示 **图标(`<icon>`)+ 名称(`base.getAttribute('name')`)+ 特殊名(`ISpecial.getSpecialName()`)+ 生命 / 攻击 / 防御 / 金币 / 经验(`base.getAttribute('hp'|'atk'|'def'|'money'|'exp')`)+ 伤害 / 临界 / 减伤 / 「1 防」**。**不使用 `{ratio}防`**(新接口无 `ratio`),改用「**1 防**」(+1 防御对应的减伤)。列表为**滚动式**(`<Scroll noscroll>` 纵向)。"
- "**D-25(临界定义):** 临界 = 玩家**再增加 x 点攻击**时,因战斗回合数减少而使玩家**受到的总伤害下降**的最小 x;减伤 = 对应的下降量。**直接调用系统已有二分法接口 `IDamageContext.calculateCritical(view, 'atk', precision?)`**(`@user/data-system` `combat/types.ts:445`,实现 `combat/damage.ts:184`)计算——取首个 `IEnemyCritical` 的 `nextDiff`(= 最小 x)与 `-damageDiff`(= 减伤)。**不重复实现二分法**。"
- "**D-26(详情为独立 UI):** `bookDetail.tsx` 是**独立 UI**,**不 import `book.tsx`、可单独运行**(后续需支持「在地图中打开指定 View 的详细信息」);入参为 **`IEnemy<IEnemyAttr>` 或 `IEnemyView<IEnemyAttr>`**;若为 `IEnemyView`:可切换「**基础属性 / 计算后属性**」——**计算后 = `view.getComputedEnemy()`(含光环等加成)**,基础 = `view.getBaseEnemy()`;详情**仅显示特殊属性列表**(`iterateSpecials()` → `getSpecialName()` / `getDescription()` / `getNameColor()`),**不显示光环**。"
- "**D-25(临界定义):** 临界 = 玩家**再增加 x 点攻击**时,因战斗回合数减少而使玩家**受到的总伤害下降**的最小 x;减伤 = 对应的下降量。**直接调用系统已有二分法接口**——**计算后**怪物视图用 `IDamageContext.calculateViewCritical(view, 'atk', precision?)`(`@user/data-system` `combat/types.ts:459`;实现 `combat/damage.ts:185`,内部取 `view.getComputedEnemy()`);**基础属性**(只读怪物对象)用**用户新补的接口** `IDamageContext.calculateEnemyCritical(enemy: IReadonlyEnemy, 'atk', locator: ITileLocator | null, precision?)`(`combat/types.ts:472`;实现 `combat/damage.ts:210`)。取首个 `IEnemyCritical` 的 `nextDiff`(= 最小 x)与 `-damageDiff`(= 减伤)。**不重复实现二分法**。"
- "**D-26(详情为独立 UI):** `bookDetail.tsx` 是**独立 UI**,**不 import `book.tsx`、可单独运行**(后续需支持「在地图中打开指定 View 的详细信息」);入参为 **`IEnemy<IEnemyAttr>` 或 `IEnemyView<IEnemyAttr>`**;若为 `IEnemyView`:可切换「**基础属性 / 计算后属性**」——**计算后 = `view.getComputedEnemy()`(含光环等加成)**,基础 = `view.getBaseEnemy()`;临界表**计算模式 = `calculateViewCritical(view,'atk')`(计算后属性)**、**基础模式 = `calculateEnemyCritical(base,'atk',locator)`(基础属性;locator 取自 `view.context.getEnemyLocatorByView(view)`,plain `IEnemy` 无 view 时传 `null`——签名 `combat/types.ts:475` 允许 `ITileLocator | null`)**;详情**仅显示特殊属性列表**(`iterateSpecials()` → `getSpecialName()` / `getDescription()` / `getNameColor()`),**不显示光环**。"
- "**D-27(详情交互):** 左下角为切换按钮「**切换至计算后属性** | **切换至基础属性**」(文案随当前模式变化);右下角为「**返回上一级**」(`props.controller.close(props.instance)`)。**打开方式不在本书范围**(不在本计划内接线如何从地图打开 book)。"
- "**D-33 / D-37(共享约束):** book 列表与详情均**占满整个游戏画面**(`FULL_LOC = [0, 0, MAIN_WIDTH, MAIN_HEIGHT]`,`../shared`),渲染于游戏画面渲染树(root renderer),**非 DOM / 非整个 window**;两侧的退出/返回按钮默认**右下角**。"
- "**接口缺口(必须在 Task 0 获得用户裁决,不得臆造):** ①**base 模式临界 / 伤害**:`IDamageContext.calculateCritical` 与 `getDamageInfo` 内部一律使用 `view.getComputedEnemy()`(`damage.ts:87,201`),**没有对「基础怪物对象 / 基础属性」计算临界或伤害的公开 API**。故 D-26「临界表在计算模式下用计算后属性」隐含的「基础模式下用基础属性」当前**无对应接口**。②**plain `IEnemy` 入参**:详情只拿到 `IEnemy`(无 view)时**无法**调用 `calculateCritical` / `getDamageInfo`(二者签名要求 `IEnemyView`)。③**类型来源**:`IEnemyView` / `IEnemyContext` / `IDamageSystem` 定义于 `@user/data-system`,而 `@user/client-modules/package.json` **未声明** `@user/data-system` 依赖(其它 `@user/data-*` 已声明)。"
- "**接口现状(用户已补齐,原缺口已闭合):** 用户新补 (1) **裸怪物临界接口** `IDamageContext.calculateEnemyCritical(enemy: IReadonlyEnemy, attribute, locator: ITileLocator | null, precision?)`(`combat/types.ts:472`,实现 `combat/damage.ts:210`)——可对**基础/只读怪物对象**(即 `view.getBaseEnemy()` 或 plain `IEnemy`)计算临界,`baseInfo.damage` 给出该状态伤害、`nextDiff`/`damageDiff` 给出临界/减伤;原 `calculateCritical` 更名为 `calculateViewCritical`(`combat/types.ts:459`,实现 `damage.ts:185`,内部仍取 `getComputedEnemy()`)。(2) `client-modules/package.json` **已声明** `@user/data-system` 依赖(commit 751f206,任务无需再改 `package.json`)。(3) plain `IEnemy` 无 view 时 `calculateEnemyCritical` 可传 `locator = null`,故详情入参可为 plain `IEnemy`。"
- "**范围零外溢 + 静态门禁:** 仅新增 `book.tsx` / `bookDetail.tsx`(+ 必要的 `package.json` 依赖声明,若用户采纳方案 (a));**不修改** 09-01..09-05 任何文件、不修改任何既有 UI / `src/render/**` / `packages/system` / `packages/render*` / 数据端(`packages-user/data-*`)/ `legacy-ui`;验证一律为**静态源码断言**(文件存在、关键符号存在、props 数组含 `controller/instance/state`、无 `client` 单例、无 `import type`、无 `?.`、CRLF),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数作门禁。"
- "**不改既有注释、不引入未批准命名:** 新建文件内所有注释按 `dev.md:72-86` 新写(不涉及修改任何既有注释);所有新增公共命名(文件名 / 组件名 / `GameUI` 名 / props 接口名 / 属性项接口名)在 Task 0 呈报用户批准后方可使用。"
artifacts:
@ -34,12 +33,11 @@ must_haves:
provides: "D-26 / D-27 的独立怪物详情 UI:入参 `IEnemy | IEnemyView`;基础 / 计算后属性切换;特殊属性列表;临界表(调用系统二分法接口);左下切换 / 右下返回。"
- path: packages-user/client-modules/src/ui/book.tsx
provides: "D-23 / D-24 / D-25 的怪物手册列表 UI:绑定 `props.state.enemyContext`;按 `code` 去重;滚动列表;行内 9 项数值;点击行打开 `bookDetail.tsx` 的详情 UI;右下退出。"
- path: packages-user/client-modules/package.json
provides: "(仅当用户采纳数据系统类型来源方案 (a) 时)新增 `@user/data-system` workspace 依赖声明,供 `IEnemyView` / `IEnemyContext` / `IDamageSystem` 具名引入。"
key_links:
- "`packages-user/client-modules/src/ui/book.tsx` ↔ `props.state.enemyContext.iterateEnemy()`(`data-system/src/combat/types.ts:566`)—— 当前地图怪物枚举"
- "`packages-user/client-modules/src/ui/book.tsx` / `bookDetail.tsx` ↔ `enemyContext.getDamageSystem()`(`combat/types.ts:576`)→ `calculateCritical(view,'atk')` / `getDamageInfo(view)`(`combat/types.ts:419,445`)—— D-25 临界 / 伤害链(**内部使用 `getComputedEnemy()`**)"
- "`packages-user/client-modules/src/ui/bookDetail.tsx` ↔ `IEnemyView.getBaseEnemy()` / `getComputedEnemy()`(`combat/types.ts:55,60`)—— D-26 基础 / 计算后切换"
- "`packages-user/client-modules/src/ui/book.tsx` ↔ `props.state.enemyContext.iterateEnemy()`(`data-system/src/combat/types.ts:594`:`Iterable<[ITileLocator, IEnemyView<TEnemy>]>`,同时提供怪物与定位符)—— 当前地图怪物枚举 + locator 来源"
- "`packages-user/client-modules/src/ui/book.tsx` / `bookDetail.tsx` ↔ `enemyContext.getDamageSystem()`(`combat/types.ts:604`)→ **基础模式** `calculateEnemyCritical(base: IReadonlyEnemy, 'atk', locator)`(`combat/types.ts:472`,实现 `damage.ts:210`;`baseInfo.damage` = 伤害、`nextDiff` = 临界、`-damageDiff` = 减伤)/ **计算后模式** `calculateViewCritical(view,'atk')`(`combat/types.ts:459`,实现 `damage.ts:185`)—— D-25 临界 / 伤害链"
- "`packages-user/client-modules/src/ui/book.tsx` / `bookDetail.tsx` ↔ `IReadonlyHeroAttribute.getModifiableClone()` / `IHeroState.getIsolatedAttribute()`(`data-base/src/hero/types.ts:142,957`)+ `IHeroAttribute.add(name,value)`(`types.ts:198`)+ `IDamageSystem.with(hero)`(`combat/types.ts:525`,实现 `damage.ts:344`)—— 列表「1 防」= 当前基础伤害 −(`with(隔离属性 add('def',1)`) 下的 `calculateEnemyCritical(base,'atk',locator).baseInfo.damage`),走同一**基础属性计算路径**"
- "`packages-user/client-modules/src/ui/bookDetail.tsx` ↔ `IEnemyView.getBaseEnemy()` / `getComputedEnemy()`(`combat/types.ts:69,74`)+ `IReadonlyEnemyContext.getEnemyLocatorByView(view)`(`combat/types.ts:558`)—— D-26 基础 / 计算后切换与 locator"
- "`packages-user/client-modules/src/ui/book*.tsx` ↔ `IReadonlyEnemy`(`data-base/src/enemy/types.ts:77-115`)+ `IEnemyAttr`(`data-common/src/types.ts:6-23`)—— 名称 / 生命 / 攻击 / 防御 / 金币 / 经验"
- "`packages-user/client-modules/src/ui/bookDetail.tsx` ↔ `ISpecial.getSpecialName()/getDescription()/getNameColor()`(`data-base/src/enemy/types.ts:31-75`)—— 特殊属性列表"
- "`packages-user/client-modules/src/ui/book.tsx` ↔ `client-base/src/elements/props.ts`(`<icon icon=...>`)+ `components/scroll.tsx`(`<Scroll noscroll>`)+ `components/misc.tsx`(`<Selection>`)—— 渲染骨架"
@ -58,11 +56,12 @@ must_haves:
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
- "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
open_items:
- "**OI-1(base 模式临界 / 伤害,同时影响列表与详情)**:现有 API(`calculateCritical` / `getDamageInfo`)仅对 `getComputedEnemy()` 计算,故**列表的伤害 / 临界 / 减伤 / 1 防**与**详情基础模式的临界表**都无法不经光环地计算。请用户在 Task 0 裁定:(a) **沿用计算后数值**(列表与详情的临界 / 伤害均取计算后;详情的属性值仍可切基础 / 计算后);(b) base 模式 / 列表**不显示**临界 / 伤害 / 减伤 / 1 防;(c) 数据端新增「对任意(基础)怪物对象计算临界 / 伤害」接口(则本计划不实现数据端,等待用户提供,计划阻塞于此)。默认建议 **(a)**(成本最低、与现有 API 一致)。"
- "**OI-2(详情入参为 plain `IEnemy`)**:无 view 时无法调用临界 / 伤害。裁定:(a) 仅显示基础属性 + 特殊列表,临界 / 伤害区留空并标注`// TODO`;(b) 要求调用方一律传 `IEnemyView`。默认建议 **(a)**。"
- "**OI-3(`@user/data-system` 类型来源)**:裁定 (a) 为 `client-modules` 新增 `@user/data-system` workspace 依赖(写入 `package.json`);(b) 由用户在 `client-base` / `data-state` 侧再导出所需类型(数据端 / 系统层改动,不在本计划)。默认建议 **(a)**。"
- "**OI-1(base 模式临界 / 伤害):已闭合** —— 用户新补 `IDamageContext.calculateEnemyCritical(enemy, attribute, locator, precision?)`(`combat/types.ts:472`);列表与详情的基础模式均改走该接口,不再需要数据端 / 系统层改动。"
- "**OI-2(详情入参为 plain `IEnemy`):已闭合** —— `calculateEnemyCritical` 接受 `locator = null`,plain `IEnemy` 亦可计算临界 / 伤害 / 减伤。"
- "**OI-3(`@user/data-system` 类型来源):已闭合** —— `client-modules/package.json` 已声明该 workspace 依赖(commit 751f206),本计划**不再修改 `package.json`**。"
- "**OI-4(数字格式化)**:默认 `core.formatBigNumber`;可否接受?"
- "**OI-5(命名)**:见 Task 0 命名清单(文件名 `book.tsx` / `bookDetail.tsx`;组件 `Book` / `BookDetail`;`GameUI` 名 `'book'` / `'book-detail'`;props 接口 `BookProps` / `BookDetailProps`)。全部待批准。"
- "**OI-6(「1 防」口径)**:以**基础属性路径**计算——`getIsolatedAttribute()` 取隔离属性并 `add('def',1)`,用 `damageSystem.with(isolated).calculateEnemyCritical(base,'atk',locator).next().value.baseInfo.damage` 与当前 `baseInfo.damage` 求差;请确认此口径。"
---
<objective>
@ -70,7 +69,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
**要解决的问题(D-23..D-27):** 把 legacy `packages/legacy-ui` 的怪物手册(`src/tools/book.tsx` + `src/ui/bookDetail.vue` + `components/enemyOne.vue` + `panel/enemySpecial.vue` + `panel/enemyCritical.vue`)移植到新 UI 接口上,并按用户已定形态实现:绑定当前 `EnemyContext`、按 `code` 去重、基础数据取 `baseAttribute`、列表 9 项数值、临界走系统二分法接口、详情为独立 UI 且支持基础 / 计算后切换。**不移植**伤害曲线与标记功能。
**大致解决方案(本计划 = 09-06):** Task 0(`blocking-human`)= 汇报范围、命名清单、接口缺口与开放项(OI-1..OI-5)并等待「可以执行」;Task 1 = 实现 `bookDetail.tsx`(独立详情:基础 / 计算后属性值 + 特殊列表 + 临界表 + 左下切换 / 右下返回);Task 2 = 实现 `book.tsx`(列表:枚举去重、9 项数值、滚动、点击开详情、右下退出)**并**跑最终静态门禁(两文件符号 / props / CRLF / 范围守卫)。
**大致解决方案(本计划 = 09-06):** Task 0(`blocking-human`)= 汇报范围、命名清单、接口现状(含用户已补齐的 `calculateEnemyCritical` 说明)与开放项(OI-4..OI-6)并等待「可以执行」;Task 1 = 实现 `bookDetail.tsx`(独立详情:基础 / 计算后属性值 + 特殊列表 + 临界表 + 左下切换 / 右下返回);Task 2 = 实现 `book.tsx`(列表:枚举去重、9 项数值、滚动、点击开详情、右下退出)**并**跑最终静态门禁(两文件符号 / props / CRLF / 范围守卫)。
**为何详情先行:** 列表通过 `props.controller.open(BookDetailUI, {...})` 打开详情,需要 `BookDetailUI` 已存在;故 Task 1 先落详情、Task 2 落列表。
@ -95,14 +94,14 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · book 怪物手册(列表 + 独立详情)」范围、命名清单、接口缺口与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环(迁移后 UI 修复完成,`statusBar.tsx`/`toolbar.tsx` 等基线稳定);仓库中 `book.tsx` / `bookDetail.tsx` **尚不存在**;数据端接口(`IEnemyContext.iterateEnemy` / `IEnemyView.getBaseEnemy` / `IDamageContext.calculateCritical`)均已存在。</precondition>
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · book 怪物手册(列表 + 独立详情)」范围、命名清单、接口现状(已补齐)与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环(迁移后 UI 修复完成,`statusBar.tsx`/`toolbar.tsx` 等基线稳定);仓库中 `book.tsx` / `bookDetail.tsx` **尚不存在**;数据端接口已齐备:`IReadonlyEnemyContext.iterateEnemy`(`combat/types.ts:594`)/ `IEnemyView.getBaseEnemy`(`:69`)/ **`IDamageContext.calculateEnemyCritical`(裸怪物临界,`:472`)** / `IDamageContext.calculateViewCritical`(`:459`);且 `client-modules/package.json` **已声明** `@user/data-system` 依赖。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / D-06 / D-18 / D-21 / D-22 / D-23 / D-24 / D-25 / D-26 / D-27 / D-33 / D-37)
- .planning/phases/09-minimum-browser-runnable/09-06-PLAN.md(本计划全文,含命名清单、接口缺口与 OI-1..OI-5)
- packages-user/data-system/src/combat/types.ts:43-72(`IEnemyView`)、:363-450(`IEnemyDamageInfoBase` / `IEnemyCritical` / `IDamageContext.calculateCritical`)、:504-577(`IReadonlyEnemyContext.iterateEnemy` / `getDamageSystem`)
- packages-user/data-system/src/combat/damage.ts:72-94,184-244(`getDamageInfo` / `calculateCritical` 内部均用 `getComputedEnemy()`)
- .planning/phases/09-minimum-browser-runnable/09-06-PLAN.md(本计划全文,含命名清单、接口现状与 OI-4..OI-6)
- packages-user/data-system/src/combat/types.ts:57-86(`IEnemyView.getBaseEnemy`/`getComputedEnemy`)、:389-478(`IEnemyCritical` / `IDamageContext.calculateViewCritical` / **`calculateEnemyCritical`**)、:525(`IDamageSystem.with`)、:558-560(`getEnemyLocatorByView`)、:594(`iterateEnemy`)、:604(`getDamageSystem`)
- packages-user/data-system/src/combat/damage.ts:73-135,185-267,344-346(`getDamageInfo`/`calculateViewCritical` 内部用 `getComputedEnemy()`;**`calculateEnemyCritical` 对只读怪物对象计算**;`with` 绑定修改后 hero)
- packages-user/data-base/src/enemy/types.ts:31-115(`ISpecial` getSpecialName/getDescription/getNameColor;`IReadonlyEnemy.getAttribute` / `iterateSpecials` / `code` / `id`)
- packages-user/data-common/src/types.ts:6-23(`IEnemyAttr`:name/hp/atk/def/money/exp/point/guard)
- packages-user/data-state/src/core.ts:86-95(`ICoreState.hero` / `enemyContext`);packages-user/data-system/src/types.ts:7-14(`IStateSystem.enemyContext`)
@ -119,7 +118,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
② **锁定裁定原文要点:** D-23(绑定 `EnemyContext`、按 num 去重、`baseAttribute`、派生信息仍显示);D-24(9 项数值 + 「1 防」不用 `{ratio}防`、滚动式);D-25(临界=最小加攻 x、减伤=对应下降量、调用系统二分法接口);D-26(详情独立 UI、`IEnemy|IEnemyView`、计算后含光环、临界在计算模式用计算后属性、仅特殊列表不显光环);D-27(左下切换 / 右下返回 / 打开方式不管);D-33 / D-37(占满游戏画面 root renderer、退出按钮右下);D-03 / D-04 / D-06(执行前获批 / 命名获批 / 不扩范围)。
③ **接口依据(已核实,须用户确认):** 逐条列出 `<key_links>` 中的符号与出处;特别说明 **临界 / 伤害 API 内部一律使用 `getComputedEnemy()`**(`damage.ts:87,201`)这一事实。
③ **接口依据(已核实,须用户确认):** 逐条列出 `<key_links>` 中的符号与出处;特别说明:**基础属性临界 / 伤害 / 减伤**经用户新补的 `IDamageContext.calculateEnemyCritical(enemy: IReadonlyEnemy, 'atk', locator, precision?)`(`combat/types.ts:472`)计算(`baseInfo.damage` = 伤害、`nextDiff` = 临界、`-damageDiff` = 减伤),**计算后**属性经 `calculateViewCritical(view,'atk')`(`:459`,内部 `getComputedEnemy()`);原 `calculateCritical` 已更名为 `calculateViewCritical`;`@user/data-system` 已声明依赖(无需改 `package.json`);plain `IEnemy` 可传 `locator = null`。
④ **命名清单(须用户逐一批准,D-04):**
- 新增文件:`packages-user/client-modules/src/ui/book.tsx`、`packages-user/client-modules/src/ui/bookDetail.tsx`。
@ -129,33 +128,28 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
- 新增导出:`BookUI`、`BookDetailUI`(`new GameUI(...)`)。
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持本计划与其它同 wave 计划互不冲突)。
⑤ **接口缺口与开放项(须用户裁定):** 原样呈报 `<open_items>` OI-1..OI-5(base 模式临界 / 伤害;plain `IEnemy` 入参;`@user/data-system` 类型来源;数字格式化;命名)。特别强调 OI-1 与 OI-3 会改变任务范围(OI-1(c) 与 OI-3(b) 涉及数据端 / 系统层改动,本计划不含)。
⑤ **原接口缺口已由用户补齐(须用户确认处置):** OI-1(base 模式临界 / 伤害)→ **已闭合**:改用 `calculateEnemyCritical`;OI-2(plain `IEnemy` 入参)→ **已闭合**:可传 `locator = null`;OI-3(`@user/data-system` 类型来源)→ **已闭合**:依赖已声明,本计划**不再改 `package.json`**。仍须裁定:OI-4(数字格式化,默认 `core.formatBigNumber`)、OI-5(命名清单);另请确认「1 防」以**基础属性路径**计算(隔离属性 +1 def 经 `with().calculateEnemyCritical()` 求差)。
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
⑦ **范围与硬边界:** 仅新增 `book.tsx` / `bookDetail.tsx`(+ 视 OI-3 裁定的 `package.json` 一行依赖);**零触碰** = 09-01..09-05 产物、`src/render/**`、`packages/system`、`packages/render*`、`packages-user/data-*`、`legacy-ui`、equipbox/toolbox/shop、用户并发改动(D-56)。
⑦ **范围与硬边界:** 仅新增 `book.tsx` / `bookDetail.tsx`(**不改 `package.json`**——`@user/data-system` 依赖已由用户声明);**零触碰** = 09-01..09-05 产物、`src/render/**`、`packages/system`、`packages/render*`、`packages-user/data-*`、`legacy-ui`、equipbox/toolbox/shop、用户并发改动(D-56)。
⑧ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字;回复其它内容视为未通过。若用户给出修订意见,退出本计划、修订后重新汇报。
</action>
<decision>是否按本计划实现任务 3 · book 怪物手册(列表 `book.tsx` + 独立详情 `bookDetail.tsx`),并采纳 OI-1..OI-5 的默认建议(OI-1(a) 基础模式沿用计算后临界 / 伤害;OI-2(a) plain `IEnemy` 仅显示基础 + 特殊列表;OI-3(a) 新增 `@user/data-system` 依赖;OI-4 用 `core.formatBigNumber`;OI-5 命名清单照准)</decision>
<decision>是否按本计划实现任务 3 · book 怪物手册(列表 `book.tsx` + 独立详情 `bookDetail.tsx`),并按用户已补齐的接口处置:OI-1 用 `calculateEnemyCritical`(基础属性路径)、OI-2 plain `IEnemy` 传 `locator = null`、OI-3 不改 `package.json`(依赖已声明)、OI-4 用 `core.formatBigNumber`、OI-5 命名清单照准、OI-6「1 防」走基础属性路径</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + 接口缺口 OI-1(base 模式临界 / 伤害无 API)+ OI-3(`@user/data-system` 依赖)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + 确认改用用户新补的 `calculateEnemyCritical`(裸怪物临界)+ 确认「1 防」口径 + 确认无 `package.json` 改动。行号为规划日实测、可能漂移,门禁按符号文本匹配。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置)</name>
<pros>一次落地列表 + 详情;OI-1(a)/OI-2(a)/OI-3(a) 成本最低且不越界</pros>
<cons>OI-1(a) 下基础模式临界 = 计算后临界</cons>
<pros>一次落地列表 + 详情;直接消费用户新补的裸怪物临界接口,UI-only 不越界、不改 package.json</pros>
<cons>无</cons>
</option>
<option id="execute-no-base-critical">
<name>可以执行 + OI-1 选 (b)(基础模式不显示临界 / 伤害)</name>
<pros>语义更贴合「不考虑光环」</pros>
<cons>基础模式信息更少</cons>
</option>
<option id="execute-await-data">
<name>可以执行 + OI-1 选 (c) / OI-3 选 (b)(等待用户提供数据端 / 系统层接口)</name>
<pros>语义最完整</pros>
<cons>本计划阻塞于用户提供接口,且涉及数据端 / 系统层(超出本计划)</cons>
<name>可以执行 + 列表 / 详情基础模式不显示临界 / 伤害 / 减伤 / 1 防</name>
<pros>信息更少但更保守</pros>
<cons>与 D-23「派生信息仍显示」、D-26 基础模式临界不符</cons>
</option>
<option id="revise">
<name>修订清单 / 命名 / 范围后重新汇报</name>
@ -163,15 +157,15 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需调整 OI-1..OI-5 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<resume-signal>回复「可以执行」四个字(如需调整 OI-4 / OI-5 / OI-6 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/book.tsx'))bad.push('post-state: book.tsx already exists');if(has('packages-user/client-modules/src/ui/bookDetail.tsx'))bad.push('post-state: bookDetail.tsx already exists');const types=read('packages-user/data-system/src/combat/types.ts');if(!/iterateEnemy\(\): Iterable/.test(types))bad.push('pre-state changed: iterateEnemy signature');if(!/calculateCritical\(/.test(types))bad.push('pre-state changed: calculateCritical missing');if(!/getBaseEnemy\(\): IReadonlyEnemy/.test(types))bad.push('pre-state changed: getBaseEnemy signature');const dmg=read('packages-user/data-system/src/combat/damage.ts');if(!/view\.getComputedEnemy\(\)/.test(dmg))bad.push('pre-state changed: calculateCritical no longer uses getComputedEnemy');const en=read('packages-user/data-base/src/enemy/types.ts');if(!/getSpecialName\(\): string/.test(en))bad.push('pre-state changed: ISpecial.getSpecialName missing');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (book files absent; data interfaces present)')"</automated>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/book.tsx'))bad.push('post-state: book.tsx already exists');if(has('packages-user/client-modules/src/ui/bookDetail.tsx'))bad.push('post-state: bookDetail.tsx already exists');const types=read('packages-user/data-system/src/combat/types.ts');if(!/iterateEnemy\(\): Iterable/.test(types))bad.push('pre-state changed: iterateEnemy signature');if(!/calculateViewCritical\(/.test(types))bad.push('pre-state changed: calculateViewCritical missing');if(!/calculateEnemyCritical\(/.test(types))bad.push('pre-state changed: calculateEnemyCritical missing');if(!/getBaseEnemy\(\): IReadonlyEnemy/.test(types))bad.push('pre-state changed: getBaseEnemy signature');if(!/locator: ITileLocator \| null/.test(types))bad.push('pre-state changed: calculateEnemyCritical locator param');const dmg=read('packages-user/data-system/src/combat/damage.ts');if(!/view\.getComputedEnemy\(\)/.test(dmg))bad.push('pre-state changed: calculateViewCritical no longer uses getComputedEnemy');const en=read('packages-user/data-base/src/enemy/types.ts');if(!/getSpecialName\(\): string/.test(en))bad.push('pre-state changed: ISpecial.getSpecialName missing');const pkg=read('packages-user/client-modules/package.json');if(!/@user\/data-system/.test(pkg))bad.push('pre-state changed: @user/data-system dependency missing');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (book files absent; calculateEnemyCritical + declared dependency present)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 book 文件已存在 / 关键接口已漂移)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-1..OI-5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-4..OI-6 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-23..D-27 / D-33 / D-37 范围、接口依据(含「临界 / 伤害 API 内部用 `getComputedEnemy()`」事实)、命名清单、OI-1..OI-5、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报内容涵盖:D-23..D-27 / D-33 / D-37 范围、接口依据(含「用户新补 `calculateEnemyCritical` 计算基础属性临界、`calculateViewCritical` 计算计算后属性临界;`@user/data-system` 依赖已声明」事实)、命名清单、OI-1..OI-3 已闭合 + OI-4..OI-6、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做伤害曲线 / 标记**
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
@ -180,10 +174,10 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
<task type="auto">
<name>Task 1(详情):新建 `bookDetail.tsx` —— 独立怪物详情 UI(基础 / 计算后切换 + 特殊列表 + 临界表 + 左下切换 / 右下返回)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-1..OI-5 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/bookDetail.tsx, packages-user/client-modules/package.json</files>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-4..OI-6 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/bookDetail.tsx</files>
<read_first>
- packages-user/data-system/src/combat/types.ts:43-72(`IEnemyView`)、:363-450(`IEnemyDamageInfoBase` / `IEnemyCritical` / `IDamageContext`)
- packages-user/data-system/src/combat/types.ts:57-86(`IEnemyView`)、:389-478(`IEnemyCritical` / `IDamageContext.calculateViewCritical` / `calculateEnemyCritical`)、:558-560(`getEnemyLocatorByView`)
- packages-user/data-base/src/enemy/types.ts:31-115(`ISpecial` / `IReadonlyEnemy` / `IEnemy`)
- packages-user/data-common/src/types.ts:6-23(`IEnemyAttr`)
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)
@ -195,25 +189,25 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF;interface 后 implements)
</read_first>
<action>
(1) **新建 `bookDetail.tsx`**(新文件;仅此文件,另按 OI-3(a) 在 `package.json` 的 `dependencies` 增补 `"@user/data-system": "workspace:*"` 一行,仅当用户采纳该方案)。
(1) **新建 `bookDetail.tsx`**(新文件;仅此文件,**不改 `package.json`**——`@user/data-system` 依赖已由用户声明,commit 751f206)。
(2) **组件与导出(命名须已在 Task 0 获批):** 定义 `export interface BookDetailProps extends IUIPropsBase { enemy: IEnemy<IEnemyAttr> | IEnemyView<IEnemyAttr>; }`;`const bookDetailProps = { props: ['controller','instance','state','enemy'] } satisfies SetupComponentOptions<BookDetailProps>;`;`export const BookDetail = defineComponent<BookDetailProps>(props => { ... }, bookDetailProps);`;`export const BookDetailUI = new GameUI('book-detail', BookDetail);`。根容器用 `FULL_LOC`(占满游戏画面,D-33)。
(3) **入参判别:** 以结构判别(`'getBaseEnemy' in props.enemy` 或等价类型守卫)区分 `IEnemyView` 与 `IEnemy`。若为 `IEnemyView`:`base = view.getBaseEnemy()`、`computed = view.getComputedEnemy()`,启用基础↔计算后切换;若为 plain `IEnemy`:仅以该 `IEnemy` 作基础数据,切换按钮禁用(按 OI-2 裁定)。
(3) **入参判别:** 以结构判别(`'getBaseEnemy' in props.enemy` 或等价类型守卫)区分 `IEnemyView` 与 `IEnemy`。若为 `IEnemyView`:`base = view.getBaseEnemy()`、`computed = view.getComputedEnemy()`,启用基础↔计算后切换;若为 plain `IEnemy`:仅以该 `IEnemy` 作基础数据,切换按钮禁用,**临界 / 伤害仍可经 `calculateEnemyCritical(enemy, 'atk', null)` 计算(locator 传 `null`)**。
(4) **属性区(D-26):** 以响应式 `ref` 记录当前模式(默认「基础属性」);当前怪物对象 = 基础模式 `base` / 计算模式 `computed`;显示 `name`(`getAttribute('name')`)+ 生命 / 攻击 / 防御 / 金币 / 经验(`getAttribute('hp'|'atk'|'def'|'money'|'exp')`)。切换按钮文案随模式变化:「切换至计算后属性」/「切换至基础属性」。
(5) **特殊属性列表(D-26,仅特殊列表、不显光环):** 遍历当前怪物对象的 `iterateSpecials()`,逐条以 `getSpecialName()`(颜色取 `getNameColor()`)+ `getDescription()`(纯文本)渲染;用纵向 `<Scroll noscroll>`。
(6) **临界表(D-25 + D-26):** 经 `props.state.enemyContext.getDamageSystem()` 取 `IDamageSystem`;当持有 `IEnemyView` 时 `const it = damageSystem.calculateCritical(view, 'atk'); const first = it.next();`,展示「加攻 `first.value.nextDiff` / 减伤 `-first.value.damageDiff`」;**计算模式下(OI-1 采纳默认)的临界即为该计算后结果**;按 OI-1 裁定处理基础模式(默认 (a):基础模式沿用该计算后临界;若 (b):基础模式不显示临界区并加 `// TODO`;若 (c):等待用户提供数据端接口,本计划阻塞)。**不重复实现二分法**。当入参为 plain `IEnemy` 时按 OI-2 默认 (a) 处理(临界区留空 + `// TODO`)。
(6) **临界表(D-25 + D-26):** 经 `props.state.enemyContext.getDamageSystem()` 取 `IDamageContext`;**基础模式** → `const base = view.getBaseEnemy(); const locator = view.context.getEnemyLocatorByView(view); const it = damageSystem.calculateEnemyCritical(base, 'atk', locator);`(plain `IEnemy` 时 `locator` 传 `null`);**计算模式** → `const it = damageSystem.calculateViewCritical(view, 'atk');`(需持有 `IEnemyView`)。两路径均取 `const first = it.next();` 后展示「加攻 `first.value.nextDiff` / 减伤 `-first.value.damageDiff`」;`first.done === true`(已达上界)时不显示该区。**不重复实现二分法**。
(7) **交互(D-27):** 左下角为切换按钮((4));右下角为「返回上一级」→ `props.controller.close(props.instance)`。
(8) **规范收口:** `import` 置首、块内无空行/注释(`dev.md:55`);无 `import type`(`dev.md:54`);无 `?.`(`dev.md:107`);不改任何既有注释;CRLF;无未使用 import;不改 `files_modified` 外文件。
</action>
<reversibility rating="reversible">纯新增文件(+ 至多一行 workspace 依赖声明);删除文件 / `git revert` 可整体回退;不改任何既有文件逻辑。</reversibility>
<reversibility rating="reversible">纯新增文件(不改 `package.json`);删除文件 / `git revert` 可整体回退;不改任何既有文件逻辑。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/bookDetail.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('bookDetail.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface BookDetailProps','props interface'],['IEnemyView','IEnemyView input'],['getBaseEnemy(','base mode'],['getComputedEnemy(','computed mode'],['iterateSpecials','special list'],['getSpecialName','special name'],['getDescription','special desc'],['calculateCritical(','critical API'],['props: [','props array'],['controller','controller prop'],['instance','instance prop'],['state','state prop'],['new GameUI(','GameUI export'],['返回上一级','back button']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/BookDetailUI\s*=\s*new GameUI\(/.test(t))bad.push('BookDetailUI not exported');}if(bad.length>0){console.error('BOOKDETAIL GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK bookDetail: independent detail UI present')"</automated>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/bookDetail.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('bookDetail.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface BookDetailProps','props interface'],['IEnemyView','IEnemyView input'],['getBaseEnemy(','base mode'],['getComputedEnemy(','computed mode'],['iterateSpecials','special list'],['getSpecialName','special name'],['getDescription','special desc'],['calculateEnemyCritical(','base critical API'],['calculateViewCritical(','computed critical API'],['props: [','props array'],['controller','controller prop'],['instance','instance prop'],['state','state prop'],['new GameUI(','GameUI export'],['返回上一级','back button']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/BookDetailUI\s*=\s*new GameUI\(/.test(t))bad.push('BookDetailUI not exported');}if(bad.length>0){console.error('BOOKDETAIL GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK bookDetail: independent detail UI present')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BOOKDETAIL GATE FAILED"(文件缺失 / 关键符号缺失 / 引入 `import type` 或 `?.`)</fails_when>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/bookDetail.tsx';const raw=fs.readFileSync(p,'utf8');const bad=[];if(/(?<!\r)\n/.test(raw))bad.push('LF without CR');if(raw.includes('\r')&&!/\r\n/.test(raw))bad.push('stray CR');if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF: bookDetail.tsx')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
@ -222,7 +216,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
- `bookDetail.tsx` 存在,导出 `BookDetail` / `BookDetailUI`;`BookDetailProps` 含 `enemy: IEnemy<IEnemyAttr> | IEnemyView<IEnemyAttr>` 且 `props` 数组含 `controller,instance,state,enemy`
- 具基础 / 计算后属性切换(`getBaseEnemy` / `getComputedEnemy`),左下切换按钮文案随模式变化,右下「返回上一级」调用 `close(props.instance)`
- 特殊属性列表来自 `iterateSpecials()` + `getSpecialName()` / `getDescription()`;**不 import `book.tsx`**
- 临界表经 `calculateCritical(view,'atk')`(系统二分法接口),未重复实现;按 OI-1 裁定处理基础模式
- 临界表经系统二分法接口(基础模式 `calculateEnemyCritical(base,'atk',locator)`;计算模式 `calculateViewCritical(view,'atk')`),未重复实现
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改任何既有注释;未改 `files_modified` 外文件
</acceptance_criteria>
<done>详情 UI 完成:独立可运行、支持基础 / 计算后切换、特殊列表与临界表具备、左下切换 / 右下返回接线,静态门禁全绿</done>
@ -234,7 +228,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
<files>packages-user/client-modules/src/ui/book.tsx</files>
<read_first>
- packages-user/client-modules/src/ui/bookDetail.tsx(`BookDetailUI` / `BookDetailProps`,本文件将打开它)
- packages-user/data-system/src/combat/types.ts:504-577(`IReadonlyEnemyContext.iterateEnemy` / `getDamageSystem`)、:363-450(临界 / 伤害)
- packages-user/data-system/src/combat/types.ts:472-478(`calculateEnemyCritical`)、:459-463(`calculateViewCritical`)、:525(`with`)、:594(`iterateEnemy`)、:604(`getDamageSystem`)
- packages-user/data-base/src/enemy/types.ts:77-115(`IReadonlyEnemy.code` / `getAttribute` / `iterateSpecials`)
- packages-user/data-common/src/types.ts:6-23(`IEnemyAttr`)
- packages-user/client-modules/src/ui/viewmap.tsx:44-56,353-388,556-568(列表 / `Scroll` / `GameUI` / `open` 组合范本)
@ -250,7 +244,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
(2) **数据源 / 去重(D-23):** `const ctx = props.state.enemyContext;`;遍历 `ctx.iterateEnemy()`(`Iterable<[ITileLocator, IEnemyView<IEnemyAttr>]>`);对每个 `view` 取 `view.getBaseEnemy()`;以 `base.code`(怪物图块数字)为键**去重**,每个唯一 `code` 保留一个代表 `view` 与 `base`。整体以响应式 `computed`(或 `ref` + 必要时在 `onMounted` 构建)暴露给模板。
(3) **派生数值(D-24 + D-25):** 取 `const damage = ctx.getDamageSystem();`。对每个代表:`damage.getDamageInfo(view)` → 伤害;`damage.calculateCritical(view,'atk').next().value` → 临界 = `nextDiff`、减伤 = `-damageDiff`;「1 防」= +1 防御的减伤(按数据端可得方式计算:以 `props.state.hero.attribute.getModifiableClone()` 克隆并 `add('def',1)`,再经 `damage.with(cloneHero).getDamageInfo(view)` 得 `base - after`)。**若 OI-1 裁定为 (b),则不显示临界 / 减伤并加 `// TODO`;若 (c),阻塞等待用户接口。** 若 `getDamageSystem()` 返回 `null`(未绑定),相关数值留空并加 `// TODO`。
(3) **派生数值(D-24 + D-25,走基础属性计算路径):** 取 `const damage = ctx.getDamageSystem();`。对每个代表(同时持有 `base = view.getBaseEnemy()` 与 `locator`):`const it = damage.calculateEnemyCritical(base, 'atk', locator); const cur = it.next();` → 伤害 = `cur.value.baseInfo.damage`、临界 = `cur.value.nextDiff`、减伤 = `-cur.value.damageDiff`;**「1 防」** = `const iso = props.state.hero.getIsolatedAttribute(); iso.add('def', 1); const after = damage.with(iso).calculateEnemyCritical(base, 'atk', locator).next(); const def1 = cur.value.baseInfo.damage - after.value.baseInfo.damage;`。`cur.done === true`(已达临界上界)或 `getDamageSystem()` 返回 `null`(未绑定)时,相关数值留空并加 `// TODO`。**不重复实现二分法;不使用 `getDamageInfo(view)`(其走计算后属性,与 D-23 基础数据不一致)。**
(4) **列表渲染(D-24):** 纵向 `<Scroll noscroll loc={[...]}>` 平铺行;每行 = 图标 `<icon icon={base.code} loc={...} />` + 名称(`base.getAttribute('name')`)+ 特殊名(`base.iterateSpecials()` 的 `getSpecialName()` 拼接,超长省略)+ 生命 / 攻击 / 防御 / 金币 / 经验(`base.getAttribute('hp'|'atk'|'def'|'money'|'exp')`)+ 伤害 / 临界 / 减伤 / 「1 防」。数字格式化沿用 `core.formatBigNumber`(OI-4 默认)。**不使用 `{ratio}防`。**
@ -260,7 +254,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
</action>
<reversibility rating="reversible">纯新增文件;删除 / `git revert` 可整体回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const p='packages-user/client-modules/src/ui/book.tsx';if(!fs.existsSync(p)){bad.push('book.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface BookProps','props interface'],['props.state.enemyContext','bind enemyContext'],['iterateEnemy(','enumerate'],['getBaseEnemy(','base enemy'],['.code','dedup key code'],['calculateCritical(','critical API'],['getDamageInfo(','damage API'],['<icon','icon'],['<Scroll','scroll list'],['new GameUI(','GameUI export'],['BookDetailUI','detail import'],['controller.open(','open detail'],['controller.close(','close/exit'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(/import\s*\{[^}]*\bclient\b/.test(t)&&/from\s*'\.\.\/\.\.\/core'/.test(t))bad.push('client singleton import');if(!/BookUI\s*=\s*new GameUI\(/.test(t))bad.push('BookUI not exported');if(!/props\.state\.enemyContext/.test(t))bad.push('not bound to props.state.enemyContext');}if(bad.length>0){console.error('BOOK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK book: list UI present and bound to enemyContext')"</automated>
<automated>node -e "const fs=require('fs');const bad=[];const p='packages-user/client-modules/src/ui/book.tsx';if(!fs.existsSync(p)){bad.push('book.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface BookProps','props interface'],['props.state.enemyContext','bind enemyContext'],['iterateEnemy(','enumerate'],['getBaseEnemy(','base enemy'],['.code','dedup key code'],['calculateEnemyCritical(','base critical API'],['calculateViewCritical(','computed critical API'],['baseInfo','damage baseInfo'],['with(','scoped hero'],['<icon','icon'],['<Scroll','scroll list'],['new GameUI(','GameUI export'],['BookDetailUI','detail import'],['controller.open(','open detail'],['controller.close(','close/exit'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(/import\s*\{[^}]*\bclient\b/.test(t)&&/from\s*'\.\.\/\.\.\/core'/.test(t))bad.push('client singleton import');if(!/BookUI\s*=\s*new GameUI\(/.test(t))bad.push('BookUI not exported');if(!/props\.state\.enemyContext/.test(t))bad.push('not bound to props.state.enemyContext');}if(bad.length>0){console.error('BOOK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK book: list UI present and bound to enemyContext')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BOOK GATE FAILED"(文件缺失 / 未绑定 `enemyContext` / 关键符号缺失 / 引入 `client` 单例 / `import type` / `?.`)</fails_when>
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/ui/book.tsx','packages-user/client-modules/src/ui/bookDetail.tsx'];const bad=[];for(const f of files){if(!fs.existsSync(f)){bad.push('missing '+f);continue}const raw=fs.readFileSync(f,'utf8');if(/(?<!\r)\n/.test(raw))bad.push(f+': LF without CR');if(raw.includes('\r')&&!/\r\n/.test(raw))bad.push(f+': stray CR')}if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF: book.tsx + bookDetail.tsx')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
@ -270,7 +264,7 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
<acceptance_criteria>
- `book.tsx` 存在,导出 `Book` / `BookUI`;绑定 `props.state.enemyContext`,按 `base.code` 去重
- 行含 图标 + 名称 + 特殊名 + 生命 / 攻击 / 防御 / 金币 / 经验 / 伤害 / 临界 / 减伤 / 「1 防」;列表可滚动;未使用 `{ratio}防`
- 临界 / 减伤经 `calculateCritical(view,'atk')`,未重复实现二分法;按 OI-1 裁定处理
- 伤害 / 临界 / 减伤经 `calculateEnemyCritical(base,'atk',locator)`(基础属性路径),未重复实现二分法;「1 防」经 `with(隔离属性 add('def',1))` 求差
- 点击行 `controller.open(BookDetailUI, { enemy: view })`;右下退出 `controller.close(props.instance)`
- 无 `client` 单例、无 `import type`、无 `?.`、CRLF、无未使用 import;范围守卫无越界改动
</acceptance_criteria>
@ -285,9 +279,9 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
| Boundary | Description |
|----------|-------------|
| 数据读取边界 | 手册只读 `props.state.enemyContext` / `props.state.hero`;不向数据端写任何状态 |
| 类型来源边界 | `IEnemyView` / `IDamageSystem` 来自 `@user/data-system`;未声明依赖前不得具名引入 |
| 类型来源边界 | `IEnemyView` / `IDamageContext` 来自 `@user/data-system`;依赖已声明,按声明引入 |
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
| 范围边界 | 只新增 `book.tsx` / `bookDetail.tsx`(+ 至多一行依赖声明);不碰既有文件与其它界面 |
| 范围边界 | 只新增 `book.tsx` / `bookDetail.tsx`(不改 `package.json`);不碰既有文件与其它界面 |
| 提交边界 | 不得把用户并发改动(D-56)纳入暂存/提交 |
## STRIDE Threat Register
@ -296,28 +290,28 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-06-01 | Tampering | 借新增之机修改既有 UI / 数据端 / 渲染系统 | high | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `git status` 范围守卫 |
| T-09-06-02 | Elevation of Privilege | 未经批准即执行 / 使用未批准命名 | high | mitigate | Task 0 `blocking-human`;命名清单呈报;prohibitions 禁未获批命名 |
| T-09-06-03 | Tampering | 臆造「base 模式临界 / 伤害」接口绕过 OI-1 | medium | mitigate | 门禁禁写数据端调用;`<action>` 明列按 OI-1 裁定;禁止改数据端 |
| T-09-06-03 | Tampering | 臆造基础属性临界接口而非用已提供的 `calculateEnemyCritical` | medium | mitigate | `<action>` 明列改用 `calculateEnemyCritical`;禁止改数据端 |
| T-09-06-04 | Information Disclosure | 详情误显示光环列表(D-26 要求不显) | low | mitigate | `<action>` 明列仅 `iterateSpecials()`;acceptance 明列「不显光环」 |
| T-09-06-05 | Tampering | 引入 `client` 单例 / `import type` / `?.` / 非 CRLF | medium | mitigate | prohibitions + Task 1/2 静态门禁 + CRLF 门禁 |
| T-09-06-06 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY;未通过不得提交 |
| T-09-06-07 | Tampering | 未声明 `@user/data-system` 依赖却具名引入其类型 | medium | mitigate | 按 OI-3(a) 显式写入 `package.json`;Task 0 呈报 |
| T-09-06-07 | Tampering | 重复修改 `package.json` 或在未获批时新增命名 | medium | mitigate | 依赖已声明,本计划不改 `package.json`;命名经 Task 0 呈报 |
| T-09-06-08 | Tampering | 提交时混入用户并发改动(D-56) | medium | mitigate | 提交前 `git status` 逐条核对,只暂存本计划条目 |
| T-09-06-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装;仅新增 workspace 依赖声明(不执行 `pnpm i` 于本计划验证) |
| T-09-06-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装、无依赖安装(`@user/data-system` 依赖已存在) |
</threat_model>
<verification>
- **Task 1**:`bookDetail.tsx` 存在且导出 `BookDetail` / `BookDetailUI`;含 `getBaseEnemy`/`getComputedEnemy`/`iterateSpecials`/`getSpecialName`/`getDescription`/`calculateCritical`/`new GameUI`;props 数组含 `controller,instance,state,enemy`;无 `import type`、无 `?.`;CRLF。
- **Task 2**:`book.tsx` 存在且导出 `Book` / `BookUI`;含 `props.state.enemyContext`/`iterateEnemy(`/`getBaseEnemy(`/`.code`/`calculateCritical(`/`getDamageInfo(`/`<icon`/`<Scroll`/`BookDetailUI`/`controller.open(`/`controller.close(`;无 `client` 单例、无 `import type`、无 `?.`;两文件 CRLF。
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增。
- **Task 1**:`bookDetail.tsx` 存在且导出 `BookDetail` / `BookDetailUI`;含 `getBaseEnemy`/`getComputedEnemy`/`iterateSpecials`/`getSpecialName`/`getDescription`/`calculateEnemyCritical`/`calculateViewCritical`/`new GameUI`;props 数组含 `controller,instance,state,enemy`;无 `import type`、无 `?.`;CRLF。
- **Task 2**:`book.tsx` 存在且导出 `Book` / `BookUI`;含 `props.state.enemyContext`/`iterateEnemy(`/`getBaseEnemy(`/`.code`/`calculateEnemyCritical(`/`baseInfo`/`with(`/`<icon`/`<Scroll`/`BookDetailUI`/`controller.open(`/`controller.close(`;无 `client` 单例、无 `import type`、无 `?.`;两文件 CRLF。
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增(不含 `package.json`)。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-06-SUMMARY.md`(含 OI-1..OI-5 的最终裁定)。
- **人工复核**:结论写入 `09-06-SUMMARY.md`(含 OI-4..OI-6 的最终裁定)。
</verification>
<success_criteria>
- book 列表:绑定当前 `EnemyContext`、按 `code` 去重、基础数据取 `baseAttribute`、9 项数值(含「1 防」、不含 `{ratio}防`)、滚动、点击开详情、右下退出。
- book 详情:独立 UI、入参 `IEnemy | IEnemyView`、基础 / 计算后切换、仅特殊列表(不显光环)、临界表经系统二分法接口、左下切换 / 右下返回。
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.`。
- OI-1..OI-5 已按用户裁定落地;未采纳数据端 / 系统层改动方案时明确阻塞并记录。
- book 详情:独立 UI、入参 `IEnemy | IEnemyView`、基础 / 计算后切换、仅特殊列表(不显光环)、临界表经系统二分法接口(基础 `calculateEnemyCritical` / 计算后 `calculateViewCritical`)、左下切换 / 右下返回。
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.`;不改 `package.json`。
- OI-4..OI-6 已按用户裁定落地;接口缺口 OI-1..OI-3 已由用户补齐并入本计划。
</success_criteria>
## Artifacts this phase produces
@ -326,7 +320,6 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
|---|---|---|
| `packages-user/client-modules/src/ui/bookDetail.tsx`(新增) | 组件 / UI | 独立怪物详情 UI:`BookDetail` / `BookDetailUI` / `BookDetailProps`(命名待批准) |
| `packages-user/client-modules/src/ui/book.tsx`(新增) | 组件 / UI | 怪物手册列表 UI:`Book` / `BookUI` / `BookProps`(命名待批准) |
| `packages-user/client-modules/package.json`(改,仅 OI-3(a)) | 配置 | 新增 `@user/data-system` workspace 依赖 |
**新增导出命名(全部须 Task 0 批准):** 文件名 `book.tsx` / `bookDetail.tsx`;组件 `Book` / `BookDetail`;`GameUI` 名 `'book'` / `'book-detail'`;props 接口 `BookProps` / `BookDetailProps`;导出 `BookUI` / `BookDetailUI`。**不向 `src/ui/index.ts` 添加导出**(保持与同 wave 计划互不冲突)。
@ -339,17 +332,17 @@ Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book
| RESEARCH | 无 RESEARCH.md(本阶段为增量派发,无独立研究产物) | N/A |
| CONTEXT | D-23(绑定 `EnemyContext` / 按 num 去重 / `baseAttribute` / 派生信息仍显示) | COVERED(Task 2) |
| CONTEXT | D-24(9 项数值 / 不用 `{ratio}防` / 滚动式) | COVERED(Task 2) |
| CONTEXT | D-25(临界=最小加攻 x / 减伤=下降量 / 系统二分法接口) | COVERED(Task 2;`calculateCritical`) |
| CONTEXT | D-26(详情独立 UI / `IEnemy|IEnemyView` / 基础↔计算后 / 临界计算模式用计算后 / 仅特殊列表不显光环) | COVERED(Task 1;base 模式缺口见 OI-1) |
| CONTEXT | D-25(临界=最小加攻 x / 减伤=下降量 / 系统二分法接口) | COVERED(Task 1/2;`calculateEnemyCritical` 基础 / `calculateViewCritical` 计算后) |
| CONTEXT | D-26(详情独立 UI / `IEnemy|IEnemyView` / 基础↔计算后 / 临界计算模式用计算后 / 仅特殊列表不显光环) | COVERED(Task 1;基础模式走 `calculateEnemyCritical`) |
| CONTEXT | D-27(左下切换 / 右下返回 / 打开方式不管) | COVERED(Task 1) |
| CONTEXT | D-33 / D-37(占满游戏画面 root renderer / 退出按钮右下) | COVERED(`FULL_LOC` + 右下按钮) |
| CONTEXT | D-03 / D-04 / D-06 / D-22(执行前获批 / 命名获批 / 不扩范围 / 逐界面讨论后规划) | COVERED(Task 0 + prohibitions + 范围门禁) |
| CONTEXT | D-19(fly/settings/hotkey 排除)/ D-20(不要求视觉一致) | EXCLUDED(不涉及) |
| CONTEXT | D-18 其余界面(equipbox/toolbox/shop) | OUT OF SCOPE(各自计划 / shop 暂缓) |
| CONTEXT | Deferred Ideas | DEFERRED(未纳入任何任务) |
| 缺口 | base 模式临界 / 伤害无法经现有 API 计算 | FLAGGED(OI-1,待用户裁定) |
| 缺口 | plain `IEnemy` 入参无法计算临界 / 伤害 | FLAGGED(OI-2,待用户裁定) |
| 缺口 | `@user/data-system` 未声明依赖 | FLAGGED(OI-3,待用户裁定) |
| 缺口 | base 模式临界 / 伤害无法经现有 API 计算 | RESOLVED(用户新补 `calculateEnemyCritical`,`combat/types.ts:472`) |
| 缺口 | plain `IEnemy` 入参无法计算临界 / 伤害 | RESOLVED(`locator` 可为 `null`) |
| 缺口 | `@user/data-system` 未声明依赖 | RESOLVED(已声明,commit 751f206) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-06-SUMMARY.md` when done

View File

@ -21,17 +21,17 @@ must_haves:
- "**D-28(布局):** **左列 = 状态区域**(属性列举,**纵向滚动、不显示滚动条**,整列);**右侧区域自上而下 = 装备描述区域 → 装备槽区域(横向滚动、不显示滚动条)→ 拥有装备区域(纵向滚动、不显示滚动条)**。占满整个游戏画面(`FULL_LOC`,D-33)。参考图 `.planning/images/equipbox.png` 仅布局参考(D-28 附注)。"
- "**D-29(属性列举):** 入参提供**属性 id + 中文名列表**(`attributes: readonly IHeroAttrItem[]`,`{ id: keyof IHeroAttr; name: string }`),按该列表顺序列举;平时显示**当前属性值**(`heroState.attribute.getFinalAttribute(id)`);选中装备时,在该数值**右方(靠右对齐)**显示**增减量**(增加绿 / 减少红)。"
- "**D-30(数据源):** 装备槽来自玩家装备系统 `heroState.equip.slots`(`IHeroEquipment.slots`,`readonly string[]`),按该列表列举;**拥有装备列表来自 `heroState.items.equipment.instances()`(`IHeroEquipsStore.instances()`),按装备道具图块数字(`IEquipmentState.item.num`)升序 `sort`**(**不做筛选 / 排序 UI**)。入参除上述外,还包含**勇士状态 `HeroState`(`IHeroState<IHeroAttr>`)**。"
- "**D-31(对比规则):** 选中装备时,与「装备可装备至的**同类型第一个槽位**」对比——同类型 = `IEquipmentState.item.equip.slots` 中,首个与 `heroState.equip.slots` 名称 / 索引匹配的槽位;若该类型槽位为空则**不对比**(增减量 = 该装备自身效果)。点击**已装备**的装备仍算选中,但**仅显示「卸下该装备」导致的属性增减**。对比经系统已有 `IHeroEquipment.compareEquip(equipA, equipB, slot)`(`data-base/src/hero/types.ts:866-877`)实现(两装备均已装备时);单装备效果 / 空槽场景见 GAP-E2。"
- "**D-31(对比规则):** 选中装备时,与「装备可装备至的**同类型第一个槽位**」对比——同类型 = `IEquipmentState.item.equip.slots` 中,首个与 `heroState.equip.slots` 名称 / 索引匹配的槽位。对比经系统已有的 **`IHeroEquipment.compareEquip(equipA, equipB, slot)`(`data-base/src/hero/types.ts:877`)** 实现,且**用户已扩展其为「与空装备对比」**:`equipA` / `equipB` 填 **`-1` 表示无装备**(`types.ts:873-874`;实现 `equipment.ts:257`,空槽在 `:262-267` 处理)。故:**空槽装填** = `compareEquip(newUid, -1, slot)`(增减量即该装备自身效果);**仅卸下**(点击已装备)= `compareEquip(-1, equippedUid, slot)`;**替换对比**(同类型槽已占用)= `compareEquip(newUid, existingUid, slot)`。点击**已装备**的装备仍算选中,但**仅显示「卸下该装备」导致的属性增减**。"
- "**D-32(交互):** 选中(拥有装备或已装备)后**再次点击** = 穿上(位置与对比槽一致,即 `heroState.equip.equip(uid, slot, true)` = **autoUnload 下的 `equip`**);**拖拽拥有装备到槽** = `heroState.equip.equip(uid, slotIndex, true)` 装到指定槽;**拖拽已装备的装备** = `heroState.equip.unequip(slotIndex)` 卸下。**打开方式不在本计划范围。**"
- "**D-33 / D-37(共享约束):** 占满整个游戏画面(`FULL_LOC = [0, 0, MAIN_WIDTH, MAIN_HEIGHT]`,`../shared`),渲染于游戏画面渲染树(root renderer),**非 DOM / 非整个 window**;退出按钮默认**右下角**(`props.controller.close(props.instance)`)。所有描述 / 列表区**纵向滚动不显示滚动条**(`<Scroll noscroll>`;装备槽区横向用 `<Scroll hor noscroll>`)。"
- "**接口缺口(必须在 Task 0 获得用户裁决,不得臆造):** ①**GAP-E1 路径名不符**:D-30 原文写「`state.equipment.slots`」,但代码库中并无 `state.equipment`——装备槽位于 `heroState.equip.slots`(`IHeroEquipment`,`data-base/src/hero/types.ts:932,814`),而 `heroState.items.equipment` 是**装备实例仓库**(`IHeroEquipsStore`,`:614`)。②**GAP-E2 单装备增减量无直接 API**:`compareEquip` 需两个 `uid`;空槽装填 / 「仅卸下」的增减量需按修饰器公式(`ValueModifier.modify = value + this.value`、`PercentageModifier.modify = value + baseValue * this.value`,`data-base/src/hero/modifier.ts:52,71`)自行计算,或由用户提供单装备效果接口。③**GAP-E3 排序键**:D-30「按 id `sort()`」——`IItemRawData` 只有 `num`(图块数字)与 `name`,无字符串 id;默认按 `num` 升序(等价身份)。④**GAP-E4 拖拽**:新渲染系统无 DOM 拖拽;须以渲染端动作事件(`IActionEvent` 的 `onDown/onMove/onUp`)实现,属本计划主要实现难点。"
- "**接口现状(用户已补齐,原 GAP-E2 已闭合):** 用户扩展 `IHeroEquipment.compareEquip` 支持 **`-1` = 无装备**(`data-base/src/hero/types.ts:873-877`,实现 `equipment.ts:257-335`,空槽处理 `:262-267`),故**空槽装填 / 仅卸下**的增减量可直接调用,**无需自行按修饰器公式计算**。其余事实确认:**GAP-E1** —— 装备槽确为 `heroState.equip.slots`(`IHeroEquipment.slots: readonly string[]`,`types.ts:819`),非 `state.equipment.slots`;**GAP-E3** —— 拥有装备排序键为 `IEquipmentState.item.num`(`IItemRawData` 无字符串 id,`num` 为等价身份键);**GAP-E4** —— 新渲染系统无 DOM 拖拽,拖拽须以渲染端动作事件(`IActionEvent` 的 `onDown/onMove/onUp`)实现。"
- "**范围零外溢 + 静态门禁:** 仅新增 `equipbox.tsx`;**不修改** 09-01..09-05 任何文件、不修改任何既有 UI / `src/render/**` / `packages/system` / `packages/render*` / 数据端(`packages-user/data-*`)/ `legacy-ui`(**不实现数据端 API**,GAP-E2 若需数据端接口则阻塞并记录);验证一律为**静态源码断言**(文件存在、关键符号存在、props 数组含 `controller/instance/state/heroState/attributes`、无 `client` 单例、无 `import type`、无 `?.`、CRLF),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数作门禁。"
- "**不改既有注释、不引入未批准命名:** 新建文件内所有注释按 `dev.md:72-86` 新写;所有新增公共命名在 Task 0 呈报用户批准后方可使用。"
artifacts:
- path: packages-user/client-modules/src/ui/equipbox.tsx
provides: "D-28..D-32 的装备 UI:左列状态区域(属性 id+中文名列举 / 选中显示增减);右侧自上而下 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚);点击选中 / 再点穿上 / 拖拽装脱;右下退出;入参 `IHeroState<IHeroAttr>` + 属性列表。"
key_links:
- "`packages-user/client-modules/src/ui/equipbox.tsx` ↔ `IHeroState.equip`(`IHeroEquipment`:`slots` / `equip(uid,slot,autoUnload)` / `unequip(slot)` / `getEquipped(slot)` / `canEquipTo` / `compareEquip`)(`data-base/src/hero/types.ts:811-877`)—— D-30 / D-31 / D-32"
- "`packages-user/client-modules/src/ui/equipbox.tsx` ↔ `IHeroState.equip`(`IHeroEquipment`:`slots` / `equip(uid,slot,autoUnload)` / `unequip(slot)` / `getEquipped(slot)` / `canEquipTo` / **`compareEquip(equipA, equipB, slot)`,`-1` = 无装备**)(`data-base/src/hero/types.ts:816-882`;`compareEquip` 于 `:877`,实现 `equipment.ts:257-335`)—— D-30 / D-31 / D-32"
- "`equipbox.tsx` ↔ `IHeroState.items.equipment.instances()`(`IHeroEquipsStore`,`data-base/src/hero/types.ts:737-793`)+ `IEquipmentState`(`uid` / `item` / `getValue` / `getPercentage`,`:673-717`)—— 拥有装备列表 / 描述"
- "`equipbox.tsx` ↔ `IItemRawData`(`name` / `text` / `category` / `equip.slots|value|percentage`,`data-common/src/store/types.ts:161-193`)—— 装备描述 / 类型匹配 / 增减"
- "`equipbox.tsx` ↔ `IHeroState.attribute.getFinalAttribute(id)`(`data-base/src/hero/types.ts:117,928`)—— 状态区域当前值"
@ -39,6 +39,7 @@ must_haves:
- "`equipbox.tsx` ↔ `client-base/src/components/scroll.tsx`(`<Scroll hor noscroll>`)+ `components/misc.tsx`(`<Selection>`)+ `elements/props.ts`(`<icon>`)—— 渲染骨架"
assumptions:
- "**装备槽数据路径以代码为准(GAP-E1)**:默认使用 `heroState.equip.slots`(`IHeroEquipment`),不使用不存在的 `state.equipment.slots`;于 Task 0 呈报用户确认。"
- "**单装备增减量以 `compareEquip` 的空装备语义实现(GAP-E2 闭合)**:空槽 `compareEquip(uid, -1, slot)`、卸下 `compareEquip(-1, uid, slot)`、替换 `compareEquip(uid, existingUid, slot)`;**不再自行按修饰器公式计算**。"
- "**拥有装备排序**:默认按 `IEquipmentState.item.num` 升序(GAP-E3);若用户坚持按字符串 id,则经 `props.state.tileStore.id(num)` 取 id 再排序(`ITileStore.id`,`data-common/src/store/types.ts:112`)。"
- "**同类型第一槽位判定**:遍历 `heroState.equip.slots`(按索引),取首个满足 `equip.canEquipTo(uid, index) !== EquipStatus.CannotEquip` 的槽;`EquipStatus` 枚举见 `data-base/src/hero/types.ts:802-809`。"
- "**拖拽以渲染端动作事件实现(GAP-E4)**:元素以 `onDown/onMove/onUp` 跟踪指针,越过阈值视为拖拽,拖入目标槽时调用 `equip(uid, slotIndex, true)`;拖拽中显示浮层图标(可用 `<icon>` + `ref` 定位)。此为参考实现,具体手感按用户验收调整。"
@ -53,7 +54,7 @@ must_haves:
- "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
open_items:
- "**OI-E1(装备槽路径)**:确认使用 `heroState.equip.slots`(而非 D-30 原文的 `state.equipment.slots`)。默认采纳。"
- "**OI-E2(单装备增减量)**:裁定 (a) 自行按修饰器公式(`modifier.ts:52,71`)对 `IEquipmentState.getValue` / `getPercentage` 求增减;(b) 由用户在数据端提供「单装备效果 / 空槽对比」接口(则本计划阻塞于该接口,且涉及数据端,超出本计划)。默认建议 **(a)**。"
- "**OI-E2(单装备增减量):已闭合** —— 用户扩展 `compareEquip` 支持 `-1 = 无装备`;空槽 / 仅卸下 / 替换均直接调用该接口,不再自算修饰器,也无需数据端改动。"
- "**OI-E3(排序键)**:默认按 `item.num` 升序;可否接受?"
- "**OI-E4(拖拽范围)**:确认拖拽以渲染端动作事件实现(GAP-E4),且拖拽穿脱为本期目标(非后续)。默认采纳。"
- "**OI-E5(命名)**:见 Task 0 命名清单。全部待批准。"
@ -88,15 +89,15 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · equipbox 装备界面」范围、命名清单、接口缺口与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环;仓库中 `equipbox.tsx` **尚不存在**;数据端接口(`IHeroEquipment` / `IHeroEquipsStore` / `compareEquip`)均已存在。</precondition>
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · equipbox 装备界面」范围、命名清单、接口现状(已补齐)与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环;仓库中 `equipbox.tsx` **尚不存在**;数据端接口(`IHeroEquipment` / `IHeroEquipsStore` / `compareEquip`(**支持 `-1` = 无装备**))均已存在。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / D-06 / D-18 / D-21 / D-22 / D-28 / D-29 / D-30 / D-31 / D-32 / D-33 / D-37)
- .planning/phases/09-minimum-browser-runnable/09-07-PLAN.md(本计划全文,含命名清单、GAP-E1..E4 与 OI-E1..E5)
- packages-user/data-base/src/hero/types.ts:598-641(`IHeroItems`)、:673-717(`IEquipmentState`)、:737-793(`IHeroEquipsStore`)、:811-877(`IHeroEquipment`:`slots`/`equip`/`unequip`/`getEquipped`/`canEquipTo`/`compareEquip`)、:921-932(`IHeroState.equip` / `items` / `attribute`)
- packages-user/data-base/src/hero/types.ts:598-641(`IHeroItems`)、:673-717(`IEquipmentState`)、:737-793(`IHeroEquipsStore`)、:816-882(`IHeroEquipment`:`slots`/`equip`/`unequip`/`getEquipped`/`canEquipTo`/**`compareEquip`(`-1` = 无装备)**)、:926-937(`IHeroState.equip` / `items` / `attribute`)
- packages-user/data-common/src/store/types.ts:125-213(`ItemCategory` / `IItemEquipData` / `IItemRawData`)
- packages-user/data-base/src/hero/modifier.ts:42-77(`ValueModifier` / `PercentageModifier` 公式,GAP-E2 依据)
- packages-user/data-base/src/hero/equipment.ts:255-335(`compareEquip` 实现;空装备 `-1` 于 `:262-267` 处理)
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`);packages-user/client-modules/src/ui/statusBar.tsx:111-130,460-480(属性行 + `<icon>` 读取范本)
- packages-user/client-base/src/components/scroll.tsx:36-73,553-580(`<Scroll hor noscroll>`)、components/misc.tsx:282-370(`<Selection>`)、elements/props.ts:6-15(`<icon>`)
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
@ -111,7 +112,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
② **锁定裁定原文要点:** D-28(布局三段 + 左列状态 / 滚动无滚动条);D-29(属性 id+中文名列举、平时值、选中增减靠右、增绿减红);D-30(`equip.slots`、拥有装备按 id sort、无筛选 UI、入参含 HeroState);D-31(同类型第一槽对比 / 空则不对比 / 已装备仅显卸下增减);D-32(再点穿上 autoUnload equip / 拖拽装脱 / 打开方式不管);D-33 / D-37(占满画面 root renderer、退出右下);D-03 / D-04 / D-06(执行前获批 / 命名获批 / 不扩范围)。
③ **接口依据(已核实,须用户确认):** 逐条列出 `<key_links>`;特别说明 **GAP-E1(`state.equipment.slots` 实为 `heroState.equip.slots`)** 与 **GAP-E2(`compareEquip` 需两个 uid,单装备效果需自算)** 两个事实。
③ **接口依据(已核实,须用户确认):** 逐条列出 `<key_links>`;特别说明 **GAP-E1(`state.equipment.slots` 实为 `heroState.equip.slots`)** 与 **用户已补齐 GAP-E2:`compareEquip` 现支持 `-1` = 无装备(`data-base/src/hero/types.ts:873-877`)**,故单装备 / 空槽 / 仅卸下增减量直接调用,无需自算。
④ **命名清单(须用户逐一批准,D-04):**
- 新增文件:`packages-user/client-modules/src/ui/equipbox.tsx`。
@ -122,7 +123,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
- 新增导出:`EquipBoxUI`(`new GameUI(...)`)。
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持与同 wave 计划互不冲突)。
⑤ **接口缺口与开放项(须用户裁定):** 原样呈报 `<open_items>` OI-E1..OI-E5(装备槽路径;单装备增减量;排序键;拖拽范围;命名)。特别强调 OI-E2(b) 涉及数据端接口(超出本计划)。
⑤ **原接口缺口已由用户补齐(须用户确认处置):** OI-E1(装备槽路径)确认用 `heroState.equip.slots`;OI-E2(单装备增减量)→ **已闭合**:改用 `compareEquip(uid, -1, slot)` / `compareEquip(-1, uid, slot)`,不再自算。仍须裁定:OI-E3(排序键,默认 `item.num` 升序)、OI-E4(拖拽以渲染端事件实现且为本期目标)、OI-E5(命名清单)。
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state,heroState,attributes` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
@ -130,20 +131,15 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
⑧ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字;回复其它内容视为未通过。若用户给出修订意见,退出本计划、修订后重新汇报。
</action>
<decision>是否按本计划实现任务 3 · equipbox 装备界面,并采纳 OI-E1..OI-E5 的默认建议(OI-E1 用 `heroState.equip.slots`;OI-E2(a) 自算单装备增减;OI-E3 按 `item.num` 升序;OI-E4 以渲染端事件实现拖拽;OI-E5 命名清单照准)</decision>
<decision>是否按本计划实现任务 3 · equipbox 装备界面,并按用户已补齐的接口处置:OI-E1 用 `heroState.equip.slots`;OI-E2(已闭合)改用 `compareEquip(uid, -1, slot)` / `compareEquip(-1, uid, slot)`;OI-E3 按 `item.num` 升序;OI-E4 以渲染端事件实现拖拽;OI-E5 命名清单照准</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + GAP-E1(路径名不符)+ GAP-E2(单装备增减量无 API)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + 确认 GAP-E1 路径(`heroState.equip.slots`)+ 确认 GAP-E2 改用用户扩展的「与空装备对比」(`compareEquip(-1)`)+ 确认拖拽范围与命名。行号为规划日实测、可能漂移,门禁按符号文本匹配。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置)</name>
<pros>一次落地布局 + 交互,UI-only 不越界</pros>
<cons>GAP-E2 的单装备增减需自行按修饰器公式计算</cons>
</option>
<option id="execute-await-data">
<name>可以执行 + OI-E2 选 (b)(等待用户提供数据端单装备效果接口)</name>
<pros>语义最权威</pros>
<cons>本计划阻塞于用户提供接口,且涉及数据端(超出本计划)</cons>
<pros>一次落地布局 + 交互;增减量直接调用 `compareEquip`,UI-only 不越界</pros>
<cons>无</cons>
</option>
<option id="execute-no-drag">
<name>可以执行 + OI-E4 调整为「暂不做拖拽」(仅点击选中 / 再点穿上)</name>
@ -156,15 +152,15 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需调整 OI-E1..OI-E5 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<resume-signal>回复「可以执行」四个字(如需调整 OI-E3..OI-E5 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/equipbox.tsx'))bad.push('post-state: equipbox.tsx already exists');const t=read('packages-user/data-base/src/hero/types.ts');if(!/readonly slots: readonly string\[\]/.test(t))bad.push('pre-state changed: IHeroEquipment.slots');if(!/compareEquip\(/.test(t))bad.push('pre-state changed: compareEquip missing');if(!/equip\(\s*uid: number,\s*slot: number \| string,\s*autoUnload\?: boolean/.test(t))bad.push('pre-state changed: equip signature');if(!/unequip\(slot: number\)/.test(t))bad.push('pre-state changed: unequip signature');if(!/interface IHeroEquipsStore/.test(t))bad.push('pre-state changed: IHeroEquipsStore missing');const m=read('packages-user/data-base/src/hero/modifier.ts');if(!/value \+ baseValue \* this\.value/.test(m))bad.push('pre-state changed: PercentageModifier formula');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (equipbox absent; equipment interfaces present)')"</automated>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/equipbox.tsx'))bad.push('post-state: equipbox.tsx already exists');const t=read('packages-user/data-base/src/hero/types.ts');if(!/readonly slots: readonly string\[\]/.test(t))bad.push('pre-state changed: IHeroEquipment.slots');if(!/compareEquip\(/.test(t))bad.push('pre-state changed: compareEquip missing');if(!/-1 表示无装备/.test(t))bad.push('pre-state changed: compareEquip no longer documents -1 = no equipment');if(!/equip\(\s*uid: number,\s*slot: number \| string,\s*autoUnload\?: boolean/.test(t))bad.push('pre-state changed: equip signature');if(!/unequip\(slot: number\)/.test(t))bad.push('pre-state changed: unequip signature');if(!/interface IHeroEquipsStore/.test(t))bad.push('pre-state changed: IHeroEquipsStore missing');const e=read('packages-user/data-base/src/hero/equipment.ts');if(!/stateA && !stateB/.test(e))bad.push('pre-state changed: compareEquip empty handling');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (equipbox absent; compareEquip supports -1 = no equipment)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 equipbox.tsx 已存在 / 关键接口已漂移)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-E1..OI-E5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-E3..OI-E5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-28..D-32 / D-33 / D-37 范围、接口依据(含 GAP-E1 路径不符、GAP-E2 单装备增减无 API)、命名清单、OI-E1..OI-E5、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报内容涵盖:D-28..D-32 / D-33 / D-37 范围、接口依据(含 GAP-E1 路径 `heroState.equip.slots`、GAP-E2 已闭合「与空装备对比 `compareEquip(-1)`」)、命名清单、OI-E3..OI-E5、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做筛选 / 排序 UI**
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
@ -173,12 +169,12 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
<task type="auto">
<name>Task 1(布局 + 数据列举):新建 `equipbox.tsx` —— 左列状态区域 + 右侧 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-E1..OI-E5 另有指定,按指定执行)。</precondition>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-E3..OI-E5 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/equipbox.tsx</files>
<read_first>
- packages-user/data-base/src/hero/types.ts:673-717(`IEquipmentState`)、:737-793(`IHeroEquipsStore.instances`)、:811-877(`IHeroEquipment`)、:921-932(`IHeroState`)
- packages-user/data-common/src/store/types.ts:125-213(`ItemCategory` / `IItemEquipData` / `IItemRawData`)
- packages-user/data-base/src/hero/modifier.ts:42-77(增减量公式依据)
- packages-user/data-base/src/hero/equipment.ts:255-335(`compareEquip` 增减量实现,`-1` = 无装备)
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)
- packages-user/client-modules/src/ui/title.tsx:56-64,352-478(组件骨架 / props 数组 / 渲染树)
- packages-user/client-base/src/components/scroll.tsx:36-73,553-580(`<Scroll hor noscroll>`)、components/misc.tsx:282-370(`<Selection>`)、elements/props.ts:6-15(`<icon>`)
@ -195,7 +191,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
(4) **布局(D-28,用 `loc` 划分区域):** 左列状态区域(`[0, 0, 左列宽, MAIN_HEIGHT]`);右侧自上而下:装备描述区域 → 装备槽区域(横向 `<Scroll hor noscroll>`)→ 拥有装备区域(纵向 `<Scroll noscroll>`)→ 右下角退出按钮。
(5) **状态区域(D-29):** 按 `props.attributes` 顺序渲染每个 `{id, name}` 行;当前值 = `hero.attribute.getFinalAttribute(id)`;当有选中装备时,在该行数值右方(靠右对齐)显示增减量(>0 绿、<0 红)。增减量按 OI-E2 默认 (a) 计算:由选中装备(及对比装备)的 `getValue(id)` / `getPercentage(id)` 依 `ValueModifier` / `PercentageModifier` 公式(`modifier.ts:52,71`)求差(percentage 的相对基数取当前 `hero.attribute.getBaseAttribute(id)`)。若 OI-E2 采纳 (b),则阻塞等待用户接口。
(5) **状态区域(D-29):** 按 `props.attributes` 顺序渲染每个 `{id, name}` 行;当前值 = `hero.attribute.getFinalAttribute(id)`;当有选中装备时,在该行数值右方(靠右对齐)显示增减量(>0 绿、<0 红)。增减量**直接调用用户的「与空装备对比」接口** `hero.equip.compareEquip(a, b, slot)`:拥有装备且目标同类型槽为空 → `compareEquip(uid, -1, slot)`;槽已占用 → `compareEquip(uid, existingUid, slot)`;已装备仅卸下 → `compareEquip(-1, equippedUid, slot)`;取结果的 `[id]` 项作为该行增减量。**不再自行按修饰器公式计算。**
(6) **装备槽区域(D-30):** 横向 `<Scroll hor noscroll>` 平铺 `hero.equip.slots` 每槽;槽内 `<icon icon={已装备装备的 item.num}>` + 槽名;高亮当前选中槽。
@ -205,14 +201,14 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
</action>
<reversibility rating="reversible">纯新增文件;删除 / `git revert` 可回退;不改任何既有文件逻辑。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/equipbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('equipbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface EquipBoxProps','props interface'],['IHeroState','hero state type'],['heroState','heroState prop'],['attributes','attributes prop'],['IHeroAttrItem','attr item type'],['.equip.slots','equip slots'],['equipment.instances','owned equipment'],['getEquipped(','equipped map'],['getFinalAttribute(','current value'],['.item.num','icon num'],['.item.text','desc text'],['<Scroll','scroll'],['<icon','icon'],['new GameUI(','GameUI export'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/EquipBoxUI\s*=\s*new GameUI\(/.test(t))bad.push('EquipBoxUI not exported');if(!/props:\s*\[\s*'controller'\s*,\s*'instance'\s*,\s*'state'\s*,\s*'heroState'\s*,\s*'attributes'\s*\]/.test(t))bad.push('props array incomplete');}if(bad.length>0){console.error('EQUIPBOX LAYOUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK equipbox: layout + data enumeration present')"</automated>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/equipbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('equipbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface EquipBoxProps','props interface'],['IHeroState','hero state type'],['heroState','heroState prop'],['attributes','attributes prop'],['IHeroAttrItem','attr item type'],['.equip.slots','equip slots'],['equipment.instances','owned equipment'],['getEquipped(','equipped map'],['compareEquip(','compare API'],['getFinalAttribute(','current value'],['.item.num','icon num'],['.item.text','desc text'],['<Scroll','scroll'],['<icon','icon'],['new GameUI(','GameUI export'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/EquipBoxUI\s*=\s*new GameUI\(/.test(t))bad.push('EquipBoxUI not exported');if(!/props:\s*\[\s*'controller'\s*,\s*'instance'\s*,\s*'state'\s*,\s*'heroState'\s*,\s*'attributes'\s*\]/.test(t))bad.push('props array incomplete');}if(bad.length>0){console.error('EQUIPBOX LAYOUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK equipbox: layout + data enumeration present')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "EQUIPBOX LAYOUT GATE FAILED"(文件缺失 / 关键符号缺失 / props 数组不含 `heroState`/`attributes` / 引入 `import type` 或 `?.`)</fails_when>
<automated>node -e "const fs=require('fs');const raw=fs.readFileSync('packages-user/client-modules/src/ui/equipbox.tsx','utf8');const bad=[];if(/(?<!\r)\n/.test(raw))bad.push('LF without CR');if(raw.includes('\r')&&!/\r\n/.test(raw))bad.push('stray CR');if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF: equipbox.tsx')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
</verify>
<acceptance_criteria>
- `equipbox.tsx` 存在,导出 `EquipBox` / `EquipBoxUI`;props 数组含 `controller,instance,state,heroState,attributes`
- 左列状态区域按 `attributes` 顺序列举、显示当前值、选中时显示增减(增绿减红、靠右)
- 左列状态区域按 `attributes` 顺序列举、显示当前值、选中时经 `compareEquip`(`-1` = 空装备)显示增减(增绿减红、靠右)
- 装备槽来自 `hero.equip.slots` 且横向滚动;拥有装备来自 `equipment.instances()` 按 `item.num` 升序且纵向滚动;均为 `noscroll`
- 装备描述区显示 `item.name` / `item.text` / 可装备槽;右下退出按钮存在
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改既有注释;未改 `files_modified` 外文件
@ -227,13 +223,13 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
<read_first>
- packages-user/client-modules/src/ui/equipbox.tsx(Task 1 产物)
- packages-user/data-base/src/hero/types.ts:802-877(`EquipStatus` / `canEquipTo` / `equip(uid,slot,autoUnload)` / `unequip(slot)` / `compareEquip`)
- packages-user/data-base/src/hero/modifier.ts:42-77(单装备增减公式)
- packages-user/data-base/src/hero/equipment.ts:255-335(`compareEquip` 增减量实现)
- packages-user/client-base/src/components/scroll.tsx:363-444(`IActionEvent` 的 down/move/up 范式)
- packages/motajs(如无)→ packages/render 的 `IActionEvent`(`onDown/onMove/onUp` 事件类型)
- dev.md(无 `?.`;避免连续 `as`;CRLF)
</read_first>
<action>
(1) **点击选中(D-31):** 拥有装备项点击 → 选中该装备(若已选中同一项则进入「再点穿上」);已装备槽点击 → 选中该已装备装备(`isCol` 标记;增减量仅显示「卸下」效果)。
(1) **点击选中(D-31):** 拥有装备项点击 → 选中该装备(若已选中同一项则进入「再点穿上」);已装备槽点击 → 选中该已装备装备(`isCol` 标记;增减量**仅**显示「卸下」效果,即 `compareEquip(-1, uid, slot)`)。
(2) **再点穿上(D-32):** 选中项再次点击 → 计算目标槽 = 同类型第一个槽(`canEquipTo(uid, index) !== EquipStatus.CannotEquip`);调用 `hero.equip.equip(uid, slotIndex, true)`(**autoUnload 下的 `equip`**)。
@ -245,7 +241,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
</action>
<reversibility rating="reversible">纯新增文件内交互逻辑;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/equipbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('equipbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['.equip.equip(','equip call'],['.equip.unequip(','unequip call'],['canEquipTo(','slot match'],['EquipStatus','equip status'],['onDown','drag down'],['onMove','drag move'],['onUp','drag up'],['controller.close(','exit']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(/as\s+\w+\s+as\s+/.test(t))bad.push('chained as');}if(bad.length>0){console.error('EQUIPBOX INTERACTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK equipbox: select/equip/unequip/drag/exit present')"</automated>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/equipbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('equipbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['.equip.equip(','equip call'],['.equip.unequip(','unequip call'],['canEquipTo(','slot match'],['EquipStatus','equip status'],['compareEquip(','compare API'],['onDown','drag down'],['onMove','drag move'],['onUp','drag up'],['controller.close(','exit']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(/as\s+\w+\s+as\s+/.test(t))bad.push('chained as');}if(bad.length>0){console.error('EQUIPBOX INTERACTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK equipbox: select/equip/unequip/drag/exit present')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "EQUIPBOX INTERACTION GATE FAILED"(交互符号缺失 / 引入 `import type` / `?.` / 连续 `as`)</fails_when>
<automated>node -e "const fs=require('fs');const raw=fs.readFileSync('packages-user/client-modules/src/ui/equipbox.tsx','utf8');const bad=[];if(/(?<!\r)\n/.test(raw))bad.push('LF without CR');if(raw.includes('\r')&&!/\r\n/.test(raw))bad.push('stray CR');if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF: equipbox.tsx')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
@ -253,7 +249,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
<fails_when>输出包含 `packages/system/`、`packages/render`、`packages-user/data-`、`legacy-ui/` 或 09-01..09-05 的既有文件(范围外改动)</fails_when>
</verify>
<acceptance_criteria>
- 点击拥有装备 / 已装备可选中;已装备选中仅显示卸下增减;再点选中项经 `equip(uid, slot, true)` 穿上(autoUnload)
- 点击拥有装备 / 已装备可选中;已装备选中仅显示卸下增减(`compareEquip(-1, uid, slot)`);再点选中项经 `equip(uid, slot, true)` 穿上(autoUnload)
- 拖拽拥有装备到槽 → `equip(uid, slotIndex, true)`;拖拽已装备 → `unequip(slotIndex)`
- 右下退出调用 `controller.close(props.instance)`
- 无 `import type`、无 `?.`、无连续 `as`、CRLF、无未使用 import;范围守卫无越界改动
@ -269,7 +265,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
| Boundary | Description |
|----------|-------------|
| 数据写入边界 | 装备穿脱写 `IHeroEquipment` / `IHeroEquipsStore`;调用均经数据端既有 API,不自造状态 |
| 增减量计算边界 | 增减量需自算(GAP-E2);公式须与 `ValueModifier`/`PercentageModifier` 一致 |
| 增减量计算边界 | 增减量经 `compareEquip`(`-1` = 空装备)取得;不再自行按修饰器公式计算 |
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
| 范围边界 | 只新增 `equipbox.tsx`;不碰既有文件与其它界面 |
| 提交边界 | 不得把用户并发改动(D-56)纳入暂存/提交 |
@ -280,8 +276,8 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-07-01 | Tampering | 借新增之机修改既有 UI / 数据端 / 渲染系统 | high | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `git status` 范围守卫 |
| T-09-07-02 | Elevation of Privilege | 未经批准即执行 / 使用未批准命名 | high | mitigate | Task 0 `blocking-human`;命名清单呈报;prohibitions |
| T-09-07-03 | Tampering | 臆造数据端单装备增减接口绕过 GAP-E2 | medium | mitigate | `<action>` 明列按 OI-E2 裁定;禁改数据端;门禁禁写数据端证据 |
| T-09-07-04 | Tampering | 增减量公式与数据端不一致导致显示错误 | medium | mitigate | 引用 `modifier.ts` 公式;acceptance 明列一致性 |
| T-09-07-03 | Tampering | 绕开用户新补的 `compareEquip(-1)` 而自行臆造增减量接口 | medium | mitigate | `<action>` 明列改用 `compareEquip`;禁改数据端 |
| T-09-07-04 | Tampering | 增减量错误(未用 `compareEquip` 而自算修饰器致与数据端不一致) | medium | mitigate | `<action>` 明列增减量取自 `compareEquip` 结果;acceptance 明列 |
| T-09-07-05 | Tampering | 引入 `client` 单例 / `import type` / `?.` / 连续 `as` / 非 CRLF | medium | mitigate | prohibitions + Task 1/2 静态门禁 + CRLF 门禁 |
| T-09-07-06 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
| T-09-07-07 | Tampering | 拖拽以 DOM API 实现(新渲染系统无 DOM) | low | mitigate | `<action>` 明列用渲染端动作事件;Task 2 门禁断言 `onDown/onMove/onUp` |
@ -290,11 +286,11 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
</threat_model>
<verification>
- **Task 1**:`equipbox.tsx` 存在且导出 `EquipBox` / `EquipBoxUI`;含 `.equip.slots` / `equipment.instances` / `getEquipped(` / `getFinalAttribute(` / `.item.text` / `<Scroll` / `<icon`;props 数组含 `controller,instance,state,heroState,attributes`;无 `import type`、无 `?.`;CRLF。
- **Task 2**:含 `.equip.equip(` / `.equip.unequip(` / `canEquipTo(` / `EquipStatus` / `onDown` / `onMove` / `onUp` / `controller.close(`;无 `import type` / `?.` / 连续 `as`;CRLF。
- **Task 1**:`equipbox.tsx` 存在且导出 `EquipBox` / `EquipBoxUI`;含 `.equip.slots` / `equipment.instances` / `getEquipped(` / `compareEquip(` / `getFinalAttribute(` / `.item.text` / `<Scroll` / `<icon`;props 数组含 `controller,instance,state,heroState,attributes`;无 `import type`、无 `?.`;CRLF。
- **Task 2**:含 `.equip.equip(` / `.equip.unequip(` / `canEquipTo(` / `EquipStatus` / `compareEquip(` / `onDown` / `onMove` / `onUp` / `controller.close(`;无 `import type` / `?.` / 连续 `as`;CRLF。
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-07-SUMMARY.md`(含 OI-E1..OI-E5 的最终裁定)。
- **人工复核**:结论写入 `09-07-SUMMARY.md`(含 OI-E3..OI-E5 的最终裁定)。
</verification>
<success_criteria>
@ -302,7 +298,7 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
- 数据:装备槽 `hero.equip.slots`;拥有装备 `instances()` 按 `item.num` 升序;无筛选 / 排序 UI;入参含 `IHeroState<IHeroAttr>` + 属性列表。
- 交互:点击选中、再点穿上(autoUnload `equip`)、拖拽装脱、右下退出。
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.` / 连续 `as`。
- OI-E1..OI-E5 已按用户裁定落地;未采纳数据端方案时明确阻塞并记录。
- OI-E3..OI-E5 已按用户裁定落地;接口缺口 OI-E2 已由用户补齐并入本计划。
</success_criteria>
## Artifacts this phase produces
@ -322,16 +318,16 @@ Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
| RESEARCH | 无 RESEARCH.md | N/A |
| CONTEXT | D-28(布局三段 / 左列状态 / 滚动无滚动条) | COVERED(Task 1) |
| CONTEXT | D-29(属性 id+中文名列举 / 当前值 / 选中增减靠右增绿减红) | COVERED(Task 1) |
| CONTEXT | D-30(`equip.slots` / 拥有装备按 id sort / 无筛选 UI / 入参含 HeroState) | COVERED(Task 1;路径名不符见 OI-E1) |
| CONTEXT | D-31(同类型第一槽对比 / 空则不对比 / 已装备仅显卸下增减) | COVERED(Task 2;单装备增减缺口见 OI-E2) |
| CONTEXT | D-30(`equip.slots` / 拥有装备按 id sort / 无筛选 UI / 入参含 HeroState) | COVERED(Task 1;路径以 `heroState.equip.slots` 为准) |
| CONTEXT | D-31(同类型第一槽对比 / 空则不对比 / 已装备仅显卸下增减) | COVERED(Task 1/2;经 `compareEquip(uid,-1,slot)` / `compareEquip(-1,uid,slot)`) |
| CONTEXT | D-32(再点穿上 autoUnload equip / 拖拽装脱 / 打开方式不管) | COVERED(Task 2) |
| CONTEXT | D-33 / D-37(占满游戏画面 root renderer / 退出按钮右下) | COVERED(`FULL_LOC` + 右下按钮) |
| CONTEXT | D-03 / D-04 / D-06 / D-22(执行前获批 / 命名获批 / 不扩范围 / 逐界面讨论后规划) | COVERED(Task 0 + prohibitions + 范围门禁) |
| CONTEXT | D-18 其余界面(book/toolbox/shop) | OUT OF SCOPE(各自计划 / shop 暂缓) |
| 缺口 | D-30 原文 `state.equipment.slots` 与代码不符 | FLAGGED(OI-E1,待用户裁定) |
| 缺口 | 单装备增减量无直接 API | FLAGGED(OI-E2,待用户裁定) |
| 缺口 | 拥有装备「按 id sort」键为 `num` | FLAGGED(OI-E3,待用户裁定) |
| 缺口 | 拖拽须以渲染端事件实现 | FLAGGED(OI-E4,待用户裁定) |
| 缺口 | D-30 原文 `state.equipment.slots` 与代码不符 | CONFIRMED(用 `heroState.equip.slots`,OI-E1) |
| 缺口 | 单装备增减量无直接 API | RESOLVED(用户扩展 `compareEquip` 支持 `-1` = 无装备) |
| 缺口 | 拥有装备「按 id sort」键为 `num` | CONFIRMED(OI-E3,用 `item.num`) |
| 缺口 | 拖拽须以渲染端事件实现 | CONFIRMED(OI-E4) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-07-SUMMARY.md` when done

View File

@ -23,7 +23,7 @@ must_haves:
- "**D-36(交互 / 入参):** **单击选中、再次点击使用**(使用时调用 `heroState.items.useItem(item)`);**无丢弃、无拖拽**功能。入参为 **`HeroState`(`IHeroState<IHeroAttr>`)**。"
- "**D-33 / D-37(共享约束):** 占满整个游戏画面(`FULL_LOC = [0, 0, MAIN_WIDTH, MAIN_HEIGHT]`,`../shared`),渲染于游戏画面渲染树(root renderer),**非 DOM / 非整个 window**;退出按钮默认**右下角**(`props.controller.close(props.instance)`)。三个列表 / 描述区**纵向滚动不显示滚动条**(`<Scroll noscroll>`)。"
- "**道具隐藏:** 以 `IItemRawData.hideInToolbox`(`data-common/src/store/types.ts:187`)过滤不在道具栏显示的道具(现代等价于 legacy `getItems` 的 `ignore = ['book']` 硬编码);仅展示 `hideInToolbox === false` 的道具。"
- "**接口缺口(必须在 Task 0 获得用户裁决,不得臆造):** **GAP-T1 —— `IHeroItems` 无「枚举已拥有道具」的公开方法**(`data-base/src/hero/types.ts:611-641` 仅有 `getItemState` / `itemCount` / `addItem` / `useItem` / `equipment`,内部 `items` 为私有 `Map`)。UI 无法直接遍历背包。可选处置:(a) 以 `heroState.items.saveState(SaveCompression.NoCompression).items`(返回 `readonly IHeroItemSave[]`,含 `{num,count}`)作为枚举来源,再对每个 `num` 调 `getItemState(num)` 取完整状态——**无数据端改动,但语义上借用存档方法**;(b) 由用户在数据端新增枚举 API(`IHeroItems` 增方法)——则本计划**阻塞**于该接口,且涉及数据端(超出本计划);(c) 由调用方以额外 prop 传入道具列表(**与 D-36「入参为 HeroState」不符**)。"
- "**接口现状(用户已补齐,原 GAP-T1 已闭合):** 用户为 `IHeroItems` 新增 **`iterateItems(): Iterable<[num: number, item: IHeroItemState<THero>]>`(`data-base/src/hero/types.ts:632`,实现 `items.ts:46-48`)**,返回内部 `Map` 的迭代器——UI 直接枚举背包,**不再借用 `saveState()`**、无需数据端进一步改动,且入参仍为 `IHeroState<IHeroAttr>`(与 D-36 一致)。"
- "**范围零外溢 + 静态门禁:** 仅新增 `toolbox.tsx`;**不修改** 09-01..09-05 任何文件、不修改任何既有 UI / `src/render/**` / `packages/system` / `packages/render*` / 数据端(`packages-user/data-*`)/ `legacy-ui`(**不实现数据端 API**);验证一律为**静态源码断言**(文件存在、关键符号存在、props 数组含 `controller/instance/state/heroState`、无 `client` 单例、无 `import type`、无 `?.`、CRLF),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数作门禁。"
- "**不改既有注释、不引入未批准命名:** 新建文件内所有注释按 `dev.md:72-86` 新写;所有新增公共命名在 Task 0 呈报用户批准后方可使用。"
artifacts:
@ -31,13 +31,13 @@ must_haves:
provides: "D-34 / D-35 / D-36 的道具栏 UI:三列(永久 / 可消耗道具列表 + 名称 / 说明);按数量降序、同数量按 id 升序;单击选中、再点使用;无丢弃 / 拖拽;右下退出;入参 `IHeroState<IHeroAttr>`。"
key_links:
- "`packages-user/client-modules/src/ui/toolbox.tsx` ↔ `IHeroState.items`(`IHeroItems`,`data-base/src/hero/types.ts:611-641`)—— 道具来源"
- "`toolbox.tsx` ↔ `IHeroItems.saveState(SaveCompression.NoCompression).items`(`ISaveableContent`,`data-common/src/save/types.ts:10-15`)—— GAP-T1 处置 (a) 的枚举来源(`IHeroItemSave {num,count}`)"
- "`toolbox.tsx` ↔ `IHeroItems.iterateItems(): Iterable<[num: number, item: IHeroItemState<THero>]>`(`data-base/src/hero/types.ts:632`,实现 `items.ts:46-48`)—— 拥有道具枚举来源(取代旧的 `saveState()` 借用)"
- "`toolbox.tsx` ↔ `IHeroItems.getItemState(num)` → `IHeroItemState`(`id` / `num` / `raw` / `consumable` / `count`,`data-base/src/hero/types.ts:598-609`)—— 名称 / 分类 / 数量"
- "`toolbox.tsx` ↔ `IItemRawData`(`name` / `text` / `category` / `hideInToolbox`,`data-common/src/store/types.ts:176-193`)+ `ItemCategory`(`:125-136`)—— 永久 / 可消耗 / 隐藏 / 描述"
- "`toolbox.tsx` ↔ `IHeroItems.useItem(item)`(`data-base/src/hero/types.ts:634`)—— D-36 使用"
- "`toolbox.tsx` ↔ `client-base/src/components/scroll.tsx`(`<Scroll noscroll>`)+ `components/misc.tsx`(`<Selection>`)+ `elements/props.ts`(`<icon>`)—— 渲染骨架"
assumptions:
- "**枚举来源默认 GAP-T1 处置 (a)(借用 `saveState`)**:以 `heroState.items.saveState(SaveCompression.NoCompression).items` 得到 `{num,count}[]`,再逐项 `getItemState(num)`。此法不改数据端、不越界;于 Task 0 呈报用户确认(并说明其为借用存档方法)。"
- "**枚举来源:用户新增的 `heroState.items.iterateItems()`(GAP-T1 闭合)**:直接遍历得到 `[num, IHeroItemState][]`;**不再借用 `saveState`**。"
- "**「永久 / 可消耗」判定**:`IHeroItemState.consumable`(`raw.category === ItemCategory.Consumable`)为「可消耗」;`false` 为「永久」。"
- "**「id 排序」等价身**:`IHeroItemState.id` 为字符串 id(`hero.items` 存表时由 `tileStore.id(raw.num)` 得到);排序键用 `num`(数值,稳定且与 id 一一对应)。"
- "**道具数量为 0 时不出现**:`HeroItems` 在 `count <= 0` 时从内部 Map 删除(`items.ts:72-81,105-110`),故枚举天然不含 0 数量道具。"
@ -51,7 +51,7 @@ must_haves:
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
- "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
open_items:
- "**OI-T1(枚举来源,GAP-T1)**:裁定 (a) 借用 `saveState().items`(默认,UI-only);(b) 用户提供数据端枚举 API(本计划阻塞,涉及数据端);(c) 额外 prop 传列表。默认建议 **(a)**。"
- "**OI-T1(枚举来源,GAP-T1):已闭合** —— 用户新增 `IHeroItems.iterateItems()`(`data-base/src/hero/types.ts:632`);本计划直接消费,UI-only。"
- "**OI-T2(使用行为的持有 / 关闭)**:legacy 使用道具前会 `holdOn()` 并关闭自身,使用后按需结束。新系统是否有等价「维持背景」诉求?默认**仅调用 `heroState.items.useItem(item)`,不改打开 / 关闭流程**(打开方式不管)。"
- "**OI-T3(不可用时提示)**:legacy 有「当前无法使用该道具」提示。默认不做提示(仅调用 `useItem`,返回 `false` 时不消费)——如需提示,须用户提供提示组件约定。"
- "**OI-T4(数字格式化)**:数量格式化默认 `core.formatBigNumber`;可否接受?"
@ -63,7 +63,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
**要解决的问题(D-34..D-36):** 把 legacy `packages/legacy-ui` 的道具栏(`src/tools/toolbox.tsx` + `src/ui/toolbox.vue`)移植到新 UI 接口上,并按用户已定形态实现:三列(左 永久道具 / 中 可消耗道具 / 右 名称 + 说明);各自按数量降序、同数量按 id 升序;单击选中、再点使用;无丢弃 / 拖拽;入参 `HeroState`。
**大致解决方案(本计划 = 09-08):** Task 0(`blocking-human`)= 汇报范围、命名清单、接口缺口(GAP-T1:`IHeroItems` 无枚举)与开放项(OI-T1..OI-T5)并等待「可以执行」;Task 1 = 实现 `toolbox.tsx` 的**布局与列表**(三列、过滤隐藏、分类排序、名称 / 说明区);Task 2 = 实现**交互**(单击选中 / 再点使用 / 右下退出)**并**跑最终静态门禁。
**大致解决方案(本计划 = 09-08):** Task 0(`blocking-human`)= 汇报范围、命名清单、接口现状(含用户已补齐的 `IHeroItems.iterateItems()`)与开放项(OI-T2..OI-T5)并等待「可以执行」;Task 1 = 实现 `toolbox.tsx` 的**布局与列表**(三列、过滤隐藏、分类排序、名称 / 说明区);Task 2 = 实现**交互**(单击选中 / 再点使用 / 右下退出)**并**跑最终静态门禁。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为渲染端 UI 移植,数据来自数据端接口(`IHeroState`),不涉及任何外部 API。`**
</objective>
@ -87,16 +87,15 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · toolbox 道具栏」范围、命名清单、接口缺口与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环;仓库中 `toolbox.tsx` **尚不存在**;数据端接口(`IHeroItems` / `IHeroItemState` / `IItemRawData`)均已存在。</precondition>
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · toolbox 道具栏」范围、命名清单、接口现状(已补齐)与开放项,等待用户回复「可以执行」</name>
<precondition>`09-05` 已闭环;仓库中 `toolbox.tsx` **尚不存在**;数据端接口(`IHeroItems`(**含 `iterateItems`**)/ `IHeroItemState` / `IItemRawData`)均已存在。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / D-06 / D-18 / D-21 / D-22 / D-34 / D-35 / D-36 / D-33 / D-37)
- .planning/phases/09-minimum-browser-runnable/09-08-PLAN.md(本计划全文,含命名清单、GAP-T1 与 OI-T1..OI-T5)
- packages-user/data-base/src/hero/types.ts:584-641(`IHeroItemSave` / `IHeroItemsSave` / `IHeroItemState` / `IHeroItems`)、:921-932(`IHeroState.items`)
- packages-user/data-base/src/hero/items.ts:18-113,157-171(内部 `Map` / `saveState` / `useItem` / 数量 0 删除)
- .planning/phases/09-minimum-browser-runnable/09-08-PLAN.md(本计划全文,含命名清单、接口现状与 OI-T2..OI-T5)
- packages-user/data-base/src/hero/types.ts:584-646(`IHeroItemSave` / `IHeroItemsSave` / `IHeroItemState` / `IHeroItems`(**含 `iterateItems(): Iterable<[num, IHeroItemState]>`,:632**))、:926-937(`IHeroState.items`)
- packages-user/data-base/src/hero/items.ts:18-113(内部 `Map` / **`iterateItems`(:46-48)** / `useItem` / 数量 0 删除)
- packages-user/data-common/src/store/types.ts:125-213(`ItemCategory` / `IItemRawData.hideInToolbox` / `IItemStore`)
- packages-user/data-common/src/save/types.ts:1-23(`SaveCompression` / `ISaveableContent.saveState`)
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`);packages-user/client-modules/src/ui/statusBar.tsx:111-130,460-480(列表 + `<icon>` 范本)
- packages-user/client-base/src/components/scroll.tsx:36-73,553-580(`<Scroll noscroll>`)、components/misc.tsx:282-370(`<Selection>`)、elements/props.ts:6-15(`<icon>`)
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
@ -111,7 +110,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
② **锁定裁定原文要点:** D-34(三列布局);D-35(永久 / 可消耗、数量降序、同数量按 id 升序);D-36(单击选中 / 再点使用 / 无丢弃无拖拽 / 入参 HeroState);D-33 / D-37(占满画面 root renderer、退出右下);D-03 / D-04 / D-06(执行前获批 / 命名获批 / 不扩范围)。
③ **接口依据与缺口(已核实,须用户确认):** 逐条列出 `<key_links>`;**重点说明 GAP-T1**:`IHeroItems` **无枚举已拥有道具的公开方法**(`data-base/src/hero/types.ts:611-641` 仅 `getItemState` / `itemCount` / `addItem` / `useItem` / `equipment`),并给出 OI-T1 的 (a)/(b)/(c) 三条路径及各自代价。
③ **接口依据(已核实,须用户确认):** 逐条列出 `<key_links>`;**重点说明 GAP-T1 已闭合**:用户为 `IHeroItems` 新增 `iterateItems(): Iterable<[num, IHeroItemState]>`(`data-base/src/hero/types.ts:632`,实现 `items.ts:46-48`),本计划**直接用它枚举背包**,不再借用 `saveState`、无需额外 prop,入参仍为 `IHeroState<IHeroAttr>`。
④ **命名清单(须用户逐一批准,D-04):**
- 新增文件:`packages-user/client-modules/src/ui/toolbox.tsx`。
@ -122,7 +121,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
- (道具行类型默认**复用** `IHeroItemState`,不新增行类型命名。)
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持与同 wave 计划互不冲突)。
⑤ **接口缺口与开放项(须用户裁定):** 原样呈报 `<open_items>` OI-T1..OI-T5。特别强调 OI-T1(b) 涉及数据端接口(超出本计划)。
⑤ **原接口缺口已由用户补齐(须用户确认处置):** OI-T1(枚举来源,GAP-T1)→ **已闭合**:改用 `hero.items.iterateItems()`,不再借用 `saveState`、无需额外 prop。仍须裁定:OI-T2(使用行为不改打开 / 关闭流程)、OI-T3(不做不可用提示)、OI-T4(数量格式化默认 `core.formatBigNumber`)、OI-T5(命名清单)。
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state,heroState` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
@ -130,25 +129,20 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
⑧ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字;回复其它内容视为未通过。若用户给出修订意见,退出本计划、修订后重新汇报。
</action>
<decision>是否按本计划实现任务 3 · toolbox 道具栏,并采纳 OI-T1..OI-T5 的默认建议(OI-T1(a) 借用 `saveState().items` 枚举;OI-T2 仅调用 `useItem` 不改打开流程;OI-T3 不做不可用提示;OI-T4 用 `core.formatBigNumber`;OI-T5 命名清单照准)</decision>
<decision>是否按本计划实现任务 3 · toolbox 道具栏,并按用户已补齐的接口处置:OI-T1(已闭合)改用 `hero.items.iterateItems()` 枚举;OI-T2 仅调用 `useItem` 不改打开流程;OI-T3 不做不可用提示;OI-T4 用 `core.formatBigNumber`;OI-T5 命名清单照准</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + GAP-T1(`IHeroItems` 无枚举)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + 确认改用用户新补的 `IHeroItems.iterateItems()` 枚举(GAP-T1 闭合)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置)</name>
<pros>一次落地布局 + 交互,UI-only 不越界</pros>
<cons>OI-T1(a) 借用 `saveState` 作枚举来源(语义非「读」)</cons>
</option>
<option id="execute-await-data">
<name>可以执行 + OI-T1 选 (b)(等待用户提供数据端 `IHeroItems` 枚举 API)</name>
<pros>语义最正规</pros>
<cons>本计划阻塞于用户提供接口,且涉及数据端(超出本计划)</cons>
<pros>一次落地布局 + 交互;直接消费 `iterateItems()`,UI-only 不越界</pros>
<cons>无</cons>
</option>
<option id="execute-extra-prop">
<name>可以执行 + OI-T1 选 (c)(以额外 prop 传道具列表)</name>
<pros>不借存档方法</pros>
<cons>与 D-36「入参为 HeroState」不符,须用户确认调整入参</cons>
<pros>不改数据端</pros>
<cons>与 D-36「入参为 HeroState」不符,且无必要(`iterateItems` 已提供枚举)</cons>
</option>
<option id="revise">
<name>修订清单 / 命名 / 范围后重新汇报</name>
@ -156,15 +150,15 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需调整 OI-T1..OI-T5 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<resume-signal>回复「可以执行」四个字(如需调整 OI-T2..OI-T5 的处置或命名方案,请在回复中指明;修订意见请按 action ⑧ 处理)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/toolbox.tsx'))bad.push('post-state: toolbox.tsx already exists');const t=read('packages-user/data-base/src/hero/types.ts');if(!/interface IHeroItems<THero>/.test(t))bad.push('pre-state changed: IHeroItems missing');if(!/getItemState\(item: number \| string\)/.test(t))bad.push('pre-state changed: getItemState signature');if(!/interface IHeroItemState<THero>/.test(t))bad.push('pre-state changed: IHeroItemState missing');if(/iterateItems|iterItems\(|items\(\)/.test(t))bad.push('pre-state changed: enumeration API now exists (revisit GAP-T1)');const s=read('packages-user/data-common/src/store/types.ts');if(!/hideInToolbox: boolean/.test(s))bad.push('pre-state changed: hideInToolbox missing');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (toolbox absent; IHeroItems has no enumeration API)')"</automated>
<automated>node -e "const fs=require('fs');const has=p=>fs.existsSync(p);const read=p=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(has('packages-user/client-modules/src/ui/toolbox.tsx'))bad.push('post-state: toolbox.tsx already exists');const t=read('packages-user/data-base/src/hero/types.ts');if(!/interface IHeroItems<THero>/.test(t))bad.push('pre-state changed: IHeroItems missing');if(!/getItemState\(item: number \| string\)/.test(t))bad.push('pre-state changed: getItemState signature');if(!/interface IHeroItemState<THero>/.test(t))bad.push('pre-state changed: IHeroItemState missing');if(!/iterateItems\(\): Iterable/.test(t))bad.push('pre-state changed: iterateItems enumeration API missing');const s=read('packages-user/data-common/src/store/types.ts');if(!/hideInToolbox: boolean/.test(s))bad.push('pre-state changed: hideInToolbox missing');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (toolbox absent; IHeroItems.iterateItems present)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 toolbox.tsx 已存在 / 枚举 API 已新增需重议 GAP-T1)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-T1..OI-T5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-T2..OI-T5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-34..D-36 / D-33 / D-37 范围、接口依据(含 GAP-T1 `IHeroItems` 无枚举)、命名清单、OI-T1..OI-T5、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报内容涵盖:D-34..D-36 / D-33 / D-37 范围、接口依据(含用户新补 `IHeroItems.iterateItems()` 枚举)、命名清单、OI-T2..OI-T5、验证方式(静态断言,无全仓门禁)、范围与硬边界
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做丢弃 / 拖拽**
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
@ -173,13 +167,13 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
<task type="auto">
<name>Task 1(布局 + 列表):新建 `toolbox.tsx` —— 三列(永久 / 可消耗道具列表 + 名称 / 说明)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-T1..OI-T5 另有指定,按指定执行)。</precondition>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-T2..OI-T5 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/toolbox.tsx</files>
<read_first>
- packages-user/data-base/src/hero/types.ts:584-641(`IHeroItemSave` / `IHeroItemState` / `IHeroItems`)
- packages-user/data-base/src/hero/items.ts:40-113(`getItemState` / `useItem` / 数量 0 删除)
- packages-user/data-common/src/store/types.ts:125-213(`ItemCategory` / `IItemRawData` / `hideInToolbox`)
- packages-user/data-common/src/save/types.ts:1-23(`SaveCompression`)
- packages-user/data-base/src/hero/items.ts:18-113(`iterateItems`(:46-48)/ `useItem` / 数量 0 删除)
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)
- packages-user/client-modules/src/ui/title.tsx:56-64,352-478(组件骨架 / props 数组 / 渲染树)
- packages-user/client-base/src/components/scroll.tsx:36-73,553-580、components/misc.tsx:282-370、elements/props.ts:6-15
@ -190,7 +184,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
<action>
(1) **新建 `toolbox.tsx`(仅此文件)。** 命名须已在 Task 0 获批:`export interface ToolBoxProps extends IUIPropsBase { heroState: IHeroState<IHeroAttr>; }`;`const toolBoxProps = { props: ['controller','instance','state','heroState'] } satisfies SetupComponentOptions<ToolBoxProps>;`;`export const ToolBox = defineComponent<ToolBoxProps>(props => { ... }, toolBoxProps);`;`export const ToolBoxUI = new GameUI('toolbox', ToolBox);`。根容器 `FULL_LOC`(D-33)。
(2) **道具枚举(GAP-T1 / OI-T1 默认 (a)):** `const hero = props.heroState;`;以 `hero.items.saveState(SaveCompression.NoCompression).items`(`readonly IHeroItemSave[]`,`{num,count}`)取全部拥有道具;对每项 `hero.items.getItemState(num)` 取 `IHeroItemState`(`id` / `raw` / `consumable` / `count`);过滤 `raw.hideInToolbox === true` 的道具。以响应式 `computed` 暴露「永久道具」(`consumable === false`)与「可消耗道具」(`consumable === true`)两组。
(2) **道具枚举(GAP-T1 闭合 / OI-T1):** `const hero = props.heroState;`;以 **`hero.items.iterateItems()`**(`Iterable<[num, item: IHeroItemState]>`,`data-base/src/hero/types.ts:632`,实现 `items.ts:46-48`)取全部拥有道具;`IHeroItemState` 含 `id` / `num` / `raw` / `consumable` / `count`;过滤 `raw.hideInToolbox === true` 的道具。以响应式 `computed` 暴露「永久道具」(`consumable === false`)与「可消耗道具」(`consumable === true`)两组。**不再借用 `saveState()`;也不再需要逐 `num` 调 `getItemState`(`iterateItems` 已直接给出状态)。**
(3) **排序(D-35):** 各组内按 `count` **从多到少**排序;`count` 相同时按 id **升序**(键取 `num`)。排序不得就地修改原数组(先 `slice()` 再 `sort`)。
@ -202,7 +196,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
</action>
<reversibility rating="reversible">纯新增文件;删除 / `git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/toolbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('toolbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface ToolBoxProps','props interface'],['IHeroState','hero state type'],['heroState','heroState prop'],['saveState(','enumerate via saveState'],['SaveCompression','save compression'],['.items.getItemState(','item state'],['consumable','consumable flag'],['hideInToolbox','hide filter'],['.raw.text','description text'],['.raw.name','item name'],['<Scroll','scroll'],['<icon','icon'],['new GameUI(','GameUI export'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/ToolBoxUI\s*=\s*new GameUI\(/.test(t))bad.push('ToolBoxUI not exported');if(!/props:\s*\[\s*'controller'\s*,\s*'instance'\s*,\s*'state'\s*,\s*'heroState'\s*\]/.test(t))bad.push('props array incomplete');}if(bad.length>0){console.error('TOOLBOX LAYOUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK toolbox: 3-column layout + item enumeration present')"</automated>
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/toolbox.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('toolbox.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface ToolBoxProps','props interface'],['IHeroState','hero state type'],['heroState','heroState prop'],['.items.iterateItems(','enumerate via iterateItems'],['consumable','consumable flag'],['hideInToolbox','hide filter'],['.raw.text','description text'],['.raw.name','item name'],['<Scroll','scroll'],['<icon','icon'],['new GameUI(','GameUI export'],['props: [','props array']];for(const [s,label] of need){if(t.indexOf(s)<0)bad.push('missing '+label+': '+s)}if(/import type/.test(t))bad.push('has import type');if(/\?\.[a-zA-Z]/.test(t))bad.push('has optional chaining');if(!/ToolBoxUI\s*=\s*new GameUI\(/.test(t))bad.push('ToolBoxUI not exported');if(!/props:\s*\[\s*'controller'\s*,\s*'instance'\s*,\s*'state'\s*,\s*'heroState'\s*\]/.test(t))bad.push('props array incomplete');}if(bad.length>0){console.error('TOOLBOX LAYOUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK toolbox: 3-column layout + item enumeration present')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "TOOLBOX LAYOUT GATE FAILED"(文件缺失 / 关键符号缺失 / props 数组不含 `heroState` / 引入 `import type` 或 `?.`)</fails_when>
<automated>node -e "const fs=require('fs');const raw=fs.readFileSync('packages-user/client-modules/src/ui/toolbox.tsx','utf8');const bad=[];if(/(?<!\r)\n/.test(raw))bad.push('LF without CR');if(raw.includes('\r')&&!/\r\n/.test(raw))bad.push('stray CR');if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF: toolbox.tsx')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
@ -259,7 +253,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
| Boundary | Description |
|----------|-------------|
| 数据读取边界 | 以 `saveState().items` 借用存档方法作枚举来源(GAP-T1 风险点) |
| 数据读取边界 | 以 `heroState.items.iterateItems()` 枚举拥有道具(GAP-T1 闭合后的正规来源) |
| 数据写入边界 | 使用道具写 `IHeroItems.useItem`;不直接改内部 Map |
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
| 范围边界 | 只新增 `toolbox.tsx`;不碰既有文件与其它界面 |
@ -271,8 +265,8 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-08-01 | Tampering | 借新增之机修改既有 UI / 数据端 / 渲染系统 | high | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `git status` 范围守卫 |
| T-09-08-02 | Elevation of Privilege | 未经批准即执行 / 使用未批准命名 | high | mitigate | Task 0 `blocking-human`;命名清单呈报;prohibitions |
| T-09-08-03 | Tampering | 臆造 `IHeroItems` 枚举 API 绕过 GAP-T1 | medium | mitigate | `<action>` 明列按 OI-T1 裁定;禁改数据端 |
| T-09-08-04 | Information Disclosure / Repudiation | 借用 `saveState` 枚举被误解为「写存档」 | low | mitigate | `<action>` / `assumptions` 明示其为读用途;Task 0 呈报 |
| T-09-08-03 | Tampering | 绕开用户新补的 `iterateItems()` 而自造枚举 / 继续借用 `saveState` | medium | mitigate | `<action>` 明列改用 `iterateItems`;禁改数据端 |
| T-09-08-04 | Information Disclosure / Repudiation | 误用 `saveState` 枚举被误解为「写存档」 | low | mitigate | `<action>` / `assumptions` 明示改用 `iterateItems` |
| T-09-08-05 | Tampering | 引入 `client` 单例 / `import type` / `?.` / 非 CRLF | medium | mitigate | prohibitions + Task 1/2 静态门禁 + CRLF 门禁 |
| T-09-08-06 | Tampering | 误实现拖拽 / 丢弃(D-36 明确不做) | low | mitigate | Task 2 门禁断言无 `onDrag`/`drop(` |
| T-09-08-07 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
@ -281,19 +275,19 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
</threat_model>
<verification>
- **Task 1**:`toolbox.tsx` 存在且导出 `ToolBox` / `ToolBoxUI`;含 `saveState(` / `SaveCompression` / `.items.getItemState(` / `consumable` / `hideInToolbox` / `.raw.text` / `.raw.name` / `<Scroll` / `<icon`;props 数组含 `controller,instance,state,heroState`;无 `import type`、无 `?.`;CRLF。
- **Task 1**:`toolbox.tsx` 存在且导出 `ToolBox` / `ToolBoxUI`;含 `.items.iterateItems(` / `consumable` / `hideInToolbox` / `.raw.text` / `.raw.name` / `<Scroll` / `<icon`;props 数组含 `controller,instance,state,heroState`;无 `import type`、无 `?.`;CRLF。
- **Task 2**:含 `items.useItem(` / `onClick` / `selected` / `controller.close(`;无 `onDrag`/`drop(`;无 `import type` / `?.` / 连续 `as`;CRLF。
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-08-SUMMARY.md`(含 OI-T1..OI-T5 的最终裁定)。
- **人工复核**:结论写入 `09-08-SUMMARY.md`(含 OI-T2..OI-T5 的最终裁定)。
</verification>
<success_criteria>
- 布局:三列(永久 / 可消耗道具列表 + 名称 / 说明);占满游戏画面;列表纵向滚动不显示滚动条;右下退出。
- 数据:`heroState.items` 枚举(GAP-T1 处置);过滤 `hideInToolbox`;按 `count` 降序、同数量按 `num` 升序;入参 `IHeroState<IHeroAttr>`。
- 数据:`heroState.items.iterateItems()` 枚举(GAP-T1 闭合);过滤 `hideInToolbox`;按 `count` 降序、同数量按 `num` 升序;入参 `IHeroState<IHeroAttr>`。
- 交互:单击选中、再点使用(`useItem`);无丢弃 / 拖拽。
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.` / 连续 `as`。
- OI-T1..OI-T5 已按用户裁定落地;未采纳数据端方案时明确阻塞并记录。
- OI-T2..OI-T5 已按用户裁定落地;接口缺口 OI-T1 已由用户补齐并入本计划。
</success_criteria>
## Artifacts this phase produces
@ -317,7 +311,7 @@ Phase 9 · 任务 3 · 第 8 个计划 —— **toolbox 道具栏**。
| CONTEXT | D-33 / D-37(占满游戏画面 root renderer / 退出按钮右下) | COVERED(`FULL_LOC` + 右下按钮) |
| CONTEXT | D-03 / D-04 / D-06 / D-22(执行前获批 / 命名获批 / 不扩范围 / 逐界面讨论后规划) | COVERED(Task 0 + prohibitions + 范围门禁) |
| CONTEXT | D-18 其余界面(book/equipbox/shop) | OUT OF SCOPE(各自计划 / shop 暂缓) |
| 缺口 | `IHeroItems` 无枚举已拥有道具的公开方法 | FLAGGED(OI-T1 / GAP-T1,待用户裁定) |
| 缺口 | `IHeroItems` 无枚举已拥有道具的公开方法 | RESOLVED(用户新补 `iterateItems()`,`data-base/src/hero/types.ts:632`) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-08-SUMMARY.md` when done