mirror of
https://github.com/motajs/template.git
synced 2026-10-12 15:50:16 +08:00
docs(09): create phase plan (09-09 shop)
This commit is contained in:
parent
7140ae6987
commit
ff71899f12
@ -481,7 +481,7 @@ Plans:
|
||||
**Goal**: 在已完成的数据端(可在 Node 环境独立运行/回放验证)之上完成渲染端框架,产出可在浏览器中运行并验证的最小版本
|
||||
**Requirements**: TBD
|
||||
**Depends on**: Phase 8
|
||||
**Plans**: 5/8 plans executed (任务 1·UI 接口适配,D-10..D-14 已闭环;任务 2·迁移后 UI 修复,D-15..D-17 已闭环;任务 3·legacy UI 移植,book/equipbox/toolbox 已规划(09-06..09-08)并已按用户补齐的数据端接口修订——裸怪物临界 `calculateEnemyCritical`、与空装备对比 `compareEquip(-1)`、道具枚举 `iterateItems`、`@user/data-system` 依赖已声明;shop 暂缓待 `ShopState`;本阶段增量派发,其余任务待用户再派发)
|
||||
**Plans**: 5/9 plans executed (任务 1·UI 接口适配,D-10..D-14 已闭环;任务 2·迁移后 UI 修复,D-15..D-17 已闭环;任务 3·legacy UI 移植,book/equipbox/toolbox 已规划(09-06..09-08)并已按用户补齐的数据端接口修订——裸怪物临界 `calculateEnemyCritical`、与空装备对比 `compareEquip(-1)`、道具枚举 `iterateItems`、`@user/data-system` 依赖已声明;shop 已规划(09-09,D-38..D-41),消费用户提供的 `ShopState` 接口(实现待补),价格/交易/支付资源/公共面导出为待用户裁定的缺口;本阶段增量派发,其余任务待用户再派发)
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
@ -509,6 +509,7 @@ Plans:
|
||||
- [ ] 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>`)
|
||||
- [ ] 09-09-PLAN.md — 任务 4·shop 商店界面(D-38..D-41):`shop.tsx`(顶部 购买/售出 切换;左 商店列表 id 排序/库存右对齐/显滚动条;右 名称·描述·购入量选择(按钮上方 买价/卖价/存货/拥有/总价)·购买按钮;四按钮 `<< < {量} > >` ±10/±1、按物品分别记忆;右下退出;入参 `IShopState`(用户提供的接口)+ `IHeroState<IHeroAttr>`;价格/交易/支付资源/导出为待裁定缺口)
|
||||
|
||||
## Progress
|
||||
|
||||
@ -525,4 +526,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/8 | In Progress| |
|
||||
| 9. 最小浏览器可运行版本 | 5/9 | In Progress| |
|
||||
|
||||
347
.planning/phases/09-minimum-browser-runnable/09-09-PLAN.md
Normal file
347
.planning/phases/09-minimum-browser-runnable/09-09-PLAN.md
Normal file
@ -0,0 +1,347 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 9
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on:
|
||||
- 09-05
|
||||
files_modified:
|
||||
- packages-user/client-modules/src/ui/shop.tsx
|
||||
- .planning/phases/09-minimum-browser-runnable/09-09-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
tokens: 78000
|
||||
raw_tokens: 78000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-22)**:Task 0 必须先汇报「本计划 = 任务 4 · shop 商店界面」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**逐一列出所有新增命名(文件名、组件名、`GameUI` 名、props 接口名、行类型名)请求批准**,并**明确列出接口缺口(GAP-S0..GAP-S3)与开放项(OI-S1..OI-S5)**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
|
||||
- "**D-38(布局):** 顶部「**购买选项 | 售出选项**」切换;左「**商店列表**」;右列自上而下 = 「**选中物品名称**」/「**选中物品描述**」/「**购入量选择**」(其**按钮上方**显示 **买价 / 卖价 / 存货 / 拥有 / 总价** 等价格信息)/「**购买按钮**」。占满整个游戏画面(`FULL_LOC`,D-33);退出按钮右下(D-37)。参考图 `.planning/images/shop.png` 仅布局参考。"
|
||||
- "**D-39(模式 / 列表):** 可在**购入 / 售出**间切换;**售出也用「商店列表」**(商店不卖的,玩家也不能从背包售出);列表**按 id 排序**(等价身份键取 `num`);**库存显示在物品最右侧、右对齐**;列表**可滚动、显示滚动条**(`<Scroll loc={...}>` 不加 `noscroll`)。"
|
||||
- "**D-40(数量语义):** 数量调整四个按钮 `<< < {当前购入/售出量} > >>`,步进**同 legacy**(`<<`/`>>` = ±10,`<`/`>` = ±1);上限 = **购入:商店库存**(`shop.getCount(num)`)、**售出:玩家拥有数量**(`heroState.items.itemCount(num)`);**数量按物品分别记忆**(切换物品时保存各自数量);「购买按钮」买**当前选中物品**的数量。"
|
||||
- "**D-41(边界 / 依赖):** 本任务**只负责 UI 编写,不负责消费者如何调用**(**打开方式不管**);商店数据由数据端 **`IShopState`**(`packages-user/data-base/src/shop/types.ts:12-43`,**用户已提供 · 实现待补**)承载;入参经 props 传入(不自建单例、不接线打开方式)。"
|
||||
- "**接口现状(须 Task 0 呈报用户确认,含缺口):** 已提供 `IShopState`(`enabled` / `addItem(token,count)` / `getCount(token)`(不存在返回 `-1`)/ `iterateItems(): Iterable<[num,count]>` / `enable()` / `disable()`,`shop/types.ts:12-43`)与 `IShopManager`(`shop/types.ts:50-95`)。**缺口:** GAP-S0(`IShopState` 未从 `@user/data-base` 公共面导出,`data-base/src/index.ts:1-6` 无 `./shop`,且无 `shop/index.ts`);GAP-S1(**买价 / 卖价**缺来源——`IShopState` / `IItemRawData` 均无价格字段,legacy 价格在 `ItemShopChoice.money`/`.sell`,`src/types/declaration/eventDec.d.ts:110,115`);GAP-S2(**购买 / 售出交易**缺方法——`IShopState` 仅 `addItem` 调库存,无金钱 / 物品转移+扣库存的整体交易);GAP-S3(**支付资源** `money` vs `exp` 缺字段——legacy `ItemShopEvent.use`,`eventDec.d.ts:135`)。**一律 flag,不 invent。**"
|
||||
- "**范围零外溢 + 静态门禁:** 仅新增 `shop.tsx`;**不修改** 09-01..09-08 任何文件、不修改任何既有 UI / `src/render/**` / `packages/system` / `packages/render*` / 数据端(`packages-user/data-*`,**含 `shop/types.ts` 与 `index.ts`**)/ `legacy-ui`(**不实现数据端 API、不补 barrel 导出**);验证一律为**静态源码断言**(文件存在、关键符号存在、props 数组含 `controller/instance/state/shop/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/shop.tsx
|
||||
provides: "D-38..D-41 的商店 UI:顶部 购买/售出 切换;左侧 商店列表(id 排序 / 库存右对齐 / 可滚动显滚动条);右侧 物品名称 / 描述(纵滚无滚动条)/ 购入量选择(`<< < {量} > >>`,按物品分别记忆,上限 库存/拥有,按钮上方 买价/卖价/存货/拥有/总价)/ 购买按钮;右下退出;入参 `IShopState` + `IHeroState<IHeroAttr>`。"
|
||||
key_links:
|
||||
- "`packages-user/client-modules/src/ui/shop.tsx` ↔ `IShopState`(`packages-user/data-base/src/shop/types.ts:12-43`)—— 商店数据来源(`iterateItems` / `getCount` / `enabled`;**当前未从包公共面导出,GAP-S0 待用户补齐**)"
|
||||
- "`shop.tsx` ↔ `IShopState.iterateItems(): Iterable<[num: number, count: number]>`(`shop/types.ts:32`)—— 商店列表来源(购入与**售出共用**同一列表,D-39);`IShopState.getCount(token)`(`:27`)—— 商店库存(不存在返回 `-1`)"
|
||||
- "`shop.tsx` ↔ `props.state.itemStore.getData(token)` → `IItemRawData`(`name` / `text` / `category`,`data-common/src/store/types.ts:176-200`)—— 物品名称 / 描述"
|
||||
- "`shop.tsx` ↔ `props.state.tileStore.num(token)` / `id(token)`(`store/types.ts:106,112`)—— num ↔ id 归一(列表按 id 排序、图标取 num)"
|
||||
- "`shop.tsx` ↔ `IHeroState.items.itemCount(num)`(`data-base/src/hero/types.ts:645`)/ `getItemState(num)`(`:627`)/ `addItem(num,count)`(`:621`)—— 玩家拥有数量 / 交易物品转移"
|
||||
- "`shop.tsx` ↔ `IHeroState.attribute.getFinalAttribute('money')`(`hero/types.ts:117,933`)+ `IHeroState.getModifiableAttribute().add('money', delta)`(`hero/types.ts:198,947`)—— 当前金币 / 交易金额变更"
|
||||
- "`shop.tsx` ↔ `props.state`(`IClientBase`,`client-base/src/types.ts:15-50`)+ `IUIPropsBase`(`client-modules/src/ui/types.ts:1-5`)+ `props.controller.close(props.instance)`(D-37)—— 主对象经 props 获取 / 退出"
|
||||
- "`shop.tsx` ↔ `client-base/src/components/scroll.tsx`(`<Scroll>`(显滚动条)/ `<Scroll noscroll>`(描述),`:109`)+ `components/misc.tsx`(`<Selection>`,`:304`)+ `elements/props.ts`(`<icon>`,`:6-15`)—— 渲染骨架"
|
||||
assumptions:
|
||||
- "**`IShopState` 可导入(GAP-S0)**:假定用户在 Task 0 前补齐 `data-base` 公共面导出(`shop/index.ts` + `index.ts` `export * from './shop'`,或等价);若未补齐,Task 1 因无法命名 `shop: IShopState` 而**阻塞**(不 invent 类型、不改数据端)。"
|
||||
- "**「列表按 id 排序」等价身**:列表项为 `[num, count]`;排序键用 `num` 升序(数值稳定,与字符串 id 一一对应,需 id 时经 `tileStore.id(num)`)。"
|
||||
- "**「拥有数量」**:默认 `heroState.items.itemCount(num)`(`hero/types.ts:645`);若用户要求显示装备等其它口径,据其指定。"
|
||||
- "**「当前金币」**:默认 `heroState.attribute.getFinalAttribute('money')`。若 GAP-S3 确认商店使用 `exp`,支付资源改为 `exp`(据用户裁定)。"
|
||||
- "**买价 / 卖价 / 交易(GAP-S1 / GAP-S2)默认处置待 Task 0 裁定**:默认方案 = UI 仅消费既有公开接口,价格与交易在数据侧接口补齐前以**明确标记的 `// TODO` 占位**(不 invent 价格 / 交易 / 支付资源接口)。若用户选择「由 UI 以既有公开接口组装交易」(`shop.addItem` + `hero.items.addItem` + `hero.getModifiableAttribute().add('money', …)`),则据其指定实现。"
|
||||
- "**执行日行号可能漂移**:接口锚点为 2026-10-10 规划日;门禁按**符号文本**匹配,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "本计划**只做任务 4 · shop**:不得实现 / 修改 book / equipbox / toolbox;不得接线「如何打开 shop」(打开方式不管);不得实现消费者调用流程。"
|
||||
- "**不得臆造接口 / 命名**:不得新增数据端 API(含价格 / 交易 / 支付资源);不得为 `IShopState` 补价格字段或 barrel 导出;不得新增未在 Task 0 呈报获批的任何公共 / 受保护 / 私有命名。"
|
||||
- "不得修改 `files_modified` 之外的任何**生产**文件;不得改 09-01..09-08 及其产物;不得改 `packages/system` / `packages/render` / `packages/render-vue`;不得改数据端(`packages-user/data-*`,含 `shop/types.ts`、`data-state/src/common.ts`、`src/content/core.jsonc`)与 `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` 的条目(当前并发改动含 `shop/types.ts`(未跟踪)、`data-state/src/common.ts`、`src/content/core.jsonc`——一律不碰)。"
|
||||
open_items:
|
||||
- "**GAP-S0(类型导出)**:`IShopState` / `IShopManager` 未从 `@user/data-base` 公共面导出(`data-base/src/index.ts:1-6` 无 `./shop`,且无 `shop/index.ts`)。**须用户补齐**(数据侧);未补齐前 Task 1 阻塞。"
|
||||
- "**GAP-S1(买价 / 卖价)**:`IShopState` / `IItemRawData` 无价格字段;legacy 价格在 `ItemShopChoice.money`/`.sell`(`eventDec.d.ts:110,115`)。**须用户提供价格来源**,否则 买价/卖价/总价 以 `// TODO` 占位(D-38 价格信息部分受阻)。"
|
||||
- "**GAP-S2(交易)**:`IShopState` 无 购买 / 售出 交易方法。**须用户裁定**:由数据侧提供交易方法,或由 UI 以既有公开接口组装(默认:UI 组装,见 assumptions)。"
|
||||
- "**GAP-S3(支付资源)**:商店支付 `money` 还是 `exp` 缺字段(legacy `ItemShopEvent.use`,`eventDec.d.ts:135`)。默认按 `money`;请确认。"
|
||||
- "**OI-S1(列表范围)**:售出列表是否严格 = 商店列表(D-39 已决「是」,商店不卖的不能售出);确认无「玩家背包中商店不卖的道具」入列。"
|
||||
- "**OI-S2(列表过滤)**:是否过滤 `enabled === false` 的商店 / 无库存物品?默认:单商店界面(入参即一个 `IShopState`),列表 = `iterateItems()` 原样,不做 enabled 过滤。"
|
||||
- "**OI-S3(数量记忆粒度)**:数量按「物品」分别记忆(D-40);默认记忆于组件内 `Map<num, count>`,切换 购买/售出 时是否共用同一记忆?默认**按期模式分别记忆**(购买量与售出量各记),待确认。"
|
||||
- "**OI-S4(数字格式化)**:价格 / 数量格式化默认 `core.formatBigNumber`(沿用仓库新 UI 约定);可否接受?"
|
||||
- "**OI-S5(命名)**:见 Task 0 命名清单。全部待批准(注意 `GameUI` 名 `'shop'` 与既有热键 id `'shop'`(`packages-user/client-modules/src/action/hotkey.ts:75,492`)是否冲突需确认)。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 9 · 任务 3 · 第 9 个计划 —— **shop 商店界面**。
|
||||
|
||||
**要解决的问题(D-38..D-41):** 把 legacy `packages/legacy-ui` 的商店界面(`src/ui/shop.vue`)移植到新 UI 接口上,并按用户已定形态实现:顶部「购买选项 | 售出选项」切换;左「商店列表」;右列自上而下 = 物品名称 / 物品描述 / 购入量选择(按钮上方显示 买价/卖价/存货/拥有/总价)/ 购买按钮;占满游戏画面;购入/售出切换、售出复用商店列表、id 排序、库存最右右对齐、列表滚动带滚动条;数量四按钮 `<< < {量} > >>`(±10 / ±1),数量按物品分别记忆;退出右下。
|
||||
|
||||
**大致解决方案(本计划 = 09-09):** Task 0(`blocking-human`)= 汇报范围、命名清单、**用户提供的 `IShopState` 接口成员**与**接口缺口(GAP-S0..GAP-S3)**、开放项(OI-S1..OI-S5)并等待「可以执行」;Task 1 = 实现 `shop.tsx` 的**布局与列表**(顶栏模式切换、左商店列表、右 名称/描述/信息区,库存右对齐、id 排序、列表显滚动条);Task 2 = 实现**数量选择 + 购买按钮 + 退出**并跑最终静态门禁。
|
||||
|
||||
**接口适配性(本计划关键):** 用户已提供数据端 `IShopState`(`shop/types.ts`,**实现待补**)——**接口成员** = `enabled` / `addItem` / `getCount` / `iterateItems` / `enable` / `disable`。**买价/卖价(GAP-S1)、交易(GAP-S2)、支付资源(GAP-S3)、公共面导出(GAP-S0)缺失**,一律 **flag 不 invent**;由 Task 0 呈报用户裁定后据其落地。**标准。**
|
||||
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为渲染端 UI 移植,数据来自数据端接口(`IShopState` / `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/shop.png
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 4 · shop 商店界面」范围、命名清单、用户提供的 IShopState 接口与接口缺口(GAP-S0..GAP-S3)与开放项,等待用户回复「可以执行」</name>
|
||||
<precondition>`09-05` 已闭环;仓库中 `shop.tsx` **尚不存在**;数据端 `shop/types.ts`(用户未跟踪新增)**已存在**(`IShopState` / `IShopManager`),但 **`IShopState` 尚未从 `@user/data-base` 公共面导出**(GAP-S0)。</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-33 / D-37 / D-38 / D-39 / D-40 / D-41;任务 3 · 数据端接口补充)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-09-PLAN.md(本计划全文,含命名清单、`IShopState` 成员、GAP-S0..GAP-S3 与 OI-S1..OI-S5)
|
||||
- packages-user/data-base/src/shop/types.ts:1-95(`IShopStateSave` / `IShopState`(`enabled`/`addItem`/`getCount`/`iterateItems`/`enable`/`disable`)/ `IShopManager`)
|
||||
- packages-user/data-base/src/index.ts(现有公共面,**无 `./shop`** → GAP-S0)
|
||||
- packages-user/data-common/src/store/types.ts:176-213(`IItemRawData`(`name`/`text`/`category`;**无价格字段** → GAP-S1)/ `IItemStore.getData` / `ITileStore`(`num`/`id`))
|
||||
- packages-user/data-base/src/hero/types.ts:598-646(`IHeroItemState` / `IHeroItems`(`itemCount`/`getItemState`/`addItem`))、:104-198(`IReadonlyHeroAttribute.getFinalAttribute` / `IHeroAttribute.add`)、:926-957(`IHeroState`)
|
||||
- packages-user/client-base/src/types.ts:15-50(`IGameUIPropsBase` / `IClientBase extends ICoreState`);packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)
|
||||
- packages-user/client-base/src/components/scroll.tsx:55-109(`<Scroll>` props:`hor`/`noscroll`/`loc`)、components/misc.tsx:282-304(`<Selection>`)、elements/props.ts:6-15(`<icon>`)
|
||||
- packages-user/client-modules/src/shared.ts:76-98(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
|
||||
- .planning/images/shop.png(**布局参考**)
|
||||
- packages/legacy-ui/src/ui/shop.vue(布局 / 交互参考;D-20:不要求视觉一致)
|
||||
- src/types/declaration/eventDec.d.ts:96-141(legacy `ItemShopChoice`(`money`/`sell`/`number`)/ `ItemShopEvent`(`use: 'money' | 'exp'`)→ GAP-S1 / GAP-S3 证据)
|
||||
- dev.md(import 置首无空行/注释;无 `import type`;避免 `as`;无 `?.`;CRLF;interface 后 implements;不改既有注释)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** 任务 4 · shop 商店界面。把 legacy 商店移植到新接口;顶部 购买/售出 切换;左 商店列表;右 物品名称 / 描述 / 购入量选择(按钮上方 买价/卖价/存货/拥有/总价)/ 购买按钮;占满游戏画面;列表 id 排序、库存右对齐、滚动带滚动条;数量 `<< < {量} > >>`(±10/±1)按物品分别记忆;右下退出;打开方式不管。
|
||||
|
||||
② **锁定裁定原文要点:** D-38(布局:顶切换 / 左列表 / 右 名称·描述·购入量选择·购买按钮;价格信息在按钮上方);D-39(购买/售出切换、售出复用商店列表、按 id 排序、库存最右右对齐、列表滚动显滚动条);D-40(四按钮 ±10/±1、上限 库存/拥有、数量按物品分别记忆、购买按钮买当前选中物品数量);D-41(只做 UI、打开方式不管、数据由 `ShopState` 承载);D-33 / D-37(占满游戏画面 root renderer、退出右下);D-03 / D-04 / D-06(执行前获批 / 命名获批 / 不扩范围)。
|
||||
|
||||
③ **用户提供的接口成员(`IShopState`,须用户确认):** 逐条列出——`enabled`(`:14`)、`addItem(token, count)`(`:21`)、`getCount(token)`(`:27`,不存在返回 `-1`)、`iterateItems(): Iterable<[num, count]>`(`:32`)、`enable()`(`:37`)、`disable()`(`:42`);另有 `IShopManager`(`:50-95`)。**须用户确认 UI 仅消费这些成员。**
|
||||
|
||||
④ **接口缺口(须用户逐一裁定,一律 flag 不 invent):**
|
||||
- **GAP-S0(导出):** `IShopState` 未从 `@user/data-base` 导出(`data-base/src/index.ts` 无 `./shop`,且无 `shop/index.ts`)。**须用户补齐**(数据侧);未补齐前 Task 1 无法命名 `shop: IShopState` 而阻塞。
|
||||
- **GAP-S1(买价 / 卖价):** 无价格来源(`IShopState` / `IItemRawData` 均无价格字段)。legacy 在 `ItemShopChoice.money`/`.sell`(`eventDec.d.ts:110,115`)。**须用户提供价格来源**,否则 买价/卖价/总价 以 `// TODO` 占位。
|
||||
- **GAP-S2(交易):** `IShopState` 无 购买/售出 交易方法(仅 `addItem` 调库存)。**须裁定**:数据侧提供,或 UI 以既有公开接口组装(`shop.addItem` + `hero.items.addItem` + `hero.getModifiableAttribute().add('money', …)`)。
|
||||
- **GAP-S3(支付资源):** `money` vs `exp` 缺字段(legacy `ItemShopEvent.use`,`eventDec.d.ts:135`)。默认 `money`。
|
||||
|
||||
⑤ **命名清单(须用户逐一批准,D-04):**
|
||||
- 新增文件:`packages-user/client-modules/src/ui/shop.tsx`。
|
||||
- 新增组件:`Shop`。
|
||||
- 新增 `GameUI` 名称字符串:`'shop'`(**注意**:与既有热键 id `'shop'`(`action/hotkey.ts:75,492`)是否同命名空间 / 冲突,请确认)。
|
||||
- 新增 props 接口:`ShopProps`(含 `shop: IShopState`、`heroState: IHeroState<IHeroAttr>`,及 `IUIPropsBase` 的 `controller/instance/state`)。
|
||||
- 新增行类型:`IShopItemEntry`(默认名,`{ readonly num: number; readonly id: string; readonly name: string; readonly text: string; readonly stock: number; readonly owned: number }`;如用户指定别名据其指定)。
|
||||
- 新增导出:`ShopUI`(`new GameUI(...)`)。
|
||||
- **不修改**任何既有命名 / 单例;不向 `src/ui/index.ts` 添加导出(保持与同 wave 计划互不冲突)。
|
||||
|
||||
⑥ **验证方式(须用户确认):** 静态源码断言(文件存在 / 关键符号 / props 数组含 `controller,instance,state,shop,heroState` / 无 `client` 单例 / 无 `import type` / 无 `?.` / CRLF / 范围守卫)。**不设全仓类型 / 构建门禁。**
|
||||
|
||||
⑦ **范围与硬边界:** 仅新增 `shop.tsx`;**零触碰** = 09-01..09-08 产物、`src/render/**`、`packages/system`、`packages/render*`、`packages-user/data-*`(含 `shop/types.ts`、`index.ts`、`data-state/src/common.ts`、`src/content/core.jsonc`)、`legacy-ui`、book/equipbox/toolbox、用户并发改动(D-56)。
|
||||
|
||||
⑧ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字;回复其它内容视为未通过。若用户给出修订意见或补齐缺口(GAP-S0..GAP-S3),退出本计划、据其裁定修订后重新汇报。
|
||||
</action>
|
||||
<decision>是否按本计划实现任务 4 · shop 商店界面,并按用户裁定处置接口缺口:GAP-S0(用户补齐 `IShopState` 公共面导出);GAP-S1(用户提供买价/卖价来源,或接受 `// TODO` 占位);GAP-S2(数据侧提供交易方法,或 UI 以既有公开接口组装);GAP-S3(支付资源默认 money);OI-S1..OI-S5 照准;命名清单照准</decision>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = 命名清单 + 确认用户提供的 `IShopState` 成员 + 裁定四处接口缺口(GAP-S0..S3)。行号为规划日实测、可能漂移,门禁按符号文本匹配。**本计划的 UI 契约依赖用户对 GAP-S0..GAP-S3 的裁定:GAP-S0 未补齐则 Task 1 阻塞;GAP-S1 未提供则价格字段以 `// TODO` 占位。**
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-default">
|
||||
<name>可以执行(用户补齐 GAP-S0 并裁定 S1/S2/S3 后采纳默认处置)</name>
|
||||
<pros>一次落地布局 + 列表 + 数量 + 购买;UI-only 不越界;缺口按用户裁定落地</pros>
|
||||
<cons>须用户先补齐 `IShopState` 导出;价格 / 交易按裁定可能先占位</cons>
|
||||
</option>
|
||||
<option id="execute-ui-compose">
|
||||
<name>可以执行 + GAP-S2 选「UI 以既有公开接口组装交易」</name>
|
||||
<pros>无需数据侧新增交易方法;用既有 `shop.addItem`/`hero.items.addItem`/`hero.getModifiableAttribute().add('money',…)`</pros>
|
||||
<cons>交易语义进入 UI 层;GAP-S1(价格)仍需来源</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订清单 / 命名 / 范围 / 缺口处置后重新汇报</name>
|
||||
<pros>避免带着错误契约落地</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(如需调整 GAP-S0..GAP-S3 的处置、OI-S1..OI-S5 或命名方案,请在回复中指明;修订意见请按 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/shop.tsx'))bad.push('post-state: shop.tsx already exists');if(!has('packages-user/data-base/src/shop/types.ts'))bad.push('pre-state changed: shop/types.ts missing');else{const s=read('packages-user/data-base/src/shop/types.ts');for(const [re,label] of [[/interface IShopState\b/,'IShopState'],[/getCount\(token/,'getCount'],[/iterateItems\(\): Iterable/,'iterateItems'],[/addItem\(token/,'addItem'],[/interface IShopManager\b/,'IShopManager']]){if(!re.test(s))bad.push('pre-state changed: '+label+' missing')}}const idx=read('packages-user/data-base/src/index.ts');if(/'\.\/shop'/.test(idx))bad.push('note: GAP-S0 already resolved (shop barrel export present) - re-review Task 1 precondition');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (shop.tsx absent; IShopState present)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(汇报基线不成立,或 `shop.tsx` 已存在 / `IShopState` 已漂移)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户须就 GAP-S0..GAP-S3 的处置与命名清单给出裁定;若用户指定非默认处置,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:D-38..D-41 / D-33 / D-37 范围、用户提供的 `IShopState` 成员清单、接口缺口 GAP-S0..GAP-S3(含 GAP-S0 未补齐则 Task 1 阻塞)、命名清单、OI-S1..OI-S5、验证方式(静态断言,无全仓门禁)、范围与硬边界
|
||||
- 汇报中已原样告知:**不新增未获批命名、不改既有注释、不改数据端 / 系统层 / 渲染系统、不补数据端 barrel 导出、不 invent 价格/交易/支付资源接口、不接线打开方式**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1(布局 + 列表):新建 `shop.tsx` —— 顶部 购买/售出 切换 + 左侧 商店列表(id 排序 / 库存右对齐 / 显滚动条)+ 右侧 名称 / 描述 / 信息区</name>
|
||||
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字,且 **GAP-S0 已由用户补齐**(`IShopState` 可从 `@user/data-base` 导入;若未补齐则本任务阻塞,不得 invent 类型或改数据端)。若用户就 GAP-S1..GAP-S3 / OI-S1..OI-S5 另有指定,按指定执行。</precondition>
|
||||
<files>packages-user/client-modules/src/ui/shop.tsx</files>
|
||||
<read_first>
|
||||
- packages-user/data-base/src/shop/types.ts:1-95(`IShopState` / `IShopManager`)
|
||||
- packages-user/data-common/src/store/types.ts:176-213(`IItemRawData` / `IItemStore.getData`;`ITileStore` 的 `num`/`id`)
|
||||
- packages-user/data-base/src/hero/types.ts:598-646(`IHeroItemState` / `IHeroItems`)
|
||||
- packages-user/client-modules/src/ui/types.ts:1-5(`IUIPropsBase`)
|
||||
- packages-user/client-modules/src/ui/title.tsx:56-64,352-478(组件骨架 / props 数组 / 渲染树 / `<text>` + `g-rectr` 按钮写法)
|
||||
- packages-user/client-modules/src/ui/viewmap.tsx:44-56,53-90(`GameUI` + props 数组 + 列表骨架)
|
||||
- packages-user/client-base/src/components/scroll.tsx:55-109(`<Scroll>` / `<Scroll noscroll>`)、components/misc.tsx:282-304(`<Selection>`)、elements/props.ts:6-15(`<icon>`)
|
||||
- packages-user/client-modules/src/shared.ts:76-98(`MAIN_WIDTH` / `MAIN_HEIGHT` / `FULL_LOC`)
|
||||
- .planning/images/shop.png、packages/legacy-ui/src/ui/shop.vue(布局参考)
|
||||
- dev.md(import 置首;无 `import type`;无 `?.`;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **新建 `shop.tsx`(仅此文件)。** 命名须已在 Task 0 获批:`export interface ShopProps extends IUIPropsBase { shop: IShopState; heroState: IHeroState<IHeroAttr>; }`;`const shopProps = { props: ['controller','instance','state','shop','heroState'] } satisfies SetupComponentOptions<ShopProps>;`;`export const Shop = defineComponent<ShopProps>(props => { ... }, shopProps);`;`export const ShopUI = new GameUI('shop', Shop);`。根容器 `FULL_LOC`(D-33)。
|
||||
|
||||
(2) **行类型与列表数据(D-39):** 定义 `export interface IShopItemEntry { readonly num: number; readonly id: string; readonly name: string; readonly text: string; readonly stock: number; readonly owned: number; }`(命名待批准)。以响应式 `computed` 由 `props.shop.iterateItems()`(`IShopState`,`shop/types.ts:32`,`Iterable<[num, count]>`)构建条目:`num` / `id`(`props.state.tileStore.id(num)`,`store/types.ts:112`)/ `name`、`text`(`props.state.itemStore.getData(num)` → `IItemRawData.name`/`.text`,`store/types.ts:182,184`)/ `stock`(`props.shop.getCount(num)`,`:27`)/ `owned`(`props.heroState.items.itemCount(num)`,`hero/types.ts:645`)。**列表按 `num` 升序排序**(D-39「按 id 排序」的等价身份键;先 `slice()` 再 `sort`,不就地改动)。
|
||||
|
||||
(3) **顶栏(D-38 / D-39):** 顶部「购买选项 | 售出选项」切换,用 `ref<'buy' | 'sell'>` 记录模式;售出模式**复用同一商店列表**(D-39,商店不卖的玩家也不能售出)。
|
||||
|
||||
(4) **左侧商店列表(D-39):** 纵向 `<Scroll loc={[...]}>`(**不加 `noscroll`**,显示滚动条)平铺 `props.shop` 条目;每行 = `<icon icon={entry.num} />` + 名称(`entry.name`)+ **库存(`entry.stock`)显示在物品最右侧、右对齐**;点击行 → 选中(`selected`)。选中项以 `<Selection>` 或高亮样式标识。
|
||||
|
||||
(5) **右侧信息区(D-38,自上而下):** 「选中物品名称」(`entry.name`)→「选中物品描述」(`entry.text`,纵向 `<Scroll noscroll>`,**不显示滚动条**)→「购入量选择」(本任务先占位于底部,具体见 Task 2)→「购买按钮」(Task 2)。价格信息行(买价/卖价/存货/拥有/总价)按 Task 0 裁定的来源实现;**GAP-S1 未提供时,买价/卖价/总价 以明确标记的 `// TODO` 占位(不 invent 价格接口)**,存货(`stock`)/拥有(`owned`)照常显示。
|
||||
|
||||
(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/shop.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('shop.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['interface ShopProps','props interface'],['IShopState','shop state type'],['IHeroState','hero state type'],['shop','shop prop'],['heroState','heroState prop'],['IShopItemEntry','row type'],['.iterateItems(','enumerate shop items'],['.getCount(','stock lookup'],['itemCount(','owned count'],['itemStore','item store'],['getData(','item raw'],['.text','description'],['.name','name'],['<Scroll','scroll'],['noscroll','description no-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(!/ShopUI\s*=\s*new GameUI\(/.test(t))bad.push('ShopUI not exported');if(!/props:\s*\[\s*'controller'\s*,\s*'instance'\s*,\s*'state'\s*,\s*'shop'\s*,\s*'heroState'\s*\]/.test(t))bad.push('props array incomplete');}if(bad.length>0){console.error('SHOP LAYOUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK shop: layout + shop list (sorted, stock right-aligned, scrollable) present')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SHOP LAYOUT GATE FAILED"(文件缺失 / 关键符号缺失 / props 数组不含 `shop`/`heroState` / 引入 `import type` 或 `?.`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const raw=fs.readFileSync('packages-user/client-modules/src/ui/shop.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: shop.tsx')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `shop.tsx` 存在,导出 `Shop` / `ShopUI` / `IShopItemEntry`;props 数组含 `controller,instance,state,shop,heroState`
|
||||
- 顶部 购买/售出 切换;售出复用同一商店列表;列表来自 `props.shop.iterateItems()` 按 `num` 升序,库存最右右对齐,纵向 `<Scroll>`(**不加 `noscroll`**,显示滚动条)
|
||||
- 右侧 名称(`name`)+ 描述(`text`,`<Scroll noscroll>` 不显示滚动条)+ 信息区占位 + 购买按钮位;存货(`getCount`)/拥有(`itemCount`)显示
|
||||
- 买价/卖价/总价:GAP-S1 已提供则按来源显示;未提供则以明确 `// TODO` 占位(**不 invent 价格接口**)
|
||||
- 无 `import type`、无 `?.`、CRLF、无未使用 import;未改既有注释;未改 `files_modified` 外文件
|
||||
</acceptance_criteria>
|
||||
<done>布局与列表完成:顶栏切换 + 商店列表(排序/库存右对齐/显滚动条)+ 名称 / 描述区,静态门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2(数量 + 购买 + 退出 + 门禁):实现四按钮数量选择(按物品分别记忆 / 上限 库存·拥有)+ 购买按钮 + 右下退出,并跑最终静态门禁</name>
|
||||
<precondition>Task 1 已完成(`shop.tsx` 布局与列表已落地)。</precondition>
|
||||
<files>packages-user/client-modules/src/ui/shop.tsx</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/ui/shop.tsx(Task 1 产物)
|
||||
- packages-user/data-base/src/shop/types.ts:12-43(`IShopState.addItem` / `getCount` / `iterateItems`)
|
||||
- packages-user/data-base/src/hero/types.ts:621-645(`IHeroItems.addItem` / `itemCount`)、:117,198,933,947(`getFinalAttribute('money')` / `IHeroAttribute.add` / `getModifiableAttribute`)
|
||||
- packages/legacy-ui/src/ui/shop.vue:113-158,205-275(数量四按钮 / 上限 watch / `confirm` 交易参考)
|
||||
- packages-user/client-modules/src/ui/title.tsx:194-253,397-412(`onClick` 事件写法)
|
||||
- dev.md(无 `?.`;避免连续 `as`;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **数量状态与记忆(D-40 / OI-S3):** 以 `ref` 记录当前选中物品与当前数量;以 `Map<number, number>`(按物品 `num`)记忆数量,**切换物品时保存各自数量、切回时恢复**;购买 / 售出模式分别记忆(默认按 OI-S3)。默认上限 = 购入:商店库存(`props.shop.getCount(num)`,为 `-1` 时视为 0)、售出:玩家拥有(`props.heroState.items.itemCount(num)`)。
|
||||
|
||||
(2) **四按钮(D-40):** `<< < {当前购入/售出量} > >>`,步进同 legacy(`<<`/`>>` = ±10,`<`/`>` = ±1);点击后 `clamp` 至 `[0, 上限]`。
|
||||
|
||||
(3) **信息区(D-38):** 在四个按钮**上方**显示 买价 / 卖价 / 存货 / 拥有 / 总价(存货 = `stock`、拥有 = `owned`、总价 = 数量 × 单价)。买价 / 卖价 / 总价按 Task 0 裁定处置(GAP-S1:来源提供则显示,否则明确 `// TODO` 占位,**不 invent**)。
|
||||
|
||||
(4) **购买按钮(D-40 / GAP-S2 / GAP-S3):** 「购买按钮」购买**当前选中物品**的数量;购买 / 售出按当前模式执行。交易按 Task 0 裁定处置:数据侧提供交易方法则调用之;否则按默认(UI 以既有公开接口组装:购入 = `heroState.items.addItem(num, count)` + `heroState.getModifiableAttribute().add('money', -总价)` + `props.shop.addItem(num, -count)`;售出 = 反向)——**不得 invent 新接口**。数量为 0 时不执行(同 legacy)。
|
||||
|
||||
(5) **退出(D-37):** 右下角按钮 → `props.controller.close(props.instance)`。
|
||||
|
||||
(6) **收口与门禁:** 规范收口(import 置首 / 无 `import type` / 无 `?.` / 无连续 `as` / CRLF / 无未使用 import / 不改既有注释);本任务 `<verify>` 运行最终静态门禁 + CRLF + 范围守卫。**不做打开方式接线**(D-41)。
|
||||
</action>
|
||||
<reversibility rating="reversible">纯新增文件内交互逻辑;`git revert` 可回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const p='packages-user/client-modules/src/ui/shop.tsx';const bad=[];if(!fs.existsSync(p)){bad.push('shop.tsx missing')}else{const t=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const need=[['购买','buy label'],['售出','sell label'],['买价','buy price label'],['卖价','sell price label'],['存货','stock label'],['拥有','owned label'],['总价','total label'],['count','quantity state'],['onClick','click handlers'],['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('SHOP ACTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK shop: mode toggle + quantity + price info + buy + exit present')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SHOP ACTION GATE FAILED"(数量 / 价格标签 / 交互符号缺失 / 引入 `import type` / `?.` / 连续 `as`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const raw=fs.readFileSync('packages-user/client-modules/src/ui/shop.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: shop.tsx')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(换行非 CRLF)</fails_when>
|
||||
<automated>git -C . status --porcelain</automated>
|
||||
<fails_when>输出包含本计划 `files_modified` 之外的生产文件改动(如 `packages/system/`、`packages/render`、`packages-user/data-`、`shop/types.ts`、`legacy-ui/` 或 09-01..09-08 的既有文件)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 四按钮 `<< < {量} > >>`:步进 ±10 / ±1,`clamp` 至 `[0, 上限]`;上限 = 购入 库存 / 售出 拥有;数量按物品分别记忆
|
||||
- 信息区在按钮上方显示 买价/卖价/存货/拥有/总价;购买按钮购买当前选中物品数量;数量 0 不执行
|
||||
- 交易:数据侧提供则调用之,否则按默认以既有公开接口组装(`addItem` / `getModifiableAttribute().add('money', …)`);**未 invent 任何新接口**
|
||||
- 右下退出调用 `controller.close(props.instance)`
|
||||
- 无 `import type`、无 `?.`、无连续 `as`、CRLF、无未使用 import;范围守卫无越界改动
|
||||
</acceptance_criteria>
|
||||
<done>数量选择 + 信息区 + 购买 + 退出完成;最终静态门禁 + CRLF + 范围守卫全绿</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 数据读取边界 | 经 `props.shop`(`IShopState`)/ `props.heroState`(`IHeroState`)/ `props.state`(itemStore/tileStore)读取商店、玩家与物品数据 |
|
||||
| 数据写入边界 | 交易写 `IShopState.addItem` / `IHeroItems.addItem` / `IHeroAttribute.add`;均经既有公开接口,不自建状态、不 invent 新接口 |
|
||||
| 命名边界 | 所有新增公共命名(文件 / 组件 / GameUI 名 / props / 行类型 / 导出)须 Task 0 获批后方可使用 |
|
||||
| 范围边界 | 只新增 `shop.tsx`;不碰既有文件、其它界面与数据端(含 `shop/types.ts`、barrel) |
|
||||
| 提交边界 | 不得把用户并发改动(D-56:`shop/types.ts`、`data-state/src/common.ts`、`src/content/core.jsonc`)纳入暂存/提交 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-09-01 | Tampering | 借新增之机修改既有 UI / 数据端 / 渲染系统(含 `shop/types.ts`、barrel) | high | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `git status` 范围守卫 |
|
||||
| T-09-09-02 | Elevation of Privilege | 未经批准即执行 / 使用未批准命名 | high | mitigate | Task 0 `blocking-human`;命名清单呈报;prohibitions |
|
||||
| T-09-09-03 | Tampering | **invent** 价格 / 交易 / 支付资源接口或为 `IShopState` 补字段 / 补 barrel 导出 | high | mitigate | `<action>` / prohibitions 明列「flag 不 invent」;缺口 GAP-S0..S3 由 Task 0 呈报裁定 |
|
||||
| T-09-09-04 | Tampering | 交易金额 / 物品转移错误(未按裁定口径) | medium | mitigate | `<action>` 明列默认组装口径;acceptance 明列;数据侧提供交易时优先调用 |
|
||||
| T-09-09-05 | Tampering | 引入 `client` 单例 / `import type` / `?.` / 连续 `as` / 非 CRLF | medium | mitigate | prohibitions + Task 1/2 静态门禁 + CRLF 门禁 |
|
||||
| T-09-09-06 | Tampering | 售出列表被扩大为「玩家背包全部道具」(D-39 要求售出复用商店列表) | low | mitigate | `<action>` 明列售出复用 `iterateItems()`;acceptance 明列 |
|
||||
| T-09-09-07 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
|
||||
| T-09-09-08 | Tampering | 提交时混入用户并发改动(D-56) | medium | mitigate | 提交前 `git status` 逐条核对,只暂存本计划条目 |
|
||||
| T-09-09-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **Task 1**:`shop.tsx` 存在且导出 `Shop` / `ShopUI` / `IShopItemEntry`;含 `interface ShopProps` / `IShopState` / `IHeroState` / `.iterateItems(` / `.getCount(` / `itemCount(` / `itemStore` / `getData(` / `.text` / `.name` / `<Scroll` / `noscroll` / `<icon` / `new GameUI(`;props 数组含 `controller,instance,state,shop,heroState`;无 `import type`、无 `?.`;CRLF。
|
||||
- **Task 2**:含 `购买` / `售出` / `买价` / `卖价` / `存货` / `拥有` / `总价` / `count` / `onClick` / `controller.close(`;无 `import type` / `?.` / 连续 `as`;CRLF。
|
||||
- **范围守卫**:`git status --porcelain` 仅本计划 `files_modified` 条目被新增(不含用户并发改动 `shop/types.ts` / `data-state/src/common.ts` / `src/content/core.jsonc`)。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
|
||||
- **人工复核**:结论写入 `09-09-SUMMARY.md`(含 GAP-S0..GAP-S3 与 OI-S1..OI-S5 的最终裁定)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 布局:顶部 购买/售出 切换;左 商店列表;右自上而下 名称 / 描述 / 购入量选择(按钮上方 买价/卖价/存货/拥有/总价)/ 购买按钮;占满游戏画面;右下退出。
|
||||
- 列表:售出复用商店列表;按 id(`num`)排序;库存最右右对齐;纵向滚动并显示滚动条;描述纵滚不显示滚动条。
|
||||
- 数量:四按钮 `<< < {量} > >>`(±10/±1);上限 购入 库存 / 售出 拥有;按物品分别记忆;购买按钮买当前选中物品数量。
|
||||
- 数据:消费用户提供的 `IShopState`(`iterateItems`/`getCount`/`addItem`)+ `IHeroState`(`items`/`attribute`);价格 / 交易 / 支付资源缺口按 Task 0 裁定处置,**未 invent 接口**。
|
||||
- 范围零外溢;零未获批命名;零既有注释改动;CRLF;无 `client` 单例;无 `import type` / `?.` / 连续 `as`。
|
||||
- GAP-S0..GAP-S3 与 OI-S1..OI-S5 已按用户裁定落地并入本计划。
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| 符号 / 路径 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `packages-user/client-modules/src/ui/shop.tsx`(新增) | 组件 / UI | 商店界面:`Shop` / `ShopUI` / `ShopProps` / `IShopItemEntry`(命名待批准) |
|
||||
|
||||
**新增导出命名(全部须 Task 0 批准):** 文件 `shop.tsx`;组件 `Shop`;`GameUI` 名 `'shop'`;props 接口 `ShopProps`;行类型 `IShopItemEntry`(默认);导出 `ShopUI`。**不向 `src/ui/index.ts` 添加导出**(保持与同 wave 计划互不冲突)。
|
||||
|
||||
## Source Coverage Audit
|
||||
|
||||
| 来源 | 项 | 处置 |
|
||||
|------|----|------|
|
||||
| GOAL | Phase 9(ROADMAP)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·任务 3 子切片:shop 移植) |
|
||||
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
|
||||
| RESEARCH | 无 RESEARCH.md | N/A |
|
||||
| CONTEXT | D-38(布局:顶切换 / 左列表 / 右 名称·描述·购入量选择·购买按钮;价格信息在按钮上方) | COVERED(Task 1/2) |
|
||||
| CONTEXT | D-39(购买/售出切换 / 售出复用商店列表 / id 排序 / 库存最右右对齐 / 列表滚动显滚动条) | COVERED(Task 1) |
|
||||
| CONTEXT | D-40(四按钮 ±10/±1 / 上限 库存·拥有 / 数量按物品分别记忆 / 购买按钮买当前选中数量) | COVERED(Task 2) |
|
||||
| CONTEXT | D-41(只做 UI / 打开方式不管 / 数据由 `ShopState` 承载) | COVERED(Task 0 边界 + prohibitions + 入参 `IShopState`) |
|
||||
| CONTEXT | D-33 / D-37(占满游戏画面 root renderer / 退出按钮右下) | COVERED(`FULL_LOC` + 右下按钮) |
|
||||
| CONTEXT | D-03 / D-04 / D-06 / D-22(执行前获批 / 命名获批 / 不扩范围 / 逐界面讨论后规划) | COVERED(Task 0 + prohibitions + 范围门禁) |
|
||||
| CONTEXT | 任务 3 · 数据端接口补充(shop:`ShopState` 接口已提供、实现待补) | COVERED(消费 `IShopState` 成员) |
|
||||
| CONTEXT | D-18 其余界面(book/equipbox/toolbox) | OUT OF SCOPE(各自计划) |
|
||||
| 缺口 | `IShopState` 未从 `@user/data-base` 公共面导出(无 `shop/index.ts`,`index.ts` 无 `./shop`) | **OPEN(GAP-S0,须用户补齐;未补齐 Task 1 阻塞)** |
|
||||
| 缺口 | 买价 / 卖价 无来源(`IShopState` / `IItemRawData` 均无价格字段;legacy 在 `ItemShopChoice.money`/`.sell`) | **OPEN(GAP-S1,须用户提供来源,否则 `// TODO` 占位)** |
|
||||
| 缺口 | 购买 / 售出 交易无方法(`IShopState` 仅 `addItem` 调库存) | **OPEN(GAP-S2,须裁定:数据侧提供 或 UI 以既有公开接口组装)** |
|
||||
| 缺口 | 支付资源 `money` vs `exp` 缺字段(legacy `ItemShopEvent.use`) | **OPEN(GAP-S3,默认 money,须确认)** |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-09-SUMMARY.md` when done
|
||||
</output>
|
||||
Loading…
Reference in New Issue
Block a user