mirror of
https://github.com/motajs/template.git
synced 2026-10-16 18:40:17 +08:00
docs(06-18): create phase plan
This commit is contained in:
parent
b2626bd438
commit
7ad903f156
@ -210,7 +210,7 @@ Plans:
|
||||
3. 测试在本地可运行且全部通过
|
||||
4. 测试由 AI 编写并运行,通过验证后可提交
|
||||
|
||||
**Plans**: 9/9 原计划 executed replanned (D-28;旧 06-01/06-02 执行结果标记 superseded,按同号重跑;数据端切片,非数据 render/legacy 覆盖延后) + gap-fill 06-10..06-15 (D-46;人工评审缺口补测,只补测试不改生产代码;15/15 executed) + perf 06-16 (性能测试补充;只加测试与配置,不改生产代码) + perf 06-17 (真实地图存读档性能补充;移入 13 张真实地图夹具,只加测试与夹具)
|
||||
**Plans**: 9/9 原计划 executed replanned (D-28;旧 06-01/06-02 执行结果标记 superseded,按同号重跑;数据端切片,非数据 render/legacy 覆盖延后) + gap-fill 06-10..06-15 (D-46;人工评审缺口补测,只补测试不改生产代码;15/15 executed) + perf 06-16 (性能测试补充;只加测试与配置,不改生产代码) + perf 06-17 (真实地图存读档性能补充;移入 13 张真实地图夹具,只加测试与夹具) + perf 06-18 (真实大地图场景性能补充;整条 lane 计时改为 `performance.mark`/`measure` + 新增 `mapScenario.perf.ts`,只加测试不改生产代码)
|
||||
|
||||
Plans:
|
||||
|
||||
@ -262,6 +262,10 @@ Plans:
|
||||
|
||||
- [x] 06-17-PLAN.md — realistic map save/load perf: move the root `floors.json` (13 real 13×13 maps) into `packages-user/data-state/test/fixtures/`; new `packages-user/data-state/test/saveablesReal.perf.ts` measuring `存档`/`读档`/`往返` (save-only / load-only / round trip) for map scale `1/5/13` × `NoCompression`/`LowCompression`/`HighCompression` (27 rows) with a fixed realistic side load (50 flags, 20 hero modifiers incl. 4 from equipped items, 4 equipped instances, 20 item kinds, 1000 replay steps); cleared live map (`2/3/4/6` → `0`) vs original `compareWith` reference so `HighCompression` stores changed rows; warmup 3 + 20 samples → median/min/p95 via `console.table`, **zero assertions**, results recorded in `06-17-SUMMARY.md`
|
||||
|
||||
**Wave 8** *(realistic large-map combat scenario perf supplement + lane-wide timing-method upgrade, user-authorized; appended after 06-17; reuses the same isolated `test:perf` lane; timing helper switch in 5 existing files + one new `*.perf.ts`, no production edits, no new dependencies)*
|
||||
|
||||
- [ ] 06-18-PLAN.md — timing method upgrade + realistic large-map combat scenario perf: switch the inlined `measureCase` in **all** `*.perf.ts` from `globalThis.performance.now()` to `performance.mark` + `performance.measure` (unique per-case tags, per-sample `clearMarks`/`clearMeasures`; same `case`/`scale`/`median ms`/`min ms`/`p95 ms` columns, warmup 3 + 20 samples, record-only); new `packages-user/data-state/test/mapScenario.perf.ts` merging the 13 real 13×13 maps into ONE grid-tiled map (`ceil(sqrt(n))` columns, placed at `(col*13, row*13)`) for scale `1/5/13` → `13×13`/`39×26`/`52×52` with 11/79/204 monster tiles, 12 real `Enemy` prefabs (4 carrying real auras: `CommonAura` Full/Manhattan/Rect + `GuardAura`), `mulberry32`-seeded assignment, `createCoreState()` real wiring + `resize` + `addPrefab` + `fromRaw` + per-tile `setEnemyAt`; measures ① `enemyContext.buildup()` ② `mapDamage.refreshAll()` + per-monster `getSeparatedDamage`/`getReducedDamage` ③ one real `calculateCritical(view, 'atk')` per monster (plus a second table reporting `monsters`/`total ms`/`avg ms`); results recorded in `06-18-SUMMARY.md`
|
||||
|
||||
### Phase 7: 数据端缺陷修复
|
||||
|
||||
**Goal**: 修复 Phase 6 单元测试暴露的数据端疑似缺陷,使正确预期用例转绿,且仅限数据端、不涉及渲染端
|
||||
|
||||
435
.planning/phases/06-unit-tests/06-18-PLAN.md
Normal file
435
.planning/phases/06-unit-tests/06-18-PLAN.md
Normal file
@ -0,0 +1,435 @@
|
||||
---
|
||||
phase: 06-unit-tests
|
||||
plan: 18
|
||||
type: execute
|
||||
wave: 8
|
||||
depends_on: [06-16, 06-17]
|
||||
files_modified:
|
||||
- packages-user/data-system/src/combat/context.perf.ts
|
||||
- packages-user/data-system/src/combat/damage.perf.ts
|
||||
- packages-user/data-base/src/hero/attribute.perf.ts
|
||||
- packages-user/data-state/test/saveables.perf.ts
|
||||
- packages-user/data-state/test/saveablesReal.perf.ts
|
||||
- packages-user/data-state/test/mapScenario.perf.ts
|
||||
autonomous: true
|
||||
requirements: [TEST-01]
|
||||
estimate:
|
||||
tokens: 66000
|
||||
raw_tokens: 66000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "TEST-01 的**真实大地图场景性能补充**:复用 06-16 的 perf 通道(`vitest.perf.config.ts` + `pnpm test:perf`),把 5 个既有 `*.perf.ts` 的计时方式改为 `performance.mark` + `performance.measure`,并新增第 6 个 `*.perf.ts`(`packages-user/data-state/test/mapScenario.perf.ts`);`pnpm test:perf` 由 **5 文件 / 45 行**变为 **6 文件 / 54 行**(45 + 新增 9,另加 3 行临界汇总第二表),全程无断言失败、无 `[WARNING Code` / `[ERROR Code` 噪声(用户已批准此设计)。"
|
||||
- "测量方式(用户裁决,全部 perf 文件一致):每个样本 `globalThis.performance.mark(起始标记)` → `run()` → `globalThis.performance.mark(结束标记)` → `const measure = globalThis.performance.measure(测量名, 起始标记, 结束标记)` → `samples.push(measure.duration)`;三个名字按 case 与规模唯一(`'perf:' + caseName + ':' + scale + ':start'` / `...:end'` / `'perf:' + caseName + ':' + scale`),**每个样本结束后 `clearMarks(startTag)` / `clearMarks(endTag)` / `clearMeasures(measureName)`**,故重复采样既不碰撞也不泄漏。"
|
||||
- "输出列与采样参数不变:列仍为 `case` / `scale` / `median ms` / `min ms` / `p95 ms`;预热 3 次不计时、采样 20 次、下中位数 `samples[10]`、`min = samples[0]`、`p95 = samples[Math.ceil(20 * 0.95) - 1] = samples[18]`,三者统一 `Number(v.toFixed(3))`;**全 lane 零断言(只记录)**、无 fake timers、无 `it.skip`/`it.todo`。"
|
||||
- "新文件场景(用户裁决):把 13 张真实 13×13 地图**网格拼接成单一大图**(列数 `ceil(sqrt(n))`、行数 `ceil(n / 列数)`、每张按 `(col * 13, row * 13)` 放置、空位补 `0`),规模 `n = 1 / 5 / 13` → 合并尺寸 **13×13 / 39×26 / 52×52**,怪物(编号 `4`)数量 **11 / 79 / 204**(数据集实测:各图 11–19、13 张合计 204);保留每张图的外墙(编号 `1`)与全部原始编号,图块注册集合恒为 `1..6`(与规模无关)。"
|
||||
- "怪物阵容(用户裁决 10–15 个模板):本计划使用 **12 个**真实 `Enemy` 模板(编号 `100..111`),属性各不相同;其中 **4 个带光环**——`CommonAura` 的 **FullRange / ManhattanRange / RectRange** 三种范围各 1 个(特殊属性 `25`)+ `GuardAura` 1 个(特殊属性 `26`),全部由 CoreState 已注册的**真实** `CommonAuraConverter` / `GuardAuraConverter` 转换;其余 8 个携带真实地图伤害特殊属性(`15` 领域 / `16` 夹击 / `18` 阻击 / `24` 激光 / `27` 捕捉)与真实伤害分支特殊属性(`1/2/3/4/6/7/8/9/11/17/22`)。"
|
||||
- "怪物分配(用户裁决):`mulberry32` **固定种子**(`PERF_SEED = 20260916`)的确定性 PRNG,按行主序对合并矩阵中每个编号 `4` 的格子取一个数,映射到 12 个模板之一;同一规模每次运行的分配完全一致(不依赖时间/随机源)。"
|
||||
- "场景装配全部用**真实实现**(用户裁决):`createCoreState()`(构造函数内已 `registerSpecials(enemyManager)`、装配真实 `MainDamageCalculator` / `MainMapDamageConverter` / `MainMapDamageReducer` / `CommonAuraConverter` / `GuardAuraConverter` / `MainEnemyFinalEffect`,见 `core.ts` L206–219)→ `enemyContext.resize(合并宽, 合并高)` → 注册图块 → `enemyManager.addPrefab(...)` × 12 → 真实勇士属性(`hero.getModifiableAttribute().set(...)`,零修饰器)→ `maps.fromRaw(...)` 注入合并地图 → 逐格 `enemyContext.setEnemyAt({x,y}, enemyManager.createEnemy(code))` → 首轮 `buildup()`(不计时)。**不手工 new** `EnemyContext`/`DamageSystem`/`MapDamage`(否则与 CoreState 双份且 `getMapDamage()` 拿到未接线实例)。"
|
||||
- "三个测量项(每规模各 1 行,共 9 行):① **怪物上下文构建** = 单次 `enemyContext.buildup()`(样本内先 `bindHero` 触发 `needUpdate`,与 06-16 ① 同构);② **地图伤害构建** = 单次 `mapDamage.refreshAll()` + 对每个怪物坐标消费 `getSeparatedDamage` 与 `getReducedDamage`(真实 `MapDamage` 路径,**含 reducer**);③ **全图怪物单次临界计算** = 对合并地图里每只怪调用一次真实 `calculateCritical(view, 'atk')` 并**完整消费生成器**。"
|
||||
- "临界项的额外报数(用户裁决):第二张表输出 `case` / `scale` / `monsters` / `total ms` / `avg ms`,`total ms` = 该规模全图扫一遍的**采样中位数**,`avg ms` = `total ms / monsters`(同规模内怪物数是常量,故「中位总量 / 怪物数」恰好等于「每采样单怪平均耗时」的中位数,口径无歧义),统一 `Number(v.toFixed(3))`;该表由同一次 `measureCase` 的返回值推导,**不改测量助手**。"
|
||||
- "通道与门禁零变化:**不修改** `vitest.perf.config.ts`、`package.json`、`vite.config.ts`、`tsconfig*.json`;**不修改** `packages-user/data-state/test/fixtures/floors.json`(只读复用)与任何既有 `*.test.ts`;`pnpm exec vitest list --filesOnly`(默认配置)输出仍**不含** `.perf.ts`,`pnpm test:ci` 的文件数 / passed / skipped 与本计划开工前记录的基线逐项一致。"
|
||||
- "质量门禁(D-44,文件级):6 个 `*.perf.ts` 改动文件 `eslint --fix` 后 `eslint <改动文件>` **0 错误**;6 个文件在 `pnpm exec vue-tsc --noEmit` 输出中按文件路径过滤 **0 类型错误**;`pnpm test:ci` 全绿。"
|
||||
- "阶段化(D-43 精神,用户裁决):阶段 1(构件:6 个 perf 文件的 mark/measure 计时改造)→ 阶段 2(组合:合并大图 + 12 个真实模板 + ① 构建 / ② 地图伤害)→ 阶段 3(完整:③ 全图临界 + 第二张表 + 汇总),每阶段跑绿后才进入下一阶段;不使用 tracer 任务(D-43 明确取消 tracer-first);出现阻断性问题按 D-05/D-07 暂停汇报、等确认。"
|
||||
- "执行节奏(D-33):本计划执行前先向用户确认;执行完成后暂停,用中文分条简要汇报耗时结果与异常(如耗时逼近 `testTimeout`、或某个测量项数值异常)。"
|
||||
artifacts:
|
||||
- path: packages-user/data-state/test/mapScenario.perf.ts
|
||||
provides: "真实大地图战斗场景耗时表:合并地图规模 1/5/13 × 3 个测量项(怪物上下文构建 / 地图伤害构建 / 全图怪物单次临界计算),含 12 个真实模板、mulberry32 固定种子分配、内联 `measureCase`(mark + measure)与临界汇总第二表"
|
||||
- path: packages-user/data-system/src/combat/context.perf.ts
|
||||
provides: "① 怪物上下文构建耗时表(3 个 case:N = 50/200/1000),计时方式改为 `performance.mark` + `performance.measure`"
|
||||
- path: packages-user/data-system/src/combat/damage.perf.ts
|
||||
provides: "② 临界计算耗时表(3 个 case:1000/10000/50000),计时方式改为 mark + measure"
|
||||
- path: packages-user/data-base/src/hero/attribute.perf.ts
|
||||
provides: "③ 勇士属性计算耗时表(3 个 case:M = 10/100/1000),计时方式改为 mark + measure"
|
||||
- path: packages-user/data-state/test/saveables.perf.ts
|
||||
provides: "④ 存读档耗时表(9 个 case:件数 10/100/1000 × 三档压缩),计时方式改为 mark + measure"
|
||||
- path: packages-user/data-state/test/saveablesReal.perf.ts
|
||||
provides: "⑤ 真实地图存读档耗时表(27 个 case:地图数量 1/5/13 × 三档压缩 × 存档/读档/往返),计时方式改为 mark + measure"
|
||||
- path: .planning/phases/06-unit-tests/06-18-SUMMARY.md
|
||||
provides: "9 行主表 + 3 行临界汇总表 + 测量方式变更说明(含 06-16/06-17 的 45 行为改造前口径、不可直接对比)+ 实现风险记录(不落盘基线 JSON)"
|
||||
key_links:
|
||||
- "**`performance.mark` / `measure` 的可用性与语义(已在本机实测,Node `v22.18.0`)**:全局 `performance` 具备 `mark` / `measure` / `clearMarks` / `clearMeasures`;`measure(name, start, end)` 返回带 `duration` 的 `PerformanceMeasure`;**对不存在的标记做 measure 会抛错(实测错误码 `12`)**,故必须「先 mark 再 measure」;同名标记在 `clearMarks` 之后可重复使用。因此每个样本必须成对 mark、及时 measure、及时按名清理;**不要**用固定的全局标记名而不清理(会让 duration 从第一次标记起算)。"
|
||||
- "**CoreState 已装配全部真实实现**(`packages-user/data-state/src/core.ts` L206–219):`new EnemyContext(this)` → `new DamageSystem(context)` → `new MapDamage(context)` → `damageSystem.useCalculator(new MainDamageCalculator())` → `mapDamage.useReducer(new MainMapDamageReducer())` → `mapDamage.useConverter(new MainMapDamageConverter())` → `attachDamageSystem` / `attachMapDamage` → `registerAuraConverter(new CommonAuraConverter())` → `registerAuraConverter(new GuardAuraConverter())` → `registerFinalEffect(new MainEnemyFinalEffect())` → `resize(TILE_WIDTH, TILE_HEIGHT)` → `bindHero(heroAttribute)`;`registerSpecials(enemyManager)` 在 L199。故新文件**只需** `createCoreState()` + 覆盖 `resize` 为合并尺寸;**不要**手工重复装配(会与 CoreState 双份,且 `state.enemyContext.getMapDamage()` 可能拿到未接线的实例)。"
|
||||
- "**`buildup()` 早退**(`data-system/src/combat/context.ts` L684–689):`if (!this.needUpdate) return;` + `if (!this.bindedHero) { logger.warn(110); return; }`。每样本必须先触发 `needUpdate`(`bindHero` 是公开且最省的触发器,L193–198);**但 `bindHero` 会顺带执行 `damageSystem.bindHeroStatus(hero)`(清伤害缓存)与 `mapDamage.refreshAll()`(全量地图伤害重建)**——这是 ① 的样本组成部分,报数时必须如实说明,不得把它说成「裸 `buildup()`」。若用户希望测裸 `buildup()`,可改为往上下文注册一个永不命中的假转换器并用 `setAuraConverterEnabled(假转换器, true)` 触发(该调用无条件置 `needUpdate`),但本计划为与 06-16 ① 同构而采用 `bindHero`。"
|
||||
- "**`requestRefresh` 会反过来触发全量构建**(`context.ts` L879–887):`if (this.needUpdate) { this.buildup(); return; }`,而 `view.getComputedEnemy()` 会调 `context.requestRefresh(view)`(`enemy.ts` L23–26)。故 ② ③ 的夹具必须在采样前先在夹具里跑一次 `buildup()`,否则第一次取计算后怪物就会把整轮全量构建算进采样。"
|
||||
- "**`MapDamage.refreshAll()` 需要 converter + reducer**(`data-system/src/combat/mapDamage.ts` L374–390):缺失分别 `logger.warn(102)` / `logger.warn(103)`;CoreState 已装配两者(且顺序为 reducer 先于 converter,`useConverter` 内部立即 `refreshAll()`,不会出现暂缺 reducer 的告警)。`getReducedDamage` 在无 reducer 时 `warn(103)`。"
|
||||
- "**`BetweenDamageView` 读全局 `core`**(`data-state/src/enemy/mapDamage.ts` L237:`core.flags.betweenAttackMax`)→ 只要模板里出现 `16` 夹击,就必须 `vi.stubGlobal('core', { flags: { betweenAttackMax: false } })`,否则 `core` 未定义直接抛 `TypeError`(既有先例:`data-state/src/enemy/mapDamage.test.ts` L21 用 `vi.hoisted` 同时 stub `main`/`location`/`core`)。本文件采用 `betweenAttackMax: false` 分支(`half = hero hp / 2`)。"
|
||||
- "**激光视图需要真实 `faceManager`**(`data-state/src/enemy/mapDamage.ts` L314:`handler.state.faceManager.get(FaceGroup.Dir4)!`):CoreState 构造时已注册 `Dir4`/`Dir8`(`core.ts` L148–153),故含 `24` 激光的模板可正常建视图;**不要**用 `{} as IStateBase` 之类的假 state(会缺 `faceManager`/`flags`/`hero.items`)。"
|
||||
- "**临界计算内部会把勇士攻击设成 `Infinity`**(`damage.ts` L218:`hero.set(attribute, right)`,而 `MainDamageCalculator.getCriticalLimit` 对 `3 坚固` 返回 `Infinity`):本文件**不给勇士挂任何修饰器**(不注册 `ValueModifier`、不建装备),使 `hero.getModifiableClone()` 的 `set` 只改基础值,避免 `Infinity` 与修饰器叠加出无意义噪声;`hero.set` 作用在克隆体上,不会污染真实勇士(`calculateCritical` L152 每次取 `getModifiableClone()`)。"
|
||||
- "**模板必须自带 `guard: new Set()`**:`GuardAura.apply` 会执行 `enemy.getAttribute('guard').add(locator)`(`aura.ts` L178),`IEnemyAttr.guard`(`data-common/src/types.ts` L21)缺失即抛错;`Enemy.clone()` / `copyFrom` 走 `structuredClone`,`Set` 可被克隆(06-17 的既有夹具已验证)。"
|
||||
- "**同一模板内特殊属性代码不得重复**(`Enemy.addSpecial` 对重复代码 `logger.warn(96)`);本计划 12 个模板的代码集合两两不重复,且 `25` 只被 `CommonAuraConverter` 命中、`26` 只被 `GuardAuraConverter` 命中(`aura.ts` L33/L129),故不会触发 `warn(97)`;两个光环 `couldApplySpecial === false`(`aura.ts` L52/L148),故不会触发 `98/99/100/101`。"
|
||||
- "**图块注册与地图注入**:合并地图的实时内容就是**原始编号**(不做 06-17 的清除规则),注册集合 = 数据集出现过的全部编号(实测 `1..6`,与规模无关),逐编号 `tileStore.addTile({ num, id, events: {}, type: TileType.Terrain, pass: { onlyEvents: false, inPass: 15, outPass: 15 }, eventPass: true })`(`id` 用 `'perf-scenario-tile-' + num`,每编号只注册一次即不触发 `warn(133/134)`);`IMapRawData.map` 是**扁平** `number[]`(`data-common/src/store/types.ts`),故合并矩阵必须 `flat()`;`layerAlias: { 0: 'bg' }`(**不要**用 `'event'`)、`events: { 0: {} }`(不模拟触发器)。"
|
||||
- "**`fromRaw` 的硬约束**(`data-base/src/map/mapState.ts` L190–215):`validateRaw` 要求 `events` 每个图层键存在;`map.length % raw.width === 0` 否则 `logger.error(61)`;同一 `floorId` 重复注入 `logger.warn(121)`——每个 `it` 都新建 `CoreState`,故固定 `floorId`(`'perf-scenario-merged'`)安全。`setMapRef` 长度不符会 `logger.warn(123)`,故宽度必须取合并矩阵自己的宽度。"
|
||||
- "**`resize` 必须在放置怪物之前**(`context.ts` L105–111 → `clear()` L897–914):`resize` 会清空全部怪物、`damageSystem.markAllDirty()` 并 `mapDamage.refreshAll()`;CoreState 默认 `resize(13,13)` 必须被覆盖成合并尺寸,否则 `52×52` 的坐标会算出越界索引。"
|
||||
- "**`setEnemyAt` 会触发 `MapDamage` 全量刷新**(`context.ts` L291–296 → `mapDamage.markEnemyDirty(view)`;`mapDamage.ts` L135–148 对新视图走 `refreshAll()`):放置 N 只怪的夹具构建成本约为 O(N²) 的点级刷新(N=204 时约 2 万次),这是**夹具构建**成本、不计入耗时,但它是 9 个 `it` 各自建一次夹具的主要开销来源;若某 `it` 逼近 `testTimeout`(30000ms),按 D-07 暂停汇报,**不得**改规模或超时。"
|
||||
- "**`enemyManager.createEnemy(code)` 可能返回 `null`**(`data-base/src/enemy/manager.ts` L119–123):夹具用 `if (!enemy) throw new Error('性能夹具:找不到怪物模板 ' + code)` 作**夹具完整性**保护(与 `enemyCombination.test.ts` L214–215 的 `if (!enemy) throw new Error(...)` 同构);这是夹具自检、不是性能断言,不违反「零断言」。"
|
||||
- "**`EnemyManager.compareWith` 本计划不调用**:不做存读档,`hasReference` 为假时 `updateDirty` 直接返回(`manager.ts` L276–289),故不会出现 `warn(117)`、也不会把怪物写进存档;**不要**为「更真实」补 `compareWith`(会引入与本计划无关的存档语义)。"
|
||||
- "**`registerSpecials` 的解释口径(须如实记录,不得含糊)**:CoreState 构造函数已调用 `registerSpecials(enemyManager)`(`data-state/src/enemy/special.ts` L51),本计划**不**再走 `fromLegacyEnemy` 的 legacy 转换路径来生成特殊属性(仓库内无任何测试先例,且 legacy `Enemy` 类型字段繁多,`src/types/declaration/enemy.d.ts`);特殊属性改用既有集成测试 `enemyCombination.test.ts` L55–74 的**内联 `createSpecial`** 写法,配 `enemyManager.addPrefab(prefab)` + `createEnemy(code)` 的真实 `EnemyManager` 路径。即:`registerSpecials` 的**注册**确实发生,但模板的特殊属性实例是内联构造的——SUMMARY 必须写明这一点,不得表述为「经 legacy 转换注册」。"
|
||||
- "**计时口径变更会作废既有表述**:06-16/06-17 SUMMARY 里「计时源 `globalThis.performance.now()`」对**改造后**的 lane 不再成立;按 06-16/06-17 的禁令**不修改**这两个 SUMMARY,只在 `06-18-SUMMARY.md` 中显式说明「45 行既有数字为 mark/measure 改造**之前**的口径,不可与本次新增 9 行直接对比」,并记录本次全 lane 运行的实测结果。"
|
||||
- "**临界项必须完整消费生成器**:`for (const _critical of damageSystem.calculateCritical(view, 'atk')) { /* 完整消费临界枚举 */ }`——只取生成器不迭代则测不到二分枚举的真实开销(06-16 ② 的既有写法与注释形态);`_` 前缀用于通过 `@typescript-eslint/no-unused-vars` 的 `varsIgnorePattern: '^_'`。若 eslint 对解构出的 `_` 仍报未使用,改用 `for (const entry of state.enemyContext.iterateEnemy())` + `const view = entry[1];` 的写法(保持「只用到的变量才声明」)。"
|
||||
- "**`getDamageInfo` 不单独计时**:`calculateCritical` 内部即 `this.calculator.calculate(handler)`(`damage.ts` L155/L220/L227),与本项意图(真实临界计算路径)一致;若用户希望额外单独测 `getDamageInfo` 的耗时,按其裁决追加,不在本计划内自行扩展。"
|
||||
prohibitions:
|
||||
- "不创建或修改任何生产/核心源码(`packages/**`、`packages-user/*/src/**` 下的非 `*.perf.ts` 文件、`src/**`)"
|
||||
- "不修改 `vitest.perf.config.ts`、`package.json`、`vite.config.ts`、`tsconfig*.json`(第 6 个 perf 文件由既有 `include: ['**/*.perf.ts']` 自动收集)"
|
||||
- "不修改 `packages-user/data-state/test/fixtures/floors.json`(只读复用:不裁剪、不重排、不重新格式化、不改换行)"
|
||||
- "不修改任何既有 `*.test.ts`、不修改 06-01..06-17 的 PLAN/SUMMARY、不修改 `06-COVERAGE-MAP.md` / `06-COVERAGE-GAPS.md`"
|
||||
- "不安装任何依赖(无新增 devDependencies;vitest 已在 devDependencies)"
|
||||
- "不在 perf 文件内写任何断言调用(`expect(...)` 0 个;也不需要 `bench`);不使用 fake timers、不使用 `it.skip`/`it.todo`、不使用 `process.hrtime` 或其它计时源"
|
||||
- "不使用 `import type` 语法(用行内 `type` 修饰符,与既有文件一致);不写连续 `as`"
|
||||
- "不手工 new `EnemyContext` / `DamageSystem` / `MapDamage`,不重复装配转换器/计算器/最终效果(CoreState 已装配)"
|
||||
- "不给勇士挂任何修饰器、不建装备/道具侧负载(本计划只测怪物侧的三条路径)"
|
||||
- "不落盘基线 JSON;不把耗时写入任何门禁脚本;不改动 `test`/`test:ci` 语义"
|
||||
- "不为让 perf 跑绿而放宽告警、调小规模或调大超时(逼近 `testTimeout` 时按 D-07 暂停汇报)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
按用户裁决为 Phase 6 的 perf lane 追加两项内容:**(A) 测量方式统一升级** —— 6 个 `*.perf.ts`(5 个既有 + 1 个新增)的计时全部从 `globalThis.performance.now()` 改为 `performance.mark` + `performance.measure`,输出列、预热/采样/统计口径、零断言约定**完全不变**;**(B) 新增真实大地图战斗场景性能文件** —— `packages-user/data-state/test/mapScenario.perf.ts`,把 13 张真实 13×13 地图网格拼接成**一张单一大图**(规模 `1 / 5 / 13` → `13×13` / `39×26` / `52×52`,怪物 11 / 79 / 204),用 12 个真实 `Enemy` 模板(其中 4 个带真实 `CommonAura`(三种范围)/ `GuardAura`)经 `mulberry32` 固定种子分配到每个怪物格子,测三个真实路径的耗时。
|
||||
|
||||
三个测量项(每规模 3 行,共 **9 行**主表):
|
||||
|
||||
1. **怪物上下文构建**:单次 `enemyContext.buildup()`(样本内先 `bindHero` 触发 `needUpdate`,与 06-16 ① 同构)。
|
||||
2. **地图伤害构建**:单次 `mapDamage.refreshAll()` + 对每个怪物坐标消费 `getSeparatedDamage` / `getReducedDamage`(真实 `MapDamage` + reducer 路径)。
|
||||
3. **全图怪物单次临界计算**:对合并地图里每只怪调用一次真实 `DamageContext.calculateCritical(view, 'atk')` 并完整消费生成器;**额外第二张表**给出 `monsters` / `total ms`(全图中位总量)/ `avg ms`(单怪平均 = 总量 / 怪物数)。
|
||||
|
||||
Purpose: 06-17 把**存读档**的真实地图场景固定下来了,但真实地图**战斗**场景至今没有耗时数据:一张 13×13 真实地图上有 11–19 只怪、13 张合计 204 只,且真实地图里怪物带着光环、领域/阻击/激光/夹击/捕捉与各种伤害分支。这些正是 `buildup` 的光环拓扑重建、`MapDamage` 的视图装箱、以及临界二分枚举的**真实规模**来源,而既有 perf 用例(06-16 ①②)用的是合成小网格(最大 1000 只怪但拓扑极简、无真实地图)。本计划用真实数据 + 真实顶层实现把这三点固定下来,供后续回归对比;同时把整条 lane 的计时方式升级为 `performance.mark`/`measure`(可被外部工具读取的标准标记),不改变任何输出列与统计口径。
|
||||
|
||||
**测量方式变更(用户明确指定)**:全部 6 个 perf 文件内联的 `measureCase` 采样循环改为成对 `mark` + `measure` + 逐样本清理(`clearMarks`/`clearMeasures`),标记名按 case 与规模唯一。输出列、`WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`、下中位数/`min`/`p95`、`toFixed(3)`、`afterAll` 的 `console.table`、**零断言**均不变。
|
||||
|
||||
**风险与解释口径(必须在 SUMMARY 如实记录)**:`bindHero` 的副作用计入 ①;模板特殊属性为内联构造(`registerSpecials` 仅完成注册,未走 legacy 转换路径);`BetweenDamageView` 依赖全局 `core` stub;06-16/06-17 的 45 行为改造**前**口径、不可与本次直接对比。以上不静默处理。
|
||||
|
||||
Output: `packages-user/data-state/test/mapScenario.perf.ts`(9 行主表 + 3 行临界汇总表)、5 个既有 `*.perf.ts` 的计时改造,以及承载数值表的 `06-18-SUMMARY.md`。
|
||||
|
||||
阶段结构(D-43 精神):阶段 1 构件(6 个文件的 mark/measure 计时改造)→ 阶段 2 组合(合并大图 + 12 个真实模板 + ① 构建 / ② 地图伤害)→ 阶段 3 完整(③ 全图临界 + 汇总第二表 + SUMMARY)。每阶段跑绿后才进入下一阶段。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/06-unit-tests/06-CONTEXT.md
|
||||
@.planning/phases/06-unit-tests/06-16-PLAN.md
|
||||
@.planning/phases/06-unit-tests/06-16-SUMMARY.md
|
||||
@.planning/phases/06-unit-tests/06-17-PLAN.md
|
||||
@.planning/phases/06-unit-tests/06-17-SUMMARY.md
|
||||
@packages-user/data-state/test/saveables.perf.ts
|
||||
@packages-user/data-state/test/saveablesReal.perf.ts
|
||||
@packages-user/data-state/test/fixtures/floors.json
|
||||
@packages-user/data-state/test/enemyCombination.test.ts
|
||||
@packages-user/data-state/src/core.ts
|
||||
@packages-user/data-state/src/enemy/aura.ts
|
||||
@packages-user/data-state/src/enemy/calculator.ts
|
||||
@packages-user/data-state/src/enemy/final.ts
|
||||
@packages-user/data-state/src/enemy/mapDamage.ts
|
||||
@packages-user/data-state/src/enemy/special.ts
|
||||
@packages-user/data-system/src/combat/context.ts
|
||||
@packages-user/data-system/src/combat/damage.ts
|
||||
@packages-user/data-system/src/combat/mapDamage.ts
|
||||
@packages-user/data-system/src/combat/types.ts
|
||||
@packages-user/data-base/src/enemy/manager.ts
|
||||
@packages-user/data-base/src/enemy/enemy.ts
|
||||
@packages-user/data-base/src/enemy/types.ts
|
||||
@packages-user/data-common/src/common/indexer.ts
|
||||
@packages-user/data-common/src/types.ts
|
||||
@vitest.perf.config.ts
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<execution_rules>
|
||||
- 只改动本计划 `files_modified` 列出的 6 个 `*.perf.ts`(其中 5 个既有文件**只改** `measureCase` 内的采样循环与配套注释,1 个新文件从零新建);**绝不修改任何生产/核心源码**(`packages/**`、`packages-user/*/src/**` 下的非 `*.perf.ts` 文件、`src/**`)。
|
||||
- **不改** `vitest.perf.config.ts` / `package.json` / `vite.config.ts` / `tsconfig*.json`:新 perf 文件由既有 `test.include: ['**/*.perf.ts']` 自动收集。
|
||||
- **不改** `packages-user/data-state/test/fixtures/floors.json`(原样只读复用,13 个 13×13 条目、编号 `1..6`、怪物格 204 个);**不改**任何既有 `*.test.ts` 与 06-01..06-17 的 PLAN/SUMMARY。
|
||||
- 新文件与改动一律 `CRLF` 换行、4 空格缩进、单引号、行宽 80(`dev.md` + `.prettierrc`);规划文档(本 PLAN / SUMMARY / ROADMAP)为 LF。
|
||||
- 每个新的顶层结构(常量组 / 接口 / 工具函数 / 工厂)前写中文单行注释或 jsDoc 说明用途;每个 `it(...)` 前写一行中文注释说明覆盖内容(`dev.md` 注释规范);注释必须解释「为什么」,不得解释「这一行做了什么」。
|
||||
- **不使用 `import type`**(用行内 `type` 修饰符,与既有 perf 文件一致);**不写连续 `as`**;能不用 `as` 就不用(类型缺口优先用显式 `interface` + 逐字段复制解决)。
|
||||
- 每个 perf 文件**内联自己的** `measureCase` 与 fixture(D-02/D-03),不新建跨文件共享文件、不引用其它 perf 文件的 helper。
|
||||
- **零断言(用户裁决)**:perf 文件不得出现任何 `expect` 调用;耗时只进表格与 SUMMARY;用例内不得因为耗时抛出或跳过。唯一的 `throw` 是夹具完整性自检(模板缺失),不得把它写成性能判据。
|
||||
- 每个 case 单独一个 `it`,`afterAll` 里统一 `console.table`,避免某个慢 case 拖垮同文件其它 case 的超时归属。
|
||||
- 测量助手参数固定且文件间逐字一致:`WARMUP_RUNS = 3`(不计时)、`SAMPLE_RUNS = 20`、下中位数 `samples[10]`、`min = samples[0]`、`p95 = samples[Math.ceil(20 * 0.95) - 1] = samples[18]`,三者统一 `Number(v.toFixed(3))`;计时只用 `globalThis.performance` 的 `mark`/`measure`(不使用 fake timers、不使用 `process.hrtime`)。
|
||||
- 未被使用的迭代变量用 `_` 前缀(`@typescript-eslint/no-unused-vars` 的 `varsIgnorePattern: '^_'`),否则会报 eslint 错误;未使用的**后置参数**直接不填(`dev.md`)。
|
||||
- 夹具必须**无告警噪声**:不得触发 `[WARNING Code 96/97/98/99/100/101/102/103/104/110/117/121/123/124/133/134/137]` 与 `[ERROR Code 55/58/59/60/61/64]`;运行输出里出现 `[WARNING Code` / `[ERROR Code` 即说明夹具有问题——**调整夹具,不要放宽**。
|
||||
- **执行协议(D-33):执行本计划前必须先向用户确认;执行完成后暂停,用中文分条简要汇报本计划结果;详细结果写入共用的 `06-TEST-FINDINGS.md`(无新增条目时注明「无新增条目」)。**
|
||||
- **阶段门禁(D-43 精神):三个阶段按 构件 → 组合 → 完整 顺序执行,每阶段跑绿后才进入下一阶段;若某阶段发现阻断性问题(例如某 case 因 `testTimeout` 失败、或 perf 文件被 `pnpm test:ci` 收集、或出现告警码),暂停并按 D-05/D-07 向用户汇报、等确认,不得跳过或弱化。**
|
||||
- **质量门禁(D-44,每个阶段执行后、提交前必过)**:对改动文件运行 `eslint --fix`,并确保 `eslint <改动文件>` **0 错误**;对本计划改动的 perf 文件运行 `pnpm exec vue-tsc --noEmit`,按输出文件路径过滤确保 **0 类型错误**(全局既有无关错误,如 `client-modules`/`legacy-plugin-data`/`legacy-ui`,不在本门禁范围);`pnpm test:ci` **全绿**,且文件数 / passed / skipped 与本计划开工前记录的基线逐项一致。任一不通过则**不得提交**。
|
||||
- **不得静默改规模或改超时**:若 `13`(52×52 / 204 怪)的某个 case 实测逼近/超过 `testTimeout`(30000ms),不得私自调小规模或调大 timeout —— 按 D-07 暂停汇报,由用户裁决。
|
||||
- **`pnpm test:ci` 基线必须现场记录**:本计划开工前先跑一次 `pnpm test:ci` 并记录文件数 / passed / skipped(06-17 收口时基线为 66 文件 / 680 passed / 1 skipped,但 Phase 7 之后可能已变化),后续每个阶段以**本次记录的基线**逐项对比,不得沿用旧数字下结论。
|
||||
</execution_rules>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>阶段 1(构件级):把 5 个既有 perf 文件的计时方式统一改为 performance.mark + performance.measure</name>
|
||||
<files>packages-user/data-system/src/combat/context.perf.ts, packages-user/data-system/src/combat/damage.perf.ts, packages-user/data-base/src/hero/attribute.perf.ts, packages-user/data-state/test/saveables.perf.ts, packages-user/data-state/test/saveablesReal.perf.ts</files>
|
||||
<read_first>
|
||||
5 个既有 perf 文件中 `measureCase` 的采样循环(5 处**逐字相同**的 6 行):
|
||||
`packages-user/data-system/src/combat/damage.perf.ts` L120–125、`packages-user/data-system/src/combat/context.perf.ts` L123–128、`packages-user/data-base/src/hero/attribute.perf.ts` L124–129、`packages-user/data-state/test/saveables.perf.ts` L95–100、`packages-user/data-state/test/saveablesReal.perf.ts` L192–197 —— 均为 `const samples: number[] = [];` + `for (let i = 0; i < SAMPLE_RUNS; i++) {` + `const start = globalThis.performance.now();` + `run();` + `samples.push(globalThis.performance.now() - start);` + `}`;上方紧邻 `for (let i = 0; i < WARMUP_RUNS; i++) { run(); }` 预热循环,下方紧邻 `samples.sort(...)` 与 `PerfRecord` 组装(`samples[SAMPLE_RUNS / 2]` / `samples[0]` / `samples[Math.ceil(SAMPLE_RUNS * 0.95) - 1]` + `Number(v.toFixed(3))`)
|
||||
同一文件内的 `afterAll(() => { console.table(records); })` 与 `WARMUP_RUNS = 3` / `SAMPLE_RUNS = 20` 常量(本任务**不得改动**)
|
||||
vitest.perf.config.ts(`test.include = ['**/*.perf.ts']`、`testTimeout`/`hookTimeout` = 30000 —— 本任务不得改动)
|
||||
dev.md(CRLF / 4 空格 / 单引号 / 行宽 80 / 注释规范 / `_` 前缀 / 禁 `import type` / 禁连续 `as`)
|
||||
本计划 key_links 第 1 条(Node `v22.18.0` 全局 `performance` 的 mark/measure/clearMarks/clearMeasures 已实测可用;对不存在的标记做 measure 会抛错;同名标记清理后可复用)
|
||||
</read_first>
|
||||
<action>
|
||||
对 5 个既有 perf 文件做**同一处、同一形态**的改动(每个文件只改 `measureCase` 内部的采样循环 + 在其上方补 2 行说明性注释;其余内容一字不动):
|
||||
|
||||
**① 把采样循环的 6 行替换为「成对 mark + measure + 清理」**(每个文件都写成下列等价形态,保持 4 空格缩进、行宽 80):
|
||||
- 采样前(紧跟预热循环之后)先算三个名字,用字符串拼接而非模板串(与既有文件的拼接风格一致):`const startTag = 'perf:' + caseName + ':' + scale + ':start';`、`const endTag = 'perf:' + caseName + ':' + scale + ':end';`、`const measureName = 'perf:' + caseName + ':' + scale;`;三个名字都基于 `caseName` 与 `scale`,故**跨 case、跨规模唯一**,不同 `it` 之间不会互相覆盖。
|
||||
- 循环体内依次为:`globalThis.performance.mark(startTag);` → `run();` → `globalThis.performance.mark(endTag);` → `const measure = globalThis.performance.measure(measureName, startTag, endTag);`(超 80 列时按 `measure(measureName,\n startTag,\n endTag\n)` 换行)→ `samples.push(measure.duration);` → `globalThis.performance.clearMarks(startTag);` → `globalThis.performance.clearMarks(endTag);` → `globalThis.performance.clearMeasures(measureName);`。
|
||||
- **顺序不可颠倒**:`measure` 必须紧跟 `endTag` 之后、清理之前;清理必须写在循环体内(每个样本后立即清理),否则同名标记会被后续 `measure` 解析成**第一次**的标记位置、`duration` 变成累计值。
|
||||
- 采样循环的 `const samples: number[] = [];` 与闭括号位置不变;`samples.push(...)` 推入的仍是「单个样本的毫秒数」(`PerformanceMeasure.duration`),故其后的排序与三档统计**逐字不变**。
|
||||
- **不要**改动 `WARMUP_RUNS`/`SAMPLE_RUNS`、排序、`median`/`min`/`p95` 的取值下标、`Number(v.toFixed(3))`、`records.push(record)`、`return record`、`afterAll` 的 `console.table`。
|
||||
|
||||
**② 在采样循环上方补注释(每文件 2 行,解释「为什么」)**:说明本文件计时经 `performance.mark`/`measure` 成对标记完成,且每样本后清理标记与测量,避免重复采样时同名标记被解析成首次位置、也避免标记随样本数无限累积。
|
||||
|
||||
**③ 同步更新 `measureCase` 的 jsDoc 描述**:原描述「先预热若干次,再采样后取中位数、最小值与 p95」保留,追加一句说明「计时经 `performance.mark`/`performance.measure` 成对标记,采样后按名清理标记与测量」。5 个文件的 jsDoc 改法一致。
|
||||
|
||||
**④ 不改动任何其它内容**:各文件的 fixture、case 数量、`it` 名称、`describe` 名称、`vi.hoisted` stub、导入、常量、缩进风格、`afterAll` 全部一字不动(本任务**只是换计时源**)。改完后 5 个文件的 `measureCase` 仍然彼此逐字一致。
|
||||
|
||||
**⑤ 现场记录 `pnpm test:ci` 基线**:本阶段第一件事是跑一次 `pnpm test:ci` 并记下文件数 / passed / skipped(06-17 收口时为 66 / 680 / 1,Phase 7 之后可能已变化);后续每个阶段都以本阶段记录的基线逐项对比。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm test:perf(须仍收集 5 个 `*.perf.ts`、`Tests 45 passed`、打印 45 行 case 表,列仍为 `case`/`scale`/`median ms`/`min ms`/`p95 ms`;无断言失败;输出中不出现 `[WARNING Code` / `[ERROR Code`);pnpm exec vitest list --filesOnly(默认配置;输出不得含 `.perf.ts`;当前实测 66 个文件、`.perf.ts` 匹配 0);pnpm test:ci(全绿,文件数 / passed / skipped 与本阶段记录的基线逐项一致);pnpm exec vue-tsc --noEmit(按 5 个改动文件路径过滤 0 条)</automated>
|
||||
<fails_when>任一文件遗漏改动(仍出现 `globalThis.performance.now()`)或只改了部分文件;或 `measure` 与 `clearMarks`/`clearMeasures` 顺序颠倒、清理写在循环外(会得到累计耗时);或改动波及 `WARMUP_RUNS`/`SAMPLE_RUNS`/统计口径/输出列/`console.table`(45 行变少或列名变化);或 `pnpm test:perf` 收集文件数不再是 5、用例数不再是 45;或运行输出出现任何 `[WARNING Code` / `[ERROR Code`;或 `pnpm exec vitest list --filesOnly` 输出中出现 `.perf.ts`;或本任务改动了任何生产源码、配置、既有 `*.test.ts`、`saveablesReal.perf.ts` 的 fixture 部分</fails_when>
|
||||
<acceptance_criteria>
|
||||
- 5 个文件(`context.perf.ts` / `damage.perf.ts` / `attribute.perf.ts` / `saveables.perf.ts` / `saveablesReal.perf.ts`)的 `measureCase` 采样循环均改为 `mark(startTag)` → `run()` → `mark(endTag)` → `measure(measureName, startTag, endTag)` → `samples.push(measure.duration)` → `clearMarks(startTag)` → `clearMarks(endTag)` → `clearMeasures(measureName)`;三个名字均由 `'perf:' + caseName + ':' + scale + ...` 拼出,跨 case/规模唯一。
|
||||
- 文件内已无 `globalThis.performance.now()`;`WARMUP_RUNS`/`SAMPLE_RUNS`/下中位数/`min`/`p95`/`toFixed(3)`/`console.table`/零断言均一字未变;5 个文件的 `measureCase` 仍彼此逐字一致。
|
||||
- `pnpm test:perf`:5 文件 / **45 passed** / 45 行表,无断言失败、无 warn/error 码噪声;`pnpm exec vitest list --filesOnly`(默认配置)输出不含 `.perf.ts`;`pnpm test:ci` 全绿且与本阶段记录的基线逐项一致(基线已现场记录)。
|
||||
- 本阶段改动的 5 个文件 `eslint --fix` 后 `eslint` 0 错误(`console.table` 只触发 `no-console` 的 warn,不是 error);`pnpm exec vue-tsc --noEmit` 按这 5 个文件路径过滤 0 条。
|
||||
- `git status` 只显示这 5 个 perf 文件的改动(加规划产物),无生产源码/配置改动、无新依赖。
|
||||
</acceptance_criteria>
|
||||
</verify>
|
||||
<done>5 个既有 perf 文件全部换成 mark/measure 计时、`pnpm test:perf` 仍为 5 文件 / 45 行且零告警、`pnpm test:ci` 与现场基线一致后,才进入阶段 2。若某文件漏改、或改出累积耗时(清理顺序问题)、或出现告警码,暂停并按 D-07 汇报、等确认。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>阶段 2(组合级):新建 mapScenario.perf.ts —— 合并大图 + 12 个真实怪物模板 + ① 上下文构建 / ② 地图伤害两个测量项</name>
|
||||
<files>packages-user/data-state/test/mapScenario.perf.ts</files>
|
||||
<read_first>
|
||||
packages-user/data-state/test/fixtures/floors.json(结构 `{datasetId: number, data: {<floorId>: {map: number[][], size: number[], ...}}}`;13 个条目、全部 13×13;出现过的编号 `1..6`;编号 `4`(怪物)每图 11–19、合计 **204**,前 1 张 11、前 5 张 79)
|
||||
packages-user/data-state/test/saveablesReal.perf.ts(同目录既有 perf 文件的完整形态:L16–40 `vi.hoisted` stub + `Map.prototype` 补丁;L97–122 用内联接口逐字段复制 JSON;L124–158 收集数据集中出现过的图块编号;L160–174 `toFlat`;L182–208 `measureCase`;L210–226 `registerTiles`;L385–411 `fromRaw` 注入 + `setMapActiveStatus`;L413–415 `afterAll` —— 新文件必须与之同构)
|
||||
packages-user/data-state/test/enemyCombination.test.ts(真实顶层链路夹具的**权威写法**:L26–48 `vi.hoisted` stub;L55–74 内联 `createSpecial`(含 `clone`/`deepEqualsTo`/`saveState`/`loadState`,返回时单次 `as ISpecial<T>`);L91–111 `createEnemy` 用真实 `Enemy` + `guard: new Set()`;L168–179 `createContext`(真实 `CommonAuraConverter` / `GuardAuraConverter` / `MainEnemyFinalEffect`);L185–205 组合特殊属性写法)
|
||||
packages-user/data-state/src/core.ts(L94–126 字段;L182–184 `HeroState` + `heroAttribute`;L189–200 `EnemyManager` + `registerSpecials`;L206–220 战斗链路装配:`EnemyContext`/`DamageSystem`/`MapDamage` + `useCalculator`/`useReducer`/`useConverter` + `attachDamageSystem`/`attachMapDamage` + 两个光环转换器 + 最终效果 + `resize(13,13)` + `bindHero`;L550–552 `createCoreState`)
|
||||
packages-user/data-state/src/enemy/aura.ts(L29–43 `CommonAuraConverter`:`shouldConvert` 仅 `code === 25`;L45–119 `CommonAura`:`priority = 25`、`couldApplySpecial = false`、`createRange` 按 `haloRange`/`haloSquare` 选 Full/Rect/Manhattan;L125–184 `GuardAuraConverter`(仅 `code === 26`)+ `GuardAura`(`priority = 26`、`range = RECT_RANGE`、`apply` 往邻居 `guard` 集合加来源坐标))
|
||||
packages-user/data-state/src/enemy/calculator.ts(L11–183 真实 `MainDamageCalculator`:`heroPerDamage <= 0` → `{ damage: Infinity, turn: 0 }`;`3 坚固` 的 `getCriticalLimit` 返回 `Infinity`;`guard` 递归支援;`handler.state.flags.getFieldValueDefaults('enableNegativeDamage', false)`)
|
||||
packages-user/data-state/src/enemy/final.ts(L4–27 `MainEnemyFinalEffect`:`3 坚固` 把 `def` 抬到 `max(def, heroAtk - 1)`;`10 模仿` 会用勇士攻防覆盖)
|
||||
packages-user/data-state/src/enemy/mapDamage.ts(L83–118 `ZoneDamageView`(`15`,按 `zoneSquare` 选 Rect/Manhattan);L120–152 `RepulseDamageView`(`18`,Manhattan 1);L154–183 `LaserDamageView`(`24`,RayRange,构造时消费 `face.mapMovement()`);L185–254 `BetweenDamageView`(`16`,**读全局 `core.flags.betweenAttackMax`**);L256–281 `AmbushDamageView`(`27`);L287–324 `MainMapDamageConverter`(L314 需要 `handler.state.faceManager.get(FaceGroup.Dir4)`);L330–356 `MainMapDamageReducer`)
|
||||
packages-user/data-state/src/enemy/special.ts(L27–33 `IHaloValue`;L51 `registerSpecials` 注册 `0..27`;`25` 的默认值 `{haloRange: 0, haloSquare: false, hpBuff: 0, atkBuff: 0, defBuff: 0}`)
|
||||
packages-user/data-system/src/combat/context.ts(L103 构造;L105–111 `resize` → `clear()`;L193–198 `bindHero`(`needUpdate = true` + `damageSystem.bindHeroStatus` + `mapDamage.refreshAll()`);L280–299 `setEnemyAt`(每次 → `mapDamage.markEnemyDirty` / `damageSystem.markDirty`);L335–340 `iterateEnemy`;L684–720 `buildup`(L685 `!needUpdate` 早退、末尾 `damageSystem.markAllDirty()` + `mapDamage.refreshAll()`);L879–895 `requestRefresh`(`needUpdate` 时反过来调 `buildup()`))
|
||||
packages-user/data-system/src/combat/mapDamage.ts(L69–77 构造 + `useConverter`;L99–102 `useReducer`;L169–189 `getReducedDamage`(无 reducer → `warn(103)`);L214–222 `getSeparatedDamage`;L374–390 `refreshAll`(无 converter/reducer → `warn(102/103)`);L131–148 `markEnemyDirty` 对新视图走 `refreshAll`)
|
||||
packages-user/data-system/src/combat/damage.ts(L134–194 `calculateCritical`(L152 `heroStatus.getModifiableClone()`、L159 上界、L166 `clamp(precision, 4, 64)`);L245–322 `DamageSystem`(继承 `DamageContext`);L319–321 `with(hero)`)
|
||||
packages-user/data-system/src/combat/types.ts(L284–350 `IMapDamage`:`refreshAll`/`getSeparatedDamage`/`getReducedDamage`;L407–443 `IDamageContext` 含 `calculateCritical`;L445–491 `IDamageSystem`;L497–572 `IReadonlyEnemyContext`(含 `iterateEnemy`/`getMapDamage`/`getDamageSystem`);L572–740 `IEnemyContext`(`resize`/`setEnemyAt`/`buildup`))
|
||||
packages-user/data-base/src/enemy/manager.ts(L119–131 `createEnemy(code)` / `createEnemyById`;L143–154 `addPrefab`(code 或 id 已存在则直接返回);L276–289 `updateDirty`(`!hasReference` 直接返回))
|
||||
packages-user/data-base/src/enemy/enemy.ts(L25–32 `addSpecial`(同代码重复 → `warn(96)`);L46–59 属性读写;L65–84 `clone`/`copyFrom`(`structuredClone`))
|
||||
packages-user/data-base/src/enemy/types.ts(L31–74 `ISpecial` 需要 `code`/`value`/`setValue`/`getValue`/`getSpecialName`/`getDescription`/`fromLegacyEnemy`/`clone`/`deepEqualsTo` + `ISaveableContent` 的 `saveState`/`loadState`;L76–160 `IReadonlyEnemy`/`IEnemy`)
|
||||
packages-user/data-common/src/types.ts(L7–22 `IEnemyAttr`:`hp`/`atk`/`def`/`money`/`exp`/`point`/`guard: Set<ITileLocator>`;L24–45 `IHeroAttr` 的 10 个字段)
|
||||
packages-user/data-common/src/common/indexer.ts(L38–59 `MapLocIndexer`:`setWidth` + `y * width + x` 索引约定 —— 合并地图坐标必须落在 `[0, width)` × `[0, height)`)
|
||||
packages-user/data-base/src/map/mapState.ts(L190–247 `fromRaw`:`validateRaw` 要求 `events` 有图层键、`map.length % width === 0`、`layerAlias[z]` 是字符串;L249–259 `createMap`(同 id 重复 → `warn(121)`))
|
||||
packages-user/data-state/src/shared.ts(L31–42 `HERO_DEFAULT_ATTRIBUTE` 字段名:`name`/`hp`/`hpmax`/`atk`/`def`/`mdef`/`mana`/`manamax`/`money`/`exp`)
|
||||
packages-user/data-state/src/enemy/mapDamage.test.ts(L18–45 既有 `vi.hoisted` 里同时 stub `main`/`location`/**`core`** 的写法 —— 含 `16` 夹击时必须有 `core` stub)
|
||||
vitest.perf.config.ts、dev.md(CRLF / 4 空格 / 单引号 / 行宽 80 / 注释规范 / 禁 `import type` / 禁连续 `as` / `_` 前缀 / 未使用后置参数不填)
|
||||
</read_first>
|
||||
<action>
|
||||
新建 `packages-user/data-state/test/mapScenario.perf.ts`(本阶段**只做 ①②,不做 ③**;文件首行中文注释说明用途:基于 13 张真实地图拼接的单一大图场景测量怪物上下文构建与地图伤害构建耗时,只记录不断言)。结构如下:
|
||||
|
||||
**① 头部与 stub**
|
||||
- `vi.hoisted(() => { ... })` 同时 stub `vi.stubGlobal('main', { replayChecking: true })`、`vi.stubGlobal('location', { origin: 'http://localhost' })`、**`vi.stubGlobal('core', { flags: { betweenAttackMax: false } })`**(`16 夹击` 的 `BetweenDamageView` 会读全局 `core.flags.betweenAttackMax`,`vm` 里不存在会直接抛错),并补 `Map.prototype.getOrInsert` / `getOrInsertComputed` 两个补丁(逐字镜像 `saveablesReal.perf.ts` L19–39)。
|
||||
- 静态 import:`{ afterAll, describe, it, vi } from 'vitest'`;`{ type IEnemyAttr, type IHeroAttr, type IMapRawData, TileType } from '@user/data-common'`;`{ Enemy, type IEnemy, type ISpecial } from '@user/data-base'`;`{ type IDamageSystem, type IMapDamage } from '@user/data-system'`;`{ type ITileLocator } from '@motajs/common'`(如未被用到必须删除,否则 D-44 eslint 不通过);`{ CoreState, createCoreState } from '../src/core'`;`import floorDataset from './fixtures/floors.json'`。**不使用 `import type` 语法**。
|
||||
- `type HeroKey = SelectKey<IHeroAttr, number>;`(`SelectKey` 是 `src/types/declaration/util.d.ts` 的全局环境类型,无需 import)。
|
||||
|
||||
**② 常量(全部显式标注 `readonly number[]` 等,不要写成 `as const` 数组以免字面量收窄)**
|
||||
- `WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`(与既有 lane 逐字一致)。
|
||||
- `MAP_SCALES: readonly number[] = [1, 5, 13]`(合并地图的规模档:用数据集前 N 张)。
|
||||
- `MAP_SIZE = 13`(单张真实地图边长;数据集实测 13 张全部 13×13)。
|
||||
- `MONSTER_TILE_CODE = 4`(数据集里怪物图块编号)。
|
||||
- `PERF_SEED = 20260916`(确定性种子,保证分配可复现)。
|
||||
- `HERO_ATTRIBUTE_VALUES: readonly [HeroKey, number][] = [['hp', 1000], ['hpmax', 1000], ['atk', 10], ['def', 5], ['mdef', 3], ['mana', 0], ['manamax', 0], ['money', 0], ['exp', 0]]`(勇士攻击 `10` 高于全部模板的 `def`(0–6),保证 `heroPerDamage > 0`、伤害有限、临界枚举非空;`3 坚固` 经最终效果把 `def` 抬到 `9`,`heroPerDamage = 1` 仍有界)。
|
||||
|
||||
**③ 数据集读取(不引入任何 `as`)**:内联 `interface IFloorEntry { readonly map: number[][]; readonly size: number[] }` 与 `interface IFloorDataset { readonly datasetId: number; readonly data: Record<string, IFloorEntry> }`,`const dataset: IFloorDataset = floorDataset;`,再用 `for (const [name, entry] of Object.entries(dataset.data))` 逐字段复制进 `const FLOOR_DATA: Record<string, IFloorEntry>`(显式只保留 `map`/`size`);`const FLOOR_NAMES: readonly string[] = Object.keys(FLOOR_DATA);`(JSON 声明顺序即稳定顺序,故「前 N 张」确定性)。**数据集文件只读,不得改动**。
|
||||
|
||||
**④ 图块编号与注册**:`const TILE_CODES: readonly number[] = collectTileCodes();` —— 遍历**全部 13 个条目**收集非零编号并升序去重(实测 `1..6`;与规模无关);`registerTiles(state: CoreState)` 对每个编号调用一次 `state.tileStore.addTile({ num, id: 'perf-scenario-tile-' + num.toString(), events: {}, type: TileType.Terrain, pass: { onlyEvents: false, inPass: 15, outPass: 15 }, eventPass: true })`(每编号只注册一次,避免 `warn(133/134)`)。
|
||||
|
||||
**⑤ 合并地图构造**:`buildMergedMap(mapCount: number): number[][]` —— 列数 `const columns = Math.ceil(Math.sqrt(mapCount));`、行数 `const rows = Math.ceil(mapCount / columns);`、`const width = columns * MAP_SIZE;`、`const height = rows * MAP_SIZE;`;先用两层循环建 `height` 行、每行 `width` 个 `0` 的矩阵(用 `const row: number[] = []; for (...) row.push(0); merged.push(row);` 而不是 `new Array`);再对前 `mapCount` 张地图按 `const originX = (i % columns) * MAP_SIZE; const originY = Math.floor(i / columns) * MAP_SIZE;` 把 13×13 逐格拷进合并矩阵(保留外墙 `1` 与怪物 `4`)。预期:`1 → 13×13`(怪物 **11**)、`5 → 39×26`(怪物 **79**)、`13 → 52×52`(怪物 **204**);只有 `5` 这一档会有 1 个空格位(`3×2 - 5`),补 `0` 后仍是矩形,满足 `map.length % width === 0`。
|
||||
|
||||
**⑥ 确定性 PRNG**:`createRandom(seed: number): () => number` —— mulberry32(`let state = seed;` 内部闭包:`state = (state + 0x6d2b79f5) | 0; let t = state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296;`);函数名按 `dev.md` 的 `createXxx` 约定,返回函数是有意为之(PRNG 必须携带内部状态),并在注释里说明。
|
||||
|
||||
**⑦ 怪物阵容:12 个真实模板(`IMonsterPrefab` 接口 + `MONSTER_PORTFOLIO` 常量)**
|
||||
- 接口:`interface IMonsterAttrs { readonly hp: number; readonly atk: number; readonly def: number; readonly money: number; readonly exp: number; readonly point: number }`;`interface IMonsterPrefab { readonly id: string; readonly code: number; readonly attrs: IMonsterAttrs; readonly specials: readonly ISpecial<any>[] }`(`dev.md`:对象类型必须单独声明 `interface`)。
|
||||
- 编号取 `100..111`(与地图图块编号 `1..6` 解耦,不进入 tileStore)。
|
||||
- 12 个模板(每个模板内部的特殊属性代码**两两不重复**;每个特殊属性都用新的 `createSpecial(...)` 调用生成,不跨模板复用实例):
|
||||
1. `'perf-aura-full'` code `100`:hp 120 / atk 12 / def 3 / money 5 / exp 7 / point 1;特殊属性 `[25 { haloRange: 0, haloSquare: false, hpBuff: 0, atkBuff: 30, defBuff: 0 }]`(→ **FullRange**)。
|
||||
2. `'perf-aura-manhattan'` code `101`:hp 90 / atk 9 / def 2 / money 4 / exp 6 / point 1;`[25 { haloRange: 3, haloSquare: false, hpBuff: 20, atkBuff: 0, defBuff: 0 }]`(→ **ManhattanRange**)。
|
||||
3. `'perf-aura-rect'` code `102`:hp 150 / atk 15 / def 5 / money 8 / exp 12 / point 2;`[25 { haloRange: 2, haloSquare: true, hpBuff: 0, atkBuff: 0, defBuff: 25 }, 7 破甲 100]`(→ **RectRange**)。
|
||||
4. `'perf-guard'` code `103`:hp 200 / atk 18 / def 6 / money 12 / exp 20 / point 3;`[26 支援(value 为 `undefined`)]`(→ 真实 `GuardAura`,会让相邻怪物的 `guard` 集合带上来源坐标,从而在伤害/临界路径上触发真实支援递归)。
|
||||
5. `'perf-zone-cross'` code `104`:hp 60 / atk 6 / def 1 / money 2 / exp 3 / point 1;`[15 { zone: 5, zoneSquare: false, range: 2 }]`(→ Manhattan 领域视图)。
|
||||
6. `'perf-zone-square'` code `105`:hp 70 / atk 7 / def 2 / money 3 / exp 4 / point 1;`[15 { zone: 8, zoneSquare: true, range: 1 }, 2 魔攻]`(→ Rect 领域视图)。
|
||||
7. `'perf-repulse'` code `106`:hp 80 / atk 8 / def 2 / money 3 / exp 5 / point 1;`[18 3, 22 固伤 7, 9 净化 2]`。
|
||||
8. `'perf-laser'` code `107`:hp 55 / atk 5 / def 1 / money 2 / exp 3 / point 1;`[24 4, 4 二连击]`。
|
||||
9. `'perf-ambush'` code `108`:hp 65 / atk 6 / def 1 / money 3 / exp 4 / point 1;`[27, 1 先攻]`。
|
||||
10. `'perf-between'` code `109`:hp 45 / atk 4 / def 0 / money 1 / exp 2 / point 1;`[16, 6 n连击 2]`。
|
||||
11. `'perf-vampire'` code `110`:hp 110 / atk 11 / def 3 / money 5 / exp 8 / point 1;`[11 { vampire: 10, add: true }, 8 反击 50]`。
|
||||
12. `'perf-sturdy'` code `111`:hp 140 / atk 13 / def 4 / money 6 / exp 9 / point 1;`[3 坚固, 17 仇恨]`。
|
||||
- **刻意排除并注明理由**:`20 无敌`(伤害恒为 `Infinity`、临界无意义)、`10 模仿`(会覆盖攻防、抹平阵容多样性)、`12/13/14/19/21/23`(不在本次测量的三条路径上生效)。
|
||||
- 内联特殊属性工厂 `createSpecial<T>(code: number, value: T): ISpecial<T>`:逐字镜像 `enemyCombination.test.ts` L55–74(`code`/`value`/`getValue`/`setValue`/`getSpecialName`/`getDescription`/`fromLegacyEnemy`/`clone`(`structuredClone(special.value)`)/`deepEqualsTo`/`saveState`(`structuredClone(special.value)`)/`loadState`),返回时单次 `as ISpecial<T>`(**不得**写连续 `as`)。
|
||||
- 模板工厂 `createPrefab(entry: IMonsterPrefab): IEnemy<IEnemyAttr>`:`const enemy = new Enemy<IEnemyAttr>(entry.id, entry.code, { hp, atk, def, money, exp, point, guard: new Set() })`(`guard: new Set()` 由 `IEnemyAttr` 上下文推断为 `Set<ITileLocator>`,缺失会让 `GuardAura.apply` 抛错);`for (const special of entry.specials) enemy.addSpecial(special);` → `return enemy;`。
|
||||
- `registerPrefabs(state: CoreState)`:`for (const entry of MONSTER_PORTFOLIO) state.enemyManager.addPrefab(createPrefab(entry));`(真实 `EnemyManager` 路径;`addPrefab` 对重复 code/id 直接返回)。
|
||||
|
||||
**⑧ 勇士属性**:`seedHero(state: CoreState)`:`const attribute = state.hero.getModifiableAttribute();` 后 `for (const [name, value] of HERO_ATTRIBUTE_VALUES) attribute.set(name, value);`(CoreState 已把该属性绑定给 `enemyContext`,故 `buildup` 的最终效果与伤害计算读到的就是这些值)。**不挂任何修饰器、不建装备/道具**。
|
||||
|
||||
**⑨ 地图注入**:`injectMergedMap(state: CoreState, matrix: number[][], width: number)`:`const raw: IMapRawData = { floorId: 'perf-scenario-merged', width, map: { 0: toFlat(matrix) }, layerAlias: { 0: 'bg' }, events: { 0: {} } };` → `state.maps.fromRaw(raw);`。说明:每个 `it` 都新建 `CoreState`,固定 `floorId` 不会触发 `warn(121)`;`layerAlias` 用 `'bg'`(**不要**用 `'event'`);实时内容就是原始编号(不套 06-17 的清除规则)。
|
||||
|
||||
**⑩ 怪物放置**:`placeMonsters(state: CoreState, matrix: number[][], mapCount: number): number`:`const random = createRandom(PERF_SEED + mapCount);`(规模进种子,保证各 scale 的分配互不影响)→ 按行主序遍历合并矩阵,命中 `MONSTER_TILE_CODE` 时 `const entry = MONSTER_PORTFOLIO[Math.floor(random() * MONSTER_PORTFOLIO.length)];` → `const enemy = state.enemyManager.createEnemy(entry.code);` → `if (!enemy) throw new Error('性能夹具:找不到怪物模板 ' + entry.code.toString());`(夹具完整性自检)→ `state.enemyContext.setEnemyAt({ x, y }, enemy);` → 计数并返回怪物总数。坐标必须落在 `[0, width)` × `[0, height)`(由合并矩阵的两层循环天然保证)。
|
||||
|
||||
**⑪ 夹具与工厂**
|
||||
- `interface IScenarioFixture { readonly state: CoreState; readonly mapDamage: IMapDamage<IEnemyAttr, IHeroAttr>; readonly width: number; readonly height: number; readonly monsterCount: number; }` ——`mapDamage` 在工厂里用 `state.enemyContext.getMapDamage()!` 取一次(CoreState 构造函数必然装配,属「必要的非空断言」,`dev.md` 允许)。
|
||||
- `createScenarioFixture(mapCount: number): IScenarioFixture` 顺序:`buildMergedMap(mapCount)` → 取 `width`/`height` → `createCoreState()` → **`state.enemyContext.resize(width, height)`**(必须在放置怪物之前:`resize` 会 `clear()` 并重置索引宽度;CoreState 默认 `resize(13,13)` 必须被覆盖)→ `registerTiles(state)` → `registerPrefabs(state)` → `seedHero(state)` → `injectMergedMap(state, matrix, width)` → `placeMonsters(...)` 得 `monsterCount` → **`state.enemyContext.buildup()`(首轮全量构建,不计时;同时把 `needUpdate` 清掉,避免第一次取计算后怪物时把全量构建算进采样)** → 返回夹具。
|
||||
|
||||
**⑫ 测量助手(与阶段 1 改造后的 5 个文件逐字同构,文件内联)**:`interface PerfRecord { case: string; scale: string; 'median ms': number; 'min ms': number; 'p95 ms': number }`、模块级 `const records: PerfRecord[] = []`、`function measureCase(caseName: string, scale: string, run: () => void): PerfRecord`:先 `WARMUP_RUNS` 次 `run()`(不计时);再采样 `SAMPLE_RUNS` 次,每样本 `globalThis.performance.mark(startTag)` → `run()` → `globalThis.performance.mark(endTag)` → `const measure = globalThis.performance.measure(measureName, startTag, endTag)` → `samples.push(measure.duration)` → 逐样本 `clearMarks`/`clearMeasures`(三个名字为 `'perf:' + caseName + ':' + scale` 系列,与阶段 1 写法一致);排序后取 `samples[SAMPLE_RUNS / 2]`(下中位数)、`samples[0]`、`samples[Math.ceil(SAMPLE_RUNS * 0.95) - 1]`,三者 `Number(v.toFixed(3))`;`records.push(record)` 后 `return record`。`afterAll(() => { console.table(records); })`。
|
||||
|
||||
**⑬ 本阶段的 6 个 `it`(每个独立 `it`、前一行中文注释;`scale` 标签统一为 `地图张数 + '/' + 怪物数`,如 `'13/204'`,含义写进文件注释与 SUMMARY)**
|
||||
- ① 怪物上下文构建(3 行):`const fixture = createScenarioFixture(n);` 后 `measureCase('怪物上下文构建', label, () => { fixture.state.enemyContext.bindHero(<绑定的勇士属性>); fixture.state.enemyContext.buildup(); });` —— **必须每样本先 `bindHero`**:`buildup()` 在 `needUpdate === false` 时直接返回,`bindHero` 是公开且最省的 `needUpdate` 触发器(与 06-16 ① 同构);勇士属性对象在工厂里一并返回(如 `IScenarioFixture` 增补 `readonly hero: IReadonlyHeroAttribute<IHeroAttr>` 字段,取 `state.enemyContext.getBindedHero()!`),**不要**在样本里做别的副作用操作。
|
||||
- ② 地图伤害构建(3 行):`measureCase('地图伤害构建', label, () => { fixture.mapDamage.refreshAll(); for (const entry of fixture.state.enemyContext.iterateEnemy()) { fixture.mapDamage.getSeparatedDamage(entry[0]); fixture.mapDamage.getReducedDamage(entry[0]); } });` —— 单样本 = 全量重建 + 对每个怪物坐标消费「未合并伤害」与「合并后伤害」(真实 `MapDamage` + `MainMapDamageReducer` 路径);两处返回值不需要使用,故只写调用语句(`dev.md`:不得把对象函数赋给临时变量)。
|
||||
- `scale` 标签用 `String(mapCount) + '/' + String(fixture.monsterCount)`。
|
||||
- 全文件**不得出现任何断言调用**;不使用 fake timers / `it.skip` / `it.todo`。
|
||||
- 若某个 `it`(尤其 `13`)实测逼近或超过 `testTimeout`(30000ms),**不得**调小规模或调大 timeout —— 按 D-07 暂停汇报、等用户裁决。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm test:perf(须收集 6 个 `*.perf.ts`、`Tests 51 passed`、打印 45 + 6 = 51 行 case 表;无断言失败;输出中不出现 `[WARNING Code` / `[ERROR Code`);pnpm exec vitest list --filesOnly(默认配置;输出仍不含 `.perf.ts`;文件数与本阶段基线一致);pnpm test:ci(全绿,与基线逐项一致);pnpm exec vue-tsc --noEmit(按 `mapScenario.perf.ts` 过滤 0 条)</automated>
|
||||
<fails_when>新文件未生成或未被 `pnpm test:perf` 收集;或合并地图尺寸不是 `13×13` / `39×26` / `52×52`、怪物数不是 11 / 79 / 204;或规模只有 1 档、或缺少 ①/② 中任一测量项;或模板数不在 10–15、或带光环模板少于 3 个、或三种光环范围(Full/Rect/Manhattan)+ `GuardAura` 未全部覆盖;或分配不是确定性的(同一规模两次运行怪物数量/模板分布不一致);或帧面里出现 `[WARNING Code 96]`(同模板重复特殊属性)、`[WARNING Code 97]`(两个转换器命中同一代码)、`[WARNING Code 98/99/100/101]`(光环特殊属性链路被误触发)、`[WARNING Code 102/103]`(`MapDamage` 缺转换器/合并器)、`[WARNING Code 104]`(怪物视图无坐标)、`[WARNING Code 110]`(`buildup` 无绑定勇士)、`[WARNING Code 121]`(floorId 重复)、`[WARNING Code 123]`(地图矩阵长度与宽高不符,说明没 `flat()` 或宽度取错)、`[WARNING Code 133/134]`(图块编号/id 重复)、`[WARNING Code 137]`(支援坐标查不到怪物)、`[ERROR Code 60/61]`(矩阵长度不一致 / 不能被宽度整除)之一,或出现 `TypeError`(多为全局 `core` 未 stub 或 `guard` 非 Set);或文件内出现 `expect(`;或本阶段改动了生产源码、配置、既有 `*.test.ts`、`floors.json`、阶段 1 已改好的 5 个文件</fails_when>
|
||||
<acceptance_criteria>
|
||||
- 新增 `packages-user/data-state/test/mapScenario.perf.ts`:`vi.hoisted` 同时 stub `main`/`location`/**`core`** + `Map` 补丁;静态 JSON 导入 + 内联 `IFloorEntry`/`IFloorDataset`/`FLOOR_DATA`/`TILE_CODES`/`TILE_CODES` 收集函数/`buildMergedMap`/`createRandom`(mulberry32)/`createSpecial`/`createPrefab`/`registerTiles`/`registerPrefabs`/`seedHero`/`injectMergedMap`/`placeMonsters`/`createScenarioFixture`/内联 `measureCase`(mark + measure + 逐样本清理)+ 6 个 `it` + `afterAll(console.table)`。
|
||||
- 合并地图:`ceil(sqrt(n))` 列、每张按 `(col*13, row*13)` 放置、空位补 `0`;`n = 1/5/13` → `13×13` / `39×26` / `52×52`,怪物 **11 / 79 / 204**;保留原始编号(外墙 `1` 与怪物 `4` 均在),图块注册集合恒为 `1..6`。
|
||||
- 12 个真实模板(编号 `100..111`)、属性各不相同、特殊属性代码在模板内唯一;其中 `100/101/102` 是 `25` 的 Full/Manhattan/Rect 三种范围、`103` 是 `26`(`GuardAura`);`104..109` 覆盖 `15`/`18`/`24`/`27`/`16` 五种地图伤害视图,`106/107/108/109/110/111` 覆盖 `1/2/3/4/6/7/8/9/11/17/22` 伤害分支;不含 `20`。
|
||||
- 分配为 `mulberry32` 固定种子(`PERF_SEED`)确定性映射;场景经 `createCoreState()`(真实 `registerSpecials` + 真实计算器/转换器/合并器/最终效果)+ `resize(合并尺寸)` + `addPrefab`×12 + 真实勇士属性 + `fromRaw` + `setEnemyAt` 逐格装配 - `buildup()` 一次,且**未手工 new 战斗对象**。
|
||||
- `pnpm test:perf` 输出 **51 行**(45 既有 + 6 新增),无断言失败、无 warn/error 码噪声;`pnpm exec vitest list --filesOnly`(默认配置)不含 `.perf.ts`;`pnpm test:ci` 全绿且与基线逐项一致。
|
||||
- 全文件 0 个断言调用;`measureCase` 与 fixture 均为文件内联(无跨文件共享)。
|
||||
- D-44 门禁通过:`mapScenario.perf.ts` `eslint --fix` 后 0 错误、`vue-tsc` 按路径过滤 0 条。
|
||||
</acceptance_criteria>
|
||||
</verify>
|
||||
<done>合并大图(3 档规模、怪物数 11/79/204)、12 个真实模板、确定性分配与 ①/② 两套 6 行表全部跑通、零告警、`pnpm test:ci` 与基线一致、D-44 三项门禁通过后,才进入阶段 3。若规模/怪物数与预期不符、或出现告警码/`TypeError`、或某 case 超时,暂停并按 D-07 汇报、等确认(不得调规模/超时、不得忽略告警)。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>阶段 3(完整级):补 ③ 全图怪物单次临界计算与 total/avg 汇总第二表,并把 9 行数值表 + 测量方式变更 + 实现风险写入 06-18-SUMMARY.md</name>
|
||||
<files>packages-user/data-state/test/mapScenario.perf.ts, .planning/phases/06-unit-tests/06-18-SUMMARY.md</files>
|
||||
<read_first>
|
||||
本文件阶段 2 的最终内容(`measureCase`、`createScenarioFixture`、`IScenarioFixture`、6 个 `it`)—— 本阶段在其上扩展,不重建
|
||||
packages-user/data-system/src/combat/damage.ts(L134–194 `calculateCritical`(`heroStatus.getModifiableClone()`、`upperLimit = Math.floor(getCriticalLimit(...))`、`while (baseValue < upperLimit)`);L206–242 `findNextCritical`(二分、`maxIterations = clamp(precision, 4, 64)`))
|
||||
packages-user/data-state/src/enemy/calculator.ts(L167–183 `getCriticalLimit`:`3 坚固` → `Infinity`;否则 `enemy def + enemy hp`)
|
||||
packages-user/data-system/src/combat/context.ts(L335–340 `iterateEnemy`;L879–887 `requestRefresh` 在 `needUpdate` 时反而调 `buildup()` —— 故本项的夹具必须先跑过一次 `buildup()`,阶段 2 的工厂已满足)
|
||||
packages-user/data-system/src/combat/types.ts(L432–443 `calculateCritical` 签名;L559 `iterateEnemy` 返回 `Iterable<[ITileLocator, IEnemyView<TEnemy>]>`)
|
||||
.planning/phases/06-unit-tests/06-16-PLAN.md 的 ② 临界计算写法(L157:`for (const _ of ...calculateCritical(view, 'atk')) { /* 完整消费生成器 */ }`,`_` 前缀用于 eslint)
|
||||
.planning/phases/06-unit-tests/06-17-PLAN.md 与 06-17-SUMMARY.md(SUMMARY 的必备栏目形态:测量参数表、数值表、观察、D-07 超时判据、D-44 门禁结果、实现风险记录、Files Created/Modified)
|
||||
.planning/phases/06-unit-tests/06-CONTEXT.md(D-05 只报告不修、D-07 暂停汇报、D-33 执行节奏、D-43 阶段化、D-44 文件级门禁)
|
||||
dev.md(注释规范、类型规范、`_` 前缀、未使用后置参数不填)
|
||||
</read_first>
|
||||
<action>
|
||||
在阶段 2 基础上补齐第 ③ 个测量项与汇总第二表,并把结果与风险写进 SUMMARY。
|
||||
|
||||
**① ③ 全图怪物单次临界计算(3 个 `it`,每规模 1 行主表 + 1 行第二表)**
|
||||
- 新增 `interface CriticalRecord { readonly case: string; readonly scale: string; readonly monsters: number; readonly 'total ms': number; readonly 'avg ms': number; }` 与模块级 `const criticalRecords: CriticalRecord[] = [];`(`dev.md`:对象类型必须单独声明 `interface`)。
|
||||
- 每个规模的 `it`:`const fixture = createScenarioFixture(n);` → `const record = measureCase('全图怪物单次临界计算', label, () => { for (const entry of fixture.state.enemyContext.iterateEnemy()) { for (const _critical of fixture.damageSystem.calculateCritical(entry[1], 'atk')) { /* 完整消费临界枚举,否则测不到二分开销 */ } } });` → 把汇总推进第二表。
|
||||
- `_critical` 的前缀用于通过 `@typescript-eslint/no-unused-vars` 的 `varsIgnorePattern: '^_'`(与 06-16 ② 同构);`entry[0]` 是定位符、本项不需要,故不声明命名变量(若 eslint 对写法有异议,改用 `const view = entry[1];` 后再用 `view`,保持「只用到的变量才声明」与 `dev.md`「未使用的后置参数直接不填」的口径)。
|
||||
- **汇总口径(写进文件注释与 SUMMARY,不得含糊)**:`'total ms'` = 该规模「全图扫一遍」的采样中位数(即 `record['median ms']`);`'avg ms'` = `Number((record['median ms'] / fixture.monsterCount).toFixed(3))`。因同规模内怪物数是常量,「中位总量 / 怪物数」恰好等于「每采样单怪平均耗时」的中位数,故口径无歧义。
|
||||
- `afterAll` 改为依次 `console.table(records);` 与 `console.table(criticalRecords);`(第二表 3 行)。
|
||||
- 3 个 `it` 前各写一行中文注释说明覆盖的规模与怪物数;全文件仍**零断言**。
|
||||
- 若 `13`(204 怪)实测逼近或超过 `testTimeout`(30000ms),**不得**调小规模或调大超时 —— 按 D-07 暂停汇报、等裁决。
|
||||
|
||||
**② 跑满全 lane 并取数**
|
||||
- 运行 `pnpm test:perf`,确认 **6 个 perf 文件 / 54 passed / 54 行主表 + 3 行第二表**、无断言失败、运行日志中 `[WARNING Code` / `[ERROR Code` 出现次数为 **0**;记录整轮耗时与 `mapScenario.perf.ts` 单文件耗时。
|
||||
- 复核 `pnpm exec vitest list --filesOnly`(默认配置)输出仍不含 `.perf.ts`,并记录 `pnpm test:ci` 的文件数 / passed / skipped 与开工前基线逐项一致。
|
||||
- 把本文件新表(9 行主表 + 3 行第二表)原样抄入 `06-18-SUMMARY.md`。
|
||||
|
||||
**③ `06-18-SUMMARY.md` 必须如实写明的内容(不得美化)**
|
||||
- **测量方式变更**:本计划把**全部 6 个** perf 文件的计时改为 `performance.mark` + `performance.measure` + 逐样本清理;输出列、预热 3 + 采样 20、下中位数/`min`/`p95`、`toFixed(3)`、零断言**未变**。**并显式声明:06-16/06-17 SUMMARY 里的 45 行数字是改造之前(`performance.now()`)的口径,不能与本次的 9 行直接对比**(本计划未修改那两个 SUMMARY,也未重录那 45 行)。
|
||||
- **场景构成**:合并规则(`ceil(sqrt(n))` 列、每张按 `(col*13, row*13)` 放置、空位补 `0`)、三档尺寸与怪物数(`13×13`/`39×26`/`52×52`,11/79/204)、图块注册集合 `1..6`、固定 `floorId`、勇士属性取值、12 个模板的完整清单(编号/属性/特殊属性)与「刻意排除 `20`/`10`/`12/13/14/19/21/23` 及其理由」、`mulberry32` 固定种子(含 `PERF_SEED` 与 `PERF_SEED + mapCount` 的用法)。
|
||||
- **① 的样本组成(必须点明)**:样本 =
|
||||
`bindHero` + `buildup()`;`bindHero` 会顺带执行 `damageSystem.bindHeroStatus`(清伤害缓存)与 `mapDamage.refreshAll()`(全量地图伤害重建),因此 ① **不是**裸 `buildup()`,而是「光环拓扑重建 + 光环施加 + 最终效果 + 伤害缓存清空 + 一次地图伤害重建」;若用户要裸 `buildup()`,按 key_links 第 3 条的替代方案另行裁决。
|
||||
- **② 的样本组成**:`mapDamage.refreshAll()` + 对每个怪物坐标 `getSeparatedDamage` / `getReducedDamage`(含真实 reducer 合并);说明 `refreshAll()` 会清空来源状态与缓存,故每个样本都是完整的重建而非缓存命中。
|
||||
- **③ 的样本组成与汇总口径**:每只怪一次 `calculateCritical(view, 'atk')` 并完整消费生成器;`total ms` / `avg ms` 的定义与「中位总量 / 怪物数 = 单怪平均中位数」的等价性;说明 `getDamageInfo` 未单独计时(`calculateCritical` 内部即 `calculator.calculate(handler)`)。
|
||||
- **实现解释口径(必须如实写,不得表述为经 legacy 转换)**:`registerSpecials(enemyManager)` 由 `CoreState` 构造函数调用(**注册确实发生**),但 12 个模板的特殊属性实例是**内联 `createSpecial`** 构造的(镜像既有集成测试 `enemyCombination.test.ts`),本计划**未**走 `fromLegacyEnemy` 的 legacy 转换路径(仓库内无先例、legacy `Enemy` 类型字段繁多);真实被驱动的是 `CommonAuraConverter` / `GuardAuraConverter` / `MainEnemyFinalEffect` / `MainDamageCalculator` / `MainMapDamageConverter` / `MainMapDamageReducer` 这几个**真实实现**。
|
||||
- **全局 `core` stub**:因为 `BetweenDamageView` 读 `core.flags.betweenAttackMax`,本文件在 `vi.hoisted` 里 stub 了 `core`(`{ flags: { betweenAttackMax: false } }`,与 `mapDamage.test.ts` L21 同构);如实的含义是「该分支在 Node 下必须由外部注入 `core`」。
|
||||
- **测量参数表**:预热 3 不计时、采样 20、下中位数/`min`/`p95` 口径、`toFixed(3)`、计时源 `globalThis.performance.mark/measure`(含标记命名与清理规则)、`testTimeout` 30000ms 与是否有 case 逼近它(**D-07 判据**:记录最慢 case 的 median/p95 及占超时预算的比例)。
|
||||
- **运行环境**:Node 版本 / 平台(实测 `node v22.18.0` / `win32`)/ `pnpm` 版本 / vitest 版本(devDependencies 写的是 `^4.0.18`,以实际运行为准)/ CPU 型号(如可得)。
|
||||
- **不落盘基线 JSON、不新增耗时门禁**:本计划的数字仅供后续回归对比时人工取用。
|
||||
- **告警/错误码**:若运行期出现任何 `[WARNING Code` / `[ERROR Code`,按 D-05 追加 `06-TEST-FINDINGS.md` 的 `#06-18-N` 并在 SUMMARY 中显式标注;无异常时注明「无新增条目」。
|
||||
- **实现风险(report-only,未改任何生产代码)**:把本计划 key_links 中列出的风险按「已规避 / 仅记录」分类写入(至少包含:全局 `core` 依赖、`setEnemyAt` 的 O(N²) 夹具构建成本、`bindHero` 副作用计入 ①、`calculateCritical` 对 `3 坚固` 会短暂把克隆勇士攻击设成 `Infinity`、`getDamageInfo` 未单独计时、45 行既有数字不可直接对比)。若执行过程中按代码阅读新发现疑似缺陷,按 D-06 追加 `06-TEST-FINDINGS.md` 条目(现象/最小复现/影响面/建议方向/严重度)**并在 SUMMARY 中标注**,**不得修改生产代码**(D-05/D-07:只报告、等用户裁决)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm test:perf(须收集 6 个 `*.perf.ts`、`Tests 54 passed`、打印 45 + 9 = 54 行主表 + 3 行临界汇总表;无断言失败;输出中不出现 `[WARNING Code` / `[ERROR Code`);pnpm exec vitest list --filesOnly(默认配置;输出不含 `.perf.ts`);pnpm test:ci(全绿,文件数 / passed / skipped 与开工前基线逐项一致);pnpm exec vue-tsc --noEmit(按 `mapScenario.perf.ts` 过滤 0 条);PowerShell 复核 `.planning/phases/06-unit-tests/06-18-SUMMARY.md` 存在且含 9 行主表 + 3 行第二表 + 测量方式变更说明</automated>
|
||||
<fails_when>③ 未落地(主表仍是 51 行)或第二表缺失/列名不是 `case`/`scale`/`monsters`/`total ms`/`avg ms`;或 `avg ms` 不等于 `total ms / monsters`(口径不一致);或运行输出出现任何 `[WARNING Code` / `[ERROR Code`(尤其 `107`(`DamageContext` 无绑定勇士,说明未走 `bindHero` 链路)、`106`(无计算器)、`137`(支援坐标查不到怪物))或未捕获异常;或 ③ 的某档超时;或 `06-18-SUMMARY.md` 缺失、或未写明「45 行既有数字为改造前口径、不可直接对比」、或未写明 ① 的样本包含 `bindHero` 副作用、或未写明模板特殊属性为内联构造(表述成经 legacy 转换);或本阶段改动了生产源码/配置/既有 `*.test.ts`/`floors.json`</fails_when>
|
||||
<acceptance_criteria>
|
||||
- `mapScenario.perf.ts` 新增 `CriticalRecord` + `criticalRecords` + 3 个 `it`(③),`afterAll` 依次打印主表与第二表;③ 的单样本 = 对合并地图里**每只怪**一次 `calculateCritical(view, 'atk')` 并完整消费生成器。
|
||||
- `pnpm test:perf` 一次运行输出 **54 行主表**(45 既有 + 9 新增)+ **3 行第二表**,`Tests 54 passed`,无断言失败、无 warn/error 码噪声;`pnpm exec vitest list --filesOnly`(默认配置)不含 `.perf.ts`;`pnpm test:ci` 全绿且与开工前基线逐项一致。
|
||||
- `06-18-SUMMARY.md` 含:9 行主表 + 3 行第二表(来自真实运行)、测量参数表(含 mark/measure 口径与清理规则)、测量方式变更说明(含 45 行既有数字不可直接对比的声明)、场景构成(三档尺寸/怪物数/12 个模板清单/种子/排除项与理由)、①②③ 三个样本的精确组成、`registerSpecials` 与内联特殊属性的解释口径、全局 `core` stub 说明、D-07 超时判据、运行环境、实现风险记录(含「已规避 / 仅记录」分类)、`06-TEST-FINDINGS.md` 是否有新增条目的结论。
|
||||
- 全文件 0 个断言调用(唯一 `throw` 是夹具完整性自检);`measureCase` 与 fixture 仍为文件内联。
|
||||
- D-44 三项门禁通过:6 个改动文件 `eslint` 0 错误;6 个文件 `vue-tsc` 按路径过滤 0 条;`pnpm test:ci` 全绿。
|
||||
- `git status` 只显示 6 个 `*.perf.ts`(5 改 1 增)+ 本 SUMMARY + `ROADMAP.md`(规划产物);无生产源码/配置改动、无新依赖、无基线 JSON 落盘、无临时探针残留。
|
||||
</acceptance_criteria>
|
||||
</verify>
|
||||
<done>6 个 perf 文件全部用 mark/measure 计时与真实大地图场景跑通(54 行主表 + 3 行第二表、零告警、`test:ci` 与基线一致、D-44 三项门禁通过),且 `06-18-SUMMARY.md` 如实写入数值与全部实现风险后,暂停并用中文分条汇报(含是否有 case 逼近 `testTimeout`、①②③ 的数量级观察),再由用户裁决后续动作。</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| none | 仅新增/修改仓库内 Node Vitest 性能测量文件(`*.perf.ts`)与一份规划产物;无运行时输入面、无网络、无 DOM、无文件写入(结果只进 `console.table` 与 SUMMARY)。 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-06-18-01 | Tampering | 计时改造后 `measure` 与 `clearMarks`/`clearMeasures` 顺序写错,导致耗时变成累计值或被解析成首次标记位置 | medium | mitigate | 每个样本严格「mark(start) → run → mark(end) → measure → push(duration) → clearMarks(start) → clearMarks(end) → clearMeasures」;5 个既有文件的 `measureCase` 改造后仍逐字一致;`pnpm test:perf` 必须仍打印 5 文件 / 45 行且数值量级不发生数量级漂移(阶段 1 的 `<verify>` 明确列出) |
|
||||
| T-06-18-02 | Tampering | 新 perf 文件被 `pnpm test:ci` 收集,污染既有门禁通道 | medium | mitigate | 文件名与扩展名保持 `mapScenario.perf.ts`;`vitest.perf.config.ts` 与 `package.json`/`vite.config.ts` 一字不改;每个阶段的 `<verify>` 都跑 `pnpm exec vitest list --filesOnly`(默认配置,实测 66 文件、`.perf.ts` 匹配 0)并复核 `pnpm test:ci` 的文件数/passed/skipped 与现场基线逐项一致 |
|
||||
| T-06-18-03 | Repudiation | 用「跑绿」冒充性能数据(夹具空转、生成器未消费、夹具构建被算进样本、`buildup` 早退导致空转) | medium | mitigate | ③ 必须完整消费生成器;① 每样本先 `bindHero` 触发 `needUpdate`(并在 SUMMARY 说明其副作用一并被计时);② 每样本 `refreshAll()` 真正重建;夹具构建与首轮 `buildup()` 全部发生在预热循环之前;零断言、禁 `it.skip`;SUMMARY 必须给出三档怪物数与尺寸作为「非空转」证据 |
|
||||
| T-06-18-04 | Denial of service | 最大规模(52×52 / 204 怪)或 `setEnemyAt` 的 O(N²) 夹具构建触发 `testTimeout`(30000ms),把计划判据变成随机失败 | medium | mitigate | 每个 case 独立 `it`(超时归因清晰);夹具只建一次/`it`;三个测量项都是有界操作(光环范围有界、`MapDamage` 视图范围有界、临界二分上限 `clamp(precision,4,64)`);① 与 ② 的规模档与 06-17 同为 13 档且既有读数在最慢 case 上仅 `~0.9ms`;若某 case 逼近/超时,按 D-07 暂停汇报,**不得**静默改规模或调大 timeout |
|
||||
| T-06-18-05 | Information disclosure | 夹具引入真实游戏数据或产物 | low | accept | 只读复用已在仓库内的测试夹具 `floors.json`(06-17 已确认其为测试夹具、无脚本引用),不新增数据文件、不写盘、不落盘基线 JSON |
|
||||
| T-06-18-06 | Tampering | 为让 perf 跑绿而修改生产源码、既有测试或夹具数据 | medium | mitigate | 明确禁止改动 `packages*/**`(非 `*.perf.ts`)、`vitest.perf.config.ts`、`package.json`、`vite.config.ts`、`tsconfig*.json`、既有 `*.test.ts`、`floors.json`、06-01..06-17 产物;每阶段提交前用 `git status` + D-44 三项门禁复核 |
|
||||
| T-06-18-SC | Tampering | 包安装(npm/pnpm) | high | mitigate | 明确不引入任何新依赖(`vitest ^4.0.18` 已在 devDependencies;`performance.mark/measure` 由 Node 全局提供,已在 `v22.18.0` 实测);若确需安装,停止并先走包合法性 checkpoint(`[ASSUMED]`/`[SUS]` 包必须经 blocking human-verify) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm test:perf` 一次运行收集 **6 个 `*.perf.ts`**、`Tests 54 passed`,打印 **54 行主表**(列 `case`/`scale`/`median ms`/`min ms`/`p95 ms`)+ **3 行临界汇总表**(列 `case`/`scale`/`monsters`/`total ms`/`avg ms`),成功退出且输出中不出现 `[WARNING Code` / `[ERROR Code`。
|
||||
- 6 个 perf 文件的计时全部经 `globalThis.performance.mark` + `globalThis.performance.measure` 完成,且每个样本后按名 `clearMarks`/`clearMeasures`;文件内已无 `globalThis.performance.now()`;列/预热/采样/统计口径/零断言一字未变。
|
||||
- `pnpm exec vitest list --filesOnly`(默认 `vite.config.ts` 配置)输出**不含**任何 `.perf.ts`(实测 66 个文件、匹配 0);`pnpm test:ci` 全绿且文件数 / passed / skipped 与本计划开工前现场记录的基线逐项一致。
|
||||
- 新文件场景可复现:`n = 1/5/13` → `13×13` / `39×26` / `52×52`,怪物 **11 / 79 / 204**;12 个真实模板(`100..111`,含 4 个光环:`CommonAura` 三种范围 + `GuardAura`),`mulberry32` 固定种子确定性分配;`registerTiles` 集合恒为 `1..6`。
|
||||
- 三个测量项定义明确且与实现一致:① `bindHero` + `buildup()`;② `mapDamage.refreshAll()` + 每怪坐标 `getSeparatedDamage` / `getReducedDamage`;③ 每怪一次 `calculateCritical(view, 'atk')` 且完整消费生成器;`avg ms = total ms / monsters`。
|
||||
- `git status` 仅显示 6 个 `*.perf.ts`(5 改 1 增)+ `06-18-SUMMARY.md` + `ROADMAP.md`;无生产源码改动、无配置改动、无新依赖、无夹具数据改动、无基线 JSON 落盘、无临时探针残留。
|
||||
- D-44 文件级门禁:6 个改动文件 `eslint --fix` 后 `eslint <改动文件>` 0 错误;6 个文件在 `pnpm exec vue-tsc --noEmit` 输出中按路径过滤 0 条;`pnpm test:ci` 全绿。
|
||||
- **执行协议**:执行前必须先向用户确认;执行后暂停并用中文分条汇报(含是否有 case 逼近 `testTimeout`,以及 ①②③ 三个测量项的数量级)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 用户批准的 **(A) 测量方式变更**落地:全部 6 个 perf 文件的每个样本都用 `performance.mark` + `performance.measure` 成对计时、按名清理;输出列 `case`/`scale`/`median ms`/`min ms`/`p95 ms`、预热 3 + 采样 20、下中位数/`min`/`p95`、`toFixed(3)`、零断言**完全不变**。
|
||||
- 用户批准的 **(B) 真实大地图场景**落地:合并单一大图(`1/5/13` → `13×13`/`39×26`/`52×52`,怪物 11/79/204)、12 个真实模板(≥3 带真实光环,覆盖 Full/Rect/Manhattan 三范围与 `GuardAura`)、`mulberry32` 固定种子分配、`createCoreState()` 真实装配 + `resize` + `addPrefab` + `fromRaw` + `setEnemyAt` 逐格放置;**9 行主表 + 3 行临界汇总表**(含总量与单怪均值)。
|
||||
- 通道与门禁零变化:`pnpm test:ci` 的语义、收集文件数与结果与本计划开工前基线**逐项一致**;`*.perf.ts` 不被默认配置收集;未改任何生产/核心源码、配置、既有测试与夹具数据、未新增依赖、未落盘基线 JSON。
|
||||
- `06-18-SUMMARY.md` 含真实运行的 9 行主表 + 3 行第二表、测量方式变更说明(含「45 行既有数字为改造前口径、不可直接对比」)、三个样本的精确组成(含 ① 的 `bindHero` 副作用)、模板特殊属性的内联构造口径、全局 `core` stub 说明、D-07 超时判据、运行环境与实现风险记录。
|
||||
- D-44 文件级三项门禁(eslint 0 错误 / 文件级 `vue-tsc` 0 类型错误 / `pnpm test:ci` 全绿)在每个阶段提交前通过。
|
||||
</success_criteria>
|
||||
|
||||
<artifacts_this_plan_produces>
|
||||
Perf test files (created):
|
||||
- `packages-user/data-state/test/mapScenario.perf.ts` — 真实大地图战斗场景(合并大图 1/5/13 × 3 测量项:怪物上下文构建 / 地图伤害构建 / 全图怪物单次临界计算),12 个真实模板 + mulberry32 固定种子 + 内联 mark/measure 助手 + 临界汇总第二表
|
||||
|
||||
Perf test files (modified — 计时方式改为 `performance.mark` + `performance.measure`,其余不变):
|
||||
- `packages-user/data-system/src/combat/context.perf.ts` — ① 怪物上下文构建(N = 50/200/1000)
|
||||
- `packages-user/data-system/src/combat/damage.perf.ts` — ② 临界计算(1000/10000/50000)
|
||||
- `packages-user/data-base/src/hero/attribute.perf.ts` — ③ 勇士属性计算(M = 10/100/1000)
|
||||
- `packages-user/data-state/test/saveables.perf.ts` — ④ 存读档(10/100/1000 × 三档压缩)
|
||||
- `packages-user/data-state/test/saveablesReal.perf.ts` — ⑤ 真实地图存读档(1/5/13 × 三档压缩 × 存档/读档/往返)
|
||||
|
||||
Read-only reuse (unchanged):
|
||||
- `packages-user/data-state/test/fixtures/floors.json` — 13 张真实 13×13 地图(编号 `1..6`、怪物格 204),本计划只读取
|
||||
|
||||
Planning artifacts:
|
||||
- `.planning/phases/06-unit-tests/06-18-SUMMARY.md` — 9 行主表 + 3 行临界汇总表 + 测量方式变更声明 + 场景构成 + 实现风险记录
|
||||
- `.planning/ROADMAP.md` — Phase 6 新增 Wave 8 小节登记「真实大地图场景性能补充(06-18)」
|
||||
</artifacts_this_plan_produces>
|
||||
|
||||
<output>
|
||||
完成时创建 `.planning/phases/06-unit-tests/06-18-SUMMARY.md`,其中必须包含:真实 `pnpm test:perf` 运行输出的 **9 行主表**(`case`/`scale`/`median ms`/`min ms`/`p95 ms`,markdown 表格)与 **3 行临界汇总表**(`case`/`scale`/`monsters`/`total ms`/`avg ms`)、测量参数说明(预热 3 次不计时、采样 20 次取中位数、`min`/`p95` 口径、计时源 `globalThis.performance.mark`/`measure` 与逐样本清理规则、`testTimeout` 30000ms 与是否有 case 逼近)、**测量方式变更声明**(全部 6 个文件改造;06-16/06-17 的 45 行是改造前口径、不可与本次直接对比,且本计划未重录那 45 行)、场景构成(三档尺寸/怪物数、12 个模板清单与排除项理由、`mulberry32` 种子、图块注册集合、勇士属性)、①②③ 三个样本的精确组成(① 必须点明包含 `bindHero` 的副作用)、`registerSpecials` 与内联特殊属性的解释口径、全局 `core` stub 说明、D-44 门禁结果、D-07 超时判据、运行环境(Node/平台/pnpm/vitest),以及「本计划不落盘基线 JSON、不做任何耗时断言」的说明。若运行期出现告警/错误码或按代码阅读发现疑似缺陷,按 D-05/D-06 追加 `06-TEST-FINDINGS.md` 的 `#06-18-N` 并在 SUMMARY 中显式标注;无异常时注明「无新增条目」。执行完成后暂停并用中文分条汇报。
|
||||
</output>
|
||||
Loading…
Reference in New Issue
Block a user