mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
docs(09): plan task 3 book/equipbox/toolbox (pending data-end interfaces)
This commit is contained in:
parent
1633ce268f
commit
1d0c7db3cc
@ -481,7 +481,7 @@ Plans:
|
||||
**Goal**: 在已完成的数据端(可在 Node 环境独立运行/回放验证)之上完成渲染端框架,产出可在浏览器中运行并验证的最小版本
|
||||
**Requirements**: TBD
|
||||
**Depends on**: Phase 8
|
||||
**Plans**: 5/5 plans executed (任务 1·UI 接口适配,D-10..D-14 已闭环;任务 2·迁移后 UI 修复,D-15..D-17,09-05 待执行;本阶段增量派发,其余任务待用户再派发)
|
||||
**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:
|
||||
**Wave 1**
|
||||
@ -504,6 +504,12 @@ Plans:
|
||||
|
||||
- [x] 09-05-PLAN.md — 任务 2·A + B + C:prettier 格式化 `background.tsx`/`statusBar.tsx`(D-15)+ `IUIController`/`UIController` 补 `<IGameUIPropsBase>`(14 处调用点 + 2 构造点,D-16)+ `src/ui/**` 迁移遗留类型错误修复(`main.tsx` `p`→`props`;`statusBar.tsx`/`toolbar.tsx` props 基类型归位;`settings.tsx` `loc` 窄化,D-17)+ 定向 `vue-tsc`/prettier 门禁
|
||||
|
||||
**Wave 6** *(blocked on Wave 5 completion · 三个界面互相独立、可并行)*
|
||||
|
||||
- [ ] 09-06-PLAN.md — 任务 3·book 怪物手册(D-23..D-27):列表 `book.tsx`(绑定 `props.state.enemyContext`、按 `code` 去重、`baseAttribute`、9 项数值含「1 防」、滚动、点击开详情)+ 独立详情 `bookDetail.tsx`(`IEnemy|IEnemyView`、基础↔计算后切换、仅特殊列表、临界走系统二分法接口、左下切换/右下返回)
|
||||
- [ ] 09-07-PLAN.md — 任务 3·equipbox 装备界面(D-28..D-32):`equipbox.tsx`(左列状态区域 + 右侧 装备描述/装备槽横滚/拥有装备纵滚;选中增减;点击选中/再点穿上/拖拽装脱/右下退出;入参 `IHeroState<IHeroAttr>` + 属性 id+中文名列表)
|
||||
- [ ] 09-08-PLAN.md — 任务 3·toolbox 道具栏(D-34..D-36):`toolbox.tsx`(三列 永久/可消耗/名称+说明;数量降序同数量按 id;单击选中/再点使用 `useItem`;无丢弃拖拽;入参 `IHeroState<IHeroAttr>`)
|
||||
|
||||
## Progress
|
||||
|
||||
**Execution Order:**
|
||||
@ -519,4 +525,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
||||
| 6. 单元测试 | 18/18 | Obsolete(由 Phase 9 承接) | - |
|
||||
| 7. 数据端缺陷修复 | 15/16 | Complete(07-16 superseded) | - |
|
||||
| 8. 测试重构与接口对齐 | 11/11 | Complete | 2026-10-08 |
|
||||
| 9. 最小浏览器可运行版本 | 5/5 | In Progress| |
|
||||
| 9. 最小浏览器可运行版本 | 5/8 | In Progress| |
|
||||
|
||||
356
.planning/phases/09-minimum-browser-runnable/09-06-PLAN.md
Normal file
356
.planning/phases/09-minimum-browser-runnable/09-06-PLAN.md
Normal file
@ -0,0 +1,356 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 6
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on:
|
||||
- 09-05
|
||||
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: []
|
||||
estimate:
|
||||
tokens: 82000
|
||||
raw_tokens: 82000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 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-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-*` 已声明)。"
|
||||
- "**范围零外溢 + 静态门禁:** 仅新增 `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:
|
||||
- path: packages-user/client-modules/src/ui/bookDetail.tsx
|
||||
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` ↔ `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>`)—— 渲染骨架"
|
||||
assumptions:
|
||||
- "**用户所称「num」映射为本代码库的 `IReadonlyEnemy.code`(怪物图块数字)**:`Enemy` 构造签名 `constructor(id: string, code: number, ...)`(`data-base/src/enemy/enemy.ts:11-15`),`code` 即「怪物在地图上的标识数字」;`EnemyManager` 以 `code` 建模板、以 `num(token)` 解析。图标用 `<icon icon={code}>`(`IconProps.icon: AllNumbers | AllIds`)。若用户认为「num」另有所指,据其指定执行。"
|
||||
- "**详情入参类型判别**:`IEnemyView` 具 `getBaseEnemy()` 方法、`IEnemy`/`IReadonlyEnemy` 不具;以「是否具 `getBaseEnemy`」作结构判别(type guard),据此启用 / 禁用基础↔计算后切换。"
|
||||
- "**特殊属性描述按纯文本渲染**:新渲染系统无 HTML / `innerHTML`(legacy `getSpecialHint` 用 `innerHTML`);`getDescription()` 以纯文本经 `<text>` / `TextContent` 渲染。富文本属后续,不在本计划。"
|
||||
- "**数字格式化沿用仓库既有新 UI 约定 `core.formatBigNumber`**(`statusBar.tsx` / `viewmap.tsx` / `save.tsx` 均如此使用;无新等价 API)。若用户要求改用普通 `String()`,据其指定执行。"
|
||||
- "**执行日行号可能漂移**:接口锚点为 2026-10-10 规划日;门禁按**符号文本**匹配,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "本计划**只做任务 3 · book(列表 + 独立详情)**:不得实现 equipbox / toolbox / shop;不得接线「如何从地图打开 book」(D-27 / 打开方式不管);不得实现 legacy `book.tsx` 的**伤害曲线**与**标记功能**(D-24 / D-25 范围外)。"
|
||||
- "**不得臆造接口 / 命名**:不得凭空为「基础模式临界 / 伤害」造数据端 API;不得为 plain `IEnemy` 入参造 view;不得新增未在 Task 0 呈报获批的任何公共 / 受保护 / 私有命名。"
|
||||
- "不得修改 `files_modified` 之外的任何**生产**文件;不得改 09-01..09-05 及其产物;不得改 `packages/system` / `packages/render` / `packages/render-vue`;不得改数据端(`packages-user/data-*`)与 `packages/**`;不得移除 / 改名任何既有单例。"
|
||||
- "不得修改任何既有 `jsDoc` 与注释;不得引入 `import type`(`dev.md:54`);不得使用 `?.`(`dev.md:107`);CRLF;不得遗留未使用 import;不得引入连续 `as`(`dev.md:96`);所有 `class` 必须先 `interface` 后 `implements`(`dev.md:100`)。"
|
||||
- "不得把门禁建立在「全仓 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-4(数字格式化)**:默认 `core.formatBigNumber`;可否接受?"
|
||||
- "**OI-5(命名)**:见 Task 0 命名清单(文件名 `book.tsx` / `bookDetail.tsx`;组件 `Book` / `BookDetail`;`GameUI` 名 `'book'` / `'book-detail'`;props 接口 `BookProps` / `BookDetailProps`)。全部待批准。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 9 · 任务 3 · 第 6 个计划 —— **book 怪物手册(列表 `book.tsx` + 独立详情 `bookDetail.tsx`)**。
|
||||
|
||||
**要解决的问题(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 / 范围守卫)。
|
||||
|
||||
**为何详情先行:** 列表通过 `props.controller.open(BookDetailUI, {...})` 打开详情,需要 `BookDetailUI` 已存在;故 Task 1 先落详情、Task 2 落列表。
|
||||
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为渲染端 UI 移植,数据来自数据端接口(`EnemyContext` / `IHeroState`),不涉及任何外部 API。`**
|
||||
</objective>
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-PATTERNS.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-05-PLAN.md
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<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>
|
||||
<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()`)
|
||||
- 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`)
|
||||
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)、packages-user/client-modules/src/ui/main.tsx:1-36,130-220(新 UI 组件骨架 / `<icon>` 不可见,另见 elements/props.ts)
|
||||
- packages-user/client-base/src/elements/props.ts:6-15(`IconProps`,`<icon icon=... loc=...>`)、packages-user/client-base/src/components/scroll.tsx:36-73,553-580(`<Scroll hor noscroll>`)、packages-user/client-base/src/components/misc.tsx:282-370(`<Selection>`)
|
||||
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
|
||||
- packages/legacy-ui/src/tools/book.tsx、src/ui/bookDetail.vue、src/components/enemyOne.vue、src/panel/enemySpecial.vue、src/panel/enemyCritical.vue(**布局 / 交互参考**;D-20:不要求视觉一致)
|
||||
- dev.md(import 置首无空行/注释;无 `import type`;避免 `as`;无 `?.`;CRLF;interface 后 implements;不改既有注释)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** 任务 3 · book 怪物手册。把 legacy 手册移植到新接口;列表绑定 `EnemyContext`、按 `code` 去重、基础数据取 `baseAttribute`、9 项数值、滚动;详情为**独立 UI**、入参 `IEnemy | IEnemyView`、基础 / 计算后切换、特殊列表、临界表(系统二分法接口)、左下切换 / 右下返回。不做伤害曲线 / 标记。
|
||||
|
||||
② **锁定裁定原文要点:** 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`)这一事实。
|
||||
|
||||
④ **命名清单(须用户逐一批准,D-04):**
|
||||
- 新增文件:`packages-user/client-modules/src/ui/book.tsx`、`packages-user/client-modules/src/ui/bookDetail.tsx`。
|
||||
- 新增组件:`Book`、`BookDetail`。
|
||||
- 新增 `GameUI` 名称字符串:`'book'`、`'book-detail'`。
|
||||
- 新增 props 接口:`BookProps`、`BookDetailProps`。
|
||||
- 新增导出:`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) 涉及数据端 / 系统层改动,本计划不含)。
|
||||
|
||||
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / 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)。
|
||||
|
||||
⑧ **关卡性质与失败语义:** 无论触发方式或 `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>
|
||||
<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` 依赖)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-default">
|
||||
<name>可以执行(采纳全部默认处置)</name>
|
||||
<pros>一次落地列表 + 详情;OI-1(a)/OI-2(a)/OI-3(a) 成本最低且不越界</pros>
|
||||
<cons>OI-1(a) 下基础模式临界 = 计算后临界</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>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订清单 / 命名 / 范围后重新汇报</name>
|
||||
<pros>避免带着错误契约落地</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(如需调整 OI-1..OI-5 的处置或命名方案,请在回复中指明;修订意见请按 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>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 book 文件已存在 / 关键接口已漂移)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-1..OI-5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:D-23..D-27 / D-33 / D-37 范围、接口依据(含「临界 / 伤害 API 内部用 `getComputedEnemy()`」事实)、命名清单、OI-1..OI-5、验证方式(静态断言,无全仓门禁)、范围与硬边界
|
||||
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做伤害曲线 / 标记**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<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>
|
||||
<read_first>
|
||||
- packages-user/data-system/src/combat/types.ts:43-72(`IEnemyView`)、:363-450(`IEnemyDamageInfoBase` / `IEnemyCritical` / `IDamageContext`)
|
||||
- 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`)
|
||||
- packages-user/client-modules/src/ui/title.tsx:56-64,352-478(组件骨架 / props 数组 / 渲染树写法)
|
||||
- packages-user/client-base/src/elements/props.ts:6-15(`IconProps`)
|
||||
- packages-user/client-base/src/components/textbox.tsx:255-540(`TextContent`,如需多行 / 自动高度文本)
|
||||
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC` / `HALF_*`)
|
||||
- packages/legacy-ui/src/components/enemyOne.vue、src/panel/enemySpecial.vue、src/panel/enemyCritical.vue(布局参考)
|
||||
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF;interface 后 implements)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **新建 `bookDetail.tsx`**(新文件;仅此文件,另按 OI-3(a) 在 `package.json` 的 `dependencies` 增补 `"@user/data-system": "workspace:*"` 一行,仅当用户采纳该方案)。
|
||||
|
||||
(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 裁定)。
|
||||
|
||||
(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`)。
|
||||
|
||||
(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>
|
||||
<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>
|
||||
<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>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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 裁定处理基础模式
|
||||
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改任何既有注释;未改 `files_modified` 外文件
|
||||
</acceptance_criteria>
|
||||
<done>详情 UI 完成:独立可运行、支持基础 / 计算后切换、特殊列表与临界表具备、左下切换 / 右下返回接线,静态门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2(列表 + 门禁):新建 `book.tsx` —— 怪物手册列表(枚举去重 + 9 项数值 + 滚动 + 点击开详情 + 右下退出),并跑最终静态门禁</name>
|
||||
<precondition>Task 1 已完成(`BookDetailUI` 已存在,可被列表引用)。</precondition>
|
||||
<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-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` 组合范本)
|
||||
- packages-user/client-base/src/components/scroll.tsx:36-73,553-580(`<Scroll noscroll hor>`)
|
||||
- packages-user/client-base/src/components/icons.tsx(`defineIcon` 范本,若需自绘图标;本计划图标用 `<icon>`)
|
||||
- packages-user/client-base/src/elements/props.ts:6-15(`<icon icon=...>`)
|
||||
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
|
||||
- packages/legacy-ui/src/components/enemyOne.vue(行布局参考:图标 + 名称 + 特殊名 + 9 项数值网格)
|
||||
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **新建 `book.tsx`(仅此文件)。** 定义 `export interface BookProps extends IUIPropsBase {}`;`const bookProps = { props: ['controller','instance','state'] } satisfies SetupComponentOptions<BookProps>;`;`export const Book = defineComponent<BookProps>(props => { ... }, bookProps);`;`export const BookUI = new GameUI('book', Book);`。根容器 `FULL_LOC`(D-33)。
|
||||
|
||||
(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`。
|
||||
|
||||
(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}防`。**
|
||||
|
||||
(5) **交互:** 点击行 → `props.controller.open(BookDetailUI, { enemy: view })`(传 `IEnemyView`,使详情可切换);右下角退出按钮 → `props.controller.close(props.instance)`(D-37)。
|
||||
|
||||
(6) **收口与门禁:** 同 Task 1 的规范收口(import 置首 / 无 `import type` / 无 `?.` / CRLF / 无未使用 import / 不改既有注释);本任务 `<verify>` 运行对**两文件**的最终静态门禁 + 范围守卫。
|
||||
</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>
|
||||
<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>
|
||||
<automated>git -C . status --porcelain</automated>
|
||||
<fails_when>输出包含 `packages/system/`、`packages/render`、`packages-user/data-`、`legacy-ui/` 或 09-01..09-05 的既有文件(范围外改动)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `book.tsx` 存在,导出 `Book` / `BookUI`;绑定 `props.state.enemyContext`,按 `base.code` 去重
|
||||
- 行含 图标 + 名称 + 特殊名 + 生命 / 攻击 / 防御 / 金币 / 经验 / 伤害 / 临界 / 减伤 / 「1 防」;列表可滚动;未使用 `{ratio}防`
|
||||
- 临界 / 减伤经 `calculateCritical(view,'atk')`,未重复实现二分法;按 OI-1 裁定处理
|
||||
- 点击行 `controller.open(BookDetailUI, { enemy: view })`;右下退出 `controller.close(props.instance)`
|
||||
- 无 `client` 单例、无 `import type`、无 `?.`、CRLF、无未使用 import;范围守卫无越界改动
|
||||
</acceptance_criteria>
|
||||
<done>列表 UI 完成并接线到详情;两文件静态门禁 + CRLF + 范围守卫全绿</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 数据读取边界 | 手册只读 `props.state.enemyContext` / `props.state.hero`;不向数据端写任何状态 |
|
||||
| 类型来源边界 | `IEnemyView` / `IDamageSystem` 来自 `@user/data-system`;未声明依赖前不得具名引入 |
|
||||
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
|
||||
| 范围边界 | 只新增 `book.tsx` / `bookDetail.tsx`(+ 至多一行依赖声明);不碰既有文件与其它界面 |
|
||||
| 提交边界 | 不得把用户并发改动(D-56)纳入暂存/提交 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| 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-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-08 | Tampering | 提交时混入用户并发改动(D-56) | medium | mitigate | 提交前 `git status` 逐条核对,只暂存本计划条目 |
|
||||
| T-09-06-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装;仅新增 workspace 依赖声明(不执行 `pnpm i` 于本计划验证) |
|
||||
</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` 条目被新增。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
|
||||
- **人工复核**:结论写入 `09-06-SUMMARY.md`(含 OI-1..OI-5 的最终裁定)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- book 列表:绑定当前 `EnemyContext`、按 `code` 去重、基础数据取 `baseAttribute`、9 项数值(含「1 防」、不含 `{ratio}防`)、滚动、点击开详情、右下退出。
|
||||
- book 详情:独立 UI、入参 `IEnemy | IEnemyView`、基础 / 计算后切换、仅特殊列表(不显光环)、临界表经系统二分法接口、左下切换 / 右下返回。
|
||||
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.`。
|
||||
- OI-1..OI-5 已按用户裁定落地;未采纳数据端 / 系统层改动方案时明确阻塞并记录。
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| 符号 / 路径 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `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 计划互不冲突)。
|
||||
|
||||
## Source Coverage Audit
|
||||
|
||||
| 来源 | 项 | 处置 |
|
||||
|------|----|------|
|
||||
| GOAL | Phase 9(ROADMAP)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·任务 3 子切片:book 怪物手册移植) |
|
||||
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
|
||||
| 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-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,待用户裁定) |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-06-SUMMARY.md` when done
|
||||
</output>
|
||||
338
.planning/phases/09-minimum-browser-runnable/09-07-PLAN.md
Normal file
338
.planning/phases/09-minimum-browser-runnable/09-07-PLAN.md
Normal file
@ -0,0 +1,338 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 7
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on:
|
||||
- 09-05
|
||||
files_modified:
|
||||
- packages-user/client-modules/src/ui/equipbox.tsx
|
||||
- .planning/phases/09-minimum-browser-runnable/09-07-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
tokens: 76000
|
||||
raw_tokens: 76000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-22)**:Task 0 必须先汇报「本计划 = 任务 3 · equipbox 装备界面」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**逐一列出所有新增命名(文件名、组件名、`GameUI` 名、props 接口名、属性项类型名)请求批准**,并**明确列出开放项与接口缺口(GAP-E1..GAP-E4)**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
|
||||
- "**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-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`)实现,属本计划主要实现难点。"
|
||||
- "**范围零外溢 + 静态门禁:** 仅新增 `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"
|
||||
- "`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`)—— 状态区域当前值"
|
||||
- "`equipbox.tsx` ↔ `IItemRawData.equip.type`(`data-common/src/store/types.ts:161-174`)与 `IHeroEquipment.canEquipTo(uid, slot)`(`data-base/src/hero/types.ts:823-827`)—— 同类型槽位判定"
|
||||
- "`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 呈报用户确认。"
|
||||
- "**拥有装备排序**:默认按 `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` 定位)。此为参考实现,具体手感按用户验收调整。"
|
||||
- "**执行日行号可能漂移**:接口锚点为 2026-10-10 规划日;门禁按**符号文本**匹配,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "本计划**只做任务 3 · equipbox**:不得实现 book / toolbox / shop;不得接线「如何打开 equipbox」(打开方式不管);不得实现筛选 / 排序 UI(D-30 明确不做)。"
|
||||
- "**不得臆造接口 / 命名**:不得新增数据端 API;不得凭空为单装备增减量造接口;不得新增未在 Task 0 呈报获批的任何公共 / 受保护 / 私有命名。"
|
||||
- "不得修改 `files_modified` 之外的任何**生产**文件;不得改 09-01..09-05 及其产物;不得改 `packages/system` / `packages/render` / `packages/render-vue`;不得改数据端(`packages-user/data-*`)与 `packages/**`;不得移除 / 改名任何既有单例。"
|
||||
- "不得修改任何既有 `jsDoc` 与注释;不得引入 `import type`(`dev.md:54`);不得使用 `?.`(`dev.md:107`);CRLF;不得遗留未使用 import;不得引入连续 `as`(`dev.md:96`);所有 `class` 必须先 `interface` 后 `implements`(`dev.md:100`)。"
|
||||
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
|
||||
- "不得触碰、暂存、提交用户的并发改动(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-E3(排序键)**:默认按 `item.num` 升序;可否接受?"
|
||||
- "**OI-E4(拖拽范围)**:确认拖拽以渲染端动作事件实现(GAP-E4),且拖拽穿脱为本期目标(非后续)。默认采纳。"
|
||||
- "**OI-E5(命名)**:见 Task 0 命名清单。全部待批准。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 9 · 任务 3 · 第 7 个计划 —— **equipbox 装备界面**。
|
||||
|
||||
**要解决的问题(D-28..D-32):** 把 legacy `packages/legacy-ui` 的装备界面(`src/tools/equipbox.tsx` + `src/ui/equipbox.vue`)移植到新 UI 接口上,并按用户已定形态实现:左列状态区域(属性列举 + 选中增减)、右侧自上而下 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚);点击选中、再点穿上、拖拽装脱;无筛选 / 排序 UI。
|
||||
|
||||
**大致解决方案(本计划 = 09-07):** Task 0(`blocking-human`)= 汇报范围、命名清单、接口缺口(GAP-E1..E4)与开放项(OI-E1..E5)并等待「可以执行」;Task 1 = 实现 `equipbox.tsx` 的**静态布局与数据列举**(状态区域 / 装备描述 / 装备槽 / 拥有装备,含选中时增减量显示);Task 2 = 实现**交互**(点击选中 / 再点穿上 / 拖拽装脱 / 右退出)**并**跑最终静态门禁。
|
||||
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为渲染端 UI 移植,数据来自数据端接口(`IHeroState`),不涉及任何外部 API。`**
|
||||
</objective>
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-PATTERNS.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-05-PLAN.md
|
||||
@.planning/images/equipbox.png
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · equipbox 装备界面」范围、命名清单、接口缺口与开放项,等待用户回复「可以执行」</name>
|
||||
<precondition>`09-05` 已闭环;仓库中 `equipbox.tsx` **尚不存在**;数据端接口(`IHeroEquipment` / `IHeroEquipsStore` / `compareEquip`)均已存在。</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-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/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`)
|
||||
- .planning/images/equipbox.png(**布局参考**,D-28 附注)
|
||||
- packages/legacy-ui/src/tools/equipbox.tsx、src/ui/equipbox.vue(布局 / 交互参考;D-20:不要求视觉一致)
|
||||
- dev.md(import 置首无空行/注释;无 `import type`;避免 `as`;无 `?.`;CRLF;interface 后 implements;不改既有注释)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** 任务 3 · equipbox 装备界面。把 legacy 装备界面移植到新接口;左列状态区域(属性列举 + 选中增减)、右侧 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚);点击选中 / 再点穿上 / 拖拽装脱;无筛选排序;打开方式不管。
|
||||
|
||||
② **锁定裁定原文要点:** 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,单装备效果需自算)** 两个事实。
|
||||
|
||||
④ **命名清单(须用户逐一批准,D-04):**
|
||||
- 新增文件:`packages-user/client-modules/src/ui/equipbox.tsx`。
|
||||
- 新增组件:`EquipBox`。
|
||||
- 新增 `GameUI` 名称字符串:`'equipbox'`。
|
||||
- 新增 props 接口:`EquipBoxProps`(含 `heroState: IHeroState<IHeroAttr>`、`attributes: readonly IHeroAttrItem[]`,及 `IUIPropsBase` 的 `controller/instance/state`)。
|
||||
- 新增属性项类型:`IHeroAttrItem`(默认名,`{ readonly id: keyof IHeroAttr; readonly name: string }`;与 09-06 的同名提案如用户希望统一,可后续合并)。
|
||||
- 新增导出:`EquipBoxUI`(`new GameUI(...)`)。
|
||||
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持与同 wave 计划互不冲突)。
|
||||
|
||||
⑤ **接口缺口与开放项(须用户裁定):** 原样呈报 `<open_items>` OI-E1..OI-E5(装备槽路径;单装备增减量;排序键;拖拽范围;命名)。特别强调 OI-E2(b) 涉及数据端接口(超出本计划)。
|
||||
|
||||
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state,heroState,attributes` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
|
||||
|
||||
⑦ **范围与硬边界:** 仅新增 `equipbox.tsx`;**零触碰** = 09-01..09-05 产物、`src/render/**`、`packages/system`、`packages/render*`、`packages-user/data-*`、`legacy-ui`、book/toolbox/shop、用户并发改动(D-56)。
|
||||
|
||||
⑧ **关卡性质与失败语义:** 无论触发方式或 `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>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + GAP-E1(路径名不符)+ GAP-E2(单装备增减量无 API)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
|
||||
</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>
|
||||
</option>
|
||||
<option id="execute-no-drag">
|
||||
<name>可以执行 + OI-E4 调整为「暂不做拖拽」(仅点击选中 / 再点穿上)</name>
|
||||
<pros>实现风险最低</pros>
|
||||
<cons>缺 D-32 拖拽装脱</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订清单 / 命名 / 范围后重新汇报</name>
|
||||
<pros>避免带着错误契约落地</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(如需调整 OI-E1..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>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 equipbox.tsx 已存在 / 关键接口已漂移)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-E1..OI-E5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:D-28..D-32 / D-33 / D-37 范围、接口依据(含 GAP-E1 路径不符、GAP-E2 单装备增减无 API)、命名清单、OI-E1..OI-E5、验证方式(静态断言,无全仓门禁)、范围与硬边界
|
||||
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做筛选 / 排序 UI**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1(布局 + 数据列举):新建 `equipbox.tsx` —— 左列状态区域 + 右侧 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚)</name>
|
||||
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-E1..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/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>`)
|
||||
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC` / `HALF_*`)
|
||||
- .planning/images/equipbox.png、packages/legacy-ui/src/ui/equipbox.vue(布局参考)
|
||||
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **新建 `equipbox.tsx`(仅此文件)。** 命名须已在 Task 0 获批:`export interface EquipBoxProps extends IUIPropsBase { heroState: IHeroState<IHeroAttr>; attributes: readonly IHeroAttrItem[]; }`;`const equipBoxProps = { props: ['controller','instance','state','heroState','attributes'] } satisfies SetupComponentOptions<EquipBoxProps>;`;`export const EquipBox = defineComponent<EquipBoxProps>(props => { ... }, equipBoxProps);`;`export const EquipBoxUI = new GameUI('equipbox', EquipBox);`。根容器 `FULL_LOC`(D-33)。
|
||||
|
||||
(2) **属性项类型:** `export interface IHeroAttrItem { readonly id: keyof IHeroAttr; readonly name: string; }`(默认名;若用户指定别名,据其指定)。
|
||||
|
||||
(3) **数据准备:** `const hero = props.heroState;`;装备槽名 `hero.equip.slots`(GAP-E1 / OI-E1);拥有装备 `hero.items.equipment.instances()`,按 `item.num` 升序 `sort`(OI-E3);已装备映射 `hero.equip.getEquipped(index)` → `hero.items.equipment.get(uid)`。以上以响应式 `computed` 暴露。
|
||||
|
||||
(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),则阻塞等待用户接口。
|
||||
|
||||
(6) **装备槽区域(D-30):** 横向 `<Scroll hor noscroll>` 平铺 `hero.equip.slots` 每槽;槽内 `<icon icon={已装备装备的 item.num}>` + 槽名;高亮当前选中槽。
|
||||
|
||||
(7) **拥有装备区域(D-30):** 纵向 `<Scroll noscroll>` 平铺排序后的拥有装备;每项 `<icon icon={item.num}>` + `item.name`;高亮当前选中项。
|
||||
|
||||
(8) **装备描述区域:** 显示当前选中装备的 `item.name` + `item.text`(纯文本,纵向 `<Scroll noscroll>`)+ `item.equip.slots`(可装备槽)。
|
||||
</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>
|
||||
<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` 顺序列举、显示当前值、选中时显示增减(增绿减红、靠右)
|
||||
- 装备槽来自 `hero.equip.slots` 且横向滚动;拥有装备来自 `equipment.instances()` 按 `item.num` 升序且纵向滚动;均为 `noscroll`
|
||||
- 装备描述区显示 `item.name` / `item.text` / 可装备槽;右下退出按钮存在
|
||||
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改既有注释;未改 `files_modified` 外文件
|
||||
</acceptance_criteria>
|
||||
<done>布局与数据列举完成:三段式布局 + 状态区域增减显示 + 装备槽 / 拥有装备列举,静态门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2(交互 + 门禁):实现点击选中 / 再点穿上 / 拖拽装脱 / 右下退出,并跑最终静态门禁</name>
|
||||
<precondition>Task 1 已完成(`equipbox.tsx` 布局与列举已落地)。</precondition>
|
||||
<files>packages-user/client-modules/src/ui/equipbox.tsx</files>
|
||||
<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/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` 标记;增减量仅显示「卸下」效果)。
|
||||
|
||||
(2) **再点穿上(D-32):** 选中项再次点击 → 计算目标槽 = 同类型第一个槽(`canEquipTo(uid, index) !== EquipStatus.CannotEquip`);调用 `hero.equip.equip(uid, slotIndex, true)`(**autoUnload 下的 `equip`**)。
|
||||
|
||||
(3) **拖拽(GAP-E4 / OI-E4):** 拥有装备项上 `onDown/onMove/onUp` 跟踪指针,越过阈值视为拖拽并显示浮层 `<icon>`;释放到目标槽 → `hero.equip.equip(uid, slotIndex, true)`;拖拽已装备槽 → 释放即 `hero.equip.unequip(slotIndex)`。目标槽判定用槽元素的命中区域(或 pointer 相对坐标落在哪一槽)。
|
||||
|
||||
(4) **退出(D-37):** 右下角按钮 → `props.controller.close(props.instance)`。
|
||||
|
||||
(5) **收口与门禁:** 规范收口(import 置首 / 无 `import type` / 无 `?.` / 无连续 `as` / CRLF / 无未使用 import / 不改既有注释);本任务 `<verify>` 运行最终静态门禁 + CRLF + 范围守卫。
|
||||
</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>
|
||||
<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>
|
||||
<automated>git -C . status --porcelain</automated>
|
||||
<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)
|
||||
- 拖拽拥有装备到槽 → `equip(uid, slotIndex, true)`;拖拽已装备 → `unequip(slotIndex)`
|
||||
- 右下退出调用 `controller.close(props.instance)`
|
||||
- 无 `import type`、无 `?.`、无连续 `as`、CRLF、无未使用 import;范围守卫无越界改动
|
||||
</acceptance_criteria>
|
||||
<done>交互完成;最终静态门禁 + CRLF + 范围守卫全绿</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 数据写入边界 | 装备穿脱写 `IHeroEquipment` / `IHeroEquipsStore`;调用均经数据端既有 API,不自造状态 |
|
||||
| 增减量计算边界 | 增减量需自算(GAP-E2);公式须与 `ValueModifier`/`PercentageModifier` 一致 |
|
||||
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
|
||||
| 范围边界 | 只新增 `equipbox.tsx`;不碰既有文件与其它界面 |
|
||||
| 提交边界 | 不得把用户并发改动(D-56)纳入暂存/提交 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| 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-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` |
|
||||
| T-09-07-08 | Tampering | 提交时混入用户并发改动(D-56) | medium | mitigate | 提交前 `git status` 逐条核对,只暂存本计划条目 |
|
||||
| T-09-07-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装 |
|
||||
</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。
|
||||
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
|
||||
- **人工复核**:结论写入 `09-07-SUMMARY.md`(含 OI-E1..OI-E5 的最终裁定)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 布局:左列状态区域(属性列举 + 选中增减);右侧自上而下 装备描述 / 装备槽(横滚)/ 拥有装备(纵滚);滚动条不显示;占满游戏画面。
|
||||
- 数据:装备槽 `hero.equip.slots`;拥有装备 `instances()` 按 `item.num` 升序;无筛选 / 排序 UI;入参含 `IHeroState<IHeroAttr>` + 属性列表。
|
||||
- 交互:点击选中、再点穿上(autoUnload `equip`)、拖拽装脱、右下退出。
|
||||
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.` / 连续 `as`。
|
||||
- OI-E1..OI-E5 已按用户裁定落地;未采纳数据端方案时明确阻塞并记录。
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| 符号 / 路径 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `packages-user/client-modules/src/ui/equipbox.tsx`(新增) | 组件 / UI | 装备界面:`EquipBox` / `EquipBoxUI` / `EquipBoxProps` / `IHeroAttrItem`(命名待批准) |
|
||||
|
||||
**新增导出命名(全部须 Task 0 批准):** 文件 `equipbox.tsx`;组件 `EquipBox`;`GameUI` 名 `'equipbox'`;props 接口 `EquipBoxProps`;属性项类型 `IHeroAttrItem`(默认);导出 `EquipBoxUI`。**不向 `src/ui/index.ts` 添加导出**(保持与同 wave 计划互不冲突)。
|
||||
|
||||
## Source Coverage Audit
|
||||
|
||||
| 来源 | 项 | 处置 |
|
||||
|------|----|------|
|
||||
| GOAL | Phase 9(ROADMAP)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·任务 3 子切片:equipbox 移植) |
|
||||
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
|
||||
| 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-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,待用户裁定) |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-07-SUMMARY.md` when done
|
||||
</output>
|
||||
324
.planning/phases/09-minimum-browser-runnable/09-08-PLAN.md
Normal file
324
.planning/phases/09-minimum-browser-runnable/09-08-PLAN.md
Normal file
@ -0,0 +1,324 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 8
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on:
|
||||
- 09-05
|
||||
files_modified:
|
||||
- packages-user/client-modules/src/ui/toolbox.tsx
|
||||
- .planning/phases/09-minimum-browser-runnable/09-08-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 70000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-22)**:Task 0 必须先汇报「本计划 = 任务 3 · toolbox 道具栏」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**逐一列出所有新增命名(文件名、组件名、`GameUI` 名、props 接口名)请求批准**,并**明确列出开放项与接口缺口(GAP-T1 等)**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
|
||||
- "**D-34(布局):** 三列 —— 左列「**永久道具**」列表、中列「**可消耗道具**」列表、右列自上而下「**道具名称**」与「**道具说明**」(描述在最右,名称位于最右上)。占满整个游戏画面(`FULL_LOC`,D-33)。参考图 `.planning/images/toolbox.png`(与旧版基本一致)仅布局参考。"
|
||||
- "**D-35(列表 / 排序):** 道具分为**永久道具**(`IHeroItemState.consumable === false`,即 `ItemCategory.Constant`)与**可消耗道具**(`consumable === true`,即 `ItemCategory.Consumable`);各自按**数量从多到少**排序,**数量相同按 id 排序**(升序;id 取 `IHeroItemState.id`,等价身份键为 `num`)。"
|
||||
- "**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」不符**)。"
|
||||
- "**范围零外溢 + 静态门禁:** 仅新增 `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:
|
||||
- path: packages-user/client-modules/src/ui/toolbox.tsx
|
||||
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.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 呈报用户确认(并说明其为借用存档方法)。"
|
||||
- "**「永久 / 可消耗」判定**:`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 数量道具。"
|
||||
- "**执行日行号可能漂移**:接口锚点为 2026-10-10 规划日;门禁按**符号文本**匹配,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "本计划**只做任务 3 · toolbox**:不得实现 book / equipbox / shop;不得接线「如何打开 toolbox」(打开方式不管);不得实现丢弃 / 拖拽(D-36 明确不做)。"
|
||||
- "**不得臆造接口 / 命名**:不得新增数据端 API;不得新增未在 Task 0 呈报获批的任何公共 / 受保护 / 私有命名。"
|
||||
- "不得修改 `files_modified` 之外的任何**生产**文件;不得改 09-01..09-05 及其产物;不得改 `packages/system` / `packages/render` / `packages/render-vue`;不得改数据端(`packages-user/data-*`)与 `packages/**`;不得移除 / 改名任何既有单例。"
|
||||
- "不得修改任何既有 `jsDoc` 与注释;不得引入 `import type`(`dev.md:54`);不得使用 `?.`(`dev.md:107`);CRLF;不得遗留未使用 import;不得引入连续 `as`(`dev.md:96`);所有 `class` 必须先 `interface` 后 `implements`(`dev.md:100`)。"
|
||||
- "不得把门禁建立在「全仓 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-T2(使用行为的持有 / 关闭)**:legacy 使用道具前会 `holdOn()` 并关闭自身,使用后按需结束。新系统是否有等价「维持背景」诉求?默认**仅调用 `heroState.items.useItem(item)`,不改打开 / 关闭流程**(打开方式不管)。"
|
||||
- "**OI-T3(不可用时提示)**:legacy 有「当前无法使用该道具」提示。默认不做提示(仅调用 `useItem`,返回 `false` 时不消费)——如需提示,须用户提供提示组件约定。"
|
||||
- "**OI-T4(数字格式化)**:数量格式化默认 `core.formatBigNumber`;可否接受?"
|
||||
- "**OI-T5(命名)**:见 Task 0 命名清单。全部待批准。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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 = 实现**交互**(单击选中 / 再点使用 / 右下退出)**并**跑最终静态门禁。
|
||||
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为渲染端 UI 移植,数据来自数据端接口(`IHeroState`),不涉及任何外部 API。`**
|
||||
</objective>
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-PATTERNS.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-05-PLAN.md
|
||||
@.planning/images/toolbox.png
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 3 · toolbox 道具栏」范围、命名清单、接口缺口与开放项,等待用户回复「可以执行」</name>
|
||||
<precondition>`09-05` 已闭环;仓库中 `toolbox.tsx` **尚不存在**;数据端接口(`IHeroItems` / `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 删除)
|
||||
- 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`)
|
||||
- .planning/images/toolbox.png(**布局参考**)
|
||||
- packages/legacy-ui/src/tools/toolbox.tsx、src/ui/toolbox.vue(布局 / 交互参考;D-20:不要求视觉一致)
|
||||
- dev.md(import 置首无空行/注释;无 `import type`;避免 `as`;无 `?.`;CRLF;interface 后 implements;不改既有注释)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** 任务 3 · toolbox 道具栏。把 legacy 道具栏移植到新接口;三列(永久 / 可消耗 / 名称+说明);数量降序同数量按 id 升序;单击选中再点使用;无丢弃拖拽;入参 HeroState。
|
||||
|
||||
② **锁定裁定原文要点:** 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) 三条路径及各自代价。
|
||||
|
||||
④ **命名清单(须用户逐一批准,D-04):**
|
||||
- 新增文件:`packages-user/client-modules/src/ui/toolbox.tsx`。
|
||||
- 新增组件:`ToolBox`。
|
||||
- 新增 `GameUI` 名称字符串:`'toolbox'`。
|
||||
- 新增 props 接口:`ToolBoxProps`(含 `heroState: IHeroState<IHeroAttr>` 及 `IUIPropsBase` 的 `controller/instance/state`)。
|
||||
- 新增导出:`ToolBoxUI`(`new GameUI(...)`)。
|
||||
- (道具行类型默认**复用** `IHeroItemState`,不新增行类型命名。)
|
||||
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持与同 wave 计划互不冲突)。
|
||||
|
||||
⑤ **接口缺口与开放项(须用户裁定):** 原样呈报 `<open_items>` OI-T1..OI-T5。特别强调 OI-T1(b) 涉及数据端接口(超出本计划)。
|
||||
|
||||
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state,heroState` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
|
||||
|
||||
⑦ **范围与硬边界:** 仅新增 `toolbox.tsx`;**零触碰** = 09-01..09-05 产物、`src/render/**`、`packages/system`、`packages/render*`、`packages-user/data-*`、`legacy-ui`、book/equipbox/shop、用户并发改动(D-56)。
|
||||
|
||||
⑧ **关卡性质与失败语义:** 无论触发方式或 `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>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + GAP-T1(`IHeroItems` 无枚举)。行号为规划日实测、可能漂移,门禁按符号文本匹配。
|
||||
</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>
|
||||
</option>
|
||||
<option id="execute-extra-prop">
|
||||
<name>可以执行 + OI-T1 选 (c)(以额外 prop 传道具列表)</name>
|
||||
<pros>不借存档方法</pros>
|
||||
<cons>与 D-36「入参为 HeroState」不符,须用户确认调整入参</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订清单 / 命名 / 范围后重新汇报</name>
|
||||
<pros>避免带着错误契约落地</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(如需调整 OI-T1..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>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 toolbox.tsx 已存在 / 枚举 API 已新增需重议 GAP-T1)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 OI-T1..OI-T5 的非默认处置或命名方案,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:D-34..D-36 / D-33 / D-37 范围、接口依据(含 GAP-T1 `IHeroItems` 无枚举)、命名清单、OI-T1..OI-T5、验证方式(静态断言,无全仓门禁)、范围与硬边界
|
||||
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不接线打开方式、不做丢弃 / 拖拽**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1(布局 + 列表):新建 `toolbox.tsx` —— 三列(永久 / 可消耗道具列表 + 名称 / 说明)</name>
|
||||
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-T1..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/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
|
||||
- packages-user/client-modules/src/shared.ts:79-87(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
|
||||
- .planning/images/toolbox.png、packages/legacy-ui/src/ui/toolbox.vue(布局参考)
|
||||
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF)
|
||||
</read_first>
|
||||
<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`)两组。
|
||||
|
||||
(3) **排序(D-35):** 各组内按 `count` **从多到少**排序;`count` 相同时按 id **升序**(键取 `num`)。排序不得就地修改原数组(先 `slice()` 再 `sort`)。
|
||||
|
||||
(4) **布局(D-34):** 三列;左列「永久道具」标题 + 纵向 `<Scroll noscroll>` 列表;中列「可消耗道具」标题 + 纵向 `<Scroll noscroll>` 列表;右列自上而下「道具名称」(当前选中项 `raw.name`)与「道具说明」(`raw.text`,纵向 `<Scroll noscroll>`);右下角退出按钮。
|
||||
|
||||
(5) **行渲染:** 每项 `<icon icon={num}>` + `raw.name` + 数量(`count`,格式化沿用 `core.formatBigNumber`,OI-T4 默认);选中项以 `<Selection>` 或高亮样式标识。
|
||||
|
||||
(6) **收口:** `import` 置首、块内无空行/注释(`dev.md:55`);无 `import type`;无 `?.`;不改既有注释;CRLF;无未使用 import;不改 `files_modified` 外文件。
|
||||
</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>
|
||||
<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>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `toolbox.tsx` 存在,导出 `ToolBox` / `ToolBoxUI`;props 数组含 `controller,instance,state,heroState`
|
||||
- 三列布局(永久 / 可消耗 / 名称+说明);各组按 `count` 降序、同数量按 `num` 升序;过滤 `hideInToolbox`
|
||||
- 名称取 `raw.name`、说明取 `raw.text`;数量展示;列表纵向滚动 `noscroll`;右下退出按钮存在
|
||||
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改既有注释;未改 `files_modified` 外文件
|
||||
</acceptance_criteria>
|
||||
<done>布局与列表完成:三列 + 分类过滤 + 排序 + 名称 / 说明区,静态门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2(交互 + 门禁):实现单击选中 / 再点使用(`useItem`)/ 右下退出,并跑最终静态门禁</name>
|
||||
<precondition>Task 1 已完成(`toolbox.tsx` 布局与列表已落地)。</precondition>
|
||||
<files>packages-user/client-modules/src/ui/toolbox.tsx</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/ui/toolbox.tsx(Task 1 产物)
|
||||
- packages-user/data-base/src/hero/types.ts:634(`useItem`)、items.ts:84-113(`useItem` 语义:Constant/Consumable 生效、Consumable 数量减一)
|
||||
- packages-user/client-modules/src/ui/title.tsx:194-253(`onClick` 事件写法)
|
||||
- dev.md(无 `?.`;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **选中状态:** 以 `ref` 记录当前选中道具 `id`(或 `num`);点击某道具时,若与当前选中不同 → 仅选中(不消费);若与当前选中相同 → 触发使用。
|
||||
|
||||
(2) **使用(D-36):** 再次点击选中项 → `heroState.items.useItem(item)`(`item` 为 `num` 或 id);返回 `false` 时不消费(按 OI-T3 默认不做额外提示,仅保持界面)。使用后界面数值随数据端删除 / 减量自然刷新(`items` 为响应式来源)。
|
||||
|
||||
(3) **退出(D-37):** 右下角按钮 → `props.controller.close(props.instance)`。
|
||||
|
||||
(4) **收口与门禁:** 规范收口(import 置首 / 无 `import type` / 无 `?.` / CRLF / 无未使用 import / 不改既有注释);本任务 `<verify>` 运行最终静态门禁 + CRLF + 范围守卫。**不做丢弃 / 拖拽**(D-36)。
|
||||
</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=[['items.useItem(','useItem call'],['onClick','click select'],['selected','selection state'],['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(/onDrag|draggable|drop\(/.test(t))bad.push('unexpected drag/drop (D-36 forbids)');}if(bad.length>0){console.error('TOOLBOX INTERACTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK toolbox: select/use/exit present; no drag/drop')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "TOOLBOX 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/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>
|
||||
<automated>git -C . status --porcelain</automated>
|
||||
<fails_when>输出包含 `packages/system/`、`packages/render`、`packages-user/data-`、`legacy-ui/` 或 09-01..09-05 的既有文件(范围外改动)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 单击选中;再次点击选中项调用 `heroState.items.useItem(item)`
|
||||
- 右下退出调用 `controller.close(props.instance)`;无丢弃 / 拖拽
|
||||
- 无 `import type`、无 `?.`、无连续 `as`、CRLF、无未使用 import;范围守卫无越界改动
|
||||
</acceptance_criteria>
|
||||
<done>交互完成;最终静态门禁 + CRLF + 范围守卫全绿</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 数据读取边界 | 以 `saveState().items` 借用存档方法作枚举来源(GAP-T1 风险点) |
|
||||
| 数据写入边界 | 使用道具写 `IHeroItems.useItem`;不直接改内部 Map |
|
||||
| 命名边界 | 所有新增公共命名须 Task 0 获批后方可使用 |
|
||||
| 范围边界 | 只新增 `toolbox.tsx`;不碰既有文件与其它界面 |
|
||||
| 提交边界 | 不得把用户并发改动(D-56)纳入暂存/提交 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| 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-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 |
|
||||
| T-09-08-08 | Tampering | 提交时混入用户并发改动(D-56) | medium | mitigate | 提交前 `git status` 逐条核对,只暂存本计划条目 |
|
||||
| T-09-08-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装 |
|
||||
</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 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 的最终裁定)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 布局:三列(永久 / 可消耗道具列表 + 名称 / 说明);占满游戏画面;列表纵向滚动不显示滚动条;右下退出。
|
||||
- 数据:`heroState.items` 枚举(GAP-T1 处置);过滤 `hideInToolbox`;按 `count` 降序、同数量按 `num` 升序;入参 `IHeroState<IHeroAttr>`。
|
||||
- 交互:单击选中、再点使用(`useItem`);无丢弃 / 拖拽。
|
||||
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.` / 连续 `as`。
|
||||
- OI-T1..OI-T5 已按用户裁定落地;未采纳数据端方案时明确阻塞并记录。
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| 符号 / 路径 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `packages-user/client-modules/src/ui/toolbox.tsx`(新增) | 组件 / UI | 道具栏:`ToolBox` / `ToolBoxUI` / `ToolBoxProps`(命名待批准) |
|
||||
|
||||
**新增导出命名(全部须 Task 0 批准):** 文件 `toolbox.tsx`;组件 `ToolBox`;`GameUI` 名 `'toolbox'`;props 接口 `ToolBoxProps`;导出 `ToolBoxUI`。**不向 `src/ui/index.ts` 添加导出**(保持与同 wave 计划互不冲突)。
|
||||
|
||||
## Source Coverage Audit
|
||||
|
||||
| 来源 | 项 | 处置 |
|
||||
|------|----|------|
|
||||
| GOAL | Phase 9(ROADMAP)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·任务 3 子切片:toolbox 移植) |
|
||||
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
|
||||
| RESEARCH | 无 RESEARCH.md | N/A |
|
||||
| CONTEXT | D-34(三列布局:永久 / 可消耗 / 名称+说明) | COVERED(Task 1) |
|
||||
| CONTEXT | D-35(永久 / 可消耗分类 / 数量降序 / 同数量按 id) | COVERED(Task 1) |
|
||||
| CONTEXT | D-36(单击选中 / 再点使用 / 无丢弃无拖拽 / 入参 HeroState) | 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/equipbox/shop) | OUT OF SCOPE(各自计划 / shop 暂缓) |
|
||||
| 缺口 | `IHeroItems` 无枚举已拥有道具的公开方法 | FLAGGED(OI-T1 / GAP-T1,待用户裁定) |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-08-SUMMARY.md` when done
|
||||
</output>
|
||||
@ -98,14 +98,21 @@
|
||||
- **D-35(列表 / 排序):** 道具分为**永久道具**与**可消耗道具**;各自按**数量从多到少**排序,**数量相同按 id 排序**。
|
||||
- **D-36(交互 / 入参):** **单击选中、再次点击使用**;**无丢弃、无拖拽**功能。入参为 **`HeroState`**。
|
||||
|
||||
### 任务 3 · shop 商店界面(讨论已定)
|
||||
### 任务 4 · shop 商店界面(讨论已定 · 暂缓规划,待 `ShopState` 接口)
|
||||
|
||||
> 参照图:`.planning/images/shop.png`(仅布局参考)。
|
||||
> 参照图:`.planning/images/shop.png`(仅布局参考)。**本界面推迟**:等用户提供数据端 `ShopState` 接口后再规划(用户决定)。以下决策暂不打入计划。
|
||||
|
||||
- **D-38(布局):** 顶部「**购买选项 | 售出选项**」切换;左「**商店列表**」;右列自上而下 = 「**选中物品名称**」/「**选中物品描述**」/「**购入量选择**」(其**按钮上方**显示 **买价 / 卖价 / 存货 / 拥有 / 总价** 等价格信息)/「**购买按钮**」。占满游戏画面。
|
||||
- **D-39(模式 / 列表):** 可在**购入 / 售出**间切换;**售出也用「商店列表」**(商店不卖的,玩家也不能从背包售出);列表按 **id 排序**;**库存显示在物品最右侧、右对齐**;列表**可滚动、显示滚动条**。
|
||||
- **D-40(数量语义):** 数量调整四个按钮 `<< < {当前购入/售出量} > >>`,步进**同 legacy**(`<<`/`>>` = ±10,`<`/`>` = ±1);上限 = **购入:商店库存**、**售出:玩家拥有数量**;**数量按物品分别记忆**(切换物品时保存各自数量);「购买按钮」买**当前选中物品**的数量。
|
||||
- **D-41(边界 / 依赖):** 本任务**只负责 UI 编写,不负责消费者如何调用**(**打开方式不管**)。商店数据需一个专门的 **`ShopState`**(放在**数据端**)——**当前无接口,后续由用户提供**;shop UI 依赖该接口。
|
||||
- **D-38[deferred](布局):** 顶部「**购买选项 | 售出选项**」切换;左「**商店列表**」;右列自上而下 = 「**选中物品名称**」/「**选中物品描述**」/「**购入量选择**」(其**按钮上方**显示 **买价 / 卖价 / 存货 / 拥有 / 总价** 等价格信息)/「**购买按钮**」。占满游戏画面。
|
||||
- **D-39[deferred](模式 / 列表):** 可在**购入 / 售出**间切换;**售出也用「商店列表」**(商店不卖的,玩家也不能从背包售出);列表按 **id 排序**;**库存显示在物品最右侧、右对齐**;列表**可滚动、显示滚动条**。
|
||||
- **D-40[deferred](数量语义):** 数量调整四个按钮 `<< < {当前购入/售出量} > >>`,步进**同 legacy**(`<<`/`>>` = ±10,`<`/`>` = ±1);上限 = **购入:商店库存**、**售出:玩家拥有数量**;**数量按物品分别记忆**(切换物品时保存各自数量);「购买按钮」买**当前选中物品**的数量。
|
||||
- **D-41[deferred](边界 / 依赖):** 本任务**只负责 UI 编写,不负责消费者如何调用**(**打开方式不管**)。商店数据需一个专门的 **`ShopState`**(放在**数据端**)——**当前无接口,后续由用户提供**;shop UI 依赖该接口。
|
||||
|
||||
### 任务 3 · 待用户补充的数据端接口(阻塞,补充后再继续规划)
|
||||
|
||||
- **book**:按「**基础属性**」计算临界/伤害的接口(当前无;现有 `calculateCritical` / `getDamageInfo` 只走计算后属性);`@user/data-system` 是否允许加入 `client-modules` 依赖。
|
||||
- **equipbox**:**单件装备增减**接口(空槽 / 仅卸下);确认槽位真实路径为 **`heroState.equip.slots`**(非 `state.equipment.slots`)。
|
||||
- **toolbox**:**已拥有道具枚举**接口。
|
||||
- **`<icon>`**:其实现为 **legacy**,需后续改造;**当前以它占位**。
|
||||
|
||||
### the agent's Discretion
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user