mirror of
https://github.com/motajs/template.git
synced 2026-10-17 11:40:16 +08:00
docs(09-19): revise plan (rev6) - fix 48px frame/cell facts (4-frame, 32x48) + add ledger-correction task
This commit is contained in:
parent
1e451bb2bc
commit
14d0dadf39
@ -5,6 +5,7 @@ type: execute
|
||||
wave: 16
|
||||
depends_on: [09-18]
|
||||
files_modified:
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md
|
||||
- src/content/tile.jsonc
|
||||
- src/content/enemy.jsonc
|
||||
- src/content/item.jsonc
|
||||
@ -22,20 +23,23 @@ files_modified:
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
tokens: 90000
|
||||
raw_tokens: 45000
|
||||
tasks: 4
|
||||
tokens: 100000
|
||||
raw_tokens: 50000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-76)**:Task 0 必须先汇报「本计划 = 任务 9 · 旧引擎数据基础移植」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,**必须列明:六类基础字段范围与三类判定(映射候选可确定 / §8.2 待定不移植 / §8.1 无目标字段进 unmapped 文档)、`num` 策略 A 的具体分配方案、PNG 生成可行性结论(零依赖 Node `zlib` 方案)、贴图布局规则(D-84:列数固定(单帧 4 列 / 多帧 = 帧数列)、行数 ≤4 且按每图实际块数裁剪、不硬编码 4 行尺寸)、48px 素材**按尺寸分组**的技术核实结论与**已裁定处置(D-85 = A:32px 组与 48px 组各自成图,`npc0..6`=7 图 / `SPLIT-COUNT`=43;不放一块、不留白、不重采样)**、地图图层键口径、产出文件清单、消费接线不在本增量、静态门禁方式与 `open_items`(OI-1/2/4/5/6/7)**,然后必须**停下等待**用户回复;**回复为「可以执行」即视为通过(OI-3 已由 D-85 裁定,无需在回复中再给出取值;本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**。"
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-76)**:Task 0 必须先汇报「本计划 = 任务 9 · 旧引擎数据基础移植(含台账事实更正)」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,**必须列明:六类基础字段范围与三类判定(映射候选可确定 / §8.2 待定不移植 / §8.1 无目标字段进 unmapped 文档)、`num` 策略 A 的具体分配方案、PNG 生成可行性结论(零依赖 Node `zlib` 方案)、贴图布局规则(D-84:列数固定(单帧 4 列 / 多帧 = 帧数列)、行数 ≤4 且按每图实际块数裁剪、不硬编码 4 行尺寸)、48px 素材的技术核实结论(**`FrameNumbers`(`src/types/declaration/source.d.ts`:81-91):`enemy48`/`npc48` 为 **4 帧**、`enemys`/`npcs` 为 2 帧;`enemy48.png` 128×384 = 4 帧列 × 8 行、`npc48.png` 128×480 = 4 帧列 × 10 行,单元格 **32×48**)与**已裁定处置(D-85 = A:按尺寸与帧数分组,32px 组与 48px 组各自成图,`npc0..6`=7 图 / `SPLIT-COUNT`=43;不放一块、不留白、不重采样)**、**盘点台账 `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实更正(Task 1)**、地图图层键口径、产出文件清单、消费接线不在本增量、静态门禁方式与 `open_items`(OI-1/2/4/5/6/7)**,然后必须**停下等待**用户回复;**回复为「可以执行」即视为通过(OI-3 已由 D-85 裁定,无需在回复中再给出取值;本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**。"
|
||||
- "**基础字段范围(D-76):** 只移植盘点台账 §1.2 / §3.1 / §4.2 / §6.2 中「**映射候选(可确定)**」的字段:tile 的 `id` / `cls→type`(§1.1)/ `cannotOut`+`cannotIn`→`pass.outPass`+`pass.inPass`;enemy 的 `num` / `attribute`(`name`/`hp`/`atk`/`def`/`money`/`exp`/`point`)/ `special`;item 的 `num` / `category`(§4.1)/ `name` / `text` / `hideInToolbox`(**旧 `items.js` 中缺 `text` 的 13 条 `wand`/`pack`/`I331..I341` 显式写入空字符串**);map 的 `floorId` / `width` / `map` / `layerAlias`。**§8.2「待定」不当移植**(tile `trigger` / `canPass`;enemy `value`/`range`/`zoneSquare`/`add`/`n`/`bigImage`/`notBomb`/`atkValue`/`defValue`;item `itemEffect`/`itemEffectTip`/`useItemEffect`/`useItemEvent`/`canUseItemEffect`/`equip` 子字段;map `events` / `bgmap`/`fgmap`/`bg2map`/`fg2map`)。**§8.1「无目标字段」不当移植且只记入 `09-DATA-MIGRATION-UNMAPPED.md`**(tile `canBreak`/`script`/`doorInfo`/`animate`/`name`/`faceIds`/`autotileConnection`;enemy `faceIds`;item `hideInReplay`;map `title`/`name`/`height`/`firstArrive`/`eachArrive`/`parallelDo`/`changeFloor`/`afterBattle`/`afterGetItem`/`afterOpenDoor`/`beforeBattle`/`cannotMove`/`cannotMoveIn`/`autoEvent`/`images`/`bgm`/`weather`/`ratio`/`defaultGround`/`canFlyTo`/`canFlyFrom`/`canUseQuickShop`/`cannotViewMap`/`upFloor`)。"
|
||||
- "**`num` 策略 A(D-79)与跨文件一致性:** 图块 `num` 全部重新分配,按 `TileType` 分段(段序 `Terrain` / `Animate` / `Item` / `Enemy` / `Npc` / `Autotile`,段内按旧 `num` 升序),`num` 从 1 起连续(0 保留空图块),唯一约束 `id` ↔ `num` 一致;同一 `id` 的新 `num` **必须在 `tile.jsonc` / `enemy.jsonc` / `item.jsonc` / `texture/.list.jsonc` 的 `map` / `maps/*.jsonc` 图层数值五处一致**(enemy/item 的 `num` = 对应图块 `num`;地图与贴图 `map` 中旧 `num` 经「旧 `num`→`id`→新 `num`」重映射)。"
|
||||
- "**贴图素材(D-72 / D-81 / D-84 / D-85,`SPLIT-COUNT` = 43):** `src/content/texture/` 生成 **43 张** `materials` 拆分图。**布局规则(D-84 修订 D-72⑤):列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际所需裁剪**(素材不足时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),**不得硬编码 4 行尺寸(如 64×128 / 128×192)**。按真实块数逐图推导:A `terrains0..2`(单帧 4 列、单元格 32×32;来源 35 块 → 16+16+3)→ `terrains0`/`terrains1` 128×128(4 行)、`terrains2` **128×32**(1 行);B `terrainsAnim0..7`(4 帧列、帧 32×32;来源 29 块 → 7×4+1)→ `terrainsAnim0..6` 128×128(4 行)、`terrainsAnim7` **128×32**(1 行);C `items0..3`(单帧 4 列、32×32;来源 62 块 → 16+16+16+14)→ 四图均 128×128(末图 14 格占满 4 行);D `enemy0..20`(2 帧列):32px 块 73 个 → `enemy0..17` 64×128(4 行)+ `enemy18` **64×32**(1 行),48px 块 8 个 → `enemy19`/`enemy20` 128×192(4 行);E `npc` **按尺寸分组(D-85)**:32px 组 14 块 → `npc0..2` 64×128(4 行)+ `npc3` **64×64**(2 行,D-84 裁剪;32×32 帧),48px 组 10 块 → `npc4`/`npc5` 128×192(4 行)+ `npc6` **128×96**(2 行,D-84 裁剪;64×48 帧)—— `npc0..6`=**7 张**,**不放一块、不留白、不重采样**(`npc0..3` 帧 32px、`npc4..6` 帧 64×48)。** + 逐字节复制 `autotiles/*.png`(4)与 `tilesets/magictower.png`(1);`animates` 并入 `terrains`、不设独立 `animates` 素材组(D-72③);`texture/.list.jsonc` 用 D-80 已批准的类型/字段(`ITextureListConfig` 及 `grid`/`rowAnimate`/`autotile`/`tileset` 条目;字段 `type`/`source`/`map`/`width`/`height`/`identifier`/`autotileType`/`cellWidth`/`cellHeight`/`textures`),且每个 `source` 文件存在、`map`/`identifier` 覆盖对应图块 `num`。**D-85 已取代 D-81 的 npc 计数部分(原 `npc0..5`=6 / 42;enemy 不受影响仍 `enemy0..20`=21)。**"
|
||||
- "**贴图素材(D-72 / D-84 / D-85,`SPLIT-COUNT` = 43):** `src/content/texture/` 生成 **43 张** `materials` 拆分图。**布局规则(D-84 修订 D-72⑤):列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际所需裁剪**(素材不足时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),**不得硬编码 4 行尺寸(如 64×128 / 128×192)**。按真实块数逐图推导:A `terrains0..2`(单帧 4 列、单元格 32×32;来源 35 块 → 16+16+3)→ `terrains0`/`terrains1` 128×128(4 行)、`terrains2` **128×32**(1 行);B `terrainsAnim0..7`(4 帧列、帧 32×32;来源 29 块 → 7×4+1)→ `terrainsAnim0..6` 128×128(4 行)、`terrainsAnim7` **128×32**(1 行);C `items0..3`(单帧 4 列、32×32;来源 62 块 → 16+16+16+14)→ 四图均 128×128(末图 14 格占满 4 行);D `enemy0..20`:32px 组(**2 帧列**、帧 32×32)73 块 → `enemy0..17` 64×128(4 行)+ `enemy18` **64×32**(1 行),48px 组(**4 帧列**、帧 **32×48**)8 块 → `enemy19`/`enemy20` 128×192(4 行);E `npc` **按尺寸与帧数分组(D-85,规则④)**:32px 组(**2 帧**、帧 32×32)14 块 → `npc0..2` 64×128(4 行)+ `npc3` **64×64**(2 行);48px 组(**4 帧**、帧 **32×48**)10 块 → `npc4`/`npc5` 128×192(4 行)+ `npc6` **128×96**(2 行)—— `npc0..6`=**7 张**,**不放一块、不留白、不重采样**(`npc0..3` 帧 32×32、`npc4..6` 帧 32×48)。** + 逐字节复制 `autotiles/*.png`(4)与 `tilesets/magictower.png`(1);`animates` 并入 `terrains`、不设独立 `animates` 素材组(D-72③);`texture/.list.jsonc` 用 D-80 已批准的类型/字段(`ITextureListConfig` 及 `grid`/`rowAnimate`/`autotile`/`tileset` 条目;字段 `type`/`source`/`map`/`width`/`height`/`identifier`/`autotileType`/`cellWidth`/`cellHeight`/`textures`);其中 `rowAnimate` 条目的 `width`/`height` = **每帧尺寸**(`terrainsAnim*`、`npc0..3`、32px `enemy*` 条目 = 32/32;`npc4..6`、48px `enemy*` 条目 = **32/48**),且每个 `source` 文件存在、`map`/`identifier` 覆盖对应图块 `num`。**D-85 已取代 D-81 的 npc 计数部分(原 `npc0..5`=6 / 42;enemy 不受影响仍 `enemy0..20`=21)。**"
|
||||
- "**地图范围(D-82):** 全部 5 楼层 `empty` / `MT0` / `sample0` / `sample1` / `sample2` 写入 `src/content/maps/{id}.jsonc`,`maps/.list.jsonc` 列出 5 个 id;每个楼层 `map` 的**每个图层数组长度 = `width` × `height`**(`mapState.fromRaw` 约束),图层内数值均为 `0` 或有效新图块 `num`。"
|
||||
- "**unmapped 说明文档(D-78):** `.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md` **仅**收录 §8.1「无目标字段」项(路径/命名待 Task 0 批准);§8.2 待定项**不写入**该文档(保留在盘点台账 `09-DATA-MIGRATION-AUDIT.md`)。"
|
||||
- "**台账事实更正(D-84 / D-85):** `09-DATA-MIGRATION-AUDIT.md` 的 48px 素材事实被更正为 **4 帧 / 单元格 32×48 / 行数 8、10**(`enemy48.png` 128×384、`npc48.png` 128×480);§2.4 D/E 组**按帧数与尺寸拆分**(`enemys`2f/`enemy48`4f、`npcs`2f/`npc48`4f),enemy = 19+2 = **21**、npc = 4+3 = **7**、`SPLIT-COUNT` = **43**;§2.5 `npc0..6`(7 图);D-81 的 `npc0..5`/42 标注**已被 D-85 取代**。"
|
||||
- "**消费接线不在本增量(D-70 / D-83):** 不把配置写入 store、不把 list 写入 `TextureManager`、不改 `packages*/` 与 `src/`(除 `src/content/**`)、不改 `public/project/**`;沿用 D-72③ 与「旧 `events`/`trigger` 等无对应目标字段者不移植」。"
|
||||
artifacts:
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md
|
||||
provides: "已更正的只读盘点台账:48px 事实(4 帧 / 单元格 32×48 / 行数 8、10)、§2.4 D/E 按帧数与尺寸分组(enemy 21 / npc 7)、`SPLIT-COUNT`=43、§2.5 `npc0..6`=7;同步 D-84 / D-85 引用并标注 D-81 的 npc 计数被取代(仅更正相关事实,不改字段分类与 §8 分区)"
|
||||
- path: src/content/tile.jsonc
|
||||
provides: "235 条图块原始数据(`num` 策略 A、`id`、`type`(`TileType`)、`pass`(`cannotOut`/`cannotIn` 可确定映射)、`events`/`eventPass` 默认),替换现有样例"
|
||||
- path: src/content/enemy.jsonc
|
||||
@ -55,36 +59,36 @@ must_haves:
|
||||
- path: src/content/maps/sample2.jsonc
|
||||
provides: "`sample2` 楼层 `IMapRawData`(26×26)"
|
||||
- path: src/content/texture/.list.jsonc
|
||||
provides: "D-80 已批准的 `ITextureListConfig`,覆盖拆分图 + 4 autotile + 1 tileset"
|
||||
provides: "D-80 已批准的 `ITextureListConfig`,覆盖拆分图 + 4 autotile + 1 tileset;`rowAnimate` 条目 `width`/`height` = 每帧尺寸(48px 条目 = 32/48)"
|
||||
- path: src/content/texture/*.png
|
||||
provides: "43 张 D-72/D-84/D-85 拆分图(`terrains*`/`terrainsAnim*`/`items*`/`enemy0..20`/`npc0..6`;列数固定、行数 ≤4 且按每图实际裁剪——末图可为 128×32 / 64×32 / 64×64 / 128×96 等;npc 按尺寸分组:`npc0..3` 帧 32px、`npc4..6` 帧 64×48)+ 复制的 `autotile*`(4)+ `magictower.png`(1)"
|
||||
provides: "43 张 D-72/D-84/D-85 拆分图(`terrains*`/`terrainsAnim*`/`items*`/`enemy0..20`/`npc0..6`;列数固定、行数 ≤4 且按每图实际裁剪——末图可为 128×32 / 64×32 / 64×64 / 128×96 等;npc 按尺寸与帧数分组:`npc0..3` 帧 32×32、`npc4..6` 帧 32×48)+ 复制的 `autotile*`(4)+ `magictower.png`(1)"
|
||||
- path: script/migrateProjectTextures.ts
|
||||
provides: "一次性零依赖 PNG 生成脚本(Node `zlib` 解码/编码;路径/命名待批准,OI-2)"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md
|
||||
provides: "§8.1 无目标字段说明文档(路径/命名待批准)"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md
|
||||
provides: "本计划执行摘要:范围与映射口径、`num` 分配、素材产出、D-85(OI-3)处置、门禁结果、`open_items`"
|
||||
provides: "本计划执行摘要:范围与映射口径、台账更正结果、`num` 分配、素材产出、D-85(OI-3)处置、门禁结果、`open_items`"
|
||||
key_links:
|
||||
- "`src/content/tile.jsonc` 的 `num`(策略 A)↔ `src/content/texture/.list.jsonc` 的 `map`/`identifier` ↔ `src/content/enemy.jsonc`/`src/content/item.jsonc` 的 `num` ↔ `src/content/maps/*.jsonc` 图层数值 —— 五处 `id`↔`num` 全局一致(D-79 唯一硬约束)"
|
||||
- "`src/content/texture/.list.jsonc` 条目 ↔ `packages-user/client-base/src/material/types.ts`(`addGrid`:261 / `addRowAnimate`:275 / `addAutotile`:289 / `addTileset`:302 / `AutotileType`:12 / `IIndexedIdentifier`:50)—— D-80 已批准 schema 的能力依据"
|
||||
- "`src/content/texture/*.png` 的单元格尺寸约束 ↔ `packages-user/client-base/src/material/manager.ts`:158-175(`addRowAnimate`:单张图内所有行共用同一 `width`/`height`,且 `source.width % width === 0`)+ `packages/render/src/assets/splitter.ts`:8-18(`TextureRowSplitter.split(tex, height)`:`lines = ceil(height / frameH)`,故裁剪后行数须为 `height / frameH` 的整数)—— **D-85「不同尺寸必须不同图」的技术依据**:32×32 与 64×48 帧无法共存于同一张 `rowAnimate` 图,故 npc 按尺寸分组为 `npc0..3`(32px)+ `npc4..6`(48px);D-84 逐图裁剪(列数固定、行数 ≤4)的技术依据同此"
|
||||
- "`src/content/texture/*.png` 的单元格尺寸约束 ↔ `packages-user/client-base/src/material/manager.ts`:158-175(`addRowAnimate`:单张图内所有行共用同一 `width`/`height`,`frames = source.width / width`,且 `source.width % width === 0`)+ `packages/render/src/assets/splitter.ts`:8-18(`TextureRowSplitter.split(tex, height)`:`lines = ceil(height / frameH)`,故裁剪后行数须为 `height / frameH` 的整数)—— **D-85「不同尺寸/帧数必须不同图」的技术依据**:32×32(2 帧)与 32×48(4 帧)帧无法共存于同一张 `rowAnimate` 图(帧尺寸与帧数均不同),故 npc 按尺寸与帧数分组为 `npc0..3`(32×32,2 帧)+ `npc4..6`(32×48,4 帧);帧数权威来自 `src/types/declaration/source.d.ts`:81-91(`FrameNumbers`)。D-84 逐图裁剪(列数固定、行数 ≤4)的技术依据同此"
|
||||
- "`src/content/{tile,enemy,item}.jsonc`/`maps/*.jsonc` 字段 ↔ `packages-user/data-common/src/store/types.ts`(`ITileRawData`:47 / `ITilePassData`:38 / `IEnemyRawData`:319 / `IItemRawData`:176 / `IMapRawData`:260)—— 基础字段判定的唯一依据"
|
||||
- "`src/content/maps/*.jsonc` 图层数组长度 ↔ `packages-user/data-base/src/map/mapState.ts`:40-95(`fromRaw`:各层等长、`length % width === 0`)—— 地图结构的硬约束"
|
||||
- "`src/content/texture/*.png` ↔ `public/project/materials|autotiles|tilesets/*.png`(D-72 拆分/复制来源)"
|
||||
- "`src/content/*.jsonc` 解析 ↔ `packages-user/data-state/src/loader/jsoncProcessor.ts`(`// --- SYSTEM PREFIX END --- //` 之后必须是严格 JSON,不得含注释)"
|
||||
assumptions:
|
||||
- "**PNG 零依赖方案可行(执行日已核实):** 仓库无 `pngjs`/`sharp`/`canvas`/`jimp` 等 PNG 库(`package.json` 与 `node_modules` 均已核实);Node 内置 `zlib`(`inflateSync`/`deflateSync`)可用;源 PNG 全部为 **8 位、非隔行** —— `materials` 为颜色类型 6(RGBA),`autotiles` 为颜色类型 **3(调色板)或 6(RGBA)**(实测 `autotile.png`/`autotile1.png`/`autotile3.png` 为 type 3,`autotile2.png` 为 type 6),`tilesets/magictower.png` 为 type 6。故「解析 IHDR/IDAT + `zlib` 解压 + 反滤镜」的纯 Node 解码可行;autotile/tileset 采用**逐字节复制、不解码**。本计划**不新增任何依赖**。"
|
||||
- "**贴图布局(D-84,修订 D-72⑤):** 不再假定每张图满 4 行。拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(单帧每图上限 16 格=4 行、多帧每图上限 4 块=4 行;末图不足则裁到 1–3 行)。故末图尺寸随实际行数变化(如 `terrains2`/`terrainsAnim7` = 128×32、`enemy18` = 64×32;`npc3` = 64×64、`npc6` = 128×96)。拆分图**总数 `SPLIT-COUNT` = 43**(D-85):A=3、B=8、C=4、D=21、E=7;本盘点的 §2「严格 4 行」描述**由 D-84 取代**(仅本迁移受影响,盘点文档不改)。"
|
||||
- "**48px 素材按尺寸分组(D-85,取代 D-81 的 npc 计数;U-9 / U-17 / OI-3 已裁定):** 实测 `npcs.png`(64×448)单元格 32×32(2 帧列 × 14 行 = 14 个 NPC 块)、`npc48.png`(128×480)单元格 64×48(2 帧列 × 10 行 = 10 个 NPC 块);`enemys.png`(64×2336)32×32、`enemy48.png`(128×384)64×48。`addRowAnimate`(`manager.ts`:158-175)要求**单张图内所有块共用同一 `width`/`height`**,故 32×32 与 64×48 帧**无法共存于同一张 `rowAnimate` 图**。**D-85 裁定(= 原 OI-3「A · 按尺寸分组」):** 32×32 组与 64×48 组**各自成图,不放一块、不留白、不重采样** —— 32px 组 14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 **64×64**)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 **128×96**),`npc` 共 **7 张**、`SPLIT-COUNT` = **43**。该处置**取代 D-81 的 `npc0..5`=6 / 42**(enemy 不受影响,仍 `enemy0..20`=21)。**禁止以「统一帧尺寸 + 透明留白」的方式把不同尺寸混入同一张图(D-85)** —— 留白会改变消费端读取的帧尺寸。"
|
||||
- "**贴图布局(D-84,修订 D-72⑤):** 不再假定每张图满 4 行。拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(单帧每图上限 16 格=4 行、多帧每图上限 4 块=4 行;末图不足则裁到 1–3 行)。故末图尺寸随实际行数变化(如 `terrains2`/`terrainsAnim7` = 128×32、`enemy18` = 64×32;`npc3` = 64×64、`npc6` = 128×96)。拆分图**总数 `SPLIT-COUNT` = 43**(D-85):A=3、B=8、C=4、D=21、E=7;本盘点的 §2「严格 4 行」描述**由 D-84 取代**(Task 1 将同步更正台账)。"
|
||||
- "**48px 素材按尺寸与帧数分组(D-85,取代 D-81 的 npc 计数;U-9 / U-17 / OI-3 已裁定):** 帧数权威 = `FrameNumbers`(`src/types/declaration/source.d.ts`:81-91):`enemys:2`、`npcs:2`、`enemy48:4`、`npc48:4`。实测 `npcs.png`(64×448)= 2 帧列 × 14 行(单元格 32×32,14 个 NPC 块)、`npc48.png`(128×480)= **4 帧列** × 10 行(单元格 **32×48**,10 个 NPC 块);`enemys.png`(64×2336)= 2 帧列 × 73 行(32×32)、`enemy48.png`(128×384)= **4 帧列** × 8 行(单元格 **32×48**)。`addRowAnimate`(`manager.ts`:158-175)要求**单张图内所有块共用同一 `width`/`height`**,故 32×32(2 帧)与 32×48(4 帧)帧**无法共存于同一张 `rowAnimate` 图**。**D-85 裁定(= 原 OI-3「A · 按尺寸分组」):** 32×32 组与 32×48 组**各自成图,不放一块、不留白、不重采样** —— 32px 组 14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 **64×64**)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 **128×96**),`npc` 共 **7 张**、`SPLIT-COUNT` = **43**。该处置**取代 D-81 的 `npc0..5`=6 / 42**(enemy 不受影响,仍 `enemy0..20`=21)。**禁止以「统一帧尺寸 + 透明留白」的方式把不同尺寸混入同一张图(D-85)** —— 留白会改变消费端读取的帧尺寸。"
|
||||
- "**地图图层键语义待确认:** `mapState.fromRaw` 显示 `IMapRawData.map` 每个键是**整张展平网格**(`length = width × height`,行主序),并非盘点 §6.2 候选所称「层键即行索引」;故本计划按「旧二维 `map` → 单张展平图层」处理,图层键取值(默认 `0`)与 `layerAlias` 记入 `open_items`。"
|
||||
- "**现有样例的省略策略有效(仅适用于 §8.2 的 `effect`/`equip`):** `src/content/{tile,enemy,item}.jsonc` 现有样例已省略部分非必填字段(如 item 省略 `effect`/`equip`),说明加载/默认合并允许省略;本计划对 §8.2 待定字段沿用同样省略策略,不臆造取值。**但 `text` 例外:** 旧 `items.js` 有 13 条(`wand`/`pack`/`I331..I341`,均 `cls: items`)**无 `text`**;本计划对其**显式写入空字符串**(与 `defaults/item.jsonc` 中 `text` 的默认空串一致),**不省略** —— 以保证 Task 1 门禁的 `typeof it.text === 'string'` 成立、且数据文件自洽,而非依赖默认合并兜底。省略策略与 `text` 处置均记入 SUMMARY。"
|
||||
- "**现有样例的省略策略有效(仅适用于 §8.2 的 `effect`/`equip`):** `src/content/{tile,enemy,item}.jsonc` 现有样例已省略部分非必填字段(如 item 省略 `effect`/`equip`),说明加载/默认合并允许省略;本计划对 §8.2 待定字段沿用同样省略策略,不臆造取值。**但 `text` 例外:** 旧 `items.js` 有 13 条(`wand`/`pack`/`I331..I341`,均 `cls: items`)**无 `text`**;本计划对其**显式写入空字符串**(与 `defaults/item.jsonc` 中 `text` 的默认空串一致),**不省略** —— 以保证 Task 2 门禁的 `typeof it.text === 'string'` 成立、且数据文件自洽,而非依赖默认合并兜底。省略策略与 `text` 处置均记入 SUMMARY。"
|
||||
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构断言」为准,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户回复通过 Task 0 关卡之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "**写入边界:** 只允许写本 `files_modified` 所列路径(`src/content/**` 数据与 PNG、`script/migrateProjectTextures.ts`、`09-DATA-MIGRATION-UNMAPPED.md`、`09-19-SUMMARY.md`);**不得**改 `packages*/**`、`src/**`(除 `src/content/**`)、`public/project/**`。"
|
||||
- "**`.planning/**` 边界:** 不得改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / `09-DATA-MIGRATION-AUDIT.md` / 任何既有 PLAN 或 SUMMARY;只允许新增 `09-19-PLAN.md`(规划期已产出)、`09-DATA-MIGRATION-UNMAPPED.md`(待批准)与 `09-19-SUMMARY.md`。"
|
||||
- "**写入边界:** 只允许写本 `files_modified` 所列路径(`src/content/**` 数据与 PNG、`script/migrateProjectTextures.ts`、`09-DATA-MIGRATION-AUDIT.md`、`09-DATA-MIGRATION-UNMAPPED.md`、`09-19-SUMMARY.md`);**不得**改 `packages*/**`、`src/**`(除 `src/content/**`)、`public/project/**`。"
|
||||
- "**`.planning/**` 边界(Task 1 唯一例外):** 不得改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 任何既有 PLAN 或 SUMMARY;**唯一允许修改的既有规划文件是 `09-DATA-MIGRATION-AUDIT.md`**,且**仅限**更正 48px 帧数/单元格尺寸/拆分分组/计数并同步 D-84/D-85 引用(不得改动字段分类判定、§8.1/§8.2 分区或增删条目);另只允许新增 `09-DATA-MIGRATION-UNMAPPED.md`(待批准)与 `09-19-SUMMARY.md`。"
|
||||
- "**不移植边界:** §8.1 无目标字段**不得**出现在 `tile.jsonc`/`enemy.jsonc`/`item.jsonc`/`maps/*.jsonc` 中(只进 unmapped 文档);§8.2 待定字段**不得**移植、也**不得**进 unmapped 文档。各实体/地图条目以**精确键集断言**兜底(tile 恰 6 键 / enemy 恰 3 键 / item 恰 5 键 / map 顶层恰 5 键)。"
|
||||
- "**贴图尺寸不硬编码(D-84 / D-85):** 贴图门禁必须按**每图实际行数(≤4)与裁剪后尺寸**断言(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`);**不得**假定每张图均为 4 行,**不得**硬编码 4 行尺寸(如 64×128 / 128×192)作为全体断言——仅当某图实际行数确为 4 时其 4 行尺寸才成立(如 `npc0..2`/`npc4`/`npc5` 恰 4 行,64×128 / 128×192 成立;但 `terrains2`/`terrainsAnim7`/`enemy18`/`npc3`/`npc6` 必须按裁剪后的尺寸断言)。**不同尺寸的素材必须用不同图片(D-85)**:不得把 32×32 与 64×48 帧放入同一张图,**不得**以「统一帧尺寸 + 透明留白」的方式混图。"
|
||||
- "**贴图尺寸不硬编码(D-84 / D-85):** 贴图门禁必须按**每图实际行数(≤4)与裁剪后尺寸**断言(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`);**不得**假定每张图均为 4 行,**不得**硬编码 4 行尺寸(如 64×128 / 128×192)作为全体断言——仅当某图实际行数确为 4 时其 4 行尺寸才成立(如 `npc0..2`/`npc4`/`npc5` 恰 4 行,64×128 / 128×192 成立;但 `terrains2`/`terrainsAnim7`/`enemy18`/`npc3`/`npc6` 必须按裁剪后的尺寸断言)。**不同尺寸 / 帧数的素材必须用不同图片(D-85)**:不得把 32×32(2 帧)与 32×48(4 帧)帧放入同一张图,**不得**以「统一帧尺寸 + 透明留白」的方式混图。"
|
||||
- "**命名边界(AGENTS.md / D-73 / D-04):** 任何未批准的新命名(一次性脚本路径、unmapped 文档路径/命名、`source` 路径形态、地图图层键口径)**先入 `open_items`,由 Task 0 报备待批准**;D-80 已批准的类型/字段名可直接使用。"
|
||||
- "**不臆造:** 不得编造字段取值 / 映射关系 / PNG 行数或尺寸;无法从盘点台账与类型定义确定者入 `open_items`,不得落入数据文件。"
|
||||
- "**不新增依赖:** 不得改 `package.json`、不得安装 `pngjs`/`sharp`/`canvas`/`jimp` 等;PNG 生成必须用 Node 内置 `zlib` 零依赖方案。"
|
||||
@ -96,36 +100,37 @@ must_haves:
|
||||
open_items:
|
||||
- "**OI-1(unmapped 文档路径/命名):** 提议 `.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md`(D-78 提议)。**待用户报备批准**;未批准前不得写入。"
|
||||
- "**OI-2(一次性脚本路径/命名):** `dev.md` 命名规则要求**代码文件名用「小驼峰」**;故提议 `script/migrateProjectTextures.ts`(小驼峰,符合规范)。备注:迭代 1 曾提议 `script/migrate-project-textures.ts`(连字符),**违反** dev.md「代码文件名 = 小驼峰」规则,本迭代已纠正为小驼峰。**待用户报备批准**;如使用其它命名须先获批。"
|
||||
- "**OI-3(48px 素材合并)= 已由 D-85 裁定 = A · 按尺寸分组(不再是开放项):** 用户裁定「不同尺寸的素材必须用不同图片」—— `npc` 按尺寸分组:32px 组 14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 64×64)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 128×96),共 **7 张**、`SPLIT-COUNT` = **43**;**不放一块、不留白、不重采样**。D-85 取代 D-81 的 npc 计数(原 6 图 / 42);enemy 不受影响仍 `enemy0..20`=21。**禁止以「统一帧尺寸 + 透明留白」混图(D-85)**。各图行数与尺寸一律按 D-84 逐图裁剪。"
|
||||
- "**OI-3(48px 素材合并)= 已由 D-85 裁定 = A · 按尺寸与帧数分组(不再是开放项):** 用户裁定「不同尺寸的素材必须用不同图片」—— `npc` 按尺寸与帧数分组:32px 组(2 帧、32×32)14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 64×64)、48px 组(**4 帧、32×48**)10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 128×96),共 **7 张**、`SPLIT-COUNT` = **43**;**不放一块、不留白、不重采样**。D-85 取代 D-81 的 npc 计数(原 6 图 / 42);enemy 不受影响仍 `enemy0..20`=21。**禁止以「统一帧尺寸 + 透明留白」混图(D-85)**。各图行数与尺寸一律按 D-84 逐图裁剪。"
|
||||
- "**OI-4(地图图层键 / `layerAlias` 口径):** 盘点 §6.2「层键即行索引」与 `mapState.fromRaw`(整张展平网格)不符;默认「旧二维 `map` → 单张展平图层(键 `0`),`layerAlias` = `{}`」。**待用户确认**(额外图层 `bgmap`/`fgmap` 等属 §8.2 U-16b 本增量不移植)。"
|
||||
- "**OI-5(`texture/.list.jsonc` 的 `source` 路径形态):** 默认用相对 `texture/` 的文件名(如 `terrains0.png`),与 `client.jsonc` 的 `textureDir: texture` 对齐。**待用户确认**是否需含目录前缀。"
|
||||
- "**OI-6(autotile / tileset 参数):** `autotile1.png`(384×128)的 `AutotileType` 无法由尺寸唯一判定(U-7),`magictower.png` 的 `cellWidth`/`cellHeight` 与 `identifier.index` 待确认(U-8);默认 `autotile.png`/`autotile2.png`/`autotile3.png` 取 `Big3x4`、`autotile1.png` 暂取 `Big3x4`、`identifier` 取图块 `num`(20/151/152/153),tileset 取 `32/32`、`index: 0`、`alias: magictower`。**待用户确认。**"
|
||||
- "**OI-7(写操作方式声明):** 本计划执行含**文件写入**(数据文件、PNG、脚本、文档);执行须按 AGENTS.md「修改方式须预先声明」(内联 / 派发子代理)。"
|
||||
- "**OI-7(写操作方式声明):** 本计划执行含**文件写入**(数据文件、PNG、脚本、台账更正、文档);执行须按 AGENTS.md「修改方式须预先声明」(内联 / 派发子代理)。"
|
||||
---
|
||||
<objective>
|
||||
Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数据基础移植**:把 `public/project` 的**基础字段与数据**移植到 `src/content`(jsonc 数据 + PNG 素材),替换现有样例。
|
||||
Phase 9 · 任务 9 · 第 19 个计划(**revision 6**)—— **旧引擎数据基础移植 + 盘点台账事实更正**:把 `public/project` 的**基础字段与数据**移植到 `src/content`(jsonc 数据 + PNG 素材),替换现有样例;并更正盘点台账 `09-DATA-MIGRATION-AUDIT.md` 中 48px 素材的帧数/单元格/拆分分组/计数事实(D-84 / D-85)。
|
||||
|
||||
**要解决的问题:** Phase 9 要在已完成的数据端之上产出最小浏览器可运行版本;当前 `src/content` 只有手写样例(`tile.jsonc` 1 条、`enemy.jsonc` 1 条、`item.jsonc` 1 条、`maps/MT0.jsonc` 1 张、`texture/.list.jsonc` 为空 `{}`),无法驱动真实的 235 图块 / 81 怪物 / 62 道具 / 5 楼层。任务 8(`09-17`/`09-18`)已产出只读盘点台账 `09-DATA-MIGRATION-AUDIT.md`,逐字段界定了「映射候选(可确定)」「§8.2 待定」「§8.1 无目标字段」三类。本计划据该台账,把**可确定的基础字段**落地为 `src/content` 数据与素材;**无对应目标字段者**(§8.1)只形成说明文档(D-78);**§8.2 待定项不移植**(D-76)。
|
||||
**要解决的问题:** Phase 9 要在已完成的数据端之上产出最小浏览器可运行版本;当前 `src/content` 只有手写样例(`tile.jsonc` 1 条、`enemy.jsonc` 1 条、`item.jsonc` 1 条、`maps/MT0.jsonc` 1 张、`texture/.list.jsonc` 为空 `{}`),无法驱动真实的 235 图块 / 81 怪物 / 62 道具 / 5 楼层。任务 8(`09-17`/`09-18`)已产出只读盘点台账 `09-DATA-MIGRATION-AUDIT.md`,逐字段界定了「映射候选(可确定)」「§8.2 待定」「§8.1 无目标字段」三类。本计划据该台账,把**可确定的基础字段**落地为 `src/content` 数据与素材;**无对应目标字段者**(§8.1)只形成说明文档(D-78);**§8.2 待定项不移植**(D-76)。**另:** 盘点台账 §2.1/§2.4 把 `enemy48.png`/`npc48.png`(128 宽)误读为「2 列 × 64px」(2 帧);`FrameNumbers`(`src/types/declaration/source.d.ts`:81-91)与实测表明其为 **4 帧、单元格 32×48**,故本计划用 Task 1 更正该事实及由此派生的拆分计数(`SPLIT-COUNT`=43、`npc0..6`=7)。
|
||||
|
||||
**大致解决方案(本计划 = 09-19 · revision 5):**
|
||||
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含 PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按需裁剪)、`num` 策略 A 方案、48px **按尺寸分组**(D-85 = 原 OI-3 的 A)技术核实与**已裁定处置**、地图图层键等待裁定项),停下等待用户回复**「可以执行」**(OI-3 已由 D-85 裁定,无需回复给出取值)。
|
||||
- **Task 1 = 三类实体 jsonc**:按 `num` 策略 A 一次性决定全局 `id`↔`num`;写 `tile.jsonc`(235 条)、`enemy.jsonc`(81 条)、`item.jsonc`(62 条)——**建立跨文件唯一的 `num` 基准**。
|
||||
- **Task 2 = 贴图素材 + `.list.jsonc`**:写一次性零依赖 PNG 脚本(Node `zlib`),按 D-72 **与 D-84/D-85** 拆分 **43 张**图(列数固定、行数 ≤4 且按每图实际块数裁剪;`npc` 按尺寸分组 `npc0..6`=7 图)并复制 autotile/tileset,据 Task 1 的新 `num` 写 `texture/.list.jsonc`。
|
||||
- **Task 3 = 地图 + 说明文档 + 收口**:写 5 楼层 jsonc 与 `maps/.list.jsonc`(旧 `num` 重映射为新 `num`)、写 `09-DATA-MIGRATION-UNMAPPED.md`(仅 §8.1)、跑最终静态门禁并写 `09-19-SUMMARY.md`。
|
||||
**大致解决方案(本计划 = 09-19 · revision 6):**
|
||||
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含 PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按需裁剪)、`num` 策略 A 方案、48px **按尺寸与帧数分组**(D-85 = 原 OI-3 的 A)技术核实结论(4 帧、单元格 32×48)与**已裁定处置**、台账更正范围、地图图层键等待裁定项),停下等待用户回复**「可以执行」**(OI-3 已由 D-85 裁定,无需回复给出取值)。
|
||||
- **Task 1 = 台账事实更正**:仅更正 `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实(§2.1 单元格 32×48 / 4 帧;§2.4 D/E 按帧数与尺寸分组、enemy 21 / npc 7、`SPLIT-COUNT`=43;§2.5 `npc0..6`=7)并同步 §0/§2/§5.2/§8.3/§9 引用,标注 D-81 的 npc 计数被取代。
|
||||
- **Task 2 = 三类实体 jsonc**:按 `num` 策略 A 一次性决定全局 `id`↔`num`;写 `tile.jsonc`(235 条)、`enemy.jsonc`(81 条)、`item.jsonc`(62 条)——**建立跨文件唯一的 `num` 基准**。
|
||||
- **Task 3 = 贴图素材 + `.list.jsonc`**:写一次性零依赖 PNG 脚本(Node `zlib`),按 D-72 **与 D-84/D-85** 拆分 **43 张**图(列数固定、行数 ≤4 且按每图实际块数裁剪;`npc` 按尺寸与帧数分组 `npc0..6`=7 图)并复制 autotile/tileset,据 Task 2 的新 `num` 写 `texture/.list.jsonc`。
|
||||
- **Task 4 = 地图 + 说明文档 + 收口**:写 5 楼层 jsonc 与 `maps/.list.jsonc`(旧 `num` 重映射为新 `num`)、写 `09-DATA-MIGRATION-UNMAPPED.md`(仅 §8.1)、跑最终静态门禁并写 `09-19-SUMMARY.md`。
|
||||
|
||||
**为何分三个执行任务:** 数据依赖为「先定 `num`(Task 1)→ 贴图 list 复用该 `num`(Task 2)→ 地图把旧 `num` 重映射为新 `num`(Task 3)」;三类互不冲突、按依赖顺序最稳,且避免同一文件反复改写。
|
||||
**为何分四个执行任务:** 数据依赖为「先更正台账事实(Task 1,使后续读取口径一致)→ 定 `num`(Task 2)→ 贴图 list 复用该 `num`(Task 3)→ 地图把旧 `num` 重映射为新 `num`(Task 4)」;三类互不冲突、按依赖顺序最稳,且避免同一文件反复改写。
|
||||
|
||||
**为何不设 `tracer` 任务:** 本增量是**批量数据搬运**(伞阶段增量派发,D-01),交付物为数据文件而非分层功能;「薄垂直切片」在数据搬运上不额外降低架构风险,且与本阶段既有只读范式(`09-17`/`09-18`)一致。故按「实体 / 素材 / 地图」三类横向拆分。
|
||||
**为何不设 `tracer` 任务:** 本增量是**批量数据搬运**(伞阶段增量派发,D-01),交付物为数据文件而非分层功能;「薄垂直切片」在数据搬运上不额外降低架构风险,且与本阶段既有只读范式(`09-17`/`09-18`)一致。故按「台账更正 / 实体 / 素材 / 地图」四类横向拆分。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入**,将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入。
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入**(含对既有台账 `09-DATA-MIGRATION-AUDIT.md` 的更正),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入。
|
||||
|
||||
**PNG 生成可行性(本规划已核实):** 仓库**无** PNG 库(`package.json` 与 `node_modules` 均核实缺失 `pngjs`/`sharp`/`canvas`/`jimp`);Node 内置 `zlib`(`inflateSync`/`deflateSync`)可用;源 PNG 均为 **8 位非隔行**(materials 为 RGBA type 6、autotiles 为调色板 type 3 **或** RGBA type 6(`autotile2.png`)、tileset 为 type 6)。故采用**零依赖**手写 PNG 编解码(解析 IHDR/IDAT + `zlib` 解压 + 反滤镜;编码则滤镜 + `zlib` 压缩 + CRC32),**不新增任何依赖**。
|
||||
|
||||
**贴图布局(D-84,本规划已核实):** 拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(素材不足以填满时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),门禁按**实际行数**断言,**不硬编码 4 行尺寸**。据来源块数(`terrains` 35 / `animates` 29 / `items` 62 / `enemys` 73 / `enemy48` 8 / `npcs` 14 / `npc48` 10)逐图推导,末图会裁剪(如 `terrains2`/`terrainsAnim7` = 128×32、`enemy18` = 64×32、`npc3` = 64×64、`npc6` = 128×96);拆分图**总数 `SPLIT-COUNT` = 43**(D-85)。§2 台账的「严格 4 行」描述**由 D-84 取代**(仅本迁移受影响,盘点文档不改)。
|
||||
**贴图布局(D-84,本规划已核实):** 拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(素材不足以填满时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),门禁按**实际行数**断言,**不硬编码 4 行尺寸**。据来源块数(`terrains` 35 / `animates` 29 / `items` 62 / `enemys` 73 / `enemy48` 8 / `npcs` 14 / `npc48` 10)逐图推导,末图会裁剪(如 `terrains2`/`terrainsAnim7` = 128×32、`enemy18` = 64×32、`npc3` = 64×64、`npc6` = 128×96);拆分图**总数 `SPLIT-COUNT` = 43**(D-85)。台账 §2 的「严格 4 行」描述**由 D-84 取代**(Task 1 将同步更正台账)。
|
||||
|
||||
**48px 按尺寸分组(D-85 = 原 OI-3 的 A,已裁定):** `addRowAnimate` 要求单图内所有块共用同一帧尺寸,32×32 与 64×48 不能同图(`manager.ts`:158-175);故 `npc` 按尺寸分组:32px 组 14 块 → `npc0..3`(`npc0..2` 64×128 + `npc3` 64×64)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 128×192 + `npc6` 128×96),共 **7 图 / 总数 43**。**不放一块、不留白、不重采样**;该处置取代 D-81 的 npc 计数(原 6 图 / 42);enemy 不受影响仍 `enemy0..20`=21。**禁止以「统一帧尺寸 + 透明留白」混图(D-85)** —— 留白会改变消费端读取的帧尺寸。
|
||||
**48px 按尺寸与帧数分组(D-85 = 原 OI-3 的 A,已裁定):** 帧数权威 = `FrameNumbers`(`source.d.ts`:81-91):`enemys`/`npcs` = 2 帧、`enemy48`/`npc48` = **4 帧**。`addRowAnimate` 要求单图内所有块共用同一帧尺寸(`manager.ts`:158-175),且 `frames = source.width / width` —— 32×32(2 帧)与 32×48(4 帧)不能同图(尺寸与帧数均不同);故 `npc` 按尺寸与帧数分组:32px 组 14 块 → `npc0..3`(`npc0..2` 64×128 + `npc3` 64×64)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 128×192 + `npc6` 128×96),共 **7 图 / 总数 43**。**不放一块、不留白、不重采样**;该处置取代 D-81 的 npc 计数(原 6 图 / 42);enemy 不受影响仍 `enemy0..20`=21。**禁止以「统一帧尺寸 + 透明留白」混图(D-85)** —— 留白会改变消费端读取的帧尺寸。
|
||||
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划为本地数据/素材移植,不涉及任何外部 API。`**
|
||||
**API coverage:** `detected:false`。**`No external API integration: 本计划为本地数据/素材移植与台账更正,不涉及任何外部 API。`**
|
||||
</objective>
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@ -147,14 +152,15 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 9 · 旧引擎数据基础移植」的要做什么 / 为什么 / 大致怎么做 / 影响范围,等待用户回复「可以执行」</name>
|
||||
<precondition>`09-DATA-MIGRATION-AUDIT.md`(09-17/09-18 产出)已存在且含 §1.5.1 `id` 全清单与 §8.1 / §8.2 分区;`packages-user/data-common/src/store/types.ts`、`packages-user/client-base/src/material/types.ts`、`packages-user/client-base/src/material/manager.ts`、`packages/render/src/assets/splitter.ts`、`packages-user/data-state/src/loader/jsoncProcessor.ts`、`packages-user/data-base/src/map/mapState.ts` 均存在;`public/project/{maps,enemys,items}.js` 与 `public/project/floors/*.js`、`public/project/{materials,autotiles,tilesets}/*.png` 均存在;`src/content/texture/.list.jsonc` 存在(现状 `{}`);`09-DATA-MIGRATION-UNMAPPED.md` 与 `09-19-SUMMARY.md`**尚不存在**;Node 内置 `zlib.inflateSync`/`deflateSync` 可用。</precondition>
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 9 · 旧引擎数据基础移植(含台账事实更正)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,等待用户回复「可以执行」</name>
|
||||
<precondition>`09-DATA-MIGRATION-AUDIT.md`(09-17/09-18 产出的待更正版本)已存在且含 §1.5.1 `id` 全清单与 §8.1 / §8.2 分区;`packages-user/data-common/src/store/types.ts`、`packages-user/client-base/src/material/types.ts`、`packages-user/client-base/src/material/manager.ts`、`packages/render/src/assets/splitter.ts`、`packages-user/data-state/src/loader/jsoncProcessor.ts`、`packages-user/data-base/src/map/mapState.ts`、`src/types/declaration/source.d.ts` 均存在;`public/project/{maps,enemys,items}.js` 与 `public/project/floors/*.js`、`public/project/{materials,autotiles,tilesets}/*.png` 均存在;`src/content/texture/.list.jsonc` 存在(现状 `{}`);`09-DATA-MIGRATION-UNMAPPED.md` 与 `09-19-SUMMARY.md`**尚不存在**;Node 内置 `zlib.inflateSync`/`deflateSync` 可用。</precondition>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 / D-03 / D-04 / D-76 / D-77 / D-78 / D-79 / D-80 / D-81 / D-82 / D-83 / D-84 / **D-85**)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§1.2 / §1.5.1 / §2 / §3.1 / §4.2 / §5.1 / §5.2 / §6.2 / §7.1 / §8.1 / §8.2 / §9)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§0.5 / §2.1 / §2.4 / §2.5 / §5.2 / §8.3 U-9/U-17 / §9 —— **待更正位置**;以及 §1.2 / §1.5.1 / §3.1 / §4.2 / §6.2 / §8.1 / §8.2)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-19-PLAN.md(本计划全文,含映射口径、OI-1/2/4/5/6/7 与**已裁定的 D-85**)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-18-SUMMARY.md(上游摘要,只读参考)
|
||||
- src/types/declaration/source.d.ts:81-91(`FrameNumbers`:`enemys:2` / `npcs:2` / `enemy48:4` / `npc48:4`)—— 帧数权威
|
||||
- packages-user/data-common/src/store/types.ts(`ITileRawData` / `ITilePassData` / `IEnemyRawData` / `IItemRawData` / `IMapRawData`)
|
||||
- packages-user/client-base/src/material/types.ts(`addGrid` / `addRowAnimate` / `addAutotile` / `addTileset` / `AutotileType` / `IIndexedIdentifier`);packages-user/client-base/src/material/manager.ts:144-190(`addGrid`/`addRowAnimate`/`addAutotile`/`addTileset` 实现);packages/render/src/assets/splitter.ts:8-18(`TextureRowSplitter`)
|
||||
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则含「代码文件名=小驼峰」、注释规范、模块原则)
|
||||
@ -162,9 +168,9 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
<action>
|
||||
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** `src/content` 目前只有样例(tile 1 条 / enemy 1 条 / item 1 条 / maps 仅 MT0 / texture list 空),无法驱动真实数据。本计划据只读台账 `09-DATA-MIGRATION-AUDIT.md`,把 `public/project` 的**基础字段**移植为 `src/content` 的 jsonc 数据与 PNG 素材,替换样例。
|
||||
① **要解决的问题:** `src/content` 目前只有样例(tile 1 条 / enemy 1 条 / item 1 条 / maps 仅 MT0 / texture list 空),无法驱动真实数据。本计划据只读台账 `09-DATA-MIGRATION-AUDIT.md`,把 `public/project` 的**基础字段**移植为 `src/content` 的 jsonc 数据与 PNG 素材,替换样例。**另:** 该台账 §2.1/§2.4 误将 128 宽的 48px 素材当成「2 列 × 64px」(2 帧);`FrameNumbers` 与实测表明其为 **4 帧、单元格 32×48**,本计划更正之。
|
||||
|
||||
② **范围(六类,D-71 / D-77):** 图块 tile(235)、怪物 enemy(81)、道具 items(62)、NPC 图块(并入 tile 的 `Npc` 段)、地图 maps(5 楼层)、贴图素材 textures(**43 张拆分图** + 复制 4 autotile + 1 tileset)。
|
||||
② **范围(六类,D-71 / D-77):** 图块 tile(235)、怪物 enemy(81)、道具 items(62)、NPC 图块(并入 tile 的 `Npc` 段)、地图 maps(5 楼层)、贴图素材 textures(**43 张拆分图** + 复制 4 autotile + 1 tileset);**外加台账更正**(`09-DATA-MIGRATION-AUDIT.md`)。
|
||||
|
||||
③ **基础字段判定(D-76,逐类):**
|
||||
- **映射候选(可确定)→ 移植:** tile 的 `id` / `cls→type`(§1.1)/ `cannotOut`+`cannotIn`→`pass.outPass`+`pass.inPass`(位掩码取反);enemy 的 `num` / `attribute`(`name`/`hp`/`atk`/`def`/`money`/`exp`/`point`)/ `special`(数字数组→数字键映射);item 的 `num` / `category`(§4.1)/ `name` / `text`(**旧缺 `text` 的 13 条 `wand`/`pack`/`I331..I341` 显式写 `""`**)/ `hideInToolbox`;map 的 `floorId` / `width` / `map` / `layerAlias`。
|
||||
@ -175,57 +181,111 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
|
||||
⑤ **PNG 生成可行性结论:** 仓库无 PNG 库(已核实);采用**零依赖** Node `zlib` 手写 PNG 编解码(源 PNG 均 8 位非隔行;autotiles 为 type 3 或 type 6,均逐字节复制不解码);**不新增依赖**。**待裁定项:** `autotile1.png` 类型(U-7)与 `magictower.png` 切格(U-8)暂取候选值(OI-6)。
|
||||
|
||||
⑥ **贴图布局规则(D-84,修订 D-72⑤):** 不再严格「单帧 4×4 / 多帧 = 帧数列×4 行」。拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(素材不足时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`)。据此按来源块数逐图推导,末图会裁剪:A `terrains0..2`(35 块 → 16+16+3)→ `terrains0`/`terrains1` 128×128(4 行)、`terrains2` **128×32**(1 行);B `terrainsAnim0..7`(29 块 → 7×4+1)→ `terrainsAnim0..6` 128×128、`terrainsAnim7` **128×32**;C `items0..3`(62 块 → 16+16+16+14)→ 均 128×128;D `enemy0..20`(32px 73 块 → `enemy0..17` 64×128 + `enemy18` **64×32**;48px 8 块 → `enemy19`/`enemy20` 128×192);E `npc` **按尺寸分组(D-85)**:`npc0..2` 64×128 + `npc3` **64×64** + `npc4`/`npc5` 128×192 + `npc6` **128×96**(共 7 图)。**门禁按实际行数断言、不硬编码 4 行尺寸。** §2 台账「严格 4 行」描述**由 D-84 取代**(盘点文档不改)。
|
||||
⑥ **贴图布局规则(D-84,修订 D-72⑤):** 不再严格「单帧 4×4 / 多帧 = 帧数列×4 行」。拆分图**列数固定**(单帧 4 列 / 多帧 = 帧数列),**行数 ≤4 且按每图实际块数裁剪**(素材不足时裁到 1–3 行);每张图 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`)。据此按来源块数逐图推导,末图会裁剪:A `terrains0..2`(35 块 → 16+16+3)→ `terrains0`/`terrains1` 128×128(4 行)、`terrains2` **128×32**(1 行);B `terrainsAnim0..7`(29 块 → 7×4+1)→ `terrainsAnim0..6` 128×128、`terrainsAnim7` **128×32**;C `items0..3`(62 块 → 16+16+16+14)→ 均 128×128;D `enemy0..20`(32px 组 2 帧列 73 块 → `enemy0..17` 64×128 + `enemy18` **64×32**;48px 组 **4 帧列** 8 块 → `enemy19`/`enemy20` 128×192);E `npc` **按尺寸与帧数分组(D-85)**:`npc0..2` 64×128 + `npc3` **64×64** + `npc4`/`npc5` 128×192 + `npc6` **128×96**(共 7 图)。**门禁按实际行数断言、不硬编码 4 行尺寸。** 台账 §2「严格 4 行」描述**由 D-84 取代**(`09-DATA-MIGRATION-AUDIT.md` 由 Task 1 同步更正)。
|
||||
|
||||
⑦ **48px 素材按尺寸分组(D-85,取代 D-81 的 npc 计数;U-9 / U-17 / OI-3 已裁定)技术核实结论与**已裁定处置**:** 实测 `npcs.png`(64×448)单元格 32×32(2 帧列 × 14 行 = 14 个 NPC 块,各 2 帧 32×32)、`npc48.png`(128×480)单元格 64×48(2 帧列 × 10 行 = 10 个 NPC 块,各 2 帧 64×48)(`enemys.png` 64×2336 单元格 32×32、`enemy48.png` 128×384 单元格 64×48)。`addRowAnimate`(`manager.ts`:158-175,配 `TextureRowSplitter`)要求**单张图内所有块共用同一帧 `width`/`height`**,故 32×32 与 64×48 帧**无法同图**。**已裁定处置 = D-85 · 按尺寸分组(= 原 OI-3 的 A):** 32×32 组与 64×48 组**各自成图**—— 32px 组 14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 **64×64**)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 **128×96**),`npc` 共 **7 张**、`SPLIT-COUNT` = **43**。**不同尺寸的素材必须用不同图片;不放一块、不留白、不重采样。** D-85 取代 D-81 的 npc 计数(原 `npc0..5`=6 / 42);**enemy 不受影响**(32px 19 + 48px 2 = 21 张,与 D-81 `enemy0..20` 一致)。**原「统一 64×48 + 透明留白」(旧默认 B)已被用户明确否决**(留白会改变消费端读取的帧尺寸),本节不作为备选。
|
||||
⑦ **48px 素材按尺寸与帧数分组的**技术核实结论与**已裁定处置**(D-85,取代 D-81 的 npc 计数;U-9 / U-17 / OI-3 已裁定):** 帧数权威 = `FrameNumbers`(`src/types/declaration/source.d.ts`:81-91):`enemys`/`npcs` = 2 帧、`enemy48`/`npc48` = **4 帧**。实测 `npcs.png`(64×448)= 2 帧列 × 14 行(单元格 32×32,14 个 NPC 块,各 2 帧 32×32)、`npc48.png`(128×480)= **4 帧列** × 10 行(单元格 **32×48**,10 个 NPC 块,各 4 帧 32×48);`enemys.png`(64×2336)单元格 32×32、`enemy48.png`(**128×384**)**4 帧列**、单元格 **32×48**。`addRowAnimate`(`manager.ts`:158-175,配 `TextureRowSplitter`,且 `frames = source.width / width`)要求**单张图内所有块共用同一帧 `width`/`height`**,故 32×32(2 帧)与 32×48(4 帧)帧**无法同图**(尺寸与帧数均不同)。**已裁定处置 = D-85 · 按尺寸与帧数分组(= 原 OI-3 的 A):** 32×32 组与 32×48 组**各自成图**—— 32px 组 14 块 → `npc0..3`(`npc0..2` 4 行 64×128、`npc3` 2 行 **64×64**)、48px 组 10 块 → `npc4..6`(`npc4`/`npc5` 4 行 128×192、`npc6` 2 行 **128×96**),`npc` 共 **7 张**、`SPLIT-COUNT` = **43**。**不同尺寸 / 帧数的素材必须用不同图片;不放一块、不留白、不重采样。** D-85 取代 D-81 的 npc 计数(原 `npc0..5`=6 / 42);**enemy 不受影响**(32px 19 + 48px 2 = 21 张,与 D-81 `enemy0..20` 一致)。**原「统一 64×48 + 透明留白」(旧默认 B)已被用户明确否决**(留白会改变消费端读取的帧尺寸),本节不作为备选。
|
||||
|
||||
⑧ **地图图层键:** `mapState.fromRaw` 要求每层为整张展平网格(`length = width × height`),与台账 §6.2 候选「层键即行索引」不符;默认「旧二维 `map` 单张展平图层、键 `0`、`layerAlias` = `{}`」,额外图层(`bgmap` 等)属 §8.2 U-16b 不移植(OI-4)。
|
||||
⑧ **台账更正(Task 1,D-84 / D-85):** 仅更正 `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实:§2.1 将 `enemy48.png`/`npc48.png` 行改为 **4 帧 / 单元格 32×48 / 行数 8、10**;§2.4 将 D/E 组**按帧数与尺寸拆为** `enemys`(2f,32×32)/`enemy48`(4f,32×48)/`npcs`(2f,32×32)/`npc48`(4f,32×48) 四条(enemy = 19+2 = **21**、npc = 4+3 = **7**),`SPLIT-COUNT` 由 42 改为 **43**;§2.5 将 npc 命名改为 `npc0..6`(7 图,enemy 保持 `enemy0..20`=21);同步 §0/§2/§5.2/§8.3(U-9/U-17)/§9 的 D-84/D-85 引用并**标注 D-81 的 `npc0..5`/42 已被取代**。**不改**字段分类判定、§8.1/§8.2 分区或条目增删。`09-DATA-MIGRATION-AUDIT.md` 是本计划**唯一允许修改的既有规划文件**。
|
||||
|
||||
⑨ **产出文件:** `src/content/tile.jsonc` / `enemy.jsonc` / `item.jsonc`(替换样例);`src/content/maps/{empty,MT0,sample0,sample1,sample2}.jsonc` + `maps/.list.jsonc`;`src/content/texture/*.png`(43 拆分图 + 5 复制素材);`src/content/texture/.list.jsonc`;`script/migrateProjectTextures.ts`(待批准,OI-2);`.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md`(待批准);`09-19-SUMMARY.md`。
|
||||
⑨ **地图图层键:** `mapState.fromRaw` 要求每层为整张展平网格(`length = width × height`),与台账 §6.2 候选「层键即行索引」不符;默认「旧二维 `map` 单张展平图层、键 `0`、`layerAlias` = `{}`」,额外图层(`bgmap` 等)属 §8.2 U-16b 不移植(OI-4)。
|
||||
|
||||
⑩ **不在本增量(D-70 / D-83):** 不实现消费接线(写 store / 写 `TextureManager`);不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`;不移植 §8.2 待定与 §8.1 无目标字段。
|
||||
⑩ **产出文件:** 更正后的 `09-DATA-MIGRATION-AUDIT.md`(Task 1);`src/content/tile.jsonc` / `enemy.jsonc` / `item.jsonc`(替换样例);`src/content/maps/{empty,MT0,sample0,sample1,sample2}.jsonc` + `maps/.list.jsonc`;`src/content/texture/*.png`(43 拆分图 + 5 复制素材);`src/content/texture/.list.jsonc`;`script/migrateProjectTextures.ts`(待批准,OI-2);`.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md`(待批准);`09-19-SUMMARY.md`。
|
||||
|
||||
⑪ **验证方式:** 静态门禁——jsonc 可解析(`// --- SYSTEM PREFIX END --- //` 后严格 JSON);tile 235 条且键集恰 6 键、`id`/`num` 唯一、`type` 合法;enemy 81 条且键集恰 3 键(`num`/`attribute`/`special`)、`special` 形状正确;item 62 条且键集恰 5 键(`num`/`category`/`name`/`text`/`hideInToolbox`)、`text` 为字符串;`texture/.list.jsonc` 每个 `source` 文件存在、`map`/`identifier` 覆盖**每一处有贴图的图块 `num`**(集合覆盖断言);**每张生成的 PNG(含全部 `enemy*`/`npc*`)均断言签名 / 8 位 / 非隔行,并按 D-84 逐图实际行数断言 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`,末图可为 128×32 / 64×32 / 64×64 / 128×96 等,不硬编码 4 行尺寸)**,`npc` 图数按 **D-85 = 7** 断言(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96);maps 5 楼层齐备、**顶层键集恰 5 键**、每层数组长度 = `width`×`height`、数值均为 `0` 或有效 `num`;unmapped 文档存在且仅含 §8.1。**不设全仓类型 / 构建门禁。**
|
||||
⑪ **不在本增量(D-70 / D-83):** 不实现消费接线(写 store / 写 `TextureManager`);不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`;不移植 §8.2 待定与 §8.1 无目标字段。
|
||||
|
||||
⑫ **修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理执行**(`gsd-executor`),非内联执行。
|
||||
⑫ **验证方式:** 静态门禁——台账更正后 48px 事实与计数自洽(4 帧 / 单元格 32×48;D/E 分组、enemy 21 / npc 7、`SPLIT-COUNT` 43、`npc0..6`=7);jsonc 可解析(`// --- SYSTEM PREFIX END --- //` 后严格 JSON);tile 235 条且键集恰 6 键、`id`/`num` 唯一、`type` 合法;enemy 81 条且键集恰 3 键(`num`/`attribute`/`special`)、`special` 形状正确;item 62 条且键集恰 5 键(`num`/`category`/`name`/`text`/`hideInToolbox`)、`text` 为字符串;`texture/.list.jsonc` 每个 `source` 文件存在、`map`/`identifier` 覆盖**每一处有贴图的图块 `num`**(集合覆盖断言),且 `rowAnimate` 条目 `width`/`height` = 每帧尺寸(48px 条目 = **32/48**、帧数 = 4;32px 条目 = 32/32、帧数 = 2);**每张生成的 PNG(含全部 `enemy*`/`npc*`)均断言签名 / 8 位 / 非隔行,并按 D-84 逐图实际行数断言 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`,末图可为 128×32 / 64×32 / 64×64 / 128×96 等,不硬编码 4 行尺寸)**,`npc` 图数按 **D-85 = 7** 断言(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96);maps 5 楼层齐备、**顶层键集恰 5 键**、每层数组长度 = `width`×`height`、数值均为 `0` 或有效 `num`;unmapped 文档存在且仅含 §8.1。**不设全仓类型 / 构建门禁。** 本计划对台账的写入须经 `SCOPE GATE` 核对:除 `files_modified` 外不改任何既有规划文件。
|
||||
|
||||
⑬ **开放项:** OI-1(unmapped 文档路径/命名)、OI-2(脚本路径/命名,已纠正为小驼峰 `migrateProjectTextures.ts`)、OI-4(地图图层键/`layerAlias`)、OI-5(`source` 路径形态)、OI-6(autotile/tileset 参数)、OI-7(写操作方式声明)。**(OI-3 = 48px 合并处理已由 D-85 裁定 = 按尺寸分组:`npc0..6`=7 图 / `SPLIT-COUNT`=43,不再是开放项。)**
|
||||
⑬ **修改方式声明(AGENTS.md):** 本计划执行含文件写入(含更正既有台账),由**派发子代理执行**(`gsd-executor`),非内联执行。
|
||||
|
||||
⑭ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过(OI-3 已由 D-85 裁定,无需在回复中再给出取值)。** 若用户就 OI-1/2/4/5/6 给出指定,按指定执行。
|
||||
⑭ **开放项:** OI-1(unmapped 文档路径/命名)、OI-2(脚本路径/命名,已纠正为小驼峰 `migrateProjectTextures.ts`)、OI-4(地图图层键/`layerAlias`)、OI-5(`source` 路径形态)、OI-6(autotile/tileset 参数)、OI-7(写操作方式声明)。**(OI-3 = 48px 合并处理已由 D-85 裁定 = 按尺寸与帧数分组:`npc0..6`=7 图 / `SPLIT-COUNT`=43,不再是开放项。)**
|
||||
|
||||
⑮ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过(OI-3 已由 D-85 裁定,无需在回复中再给出取值)。** 若用户就 OI-1/2/4/5/6 或台账更正范围给出指定,按指定执行。
|
||||
</action>
|
||||
<decision>是否按本计划(09-19)执行「任务 9 · 旧引擎数据基础移植」——以 `num` 策略 A 写入 tile/enemy/item jsonc、按 D-72/D-84/D-85 生成 **43 张**贴图素材(`npc` 按尺寸分组 `npc0..6`=7 图)与 `texture/.list.jsonc`、写入 5 楼层与 `maps/.list.jsonc`、产出 unmapped 说明文档;基础字段全量、§8.2 待定与 §8.1 无目标字段不落数据文件(各实体/地图以精确键集断言兜底);不实现消费接线。**48px 合并(OI-3)已由 D-85 裁定 = A · 按尺寸分组(`npc0..6`=7 图 / `SPLIT-COUNT`=43),无需在本关卡再裁定。**</decision>
|
||||
<decision>是否按本计划(09-19)执行「任务 9 · 旧引擎数据基础移植(含台账事实更正)」——先更正 `09-DATA-MIGRATION-AUDIT.md` 的 48px 帧数/单元格/分组/计数(D-84/D-85),再以 `num` 策略 A 写入 tile/enemy/item jsonc、按 D-72/D-84/D-85 生成 **43 张**贴图素材(`npc` 按尺寸与帧数分组 `npc0..6`=7 图)与 `texture/.list.jsonc`、写入 5 楼层与 `maps/.list.jsonc`、产出 unmapped 说明文档;基础字段全量、§8.2 待定与 §8.1 无目标字段不落数据文件(各实体/地图以精确键集断言兜底);不实现消费接线。**48px 合并(OI-3)已由 D-85 裁定 = A · 按尺寸与帧数分组(`npc0..6`=7 图 / `SPLIT-COUNT`=43),无需在本关卡再裁定。**</decision>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期为**写操作**(新增/替换 `src/content/**` 数据与 PNG、新增脚本与两份文档)。OI-3(48px 合并)已由用户裁定为 **D-85 = 按尺寸分组**(`npc0..6`=7 图 / `SPLIT-COUNT`=43;不放一块、不留白、不重采样),故回复只需「可以执行」即可,不再要求显式给出 OI-3 取值。执行期工作树存在用户并发改动(`AGENTS.md` / `09-CONTEXT.md`,D-56),提交时须逐条核对暂存面。贴图尺寸按 D-84 逐图实际行数(≤4)断言,不硬编码 4 行尺寸;`npc` 按 D-85 尺寸分组断言。行号为规划日实测、可能漂移,门禁以结构断言为准。
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期为**写操作**(更正 `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实、新增/替换 `src/content/**` 数据与 PNG、新增脚本与两份文档)。OI-3(48px 合并)已由用户裁定为 **D-85 = 按尺寸与帧数分组**(`npc0..6`=7 图 / `SPLIT-COUNT`=43;不放一块、不留白、不重采样),故回复只需「可以执行」即可,不再要求显式给出 OI-3 取值。执行期工作树存在用户并发改动(`AGENTS.md` / `09-CONTEXT.md`,D-56),提交时须逐条核对暂存面。贴图尺寸按 D-84 逐图实际行数(≤4)断言,不硬编码 4 行尺寸;`npc` 按 D-85 尺寸与帧数分组断言。行号为规划日实测、可能漂移,门禁以结构断言为准。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-bysize">
|
||||
<name>可以执行(D-85 已裁定 48px 处置 = 按尺寸分组:`npc0..6`=7 图 / `SPLIT-COUNT`=43)</name>
|
||||
<pros>素材与源像素完全一致——不重采样、不留白;32×32 与 64×48 帧各自成图,消费端读取的帧尺寸不被改写;与 D-85 逐字一致;`npc0..2` 64×128 + `npc3` 64×64 + `npc4`/`npc5` 128×192 + `npc6` 128×96;基础字段全量落地;零依赖 PNG 方案;跨文件 `id`↔`num` 一致</pros>
|
||||
<cons>`npc` 图数 7、`SPLIT-COUNT` 43(取代 D-81 的 6 / 42,已由 D-85 明确取代)</cons>
|
||||
<name>可以执行(D-85 已裁定 48px 处置 = 按尺寸与帧数分组:`npc0..6`=7 图 / `SPLIT-COUNT`=43;含台账更正)</name>
|
||||
<pros>素材与源像素完全一致——不重采样、不留白;32×32(2 帧)与 32×48(4 帧)帧各自成图,消费端读取的帧尺寸不被改写;与 D-85 逐字一致;台账 48px 事实(4 帧 / 单元格 32×48 / 行数 8、10)与拆分计数更正为自洽(enemy 21 / npc 7 / 43 / `npc0..6`=7);`npc0..2` 64×128 + `npc3` 64×64 + `npc4`/`npc5` 128×192 + `npc6` 128×96;基础字段全量落地;零依赖 PNG 方案;跨文件 `id`↔`num` 一致</pros>
|
||||
<cons>台账 `09-DATA-MIGRATION-AUDIT.md` 将由只读产物更新为更正版(仅 48px 相关事实);`npc` 图数 7、`SPLIT-COUNT` 43(取代 D-81 的 6 / 42,已由 D-85 明确取代)</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订落实方式 / `num` 分段口径 / 图层键口径 / 布局裁剪口径后重新汇报</name>
|
||||
<pros>避免带着未确认口径写数据</pros>
|
||||
<name>修订落实方式 / `num` 分段口径 / 图层键口径 / 布局裁剪口径 / 台账更正范围后重新汇报</name>
|
||||
<pros>避免带着未确认口径写数据或改台账</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」即可(OI-3 已由 D-85 裁定 = 按尺寸分组,无需在回复中再给出取值)。如需调整 OI-1/2/4/5/6 的其它处置,请在回复中一并指明;修订意见请按 action ⑭ 处理。</resume-signal>
|
||||
<resume-signal>回复「可以执行」即可(OI-3 已由 D-85 裁定 = 按尺寸与帧数分组,无需在回复中再给出取值)。如需调整 OI-1/2/4/5/6 或台账更正范围,请在回复中一并指明;修订意见请按 action ⑮ 处理。</resume-signal>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const z=require('zlib');const bad=[];const doc='.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md';if(!fs.existsSync(doc))bad.push('missing audit doc');for(const p of ['packages-user/data-common/src/store/types.ts','packages-user/client-base/src/material/types.ts','packages-user/client-base/src/material/manager.ts','packages/render/src/assets/splitter.ts','packages-user/data-state/src/loader/jsoncProcessor.ts','packages-user/data-base/src/map/mapState.ts','public/project/maps.js','public/project/enemys.js','public/project/items.js','src/content/texture/.list.jsonc']){if(!fs.existsSync(p))bad.push('missing ref: '+p)}for(const p of ['.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md','.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}if(typeof z.inflateSync!=='function'||typeof z.deflateSync!=='function')bad.push('node zlib inflate/deflate unavailable');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state: audit doc present; unmapped+summary absent; refs present; node zlib available')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const z=require('zlib');const bad=[];const doc='.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md';if(!fs.existsSync(doc))bad.push('missing audit doc');for(const p of ['packages-user/data-common/src/store/types.ts','packages-user/client-base/src/material/types.ts','packages-user/client-base/src/material/manager.ts','packages/render/src/assets/splitter.ts','packages-user/data-state/src/loader/jsoncProcessor.ts','packages-user/data-base/src/map/mapState.ts','src/types/declaration/source.d.ts','public/project/maps.js','public/project/enemys.js','public/project/items.js','src/content/texture/.list.jsonc']){if(!fs.existsSync(p))bad.push('missing ref: '+p)}for(const p of ['.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md','.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}if(typeof z.inflateSync!=='function'||typeof z.deflateSync!=='function')bad.push('node zlib inflate/deflate unavailable');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state: audit doc present; unmapped+summary absent; refs present; node zlib available')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(被移植来源或参考文件缺失,或产出文件已存在,或 Node `zlib` 不可用)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」(OI-3 已由 D-85 裁定,无需再给出 OI-3 取值;用户若指定 OI-1/2/4/5/6 的非默认处置,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
<human-check>用户在对话中明确回复「可以执行」(OI-3 已由 D-85 裁定,无需再给出 OI-3 取值;用户若指定 OI-1/2/4/5/6 或台账更正范围的非默认处置,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」(OI-3 已由 D-85 裁定,回复**无需**再给出 OI-3 取值)
|
||||
- 汇报内容涵盖:要解决的问题、六类范围、三类基础字段判定(映射候选可确定 / §8.2 待定 / §8.1 无目标字段)、`num` 策略 A 具体方案、PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按每图实际块数裁剪)、48px **按尺寸分组**(D-85)技术核实结论与已裁定处置(`npc0..6`=7 图 / 总数 43)、地图图层键口径、产出文件清单、消费接线不在本增量、验证方式(静态门禁,无全仓门禁)、OI-1/2/4/5/6/7
|
||||
- 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/既有计划与台账)**、**不新增依赖**、**§8.1/§8.2 不落数据文件(精确键集断言兜底)**、**贴图尺寸按 D-84 逐图实际行数断言、不硬编码 4 行尺寸;npc 按 D-85 尺寸分组**、**命名待批准项先入 `open_items`**
|
||||
- 汇报内容涵盖:要解决的问题、六类范围 + 台账更正、三类基础字段判定(映射候选可确定 / §8.2 待定 / §8.1 无目标字段)、`num` 策略 A 具体方案、PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按每图实际块数裁剪)、48px **按尺寸与帧数分组**(D-85)技术核实结论(**4 帧 / 单元格 32×48**)与已裁定处置(`npc0..6`=7 图 / 总数 43)、**台账更正范围(§2.1/§2.4/§2.5 + §0/§2/§5.2/§8.3/§9 同步;仅此一个既有规划文件被改)**、地图图层键口径、产出文件清单、消费接线不在本增量、验证方式(静态门禁,无全仓门禁)、OI-1/2/4/5/6/7
|
||||
- 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;既有规划文件仅 `09-DATA-MIGRATION-AUDIT.md` 可改,且仅限 48px 事实更正;不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/`09-CONTEXT.md`/既有 PLAN 与 SUMMARY)**、**不新增依赖**、**§8.1/§8.2 不落数据文件(精确键集断言兜底)**、**贴图尺寸按 D-84 逐图实际行数断言、不硬编码 4 行尺寸;npc 按 D-85 尺寸与帧数分组**、**命名待批准项先入 `open_items`**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;回复「可以执行」即通过,OI-3 已裁定无需再给取值)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 建立全局 `id`↔`num` 基准并写三类实体 jsonc —— `tile.jsonc`(235)/ `enemy.jsonc`(81)/ `item.jsonc`(62)</name>
|
||||
<name>Task 1: 更正盘点台账 `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实(4 帧 / 单元格 32×48 / 行数 8、10)与拆分计数(D/E 按帧数与尺寸分组、`SPLIT-COUNT`=43、`npc0..6`=7)</name>
|
||||
<precondition>Task 0 已闭环:用户明确回复「可以执行」(若用户就台账更正范围另有指定,按指定执行)。`09-DATA-MIGRATION-AUDIT.md` 为 09-17/09-18 产出的待更正版本(§2.1 的 48px 行仍为「2 列 / 2 帧」口径)。</precondition>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§0.5 / §0.6 / §2.1 / §2.4 / §2.5 / §5.2 / §8.3 U-9 / §8.3 U-17 / §9)—— 待更正位置
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-72 / D-81 / D-84 / D-85)
|
||||
- src/types/declaration/source.d.ts:81-91(`FrameNumbers`:`animates:4` / `enemys:2` / `npcs:2` / `enemy48:4` / `npc48:4`)—— 帧数权威
|
||||
- packages-user/client-base/src/material/manager.ts:158-175(`addRowAnimate`:单图单帧尺寸、`source.width % width === 0`、`frames = source.width / width`)+ packages/render/src/assets/splitter.ts:8-18(`TextureRowSplitter`)—— 「不同尺寸/帧数必须不同图」的依据
|
||||
- public/project/materials/{enemys,enemy48,npcs,npc48}.png(`IHDR` 实测:`enemys` 64×2336、`enemy48` 128×384、`npcs` 64×448、`npc48` 128×480)
|
||||
</read_first>
|
||||
<action>
|
||||
仅更正 `09-DATA-MIGRATION-AUDIT.md` 中的 **48px 素材帧数 / 单元格尺寸 / 拆分分组 / 计数** 与相关 D-84 / D-85 引用;**不做任何其它改动**(不改字段分类判定、§8.1/§8.2 分区、条目增删;不动 §1/§3/§4 内容)。
|
||||
|
||||
(1) **§2.1(48px 两行):** 将 `public/project/materials/enemy48.png:1` 行改为尺寸 `128×384`、**4 帧列 × 8 行、每怪 4 帧、单元格 32×48**(原「2 列 × 8 行,每怪 2 帧」错误);将 `public/project/materials/npc48.png:1` 行改为尺寸 `128×480`、**4 帧列 × 10 行、每 NPC 4 帧、单元格 32×48**(原「2 列 × 10 行,每 NPC 2 帧」错误)。`enemys.png`/`npcs.png`(2 帧、32×32)保持不变。
|
||||
|
||||
(2) **§2.4(D/E 组按帧数与尺寸拆分):** 将原 D、E 两行替换为四行:
|
||||
- D1:`enemys.png`(合并,D-72①)| 多帧 | 帧数 **2** | 73 块 | 帧数列(2) × ≤4 行(4 块/图,`addRowAnimate`;D-84) | **19**
|
||||
- D2:`enemy48.png`(合并,D-72①)| 多帧 | 帧数 **4** | 8 块 | 帧数列(4) × ≤4 行(D-84) | **2**
|
||||
- E1:`npcs.png`(合并,D-72①)| 多帧 | 帧数 **2** | 14 块 | 帧数列(2) × ≤4 行(D-84) | **4**
|
||||
- E2:`npc48.png`(合并,D-72①)| 多帧 | 帧数 **4** | 10 块 | 帧数列(4) × ≤4 行(D-84) | **3**
|
||||
并将「落实规则④」句更正为:**多帧素材按帧数分图**——B 为 4 帧、`enemys`/`npcs` 为 2 帧、`enemy48`/`npc48` 为 4 帧,**故 D 组拆为 `enemys`(2f) / `enemy48`(4f),E 组拆为 `npcs`(2f) / `npc48`(4f)**(原 D/E 各 2 帧系误读)。在 §2.4 增加一行更正说明:**`enemy = ceil(73/4)+ceil(8/4) = 19+2 = 21`;`npc = ceil(14/4)+ceil(10/4) = 4+3 = 7`**;并将 **`SPLIT-COUNT: 42` 改为 `SPLIT-COUNT: 43`**(= A 3 + B 8 + C 4 + D 21 + E 7),同时注明布局受 **D-84**(列数固定、行数 ≤4 按需裁剪)约束。
|
||||
|
||||
(3) **§2.5(命名候选):** `enemy` 保持 `enemy0.png` … `enemy20.png`(**21 图**);`npc` 由 `npc0.png` … `npc5.png`(6 图)改为 **`npc0.png` … `npc6.png`(7 图,按尺寸与帧数分组)**。
|
||||
|
||||
(4) **§9(汇总):** 汇总表「素材拆分目标图 split」计数由 **42 改为 43**,说明补「并按 **D-84**(列数固定、行数 ≤4 裁剪)/ **D-85**(按尺寸与帧数分组)」;机器可读计数行的 **`SPLIT-COUNT: 42` → `SPLIT-COUNT: 43`**。
|
||||
|
||||
(5) **§0(口径同步):** 新增一条修订/口径说明(置于 §0.5 之后或 §0.6 之前):**D-72⑤ 的行数规则由 `D-84` 放宽**(列数固定,行数 ≤4 且按每图实际块数裁剪);**48px 素材分组由 `D-85` 裁定**(按尺寸与帧数分组;帧数权威 `FrameNumbers`:`enemy48`/`npc48` = **4 帧**、`enemys`/`npcs` = 2 帧)。并在文件头附近标注「**本文件于 2026-10-13 经用户更正 48px 素材事实(4 帧 / 单元格 32×48)**」。
|
||||
|
||||
(6) **§5.2(交叉引用):** 将 `npc48.png`「10 NPC × 2 帧」更正为「10 NPC × **4 帧**」;E 组引用由「归属 §2.4 的 E 组(目标图 6 张,`npc0.png` … `npc5.png`)」改为「(目标图 **7 张**,`npc0.png` … `npc6.png`,**按尺寸与帧数分组**)」;该节末尾「是否与 32px NPC 混入同一目标图…受 D-72④…需进一步确认(§8)」标注为**已由 D-85 裁定**。
|
||||
|
||||
(7) **§8.3(观察项同步):** U-9 更正为「48px 素材为 **4 帧列**、单元格 **32×48**(`enemy48.png` 128×384、`npc48.png` 128×480;帧数权威 `FrameNumbers`)—— 由 D-84/D-85 裁定」;U-17 标注**已由 D-85 裁定**(`npc48` 高门与 32px NPC 各自成图、不混图)。
|
||||
|
||||
(8) **标注 D-81 的 npc 计数被取代:** 在 §2.5 与 §9 相关处注明「D-81 的 `npc0..5`=6 / `SPLIT-COUNT`=42 的 **npc 计数部分已由 D-85 取代**;enemy 不受影响仍 `enemy0..20`=21」。
|
||||
|
||||
(9) **收口:** 仅改上述 48px 相关事实与引用;**不改**字段分类判定、§8.1/§8.2 分区、条目增删;**不改任何其它文件**(本文件是本计划唯一允许修改的既有规划文件)。
|
||||
</action>
|
||||
<reversibility rating="reversible">仅更正一份规划文档内的事实与引用;`git checkout` 可整体回退;未触及任何源码、数据文件或既有注释。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const p='.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md';const t=fs.readFileSync(p,'utf8');const bad=[];const rows=t.split(/\r?\n/);const lineOf=sub=>{const i=rows.findIndex(l=>l.indexOf(sub)>=0);return i<0?'':rows[i]};const e48=lineOf('materials/enemy48.png');const n48=lineOf('materials/npc48.png');if(!/4 帧/.test(e48))bad.push('§2.1 enemy48 row lacks 4 帧');if(!/32×48/.test(e48))bad.push('§2.1 enemy48 row lacks 单元格 32×48');if(/2 列 × 8 行/.test(e48))bad.push('§2.1 enemy48 row still 2 列 × 8 行');if(!/4 帧/.test(n48))bad.push('§2.1 npc48 row lacks 4 帧');if(!/32×48/.test(n48))bad.push('§2.1 npc48 row lacks 单元格 32×48');if(/2 列 × 10 行/.test(n48))bad.push('§2.1 npc48 row still 2 列 × 10 行');if(t.indexOf('SPLIT-COUNT: 42')>=0)bad.push('SPLIT-COUNT still 42');if(t.indexOf('SPLIT-COUNT: 43')<0)bad.push('SPLIT-COUNT not 43');if(t.indexOf('npc6.png')<0)bad.push('§2.5 npc not extended to npc6.png');if(t.indexOf('npc5.png')>=0)bad.push('§2.5 still references npc5.png');if(t.indexOf('D-84')<0||t.indexOf('D-85')<0)bad.push('missing D-84/D-85 sync');if(/npc0\.png` … `npc5\.png/.test(t))bad.push('old npc range still present');if(t.indexOf('ceil(73/4)+ceil(8/4)')<0||t.indexOf('ceil(14/4)+ceil(10/4)')<0)bad.push('missing recomputed group counts');if(bad.length>0){console.error('LEDGER GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK ledger: 48px = 4 frames / cell 32×48 (rows 8,10); SPLIT-COUNT 43 (enemy 21 / npc 7); npc0..6; D-84/D-85 synced')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "LEDGER GATE FAILED"(§2.1 的 48px 行未更正为 4 帧 / 单元格 32×48 / 行数 8、10;`SPLIT-COUNT` 未由 42 改为 43;`npc` 命名未扩到 `npc0..6`;D-84/D-85 引用缺失;旧的 `npc0..npc5` 区间仍在)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- §2.1 的 `enemy48.png` 行为 `128×384`、4 帧列 × 8 行、每怪 4 帧、单元格 32×48;`npc48.png` 行为 `128×480`、4 帧列 × 10 行、每 NPC 4 帧、单元格 32×48
|
||||
- §2.4 D/E 各拆为两条(按帧数与尺寸):`enemys`(2f)/`enemy48`(4f)、`npcs`(2f)/`npc48`(4f);含 `enemy = 19+2 = 21`、`npc = 4+3 = 7` 的推导;`SPLIT-COUNT` = **43**
|
||||
- §2.5 `npc0..6`(7 图);`enemy0..20`(21 图)保持不变
|
||||
- §0 新增 D-84/D-85 口径说明与「2026-10-13 用户更正」标注;§5.2 / §8.3 U-9/U-17 同步;D-81 的 npc 计数标注被取代
|
||||
- §9 汇总与机器可读计数均为 43;未改动字段分类判定、§8.1/§8.2 分区或条目增删;未改任何其它文件
|
||||
</acceptance_criteria>
|
||||
<done>台账 48px 事实与拆分计数更正为自洽(4 帧 / 单元格 32×48 / 行数 8、10;enemy 21 / npc 7 / `SPLIT-COUNT` 43 / `npc0..6`=7);D-84/D-85 引用同步;门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 建立全局 `id`↔`num` 基准并写三类实体 jsonc —— `tile.jsonc`(235)/ `enemy.jsonc`(81)/ `item.jsonc`(62)</name>
|
||||
<precondition>Task 0 已闭环:用户明确回复「可以执行」(OI-3 已由 D-85 裁定;若用户就 OI-1/2/4/5/6 另有指定,按指定执行)。</precondition>
|
||||
<files>src/content/tile.jsonc, src/content/enemy.jsonc, src/content/item.jsonc</files>
|
||||
<read_first>
|
||||
@ -240,7 +300,7 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
(0) **一次性计算全局 `id`↔`num` 映射(策略 A,D-79):**
|
||||
- 用 `node` 只读求值 `public/project/maps.js` 得到 235 条图块(键=旧 `num`,值含 `cls` / `id`),据 §1.1 得每条的目标 `TileType`(`animates`→`Animate`=3,`terrains`→`Terrain`=2,`autotile`→`Autotile`=7,`items`→`Item`=4,`enemys`+`enemy48`→`Enemy`=5,`npcs`+`npc48`→`Npc`=6)。
|
||||
- 分段顺序 `Terrain → Animate → Item → Enemy → Npc → Autotile`;段内按旧 `num` 升序;从 `num=1` 起**连续**分配(`0` 保留空图块);产出 `oldNum → id → newNum` 与 `id → newNum` 两张表(内存中使用,不写额外文件)。
|
||||
- **硬约束:** 新 `num` 全体唯一、`id` 全体唯一;后续所有文件(含 Task 2 的 `texture/.list.jsonc` `map`、Task 3 的 maps 数值与 enemy/item `num`)必须复用本表。
|
||||
- **硬约束:** 新 `num` 全体唯一、`id` 全体唯一;后续所有文件(含 Task 3 的 `texture/.list.jsonc` `map`、Task 4 的 maps 数值与 enemy/item `num`)必须复用本表。
|
||||
|
||||
(1) **写 `src/content/tile.jsonc`(235 条,替换样例):** 保留既有头部 `// Type:` 注释与 `// --- SYSTEM PREFIX END --- //` 分隔行不动(其后必须是严格 JSON)。每条图块对象字段(**键集精确 = `num`/`id`/`events`/`type`/`eventPass`/`pass`,恰 6 键**):
|
||||
- `num` = 上表新 `num`;`id` = 旧 `id`;`type` = 上表 `TileType` 数值;`events` = `{}`;`eventPass` = `false`。
|
||||
@ -280,15 +340,16 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 一次性零依赖 PNG 脚本 + 按 D-72/D-84/D-85 生成 43 张贴图素材(列数固定、行数 ≤4 且按每图实际裁剪;npc 按尺寸分组 `npc0..6`)+ 写 `texture/.list.jsonc`</name>
|
||||
<precondition>Task 1 已完成(`tile.jsonc` 235 条与全局 `id`↔`num` 基准就位,可被本任务复用);Task 0 已闭环(OI-3 = D-85 按尺寸分组,无需再裁定)。</precondition>
|
||||
<name>Task 3: 一次性零依赖 PNG 脚本 + 按 D-72/D-84/D-85 生成 43 张贴图素材(列数固定、行数 ≤4 且按每图实际裁剪;npc 按尺寸与帧数分组 `npc0..6`)+ 写 `texture/.list.jsonc`</name>
|
||||
<precondition>Task 2 已完成(`tile.jsonc` 235 条与全局 `id`↔`num` 基准就位,可被本任务复用);Task 0 已闭环(OI-3 = D-85 按尺寸与帧数分组,无需再裁定)。</precondition>
|
||||
<files>script/migrateProjectTextures.ts, src/content/texture/.list.jsonc, src/content/texture/*.png</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§0.5 D-72 五规则 + tilesets/autotiles 例外、§0.5.1 `animates` 并入 `terrains`、§1.4 图块→贴图归属、§2.1 `materials` 尺寸、§2.2 autotiles、§2.3 tilesets、§2.4 拆分方案、§2.5 命名、§5.1 / §5.2 npc 合并说明、§7 / §7.1 schema 与类型)—— **注意:§2.4「单帧 4×4 / 多帧 帧数列×4 行」的严格 4 行读法由 D-84 取代(裁到实际行数)、§2.5 的 `npc0..5` 计数由 D-85 取代(改为 `npc0..6`=7);盘点文档本身不改写。**
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§0.5 D-72 五规则 + tilesets/autotiles 例外、§0.5.1 `animates` 并入 `terrains`、§1.4 图块→贴图归属、§2.1 `materials` 尺寸、§2.2 autotiles、§2.3 tilesets、§2.4 拆分方案、§2.5 命名、§5.1 / §5.2 npc 合并说明、§7 / §7.1 schema 与类型)—— **注意:Task 1 已更正台账 §2.1 的 48px 事实(4 帧 / 单元格 32×48)与 §2.4/§2.5 的拆分计数(D/E 按帧数与尺寸分组、`SPLIT-COUNT`=43、`npc0..6`=7);§2.4 的「严格 4 行」读法由 D-84 取代。**
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-72 / D-73 / D-80 / D-81 / D-82 / D-83 / **D-84** / **D-85**)
|
||||
- packages-user/client-base/src/material/types.ts:12-17(`AutotileType`)、:50-55(`IIndexedIdentifier`)、:261-307(`addGrid`/`addRowAnimate`/`addAutotile`/`addTileset`)—— list schema 能力依据;packages-user/client-base/src/material/manager.ts:144-190(实现:`addRowAnimate` 单图单尺寸、`source.width % width === 0`);packages/render/src/assets/splitter.ts:8-18(`TextureRowSplitter`:`lines = ceil(height / frameH)`)—— D-85 按尺寸分组与 D-84 逐图裁剪依据
|
||||
- src/types/declaration/source.d.ts:81-91(`FrameNumbers`:`animates:4` / `enemys:2` / `npcs:2` / `enemy48:4` / `npc48:4`)—— 帧数权威
|
||||
- packages-user/client-base/src/material/types.ts:12-17(`AutotileType`)、:50-55(`IIndexedIdentifier`)、:261-307(`addGrid`/`addRowAnimate`/`addAutotile`/`addTileset`)—— list schema 能力依据;packages-user/client-base/src/material/manager.ts:144-190(实现:`addRowAnimate` 单图单尺寸、`source.width % width === 0`、`frames = source.width / width`);packages/render/src/assets/splitter.ts:8-18(`TextureRowSplitter`:`lines = ceil(height / frameH)`)—— D-85 按尺寸与帧数分组与 D-84 逐图裁剪依据
|
||||
- src/content/client.jsonc:9(`textureListDir: texture/.list.jsonc`、`textureDir: texture`)、src/content/texture/.list.jsonc(现状 `{}`)—— 落点与路径形态参考
|
||||
- src/content/tile.jsonc(Task 1 产出:新 `num` 与 `id`)
|
||||
- src/content/tile.jsonc(Task 2 产出:新 `num` 与 `id`)
|
||||
- public/project/materials/*.png、public/project/autotiles/*.png、public/project/tilesets/magictower.png —— 源素材(实测块数:`terrains` 35、`animates` 29、`items` 62、`enemys` 73、`enemy48` 8、`npcs` 14、`npc48` 10)
|
||||
- dev.md(命名规则:**代码文件名小驼峰**、常量全大写、模块无副作用);AGENTS.md(不新增依赖、命名先报备)
|
||||
</read_first>
|
||||
@ -302,27 +363,27 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
- 生成 A 组 `terrains` 单帧:源 `terrains.png`(32×1120,1 列 × 35 行,每格 32×32)→ 依序填入 4 列网格,每图 16 格,共 3 图 `terrains0..2.png`;`terrains0`/`terrains1` 各 4 行(128×128)、**`terrains2` 仅 3 格 → 1 行 → 128×32**。
|
||||
- 生成 B 组(`animates` 并入 `terrains`,D-72③):源 `animates.png`(128×928,4 帧列 × 29 行,每格帧 32×32)→ 每图 4 帧列 × 4 行(4 块/图),共 8 图 `terrainsAnim0..7.png`;`terrainsAnim0..6` 各 4 行(128×128)、**`terrainsAnim7` 仅 1 块 → 1 行 → 128×32**。
|
||||
- 生成 C 组 `items` 单帧:源 `items.png`(32×1984,1 列 × 62 行,每格 32×32)→ 4 列网格,每图 16 格,共 4 图 `items0..3.png`;`items0..2` 各 4 行、`items3` 有 14 格 → 占满 4 行,**四图均 128×128**。
|
||||
- 生成 D 组 `enemy`(合并 `enemys`+`enemy48`,D-72①):源 `enemys.png`(64×2336,2 帧列 × 73 行,每格帧 32×32)与 `enemy48.png`(128×384,2 帧列 × 8 行,每格帧 64×48);多帧 → 每图 4 行(4 块/图)、每行 2 帧列;**按单元格尺寸分组**:32px 组 73 块 → 19 图(`enemy0..17` 各 4 行 64×128、**`enemy18` 仅 1 块 → 1 行 → 64×32**)、48px 组 8 块 → 2 图(`enemy19`/`enemy20` 各 4 行 128×192),共 21 图 `enemy0..20.png`(与 D-81 一致)。
|
||||
- 生成 E 组 `npc`(合并 `npcs`+`npc48`,D-72①)**按尺寸分组(D-85)**:源 `npcs.png`(64×448,2 帧列 × 14 行 = 14 个 NPC 块,各 2 帧 32×32)与 `npc48.png`(128×480,2 帧列 × 10 行 = 10 个 NPC 块,各 2 帧 64×48);多帧 → 每图 4 行(4 块/图)、每行 2 帧列。**不同尺寸各自成图、不放一块、不留白、不重采样(D-85);各图行数按 D-84 逐图裁剪:**
|
||||
- 32px 组(帧 32×32)14 块 → 4 图 `npc0..3.png`:`npc0`/`npc1`/`npc2` 各 4 行(64×128)、**`npc3` 2 块 → 2 行 → 64×64**。
|
||||
- 48px 组(帧 64×48)10 块 → 3 图 `npc4..6.png`:`npc4`/`npc5` 各 4 行(128×192)、**`npc6` 2 块 → 2 行 → 128×96**。
|
||||
- 生成 D 组 `enemy`(合并 `enemys`+`enemy48`,D-72①,**按帧数与尺寸分组**):源 `enemys.png`(64×2336,**2 帧列** × 73 行,每格帧 32×32)与 `enemy48.png`(**128×384,4 帧列** × 8 行,每格帧 **32×48**);多帧 → 每图 4 行(4 块/图)、每行 = 帧数列列。**32px 组** 73 块 → 19 图(`enemy0..17` 各 4 行 64×128、**`enemy18` 仅 1 块 → 1 行 → 64×32**)、**48px 组** 8 块 → 2 图(`enemy19`/`enemy20` 各 4 行 **128×192**,帧 **32×48**),共 21 图 `enemy0..20.png`(与 D-81 一致)。
|
||||
- 生成 E 组 `npc`(合并 `npcs`+`npc48`,D-72①)**按尺寸与帧数分组(D-85)**:源 `npcs.png`(64×448,**2 帧列** × 14 行 = 14 个 NPC 块,各 **2 帧 32×32**)与 `npc48.png`(**128×480,4 帧列** × 10 行 = 10 个 NPC 块,各 **4 帧 32×48**);多帧 → 每图 4 行(4 块/图)、每行 = 帧数列列。**不同尺寸 / 帧数各自成图、不放一块、不留白、不重采样(D-85);各图行数按 D-84 逐图裁剪:**
|
||||
- 32px 组(**2 帧**,帧 32×32)14 块 → 4 图 `npc0..3.png`:`npc0`/`npc1`/`npc2` 各 4 行(64×128)、**`npc3` 2 块 → 2 行 → 64×64**。
|
||||
- 48px 组(**4 帧**,帧 **32×48**)10 块 → 3 图 `npc4..6.png`:`npc4`/`npc5` 各 4 行(**128×192**)、**`npc6` 2 块 → 2 行 → 128×96**。
|
||||
- 合共 **7 图 `npc0..6.png`**(`SPLIT-COUNT` = 3+8+4+21+7 = **43**)。
|
||||
- 复制(逐字节,不拆分):`public/project/autotiles/{autotile,autotile1,autotile2,autotile3}.png` → `src/content/texture/` 同名;`public/project/tilesets/magictower.png` → `src/content/texture/magictower.png`。
|
||||
- 生成后自检:每张产出图 `width == 列数 × 帧宽` 且 `height == 行数 × 帧高`(`1 ≤ 行数 ≤ 4`);`materials` 拆分图总格数 ≥ 源图块数。
|
||||
|
||||
(2) **写 `src/content/texture/.list.jsonc`(D-80 已批准 schema):** 保留头部注释与分隔行;`// --- SYSTEM PREFIX END --- //` 后为严格 JSON 对象 `{ "textures": [ ... ] }`,条目:
|
||||
- A/C 组 `{ "type": "grid", "source": "<文件名>", "map": [<新 num…>], "width": 32, "height": 32 }`。
|
||||
- B/D/E 组 `{ "type": "rowAnimate", "source": "<文件名>", "map": [<新 num…>], "width": <帧宽>, "height": <帧高> }`;**npc 按尺寸分组(D-85)**:`npc0..3` 条目 `width`/`height` = 32/32(32px 帧)、`npc4..6` 条目 `width`/`height` = 64/48(48px 帧);enemy 32px 条目 = 32/32、48px 条目 = 64/48。
|
||||
- B/D/E 组 `{ "type": "rowAnimate", "source": "<文件名>", "map": [<新 num…>], "width": <帧宽>, "height": <帧高> }`;`width`/`height` = **每帧尺寸**。**npc 按尺寸与帧数分组(D-85)**:`npc0..3` 条目 `width`/`height` = **32/32**(32px 帧,2 帧)、`npc4..6` 条目 `width`/`height` = **32/48**(48px 帧,4 帧);enemy 32px 条目 = 32/32(2 帧)、48px 条目 = **32/48**(4 帧);`terrainsAnim*` 条目 = 32/32(4 帧)。
|
||||
- autotile `{ "type": "autotile", "source": "autotile*.png", "identifier": <20|151|152|153 对应新 num>, "autotileType": <AutotileType 数值> }`(参数名用已批准的 `autotileType`;默认 `autotile.png`/`autotile2.png`/`autotile3.png` 取 `Big3x4`=1,`autotile1.png` 暂取 `Big3x4`=1,OI-6)。
|
||||
- tileset `{ "type": "tileset", "source": "magictower.png", "identifier": { "index": 0, "alias": "magictower" }, "cellWidth": 32, "cellHeight": 32 }`(默认候选,OI-6)。
|
||||
- `source` 默认用相对 `texture/` 的文件名(OI-5);`map` 中每个 `num` 必须来自 Task 1 的 `tile.jsonc`;**集合覆盖约束:** `map`(所有 grid/rowAnimate 条目)∪ autotile 的 `identifier` 必须覆盖**每一处有贴图的图块 `num`**——即 `tile.jsonc` 中 `type ∈ {Terrain(2),Animate(3),Item(4),Enemy(5),Npc(6),Autotile(7)}` 的全部 `num`(Tile not present 时为空集)。**裁剪只改变图的行数/尺寸,不改变 `map` 的条目集合与拆分图数;D-85 使 npc 拆分图由 6 变 7,`map` 条目集合随之为 24 个 Npc 图块(14 个 32px 归 `npc0..3`、10 个 48px 归 `npc4..6`)。**
|
||||
- `source` 默认用相对 `texture/` 的文件名(OI-5);`map` 中每个 `num` 必须来自 Task 2 的 `tile.jsonc`;**集合覆盖约束:** `map`(所有 grid/rowAnimate 条目)∪ autotile 的 `identifier` 必须覆盖**每一处有贴图的图块 `num`**——即 `tile.jsonc` 中 `type ∈ {Terrain(2),Animate(3),Item(4),Enemy(5),Npc(6),Autotile(7)}` 的全部 `num`(Tile not present 时为空集)。**裁剪只改变图的行数/尺寸,不改变 `map` 的条目集合与拆分图数;D-85 使 npc 拆分图由 6 变 7,`map` 条目集合随之为 24 个 Npc 图块(14 个 32px 归 `npc0..3`、10 个 48px 归 `npc4..6`)。**
|
||||
|
||||
(3) **收口:** 不写 §8.2/§8.1 内容;不改 `client.jsonc`/`defaults/**`;不新增依赖、不改 `package.json`;脚本运行后 `src/content/texture/` 内文件与 `.list.jsonc` 互一致。
|
||||
</action>
|
||||
<reversibility rating="reversible">新增脚本 + 生成 `src/content/texture/**`;可删除生成物或 `git checkout` 回退;未触及任何既有源码与注释。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const dir='src/content/texture/';function ihdr(f){const b=fs.readFileSync(dir+f);if(b.readUInt32BE(0)!==0x89504e47)throw new Error(f+' not PNG');const w=b.readUInt32BE(16),h=b.readUInt32BE(20),bd=b[24],il=b[28];if(il!==0)throw new Error(f+' interlace');if(bd!==8)throw new Error(f+' bitDepth '+bd);return{w,h}}function srcWH(f){const b=fs.readFileSync('public/project/materials/'+f);return{w:b.readUInt32BE(16),h:b.readUInt32BE(20)}}const bad=[];function sf(prefix,cells,cw,ch){const cols=4,per=16,sheets=Math.ceil(cells/per),out={};for(let i=0;i<sheets;i++){const c=Math.min(per,cells-per*i),rows=Math.ceil(c/cols);out[prefix+i+'.png']={w:cols*cw,h:rows*ch,rows,cols}}return out}function mf(prefix,blocks,frameCols,fw,fh,start){const sheets=Math.ceil(blocks/4),out={};for(let i=0;i<sheets;i++){const b=Math.min(4,blocks-4*i),rows=b;out[prefix+(start+i)+'.png']={w:frameCols*fw,h:rows*fh,rows,cols:frameCols}}return{out,next:start+sheets}}const S={terrains:srcWH('terrains.png'),animates:srcWH('animates.png'),items:srcWH('items.png'),enemys:srcWH('enemys.png'),enemy48:srcWH('enemy48.png'),npcs:srcWH('npcs.png'),npc48:srcWH('npc48.png')};const tCells=(S.terrains.w/32)*(S.terrains.h/32),iCells=(S.items.w/32)*(S.items.h/32),aBlocks=S.animates.h/32,eBlocks=S.enemys.h/32,e48Blocks=S.enemy48.h/48,nBlocks=S.npcs.h/32,n48Blocks=S.npc48.h/48;const e1=mf('enemy',eBlocks,2,32,32,0),e2=mf('enemy',e48Blocks,2,64,48,e1.next);const exp=Object.assign({},sf('terrains',tCells,32,32),sf('items',iCells,32,32),mf('terrainsAnim',aBlocks,4,32,32,0).out,e1.out,e2.out);let K=0;while(fs.existsSync(dir+'npc'+K+'.png'))K++;if(K!==7)bad.push('npc count '+K+' != 7');const a32=mf('npc',nBlocks,2,32,32,0);Object.assign(exp,a32.out,mf('npc',n48Blocks,2,64,48,a32.next).out);for(const f of Object.keys(exp)){const e=exp[f];if(e.rows<1||e.rows>4)bad.push(f+' computed rows out of range: '+e.rows);if(!fs.existsSync(dir+f)){bad.push('missing '+f);continue}let m;try{m=ihdr(f)}catch(err){bad.push(err.message);continue}if(m.w!==e.w||m.h!==e.h)bad.push(f+' = '+m.w+'x'+m.h+' != expected '+e.w+'x'+e.h+' (cols='+e.cols+', rows='+e.rows+')')}const gen=fs.readdirSync(dir).filter(f=>/^(terrainsAnim|terrains|items|enemy|npc)\d+\.png$/.test(f));for(const f of gen)if(!exp[f])bad.push('unexpected extra sheet '+f);const tiles=JSON.parse(fs.readFileSync('src/content/tile.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //')[1]);const tnums=new Set(tiles.map(x=>x.num));const textured=new Set(tiles.filter(x=>[2,3,4,5,6,7].indexOf(x.type)>=0).map(x=>x.num));const lp=fs.readFileSync('src/content/texture/.list.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //');if(lp.length!==2)bad.push('.list.jsonc: marker');else{let cfg;try{cfg=JSON.parse(lp[1])}catch(e){bad.push('.list.jsonc parse '+e.message)}if(cfg){if(!Array.isArray(cfg.textures))bad.push('textures not array');else{const covered=new Set();for(const e of cfg.textures){if(['grid','rowAnimate','autotile','tileset'].indexOf(e.type)<0){bad.push('unknown entry type '+e.type);continue}if(!fs.existsSync(dir+e.source))bad.push('source missing: '+e.source);if(e.type==='grid'||e.type==='rowAnimate'){if(!Array.isArray(e.map))bad.push(e.source+' map not array');else for(const n of e.map){if(!tnums.has(n))bad.push(e.source+' map num '+n+' not in tile.jsonc');covered.add(n)}}else if(e.type==='autotile'){if(typeof e.autotileType!=='number')bad.push(e.source+' autotileType not number');if(typeof e.identifier!=='number'||!tnums.has(e.identifier))bad.push(e.source+' identifier '+e.identifier+' not in tile.jsonc');else covered.add(e.identifier)}else if(e.type==='tileset'){if(!e.identifier||typeof e.identifier.alias!=='string')bad.push(e.source+' tileset identifier.alias missing')}}for(const n of textured)if(!covered.has(n))bad.push('textured tile num '+n+' not covered')}}}if(bad.length>0){console.error('TEXTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK textures: '+Object.keys(exp).length+' split sheets (per-sheet cols x frameW / rows x frameH, rows<=4; npc=7 by size) + copies; list covers all textured nums')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "TEXTURE GATE FAILED"(拆分图缺失/非 PNG/隔行/位深≠8/尺寸不等于按 D-84 逐图推导的「列数×帧宽 × 行数×帧高」(含任一 `enemy*`/`npc*`;末图可按实际行数裁剪,如 128×32 / 64×32 / 64×64 / 128×96)/ 出现多余拆分图 / `npc` 文件数 `K` ≠ 7 / npc 尺寸与 D-85 不符(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96)/ `.list.jsonc` 标记缺失或解析失败 / `textures` 非数组 / 条目 `type` 未知 / `source` 文件在磁盘不存在 / `map` 或 autotile `identifier` 的 `num` 不在 `tile.jsonc` / `autotileType` 非数值 / tileset `identifier.alias` 缺失 / **存在未被任何 `map` 或 autotile `identifier` 覆盖的有贴图图块 `num`**)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const dir='src/content/texture/';function ihdr(f){const b=fs.readFileSync(dir+f);if(b.readUInt32BE(0)!==0x89504e47)throw new Error(f+' not PNG');const w=b.readUInt32BE(16),h=b.readUInt32BE(20),bd=b[24],il=b[28];if(il!==0)throw new Error(f+' interlace');if(bd!==8)throw new Error(f+' bitDepth '+bd);return{w,h}}function srcWH(f){const b=fs.readFileSync('public/project/materials/'+f);return{w:b.readUInt32BE(16),h:b.readUInt32BE(20)}}const bad=[];function sf(prefix,cells,cw,ch){const cols=4,per=16,sheets=Math.ceil(cells/per),out={};for(let i=0;i<sheets;i++){const c=Math.min(per,cells-per*i),rows=Math.ceil(c/cols);out[prefix+i+'.png']={w:cols*cw,h:rows*ch,rows,cols}}return out}function mf(prefix,blocks,frameCols,fw,fh,start){const sheets=Math.ceil(blocks/4),out={};for(let i=0;i<sheets;i++){const b=Math.min(4,blocks-4*i),rows=b;out[prefix+(start+i)+'.png']={w:frameCols*fw,h:rows*fh,rows,cols:frameCols}}return{out,next:start+sheets}}const S={terrains:srcWH('terrains.png'),animates:srcWH('animates.png'),items:srcWH('items.png'),enemys:srcWH('enemys.png'),enemy48:srcWH('enemy48.png'),npcs:srcWH('npcs.png'),npc48:srcWH('npc48.png')};const tCells=(S.terrains.w/32)*(S.terrains.h/32),iCells=(S.items.w/32)*(S.items.h/32),aBlocks=S.animates.h/32,eBlocks=S.enemys.h/32,e48Blocks=S.enemy48.h/48,nBlocks=S.npcs.h/32,n48Blocks=S.npc48.h/48;const e1=mf('enemy',eBlocks,2,32,32,0),e2=mf('enemy',e48Blocks,4,32,48,e1.next);const exp=Object.assign({},sf('terrains',tCells,32,32),sf('items',iCells,32,32),mf('terrainsAnim',aBlocks,4,32,32,0).out,e1.out,e2.out);let K=0;while(fs.existsSync(dir+'npc'+K+'.png'))K++;if(K!==7)bad.push('npc count '+K+' != 7');const a32=mf('npc',nBlocks,2,32,32,0);Object.assign(exp,a32.out,mf('npc',n48Blocks,4,32,48,a32.next).out);for(const f of Object.keys(exp)){const e=exp[f];if(e.rows<1||e.rows>4)bad.push(f+' computed rows out of range: '+e.rows);if(!fs.existsSync(dir+f)){bad.push('missing '+f);continue}let m;try{m=ihdr(f)}catch(err){bad.push(err.message);continue}if(m.w!==e.w||m.h!==e.h)bad.push(f+' = '+m.w+'x'+m.h+' != expected '+e.w+'x'+e.h+' (cols='+e.cols+', rows='+e.rows+')')}const gen=fs.readdirSync(dir).filter(f=>/^(terrainsAnim|terrains|items|enemy|npc)\d+\.png$/.test(f));for(const f of gen)if(!exp[f])bad.push('unexpected extra sheet '+f);const tiles=JSON.parse(fs.readFileSync('src/content/tile.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //')[1]);const tnums=new Set(tiles.map(x=>x.num));const textured=new Set(tiles.filter(x=>[2,3,4,5,6,7].indexOf(x.type)>=0).map(x=>x.num));const lp=fs.readFileSync('src/content/texture/.list.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //');if(lp.length!==2)bad.push('.list.jsonc: marker');else{let cfg;try{cfg=JSON.parse(lp[1])}catch(e){bad.push('.list.jsonc parse '+e.message)}if(cfg){if(!Array.isArray(cfg.textures))bad.push('textures not array');else{const covered=new Set();for(const e of cfg.textures){if(['grid','rowAnimate','autotile','tileset'].indexOf(e.type)<0){bad.push('unknown entry type '+e.type);continue}if(!fs.existsSync(dir+e.source))bad.push('source missing: '+e.source);if(e.type==='grid'||e.type==='rowAnimate'){if(!Array.isArray(e.map))bad.push(e.source+' map not array');else for(const n of e.map){if(!tnums.has(n))bad.push(e.source+' map num '+n+' not in tile.jsonc');covered.add(n)}const ex=exp[e.source]||exp[String(e.source).split('/').pop()];if(ex){if(e.width!==ex.w/ex.cols)bad.push(e.source+' entry.width '+e.width+' != per-frame '+ex.w/ex.cols);if(e.height!==ex.h/ex.rows)bad.push(e.source+' entry.height '+e.height+' != per-frame '+ex.h/ex.rows);if(e.type==='rowAnimate'){const fexp=/^npc/.test(e.source)?(parseInt(e.source.slice(3),10)<=3?2:4):(/^enemy/.test(e.source)?(parseInt(e.source.slice(5),10)<=18?2:4):(/^terrainsAnim/.test(e.source)?4:null));if(fexp!==null&&(ex.w/e.width)!==fexp)bad.push(e.source+' frames '+(ex.w/e.width)+' != '+fexp)}}}else if(e.type==='autotile'){if(typeof e.autotileType!=='number')bad.push(e.source+' autotileType not number');if(typeof e.identifier!=='number'||!tnums.has(e.identifier))bad.push(e.source+' identifier '+e.identifier+' not in tile.jsonc');else covered.add(e.identifier)}else if(e.type==='tileset'){if(!e.identifier||typeof e.identifier.alias!=='string')bad.push(e.source+' tileset identifier.alias missing')}}for(const n of textured)if(!covered.has(n))bad.push('textured tile num '+n+' not covered')}}}if(bad.length>0){console.error('TEXTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK textures: '+Object.keys(exp).length+' split sheets (per-sheet cols x frameW / rows x frameH, rows<=4; per-frame w/h + frames asserted; npc=7 by size+frames) + copies; list covers all textured nums')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "TEXTURE GATE FAILED"(拆分图缺失/非 PNG/隔行/位深≠8/尺寸不等于按 D-84 逐图推导的「列数×帧宽 × 行数×帧高」(含任一 `enemy*`/`npc*`;末图可按实际行数裁剪,如 128×32 / 64×32 / 64×64 / 128×96)/ 出现多余拆分图 / `npc` 文件数 `K` ≠ 7 / `.list.jsonc` 条目 `width`/`height` ≠ 每帧尺寸(48px 条目应为 **32/48**、帧数应为 **4**;32px 条目应为 32/32、帧数应为 2)/ npc 尺寸与 D-85 不符(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96)/ `.list.jsonc` 标记缺失或解析失败 / `textures` 非数组 / 条目 `type` 未知 / `source` 文件在磁盘不存在 / `map` 或 autotile `identifier` 的 `num` 不在 `tile.jsonc` / `autotileType` 非数值 / tileset `identifier.alias` 缺失 / **存在未被任何 `map` 或 autotile `identifier` 覆盖的有贴图图块 `num`**)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const dir='src/content/texture/';const need=['autotile.png','autotile1.png','autotile2.png','autotile3.png','magictower.png'];const bad=[];for(const f of need){if(!fs.existsSync(dir+f))bad.push('missing copied '+f)}const a=fs.readFileSync('public/project/autotiles/autotile.png');const b=fs.existsSync(dir+'autotile.png')?fs.readFileSync(dir+'autotile.png'):null;if(b&&!a.equals(b))bad.push('autotile.png not byte-identical');const a2=fs.readFileSync('public/project/autotiles/autotile2.png');const b2=fs.existsSync(dir+'autotile2.png')?fs.readFileSync(dir+'autotile2.png'):null;if(b2&&!a2.equals(b2))bad.push('autotile2.png not byte-identical');const ta=fs.readFileSync('public/project/tilesets/magictower.png');const tb=fs.existsSync(dir+'magictower.png')?fs.readFileSync(dir+'magictower.png'):null;if(tb&&!ta.equals(tb))bad.push('magictower.png not byte-identical');if(bad.length>0){console.error('COPY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK autotiles (4) + tileset (1) copied byte-identical (not re-encoded)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "COPY GATE FAILED"(autotile/tileset 复制缺失,或未逐字节复制而重编码)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const p='script/migrateProjectTextures.ts';const bad=[];if(!fs.existsSync(p))bad.push('script missing');else{const t=fs.readFileSync(p,'utf8');if(!/node:zlib|require\(['\\u0022]zlib['\\u0022]\)/.test(t))bad.push('script does not use node zlib');if(/from ['\\u0022](pngjs|sharp|canvas|jimp)/.test(t))bad.push('script imports a forbidden png lib')}const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const all=Object.assign({},pkg.dependencies||{},pkg.devDependencies||{});for(const n of ['pngjs','sharp','canvas','jimp'])if(all[n])bad.push('dependency added: '+n);if(bad.length>0){console.error('DEP GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK zero-dependency: script uses node zlib; no png lib added to package.json')"</automated>
|
||||
@ -330,23 +391,23 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `script/migrateProjectTextures.ts` 存在(小驼峰,符合 dev.md),仅用 Node `zlib` + `fs`(零外部依赖),未改 `package.json`
|
||||
- `src/content/texture/` 含 **43 张**拆分图(`terrains0..2` / `terrainsAnim0..7` / `items0..3` / `enemy0..20` / `npc0..6`)与 5 张逐字节复制素材;**每张 PNG**(含全部 `enemy*`/`npc*`)均为非隔行 8 位、且尺寸 = **按 D-84 逐图推导**的「`列数 × 帧宽` × `行数 × 帧高`」(`1 ≤ 行数 ≤ 4`,列数固定;末图可裁剪,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32、`npc3`=64×64、`npc6`=128×96);`npc` 图数恰 **7** 且按 D-85 尺寸分组(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96)
|
||||
- `texture/.list.jsonc` 为 D-80 已批准 schema(`textures` 数组,条目 `type` ∈ {`grid`,`rowAnimate`,`autotile`,`tileset`});每个 `source` 文件存在;`map`/`identifier` 的 `num` 全部来自 `tile.jsonc`;npc 条目 `width`/`height` 按尺寸分组(`npc0..3` = 32/32、`npc4..6` = 64/48);**`map ∪ autotile identifier` 覆盖每一处有贴图的图块 `num`**
|
||||
- `src/content/texture/` 含 **43 张**拆分图(`terrains0..2` / `terrainsAnim0..7` / `items0..3` / `enemy0..20` / `npc0..6`)与 5 张逐字节复制素材;**每张 PNG**(含全部 `enemy*`/`npc*`)均为非隔行 8 位、且尺寸 = **按 D-84 逐图推导**的「`列数 × 帧宽` × `行数 × 帧高`」(`1 ≤ 行数 ≤ 4`,列数固定;末图可裁剪,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32、`npc3`=64×64、`npc6`=128×96);`npc` 图数恰 **7** 且按 D-85 尺寸与帧数分组(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96)
|
||||
- `texture/.list.jsonc` 为 D-80 已批准 schema(`textures` 数组,条目 `type` ∈ {`grid`,`rowAnimate`,`autotile`,`tileset`});每个 `source` 文件存在;`map`/`identifier` 的 `num` 全部来自 `tile.jsonc`;`rowAnimate` 条目 `width`/`height` = 每帧尺寸(`npc0..3` 与 32px enemy 条目 = 32/32、帧数 2;`npc4..6` 与 48px enemy 条目 = **32/48**、帧数 4;`terrainsAnim*` = 32/32、帧数 4);**`map ∪ autotile identifier` 覆盖每一处有贴图的图块 `num`**
|
||||
- 分隔标记后为严格 JSON;头部注释保留;未写 §8.1/§8.2 内容
|
||||
</acceptance_criteria>
|
||||
<done>43 张拆分图(含按尺寸分组的 `npc0..6`=7)+ 5 张复制素材生成;**每张图尺寸按 D-84 逐图实际行数(≤4)经门禁断言**(不硬编码 4 行尺寸);`npc` 图数 = 7 且按 D-85 尺寸分组经门禁断言;`texture/.list.jsonc` 落盘且 `source`/`map`/`identifier` 与 `tile.jsonc` 一致且覆盖全部有贴图图块;零依赖门禁全绿</done>
|
||||
<done>43 张拆分图(含按尺寸与帧数分组的 `npc0..6`=7)+ 5 张复制素材生成;**每张图尺寸按 D-84 逐图实际行数(≤4)经门禁断言**(不硬编码 4 行尺寸);`npc` 图数 = 7 且按 D-85 尺寸与帧数分组经门禁断言(每帧尺寸 + 帧数 4/2);`texture/.list.jsonc` 落盘且 `source`/`map`/`identifier` 与 `tile.jsonc` 一致且覆盖全部有贴图图块;零依赖门禁全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: 写 5 楼层地图 + `maps/.list.jsonc` + `09-DATA-MIGRATION-UNMAPPED.md`(仅 §8.1),跑最终门禁并写 SUMMARY</name>
|
||||
<precondition>Task 1 / Task 2 已完成(全局 `id`↔`num` 基准与 `tile.jsonc` 就位,供地图旧 `num` 重映射)。</precondition>
|
||||
<name>Task 4: 写 5 楼层地图 + `maps/.list.jsonc` + `09-DATA-MIGRATION-UNMAPPED.md`(仅 §8.1),跑最终门禁并写 SUMMARY</name>
|
||||
<precondition>Task 2 / Task 3 已完成(全局 `id`↔`num` 基准与 `tile.jsonc` 就位,供地图旧 `num` 重映射);Task 1 已完成(台账已更正)。</precondition>
|
||||
<files>src/content/maps/empty.jsonc, src/content/maps/MT0.jsonc, src/content/maps/sample0.jsonc, src/content/maps/sample1.jsonc, src/content/maps/sample2.jsonc, src/content/maps/.list.jsonc, .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md, .planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§6.1 楼层登记、§6.2 map 字段分类、§8.1 U-16 无目标字段清单、§8.2 U-15/U-16b 待定)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-76 / D-78 / D-82 / D-83 / D-84 / D-85)
|
||||
- packages-user/data-common/src/store/types.ts:260-271(`IMapRawData`);packages-user/data-base/src/map/mapState.ts:40-95(`fromRaw`:各层等长、`length % width === 0`)
|
||||
- src/content/defaults/map.jsonc(`@required` = `floorId`;`map` / `layerAlias` / `events` 骨架);src/content/maps/MT0.jsonc(现状样例);src/content/maps/.list.jsonc(现状 `["MT0"]`)
|
||||
- src/content/tile.jsonc(Task 1 产出:新 `num` 与 `id`,用于旧→新重映射)
|
||||
- src/content/tile.jsonc(Task 2 产出:新 `num` 与 `id`,用于旧→新重映射)
|
||||
- public/project/floors/{empty,MT0,sample0,sample1,sample2}.js —— 源楼层
|
||||
- dev.md(Markdown 文档文件名=连字符命名法、注释规范);AGENTS.md(不臆造、命名先报备)
|
||||
</read_first>
|
||||
@ -362,26 +423,26 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
|
||||
(3) **写 `09-DATA-MIGRATION-UNMAPPED.md`(路径/命名待批准,OI-1;仅 §8.1):** 采用连字符命名(Markdown 文档文件名规范)、裸 LF。内容仅收录「无目标字段 → 只记录、不移植」项,按维度分组并附「所查接口与缺失字段」依据(引用 `09-DATA-MIGRATION-AUDIT.md` §8.1):tile(`canBreak`/`script`/`doorInfo`/`animate`/`name`/`faceIds`/`autotileConnection`)、enemy(`faceIds`)、item(`hideInReplay`)、map(`title`/`name`/`height`/`firstArrive`/`eachArrive`/`parallelDo`/`changeFloor`/`afterBattle`/`afterGetItem`/`afterOpenDoor`/`beforeBattle`/`cannotMove`/`cannotMoveIn`/`autoEvent`/`images`/`bgm`/`weather`/`ratio`/`defaultGround`/`canFlyTo`/`canFlyFrom`/`canUseQuickShop`/`cannotViewMap`/`upFloor`)。**不得**收录 §8.2 待定项——它们保留在盘点台账。
|
||||
|
||||
(4) **写 `09-19-SUMMARY.md`:** 记录本计划范围与逐类映射口径、`num` 策略 A 的分段/取值、`text` 缺失 13 条的 `""` 处置、素材产出(**43 张拆分图**、**每张图的实际行数与尺寸(含裁剪末图,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32、`npc3`=64×64、`npc6`=128×96)**、**D-85 处置**(= OI-3 的 A · 按尺寸分组:`npc0..6`=7 张——32px 组 `npc0..2` 64×128 + `npc3` 64×64、48px 组 `npc4`/`npc5` 128×192 + `npc6` 128×96;不放一块、不留白、不重采样;**取代 D-81 的 6 图 / 42**;enemy 不受影响仍 `enemy0..20`=21)+ 5 张复制素材)、**D-84 布局口径(列数固定、行数 ≤4 且按每图实际裁剪)与「§2 严格 4 行读法已被 D-84 取代、§2.5 `npc0..5` 计数已被 D-85 取代」说明**、地图图层键与旧→新重映射口径、消费接线未实现(D-70/D-83)、门禁结果(含 enemy/item/map 精确键集断言与 D-84 逐图尺寸断言、npc 图数 = 7 断言)、`open_items`(OI-1/2/4/5/6/7 的最终处置;OI-3 标注「已由 D-85 裁定」)。
|
||||
(4) **写 `09-19-SUMMARY.md`:** 记录本计划范围与逐类映射口径、**台账更正结果(§2.1 的 48px 事实 4 帧 / 单元格 32×48 / 行数 8、10;§2.4 D/E 按帧数与尺寸分组、enemy 21 / npc 7、`SPLIT-COUNT`=43;§2.5 `npc0..6`=7;D-84/D-85 同步;D-81 的 npc 计数被取代)**、`num` 策略 A 的分段/取值、`text` 缺失 13 条的 `""` 处置、素材产出(**43 张拆分图**、**每张图的实际行数与尺寸(含裁剪末图,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32、`npc3`=64×64、`npc6`=128×96)**、**D-85 处置**(= OI-3 的 A · 按尺寸与帧数分组:`npc0..6`=7 张——32px 组 `npc0..2` 64×128 + `npc3` 64×64、48px 组 `npc4`/`npc5` 128×192 + `npc6` 128×96;不放一块、不留白、不重采样;**取代 D-81 的 6 图 / 42**;enemy 不受影响仍 `enemy0..20`=21)+ 5 张复制素材)、**D-84 布局口径(列数固定、行数 ≤4 且按每图实际裁剪)与「台账 §2 严格 4 行读法已被 D-84 取代、§2.5 `npc0..5` 计数已被 D-85 取代」说明**、地图图层键与旧→新重映射口径、消费接线未实现(D-70/D-83)、门禁结果(含 enemy/item/map 精确键集断言与 D-84 逐图尺寸断言、npc 图数 = 7 断言、台账更正断言)、`open_items`(OI-1/2/4/5/6/7 的最终处置;OI-3 标注「已由 D-85 裁定」)。
|
||||
|
||||
(5) **收口:** 不改任何源码 / `public/project/**` / `ROADMAP.md` / `STATE.md` / 既有台账与计划;提交前 `git status` 逐条核对,仅暂存本计划 `files_modified` 条目(避开用户并发改动 D-56)。
|
||||
(5) **收口:** 不改任何源码 / `public/project/**` / `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY(**唯一允许修改的既有规划文件是 `09-DATA-MIGRATION-AUDIT.md`,已由 Task 1 更正**);提交前 `git status` 逐条核对,仅暂存本计划 `files_modified` 条目(避开用户并发改动 D-56)。
|
||||
</action>
|
||||
<reversibility rating="reversible">新增/替换 `src/content/maps/**` 与两份文档;`git checkout` / 删除可回退;未触及任何既有源码与注释。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const dir='src/content/maps/';const bad=[];const tiles=JSON.parse(fs.readFileSync('src/content/tile.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //')[1]);const numSet=new Set(tiles.map(x=>x.num));const ids=['empty','MT0','sample0','sample1','sample2'];for(const id of ids){const p=dir+id+'.jsonc';if(!fs.existsSync(p)){bad.push('missing '+id+'.jsonc');continue}const parts=fs.readFileSync(p,'utf8').split('// --- SYSTEM PREFIX END --- //');if(parts.length!==2){bad.push(id+': marker');continue}let m;try{m=JSON.parse(parts[1])}catch(e){bad.push(id+': parse '+e.message);continue}const mkeys=Object.keys(m).sort().join(',');if(mkeys!=='events,floorId,layerAlias,map,width')bad.push(id+': forbidden/extra top-level keys: '+mkeys);if(m.floorId!==id)bad.push(id+': floorId '+m.floorId);if(typeof m.width!=='number')bad.push(id+': width');if(typeof m.layerAlias!=='object')bad.push(id+': layerAlias');if(typeof m.events!=='object')bad.push(id+': events');const keys=Object.keys(m.map||{});if(keys.length<1)bad.push(id+': no map layers');for(const k of keys){const a=m.map[k];if(!Array.isArray(a)){bad.push(id+': layer '+k+' not array');continue}if(a.length%m.width!==0)bad.push(id+': layer '+k+' length '+a.length+' % width '+m.width+' != 0');for(const v of a){if(v!==0&&!numSet.has(v)){bad.push(id+': value '+v+' not a tile num');break}}}}let list;try{list=JSON.parse(fs.readFileSync(dir+'.list.jsonc','utf8').split('// --- SYSTEM PREFIX END --- //')[1])}catch(e){bad.push('.list.jsonc: '+e.message)}if(list&&(!Array.isArray(list)||list.length!==5||!ids.every(x=>list.indexOf(x)>=0)))bad.push('.list.jsonc not the 5 floor ids');const up='.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md';if(!fs.existsSync(up))bad.push('unmapped doc missing');else{const u=fs.readFileSync(up,'utf8');for(const tok of ['doorInfo','faceIds','hideInReplay','firstArrive','changeFloor','defaultGround']){if(u.indexOf(tok)<0)bad.push('unmapped missing token: '+tok)}if(/U-15|bgmap|fgmap/.test(u))bad.push('unmapped doc must NOT contain §8.2 tokens (U-15/bgmap/fgmap)')}const sum='.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md';if(!fs.existsSync(sum))bad.push('SUMMARY missing');if(bad.length>0){console.error('MAP GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK maps: 5 floors (5-key top level, layer length % width==0, nums in tile) + list + unmapped(§8.1 only) + SUMMARY')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "MAP GATE FAILED"(楼层文件缺失/标记或解析失败 / 顶层键集非 5 键或含 §8.1/§8.2 键 / `floorId` 不符 / `map` 无图层或图层非数组 / 图层长度不能被 `width` 整除 / 数值非 `0` 且非有效 `tile` `num` / `.list.jsonc` 非 5 个 id / unmapped 文档缺失或含 §8.2 项 / SUMMARY 缺失)</fails_when>
|
||||
<!-- plan-criteria-allow: R4 - SCOPE GATE 内的 git status 由 node execSync 调用并以 try/catch 捕获失败(失败即非零退出),同行的 `|` 属 JS 正则/`||` 运算符,非 shell 管道阶段 -->
|
||||
<automated>node -e "const {execSync}=require('child_process');let rawStatus;try{rawStatus=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('SCOPE GATE FAILED :: git status failed :: '+e.message);process.exit(1)}const status=rawStatus.split(/\r?\n/).filter(Boolean);const bad=[];for(const l of status){const p=l.slice(3).trim().replace(/^\u0022|\u0022$/g,'');if(p==='AGENTS.md'||p==='.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md')continue;if(p==='ROADMAP.md'||p.endsWith('/ROADMAP.md')||p.endsWith('STATE.md')||p.endsWith('/09-DATA-MIGRATION-AUDIT.md'))bad.push('forbidden planning edit: '+p);else if(p.startsWith('packages/')||p.startsWith('packages-user/')||p.startsWith('public/project/'))bad.push(p);else if(p.startsWith('src/')&&!p.startsWith('src/content/'))bad.push(p);else if(/\.(ts|tsx)$/.test(p)&&p!=='script/migrateProjectTextures.ts'&&!p.startsWith('src/content/'))bad.push(p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: out-of-scope changes :: '+bad.join(' | '));process.exit(1)}console.log('OK scope: only src/content/** + migration script + planning docs changed (user concurrent changes ignored)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(本计划执行期改动了 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/盘点台账,或新增了 `files_modified` 外的 `.ts`/`.tsx`)</fails_when>
|
||||
<automated>node -e "const {execSync}=require('child_process');let rawStatus;try{rawStatus=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('SCOPE GATE FAILED :: git status failed :: '+e.message);process.exit(1)}const status=rawStatus.split(/\r?\n/).filter(Boolean);const bad=[];for(const l of status){const p=l.slice(3).trim().replace(/^\u0022|\u0022$/g,'');if(p==='AGENTS.md'||p==='.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md')continue;if(p==='ROADMAP.md'||p.endsWith('/ROADMAP.md')||p.endsWith('STATE.md'))bad.push('forbidden planning edit: '+p);else if(p.startsWith('packages/')||p.startsWith('packages-user/')||p.startsWith('public/project/'))bad.push(p);else if(p.startsWith('src/')&&!p.startsWith('src/content/'))bad.push(p);else if(/\.(ts|tsx)$/.test(p)&&p!=='script/migrateProjectTextures.ts'&&!p.startsWith('src/content/'))bad.push(p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: out-of-scope changes :: '+bad.join(' | '));process.exit(1)}console.log('OK scope: only src/content/** + migration script + planning docs (incl. corrected audit) changed (user concurrent changes ignored)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(本计划执行期改动了 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`,或新增了 `files_modified` 外的 `.ts`/`.tsx`);`09-DATA-MIGRATION-AUDIT.md` 的更正属白名单内,不触发本门禁</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 5 份楼层 jsonc 齐备(`empty`/`MT0`/`sample0`/`sample1`/`sample2`),顶层键集恰为 `floorId`/`width`/`map`/`layerAlias`/`events`(5 键,无 §8.1/§8.2 键);每个图层数组长度 = `width`×`height`;数值均为 `0` 或有效新 `num`
|
||||
- `maps/.list.jsonc` 恰 5 个 id;旧 `num` 已重映射为新 `num`
|
||||
- `09-DATA-MIGRATION-UNMAPPED.md` 存在且仅含 §8.1 项(含 tile/enemy/item/map 四组,含 `src`/接口依据),不含 §8.2 项
|
||||
- `09-19-SUMMARY.md` 已写(含范围、`num` 分配、`text` 缺失 13 条的 `""` 处置、素材产出(含**每张图实际行数与裁剪末图尺寸**、**43 张总量**)与 **D-85 处置**(`npc0..6`=7 / 取代 D-81 的 6/42)、**D-84 布局口径**、地图口径、门禁、`open_items`)
|
||||
- 未改任何源码 / `public/project/**` / 既有台账与计划;未写 §8.2/§8.1 进数据文件(精确键集断言)
|
||||
- `09-19-SUMMARY.md` 已写(含范围、台账更正结果、`num` 分配、`text` 缺失 13 条的 `""` 处置、素材产出(含**每张图实际行数与裁剪末图尺寸**、**43 张总量**)与 **D-85 处置**(`npc0..6`=7 / 取代 D-81 的 6/42)、**D-84 布局口径**、地图口径、门禁、`open_items`)
|
||||
- 未改任何源码 / `public/project/**` / 既有台账(除 Task 1 的更正)与其它规划文件;未写 §8.2/§8.1 进数据文件(精确键集断言)
|
||||
</acceptance_criteria>
|
||||
<done>5 楼层 + `maps/.list.jsonc` + unmapped 文档 + SUMMARY 落盘;地图/作用域门禁全绿</done>
|
||||
<done>5 楼层 + `maps/.list.jsonc` + unmapped 文档 + SUMMARY 落盘;地图/作用域门禁全绿(台账更正不触发作用域门禁)</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@ -391,57 +452,61 @@ Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 写入边界 | 本增量是写操作;只允许写 `files_modified` 白名单(`src/content/**` 数据与 PNG + 迁移脚本 + 两份规划文档) |
|
||||
| 范围边界 | 不得改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/既有台账与计划 |
|
||||
| 写入边界 | 本增量是写操作;只允许写 `files_modified` 白名单(`src/content/**` 数据与 PNG + 迁移脚本 + `09-DATA-MIGRATION-AUDIT.md` 更正 + 两份规划文档) |
|
||||
| 范围边界 | 除 `09-DATA-MIGRATION-AUDIT.md`(仅 48px 事实更正)外,不得改任何既有规划文件;不得改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md` |
|
||||
| 台账更正边界 | 台账 `09-DATA-MIGRATION-AUDIT.md` 仅可更正 48px 帧数/单元格/拆分分组/计数与 D-84/D-85 引用;不得改字段分类判定、§8.1/§8.2 分区或增删条目 |
|
||||
| 判定边界 | §8.2「待定」与 §8.1「无目标字段」均不得落入数据文件;§8.1 只入 unmapped 文档,§8.2 只留台账;各实体/地图以精确键集断言兜底 |
|
||||
| `num` 一致性边界 | `id`↔`num` 跨 `tile`/`enemy`/`item`/`texture.list`/`maps` 五处必须一致(D-79) |
|
||||
| 贴图布局边界 | 拆分图列数固定(单帧 4 / 多帧 = 帧数列),行数 ≤4 且按每图实际块数裁剪;门禁须按**实际行数**推导尺寸(D-84),不得硬编码 4 行尺寸 |
|
||||
| 贴图尺寸分组边界 | **不同尺寸的素材必须用不同图片(D-85)**:32×32 与 64×48 帧不得同图;`npc` 按尺寸分组 `npc0..6`=7 图;不得以「统一帧尺寸 + 透明留白」混图 |
|
||||
| 贴图尺寸/帧数分组边界 | **不同尺寸 / 帧数的素材必须用不同图片(D-85)**:32×32(2 帧)与 32×48(4 帧)帧不得同图;`npc` 按尺寸与帧数分组 `npc0..6`=7 图;不得以「统一帧尺寸 + 透明留白」混图 |
|
||||
| 贴图覆盖边界 | `texture/.list.jsonc` 的 `map ∪ autotile identifier` 必须覆盖每一处有贴图的图块 `num` |
|
||||
| 决策边界 | OI-3(48px 合并)已由 **D-85** 裁定 = 按尺寸分组(`npc0..6`=7 图 / `SPLIT-COUNT`=43);执行须据此落地,不得改按别的口径 |
|
||||
| 决策边界 | OI-3(48px 合并)已由 **D-85** 裁定 = 按尺寸与帧数分组(`npc0..6`=7 图 / `SPLIT-COUNT`=43);执行须据此落地,不得改按别的口径 |
|
||||
| 依赖边界 | 不得新增依赖;PNG 生成须零依赖 Node `zlib` |
|
||||
| 命名边界 | 未批准命名(脚本路径、unmapped 文档路径、`source` 形态、图层键)先入 `open_items` 待批准(D-73 / AGENTS.md) |
|
||||
| 提交边界 | 不得把用户并发改动(D-56,`AGENTS.md` / `09-CONTEXT.md`)纳入暂存 / 提交 |
|
||||
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**本地数据/素材移植**,不执行第三方代码、不新增依赖、不引入网络面,**不存在未缓解的 `high` 级威胁**(寄存器 `T-09-19-SC` 虽标记 `high`,已由「不新增依赖 + `DEP GATE`」缓解);其余寄存器均为 `medium` 及以下,并逐条给出缓解。
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**本地数据/素材移植与台账更正**,不执行第三方代码、不新增依赖、不引入网络面,**不存在未缓解的 `high` 级威胁**(寄存器 `T-09-19-SC` 虽标记 `high`,已由「不新增依赖 + `DEP GATE`」缓解);其余寄存器均为 `medium` 及以下,并逐条给出缓解。
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-19-01 | Tampering | 越界写入生产源码 / 数据(`packages*/`、`src/`(除 `src/content/**`)、`public/project/**`) | medium | mitigate | `files_modified` 白名单 + prohibitions + Task 3 `SCOPE GATE`(`git status` 逐条校验);Task 2 `DEP GATE` 校验 `package.json` 未新增依赖 |
|
||||
| T-09-19-02 | Tampering | 移植了 §8.2「待定」或 §8.1「无目标字段」,或 unmapped 文档混入 §8.2 | medium | mitigate | Task 1 门禁按**精确键集**校验 tile(6 键)/ enemy(3 键)/ item(5 键)——任何 §8.1/§8.2 键即失败;Task 3 门禁按**精确键集**校验 map 顶层(5 键)并校验 unmapped 文档含 §8.1 token 且不含 §8.2 token |
|
||||
| T-09-19-03 | Tampering | `id`↔`num` 跨文件不一致(贴图 `map`/地图数值/实体 `num` 失配) | medium | mitigate | Task 1 建立唯一基准表;Task 2 门禁校验 `list` 的 `map`/`identifier` `num` 均在 `tile.jsonc`;Task 3 门禁校验地图数值均为 `0` 或有效 `num` |
|
||||
| T-09-19-04 | Tampering | 生成 PNG 尺寸 / 数量偏离 D-72/D-84/D-85 方案或损坏(编造像素) | medium | mitigate | Task 2 `TEXTURE GATE` 从**来源 PNG 块数与列数**逐图推导期望 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),对**全部**产出图(含 `enemy*`/`npc*`)校验非隔行 8 位且尺寸相符;`npc` 图数断言 = **7** 且按 D-85 尺寸分组(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96);`COPY GATE` 校验 autotile/tileset 逐字节复制 |
|
||||
| T-09-19-05 | Tampering | 贴图列表漏项:某有贴图图块未被任何 `map`/autotile `identifier` 覆盖 | medium | mitigate | Task 2 `TEXTURE GATE` 集合覆盖断言:`map ∪ autotile identifier ⊇` 所有 `type ∈ {2,3,4,5,6,7}` 的图块 `num` |
|
||||
| T-09-19-06 | Tampering | 地图结构非法(图层长度非 `width×height` 倍数、含越界 `num`、顶层混入 §8.1/§8.2 键) | medium | mitigate | Task 3 `MAP GATE` 校验顶层键集恰 5 键、每层 `length % width === 0` 且数值 ∈ {0} ∪ `tile` nums |
|
||||
| T-09-19-07 | Elevation of Privilege | 未经批准即执行 / 使用未报备命名 / 新增依赖 | medium | mitigate | Task 0 `blocking-human`(回复「可以执行」,OI-3 已由 D-85 裁定);命名候选入 `open_items`;Task 2 `DEP GATE` 禁依赖新增 |
|
||||
| T-09-19-08 | Elevation of Privilege | 48px 分组口径被偏离 D-85,或误用 D-85 禁止的「统一帧尺寸 + 透明留白」 | medium | mitigate | OI-3 已由 **D-85** 裁定 = 按尺寸分组;Task 2 明确「按尺寸分组、不放一块、不留白、不重采样」,`TEXTURE GATE` 断言 `npc` 文件数 = 7 且各图尺寸按 D-85(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96);prohibitions 明列禁止留白混图 |
|
||||
| T-09-19-01 | Tampering | 越界写入生产源码 / 数据(`packages*/`、`src/`(除 `src/content/**`)、`public/project/**`) | medium | mitigate | `files_modified` 白名单 + prohibitions + Task 4 `SCOPE GATE`(`git status` 逐条校验);Task 3 `DEP GATE` 校验 `package.json` 未新增依赖 |
|
||||
| T-09-19-02 | Tampering | 移植了 §8.2「待定」或 §8.1「无目标字段」,或 unmapped 文档混入 §8.2 | medium | mitigate | Task 2 门禁按**精确键集**校验 tile(6 键)/ enemy(3 键)/ item(5 键)——任何 §8.1/§8.2 键即失败;Task 4 门禁按**精确键集**校验 map 顶层(5 键)并校验 unmapped 文档含 §8.1 token 且不含 §8.2 token |
|
||||
| T-09-19-03 | Tampering | `id`↔`num` 跨文件不一致(贴图 `map`/地图数值/实体 `num` 失配) | medium | mitigate | Task 2 建立唯一基准表;Task 3 门禁校验 `list` 的 `map`/`identifier` `num` 均在 `tile.jsonc`;Task 4 门禁校验地图数值均为 `0` 或有效 `num` |
|
||||
| T-09-19-04 | Tampering | 生成 PNG 尺寸 / 数量偏离 D-72/D-84/D-85 方案或损坏(编造像素);48px 条目误用 2 帧 / 64×48 口径 | medium | mitigate | Task 3 `TEXTURE GATE` 从**来源 PNG 块数与列数**逐图推导期望 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),对**全部**产出图(含 `enemy*`/`npc*`)校验非隔行 8 位且尺寸相符;`npc` 图数断言 = **7**;`.list.jsonc` 断言 `rowAnimate` 条目 `width`/`height` = 每帧尺寸(48px 条目 = **32/48**、帧数 = **4**;32px 条目 = 32/32、帧数 = 2);`COPY GATE` 校验 autotile/tileset 逐字节复制 |
|
||||
| T-09-19-05 | Tampering | 贴图列表漏项:某有贴图图块未被任何 `map`/autotile `identifier` 覆盖 | medium | mitigate | Task 3 `TEXTURE GATE` 集合覆盖断言:`map ∪ autotile identifier ⊇` 所有 `type ∈ {2,3,4,5,6,7}` 的图块 `num` |
|
||||
| T-09-19-06 | Tampering | 地图结构非法(图层长度非 `width×height` 倍数、含越界 `num`、顶层混入 §8.1/§8.2 键) | medium | mitigate | Task 4 `MAP GATE` 校验顶层键集恰 5 键、每层 `length % width === 0` 且数值 ∈ {0} ∪ `tile` nums |
|
||||
| T-09-19-07 | Elevation of Privilege | 未经批准即执行 / 使用未报备命名 / 新增依赖 | medium | mitigate | Task 0 `blocking-human`(回复「可以执行」,OI-3 已由 D-85 裁定);命名候选入 `open_items`;Task 3 `DEP GATE` 禁依赖新增 |
|
||||
| T-09-19-08 | Elevation of Privilege | 48px 分组口径被偏离 D-85,或误用 D-85 禁止的「统一帧尺寸 + 透明留白」;仍沿用 2 帧 / 64×48 的误读 | medium | mitigate | OI-3 已由 **D-85** 裁定 = 按尺寸与帧数分组;Task 1 更正台账 48px 事实为 4 帧 / 单元格 32×48 并断言;Task 3 明确「按尺寸与帧数分组、不放一块、不留白、不重采样」,`TEXTURE GATE` 断言 `npc` 文件数 = 7 与各图尺寸/每帧尺寸/帧数;prohibitions 明列禁止留白混图 |
|
||||
| T-09-19-09 | Tampering | 破坏加载:`// --- SYSTEM PREFIX END --- //` 之后写入注释 / 尾逗号(`JSON.parse` 失败) | medium | mitigate | 各门禁均以 `split(marker)` 后 `JSON.parse` 校验;prohibitions 明列严格 JSON |
|
||||
| T-09-19-10 | Information Disclosure | 改动既有 `// Type:` 头部注释 / 既有注释(AGENTS.md 禁改既有注释) | low | mitigate | Task 1 保留头部门禁;prohibitions 明列不改 `// Type:` 前缀与既有注释 |
|
||||
| T-09-19-11 | Repudiation | 未跑门禁即收口 / 未记录 TWO PASS 结果与 `open_items` | low | mitigate | Task 1/2/3 各有自动化门禁 + `09-19-SUMMARY.md` 记录范围、`text` 处置、D-85 处置、D-84 布局口径与 `open_items` |
|
||||
| T-09-19-12 | Tampering | 门禁硬编码 4 行尺寸(64×128 / 128×192)导致裁剪末图被误判为缺失/失败 | medium | mitigate | Task 2 `TEXTURE GATE` 从来源块数**逐图推导**行数与尺寸(不硬编码 4 行);prohibitions 明列「贴图尺寸不硬编码(D-84 / D-85)」 |
|
||||
| T-09-19-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本增量**不新增依赖**;Task 2 `DEP GATE` 阻断任何 PNG 依赖写入 `package.json`;如需新增依赖须回 Task 0 报备获批 |
|
||||
| T-09-19-10 | Information Disclosure | 改动既有 `// Type:` 头部注释 / 既有注释(AGENTS.md 禁改既有注释) | low | mitigate | Task 2 保留头部门禁;prohibitions 明列不改 `// Type:` 前缀与既有注释 |
|
||||
| T-09-19-11 | Repudiation | 未跑门禁即收口 / 未记录 TWO PASS 结果与 `open_items` | low | mitigate | Task 1/2/3/4 各有自动化门禁 + `09-19-SUMMARY.md` 记录范围、台账更正、`text` 处置、D-85 处置、D-84 布局口径与 `open_items` |
|
||||
| T-09-19-12 | Tampering | 门禁硬编码 4 行尺寸(64×128 / 128×192)导致裁剪末图被误判为缺失/失败 | medium | mitigate | Task 3 `TEXTURE GATE` 从来源块数**逐图推导**行数与尺寸(不硬编码 4 行);prohibitions 明列「贴图尺寸不硬编码(D-84 / D-85)」 |
|
||||
| T-09-19-13 | Tampering | 台账更正越界——改动字段分类判定、§8.1/§8.2 分区、条目增删,或改动 `09-DATA-MIGRATION-AUDIT.md` 之外的既有规划文件 | medium | mitigate | Task 1 action 逐条枚举仅可更正的位置(§2.1/§2.4/§2.5 + §0/§5.2/§8.3/§9 同步);prohibitions 明列「`.planning/**` 边界(Task 1 唯一例外)」;Task 4 `SCOPE GATE` 允许台账更正但不允许 `ROADMAP.md`/`STATE.md`/源码越界 |
|
||||
| T-09-19-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本增量**不新增依赖**;Task 3 `DEP GATE` 阻断任何 PNG 依赖写入 `package.json`;如需新增依赖须回 Task 0 报备获批 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **Task 0**:预执行状态(台账 + 类型/地图管理器/切分器/加载器/源数据 + `node zlib` 就位;unmapped/summary 不存在);人工关卡(用户回复「可以执行」;OI-3 已由 D-85 裁定,无需再给取值)。
|
||||
- **Task 1**:三份实体 jsonc 分隔标记后严格 JSON;tile 235 且 `id`/`num` 唯一、`type` ∈ {2,3,4,5,6,7}、键集恰 6 键;enemy 81 且键集恰 3 键(`num`/`attribute`/`special`)、`num` ∈ `Enemy` 图块、`attribute`/`special` 形状正确;item 62 且键集恰 5 键(`num`/`category`/`name`/`text`/`hideInToolbox`)、`text` 为字符串(旧缺 `text` 的 13 条写 `""`)、`num` ∈ `Item` 图块;头部 `// Type:` 保留。
|
||||
- **Task 2**:**43 张**拆分图(`terrains0..2`/`terrainsAnim0..7`/`items0..3`/`enemy0..20`/`npc0..6`)+ 复制素材存在、PNG `IHDR` 合法(8 位非隔行)、**尺寸按 D-84 从来源块数逐图推导**(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`;含全部 `enemy*`/`npc*`;末图可裁剪为 128×32 / 64×32 / 64×64 / 128×96 等),`npc` 图数 = **7** 且按 D-85 尺寸分组断言(`npc0..2` 64×128、`npc3` 64×64、`npc4`/`npc5` 128×192、`npc6` 128×96);`texture/.list.jsonc` 解析成功、`source` 存在、`map`/`identifier` `num` 均在 `tile.jsonc` 且 **覆盖全部有贴图图块 `num`**;autotile/tileset 逐字节复制;脚本零依赖、`package.json` 未变。
|
||||
- **Task 3**:5 楼层齐备、顶层键集恰 5 键(无 §8.1/§8.2 键)、每层长度 = `width×height`、数值 ∈ {0} ∪ `tile` nums;`maps/.list.jsonc` = 5 id;unmapped 文档仅含 §8.1;SUMMARY 存在;`SCOPE GATE` 无越界改动。
|
||||
- **一致性**:五处 `id`↔`num` 全局一致(D-79);各实体/地图键集精确(§8.1/§8.2 键均不落数据文件);maps 5 楼层;unmapped 文档只含 §8.1;D-85 处置与 `npc` 图数 / 总数自洽(7 / 43);贴图尺寸与 D-84 实际行数自洽(不硬编码 4 行)。
|
||||
- **Task 0**:预执行状态(台账 + 类型/地图管理器/切分器/加载器/源数据 + `src/types/declaration/source.d.ts` + `node zlib` 就位;unmapped/summary 不存在);人工关卡(用户回复「可以执行」;OI-3 已由 D-85 裁定,无需再给取值)。
|
||||
- **Task 1**:台账更正断言——§2.1 的 `enemy48.png`/`npc48.png` 行为 **4 帧 / 单元格 32×48 / 行数 8、10**(旧「2 列 × 8 行 / 2 列 × 10 行」消失);§2.4 含 `ceil(73/4)+ceil(8/4)`(enemy 19+2=21)与 `ceil(14/4)+ceil(10/4)`(npc 4+3=7)推导;`SPLIT-COUNT` 由 `42` 变为 `43`;§2.5 `npc0..6`(旧 `npc0..npc5` 区间消失);D-84/D-85 引用就位。
|
||||
- **Task 2**:三份实体 jsonc 分隔标记后严格 JSON;tile 235 且 `id`/`num` 唯一、`type` ∈ {2,3,4,5,6,7}、键集恰 6 键;enemy 81 且键集恰 3 键(`num`/`attribute`/`special`)、`num` ∈ `Enemy` 图块、`attribute`/`special` 形状正确;item 62 且键集恰 5 键(`num`/`category`/`name`/`text`/`hideInToolbox`)、`text` 为字符串(旧缺 `text` 的 13 条写 `""`)、`num` ∈ `Item` 图块;头部 `// Type:` 保留。
|
||||
- **Task 3**:**43 张**拆分图(`terrains0..2`/`terrainsAnim0..7`/`items0..3`/`enemy0..20`/`npc0..6`)+ 复制素材存在、PNG `IHDR` 合法(8 位非隔行)、**尺寸按 D-84 从来源块数逐图推导**(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`;含全部 `enemy*`/`npc*`;末图可裁剪为 128×32 / 64×32 / 64×64 / 128×96 等),`npc` 图数 = **7**,且 `.list.jsonc` 的 `rowAnimate` 条目 `width`/`height`/帧数按 D-85 断言(48px 条目 = **32/48** / 帧数 4;32px 条目 = 32/32 / 帧数 2);`texture/.list.jsonc` 解析成功、`source` 存在、`map`/`identifier` `num` 均在 `tile.jsonc` 且 **覆盖全部有贴图图块 `num`**;autotile/tileset 逐字节复制;脚本零依赖、`package.json` 未变。
|
||||
- **Task 4**:5 楼层齐备、顶层键集恰 5 键(无 §8.1/§8.2 键)、每层长度 = `width×height`、数值 ∈ {0} ∪ `tile` nums;`maps/.list.jsonc` = 5 id;unmapped 文档仅含 §8.1;SUMMARY 存在;`SCOPE GATE` 无越界改动(台账更正属白名单)。
|
||||
- **一致性**:五处 `id`↔`num` 全局一致(D-79);各实体/地图键集精确(§8.1/§8.2 键均不落数据文件);maps 5 楼层;unmapped 文档只含 §8.1;台账 48px 事实与拆分计数自洽(4 帧 / 单元格 32×48;enemy 21 / npc 7 / `SPLIT-COUNT` 43 / `npc0..6`=7);D-85 处置与 `npc` 图数 / 总数自洽(7 / 43);贴图尺寸与 D-84 实际行数自洽(不硬编码 4 行);`.list.jsonc` 每帧尺寸与帧数自洽(48px 条目 32/48 / 4 帧;32px 条目 32/32 / 2 帧)。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
|
||||
- **人工复核**:结论写入 `09-19-SUMMARY.md`(含范围与映射口径、`num` 分配、`text` 处置、素材产出(含每图实际行数与裁剪末图尺寸、43 张总量)与 D-85 处置、D-84 布局口径、地图口径、门禁结果、`open_items`)。
|
||||
- **人工复核**:结论写入 `09-19-SUMMARY.md`(含范围与映射口径、台账更正结果、`num` 分配、`text` 处置、素材产出(含每图实际行数与裁剪末图尺寸、43 张总量)与 D-85 处置、D-84 布局口径、地图口径、门禁结果、`open_items`)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- **台账更正(Task 1,D-84 / D-85):** `09-DATA-MIGRATION-AUDIT.md` 的 48px 事实更正为 **4 帧 / 单元格 32×48 / 行数 8、10**;§2.4 D/E 按帧数与尺寸拆分(enemy 19+2 = **21**、npc 4+3 = **7**)、`SPLIT-COUNT` = **43**;§2.5 `npc0..6`(7 图,`enemy0..20` 保持 21);§0/§2/§5.2/§8.3/§9 同步 D-84/D-85 并标注 D-81 的 npc 计数被取代;未改字段分类判定与 §8 分区。
|
||||
- **实体(D-76):** `tile.jsonc` 235 / `enemy.jsonc` 81 / `item.jsonc` 62 条,基础字段按「映射候选(可确定)」全量、按 `num` 策略 A 分配;旧缺 `text` 的 13 条写 `""`;§8.2/§8.1 字段均未落入数据文件(各实体精确键集断言)。
|
||||
- **素材(D-72/D-81/D-84/D-85):** **43 张**拆分图(`npc` 按尺寸分组 `npc0..6`=7:`npc0..2` 64×128 + `npc3` 64×64 + `npc4`/`npc5` 128×192 + `npc6` 128×96)+ 4 autotile + 1 tileset 落 `src/content/texture/`;`animates` 并入 `terrains`;零依赖生成;**每张 PNG 尺寸按 D-84 从来源块数逐图推导(列数固定、行数 ≤4 且裁剪),门禁按实际行数断言(不硬编码 4 行尺寸)**;`npc` 图数 = 7 且按 D-85 尺寸分组经门禁断言;`texture/.list.jsonc` 用 D-80 已批准 schema 且与 `tile.jsonc` `num` 一致、覆盖全部有贴图图块。
|
||||
- **素材(D-72/D-81/D-84/D-85):** **43 张**拆分图(`npc` 按尺寸与帧数分组 `npc0..6`=7:`npc0..2` 64×128 + `npc3` 64×64 + `npc4`/`npc5` 128×192 + `npc6` 128×96)+ 4 autotile + 1 tileset 落 `src/content/texture/`;`animates` 并入 `terrains`;零依赖生成;**每张 PNG 尺寸按 D-84 从来源块数逐图推导(列数固定、行数 ≤4 且裁剪),门禁按实际行数断言(不硬编码 4 行尺寸)**;`npc` 图数 = 7,且 `.list.jsonc` 每帧尺寸与帧数按 D-85 断言(48px 条目 = 32/48、4 帧);`texture/.list.jsonc` 用 D-80 已批准 schema 且与 `tile.jsonc` `num` 一致、覆盖全部有贴图图块。
|
||||
- **地图(D-82):** 5 楼层全部写入;顶层键集恰 5 键;每层展平数组长度 = `width×height`;旧 `num` 重映射为新 `num`。
|
||||
- **unmapped(D-78):** 文档仅含 §8.1;§8.2 未落入文档且未移植。
|
||||
- **边界(D-70/D-83):** 未实现消费接线;未改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/既有台账与计划;未新增依赖。
|
||||
- **决策(D-81 / D-84 / D-85):** 48px 合并由 **D-85** 裁定 = 按尺寸分组(`npc0..6`=7 图 / `SPLIT-COUNT` 43,取代 D-81 的 6/42;enemy 不受影响仍 `enemy0..20`=21);贴图布局按 D-84 逐图裁剪、门禁按实际行数断言(§2 严格 4 行读法已由 D-84 取代;`npc0..5` 计数已由 D-85 取代)。
|
||||
- **门禁:** 全部静态门禁(结构 / 标记 / 精确键集 / D-84 逐图尺寸 / D-85 npc 分组 / 覆盖 / 一致性 / 作用域)全绿;产出经 Task 0 获批。
|
||||
- **边界(D-70/D-83):** 未实现消费接线;未改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`;既有规划文件仅 `09-DATA-MIGRATION-AUDIT.md` 被更正(且仅限 48px 事实);未新增依赖。
|
||||
- **决策(D-81 / D-84 / D-85):** 48px 合并由 **D-85** 裁定 = 按尺寸与帧数分组(`npc0..6`=7 图 / `SPLIT-COUNT` 43,取代 D-81 的 6/42;enemy 不受影响仍 `enemy0..20`=21);贴图布局按 D-84 逐图裁剪、门禁按实际行数断言(台账 §2 严格 4 行读法已由 D-84 更新;`npc0..5` 计数已由 D-85 取代)。
|
||||
- **门禁:** 全部静态门禁(台账更正 / 结构 / 标记 / 精确键集 / D-84 逐图尺寸 / D-85 npc 分组与每帧尺寸/帧数 / 覆盖 / 一致性 / 作用域)全绿;产出经 Task 0 获批。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
@ -452,18 +517,19 @@ Create `.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md` when done
|
||||
|
||||
| 符号 / 路径 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md`(更正) | 文档 | 48px 事实(4 帧 / 单元格 32×48 / 行数 8、10)与拆分计数(D/E 按帧数与尺寸分组、`SPLIT-COUNT`=43、`npc0..6`=7);D-84/D-85 同步;D-81 的 npc 计数标注被取代 |
|
||||
| `src/content/tile.jsonc`(替换样例) | 数据 | 235 条图块(`num` 策略 A、`id`、`type`、`pass`;键集恰 6 键) |
|
||||
| `src/content/enemy.jsonc`(替换样例) | 数据 | 81 条怪物(`num` 对应图块、`attribute`、`special`;键集恰 3 键) |
|
||||
| `src/content/item.jsonc`(替换样例) | 数据 | 62 条道具(`num` 对应图块、`category`、`name`、`text`(缺则 `""`)、`hideInToolbox`;键集恰 5 键) |
|
||||
| `src/content/maps/*.jsonc`(新增 4 + 替换 MT0) | 数据 | 5 楼层 `IMapRawData`(顶层键集恰 5 键) |
|
||||
| `src/content/maps/.list.jsonc`(替换) | 数据 | 5 楼层 id 列表 |
|
||||
| `src/content/texture/*.png`(新增) | 素材 | **43 张**拆分图(`npc` 按尺寸分组 `npc0..6`=7)+ 4 autotile + 1 tileset;**列数固定、行数 ≤4 且按每图实际裁剪(末图如 128×32 / 64×32 / 64×64 / 128×96)** |
|
||||
| `src/content/texture/.list.jsonc`(替换) | 数据 | D-80 已批准 `ITextureListConfig` |
|
||||
| `src/content/texture/*.png`(新增) | 素材 | **43 张**拆分图(`npc` 按尺寸与帧数分组 `npc0..6`=7)+ 4 autotile + 1 tileset;**列数固定、行数 ≤4 且按每图实际裁剪(末图如 128×32 / 64×32 / 64×64 / 128×96)** |
|
||||
| `src/content/texture/.list.jsonc`(替换) | 数据 | D-80 已批准 `ITextureListConfig`;`rowAnimate` 条目 `width`/`height` = 每帧尺寸(48px 条目 = 32/48) |
|
||||
| `script/migrateProjectTextures.ts`(新增,待批准) | 脚本 | 一次性零依赖 PNG 生成(小驼峰) |
|
||||
| `.planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-UNMAPPED.md`(新增,待批准) | 文档 | 仅 §8.1 无目标字段说明 |
|
||||
| `.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md`(新增) | 摘要 | 执行结果与 `open_items` |
|
||||
|
||||
**命名说明(AGENTS.md / D-73 / D-04):** 本计划不引入未获批的源码命名;`texture/.list.jsonc` 类型/字段名用 D-80 已批准项;一次性脚本路径(OI-2,提议小驼峰 `migrateProjectTextures.ts`)、unmapped 文档路径/命名(OI-1)、`source` 路径形态(OI-5)、地图图层键(OI-4)**均入 `open_items`,由 Task 0 报备待批准**。(48px 处理 OI-3 已由 **D-85** 裁定 = 按尺寸分组,不再是开放项。)
|
||||
**命名说明(AGENTS.md / D-73 / D-04):** 本计划不引入未获批的源码命名;`texture/.list.jsonc` 类型/字段名用 D-80 已批准项;一次性脚本路径(OI-2,提议小驼峰 `migrateProjectTextures.ts`)、unmapped 文档路径/命名(OI-1)、`source` 路径形态(OI-5)、地图图层键(OI-4)**均入 `open_items`,由 Task 0 报备待批准**。(48px 处理 OI-3 已由 **D-85** 裁定 = 按尺寸与帧数分组,不再是开放项。)
|
||||
|
||||
## Source Coverage Audit
|
||||
|
||||
@ -471,19 +537,20 @@ Create `.planning/phases/09-minimum-browser-runnable/09-19-SUMMARY.md` when done
|
||||
|------|----|------|
|
||||
| GOAL | Phase 9(ROADMAP)「在已完成的数据端之上完成渲染端框架,产出可在浏览器中运行并验证的最小版本」 | COVERED(增量·任务 9 子切片:基础数据移植,为可运行版本提供真实数据) |
|
||||
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
|
||||
| RESEARCH | 无 RESEARCH.md(本阶段为增量派发,无独立研究产物) | N/A(PNG 可行性、48px 按尺寸分组可行性与 D-84 逐图裁剪可行性已在本规划直接核实并记入 assumptions) |
|
||||
| CONTEXT | D-76(本次规划单元 = 基础移植;§8.1 不移植) | COVERED(Task 1/3 + prohibitions + 精确键集门禁) |
|
||||
| CONTEXT | D-77(产出形态:tile/enemy/item/maps/texture + 六类基础字段全量) | COVERED(Task 1/2/3) |
|
||||
| CONTEXT | D-78(unmapped 文档仅 §8.1;§8.2 不移植不入文档) | COVERED(Task 3 + `MAP GATE` token 校验) |
|
||||
| CONTEXT | D-79(`num` 策略 A,`id`↔`num` 一致) | COVERED(Task 1 (0) + 跨文件一致性门禁) |
|
||||
| CONTEXT | D-80(`texture/.list.jsonc` 类型/字段命名批准) | COVERED(Task 2 schema) |
|
||||
| CONTEXT | D-81(素材落点与命名 `npc*`/`enemy0..20`;`SPLIT-COUNT`)—— **npc 计数部分由 D-85 取代** | COVERED(Task 2:`enemy0..20`=21 不变;`npc` 计数按 D-85 改为 7 图) |
|
||||
| CONTEXT | D-82(maps 全部 5 楼层) | COVERED(Task 3) |
|
||||
| CONTEXT | D-83(消费接线不在本增量;`animates` 并入 `terrains`;events/trigger 不移植) | COVERED(Task 2 B 组 + Task 3 + prohibitions) |
|
||||
| CONTEXT | **D-84(贴图布局放宽:列数固定、行数 ≤4 且按需裁剪;门禁按实际行数断言、不硬编码 4 行尺寸)** | COVERED(Task 0 ⑥ + Task 2 布局总则与逐图行数推导 + `TEXTURE GATE` 逐图尺寸断言 + prohibitions「贴图尺寸不硬编码」+ 威胁寄存器 T-09-19-12) |
|
||||
| CONTEXT | **D-85(OI-3 裁定 = A · 按尺寸分组;取代 D-81 的 npc 计数;不同尺寸必须不同图、不留白不重采样)** | COVERED(Task 0 ⑦ 已裁决项汇报 + Task 2 E 组按尺寸分组(`npc0..6`=7)+ `TEXTURE GATE` 断言 `K=7` 与各图尺寸 + `must_haves`/success_criteria/威胁寄存器 T-09-19-08 + prohibitions「不同尺寸必须不同图」) |
|
||||
| CONTEXT | D-70 / D-73 / D-75(落点与接线边界 / 命名先报备 / 素材由脚本生成) | COVERED(Task 0 + `open_items` + Task 2 脚本) |
|
||||
| CONTEXT | D-01 / D-03 / D-04 / D-06(增量派发 / 执行前获批 / 命名获批 / 不扩范围) | COVERED(Task 0 + prohibitions) |
|
||||
| 缺口 | 无(六类基础字段已逐项落到 Task 1/2/3 与门禁;OI-3 已由 D-85 裁定并按尺寸分组合入门禁;D-84 逐图行数/尺寸推导已并入 Task 2 与门禁;旧缺 `text` 的 13 条已明确 `""` 处置) | CLOSED |
|
||||
| RESEARCH | 无 RESEARCH.md(本阶段为增量派发,无独立研究产物) | N/A(PNG 可行性、48px **按尺寸与帧数分组**(4 帧 / 单元格 32×48)可行性与 D-84 逐图裁剪可行性已在本规划直接核实并记入 assumptions) |
|
||||
| CONTEXT | D-76(本次规划单元 = 基础移植;§8.1 不移植) | COVERED(Task 2/4 + prohibitions + 精确键集门禁) |
|
||||
| CONTEXT | D-77(产出形态:tile/enemy/item/maps/texture + 六类基础字段全量) | COVERED(Task 2/3/4) |
|
||||
| CONTEXT | D-78(unmapped 文档仅 §8.1;§8.2 不移植不入文档) | COVERED(Task 4 + `MAP GATE` token 校验) |
|
||||
| CONTEXT | D-79(`num` 策略 A,`id`↔`num` 一致) | COVERED(Task 2 (0) + 跨文件一致性门禁) |
|
||||
| CONTEXT | D-80(`texture/.list.jsonc` 类型/字段命名批准) | COVERED(Task 3 schema) |
|
||||
| CONTEXT | D-81(素材落点与命名 `npc*`/`enemy0..20`;`SPLIT-COUNT`)—— **npc 计数部分由 D-85 取代** | COVERED(Task 3:`enemy0..20`=21 不变;`npc` 计数按 D-85 改为 7 图;Task 1 在台账标注取代) |
|
||||
| CONTEXT | D-82(maps 全部 5 楼层) | COVERED(Task 4) |
|
||||
| CONTEXT | D-83(消费接线不在本增量;`animates` 并入 `terrains`;events/trigger 不移植) | COVERED(Task 3 B 组 + Task 4 + prohibitions) |
|
||||
| CONTEXT | **D-84(贴图布局放宽:列数固定、行数 ≤4 且按需裁剪;门禁按实际行数断言、不硬编码 4 行尺寸)** | COVERED(Task 0 ⑥ + Task 3 布局总则与逐图行数推导 + `TEXTURE GATE` 逐图尺寸断言 + prohibitions「贴图尺寸不硬编码」+ Task 1 台账同步 + 威胁寄存器 T-09-19-12) |
|
||||
| CONTEXT | **D-85(OI-3 裁定 = A · 按尺寸与帧数分组;取代 D-81 的 npc 计数;不同尺寸/帧数必须不同图、不留白不重采样)** | COVERED(Task 0 ⑦ 已裁决项汇报 + Task 1 台账更正(4 帧 / 单元格 32×48 / `SPLIT-COUNT` 43 / `npc0..6`=7)+ Task 3 E 组按尺寸与帧数分组 + `TEXTURE GATE` 断言 `K=7`、各图尺寸、每帧尺寸与帧数 + `must_haves`/success_criteria/威胁寄存器 T-09-19-08 + prohibitions「不同尺寸必须不同图」) |
|
||||
| CONTEXT | **用户更正(2026-10-13):台账 `09-DATA-MIGRATION-AUDIT.md` §2.1/§2.4 误将 128 宽 48px 素材当作「2 列 × 64px」(2 帧)** | COVERED(Task 1 更正 §2.1 为 4 帧 / 单元格 32×48 / 行数 8、10;§2.4 D/E 按帧数与尺寸拆分;§2.5 `npc0..6`;§9 计数 43;帧数权威 `FrameNumbers`(`source.d.ts`:81-91)作为 `read_first` 与门禁依据) |
|
||||
| CONTEXT | D-70 / D-73 / D-75(落点与接线边界 / 命名先报备 / 素材由脚本生成) | COVERED(Task 0 + `open_items` + Task 3 脚本) |
|
||||
| CONTEXT | D-01 / D-03 / D-04 / D-06(增量派发 / 执行前获批 / 命名获批 / 不扩范围) | COVERED(Task 0 + prohibitions;台账更正范围由用户更正明确限定) |
|
||||
| 缺口 | 无(六类基础字段已逐项落到 Task 2/3/4 与门禁;台账 48px 事实更正落到 Task 1 与门禁;OI-3 已由 D-85 裁定并按尺寸与帧数分组合入门禁;D-84 逐图行数/尺寸推导已并入 Task 3 与门禁;旧缺 `text` 的 13 条已明确 `""` 处置) | CLOSED |
|
||||
|
||||
> **范围外(非缺口):** animation 素材(D-71)、`images`/`sounds`/`bgms`/`fonts`、§8.2 待定项、消费接线(D-70/D-83)—— 依决策不属本增量。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user