From 1e451bb2bcb8f4f1622305053aff2d4d620bfd32 Mon Sep 17 00:00:00 2001
From: unanmed <1319491857@qq.com>
Date: Wed, 14 Oct 2026 11:14:03 +0800
Subject: [PATCH] docs(09-19): revise plan (rev5) - OI-3 decided D-85 npc
by-size grouping (npc0..6=7, SPLIT-COUNT 43)
---
.../09-minimum-browser-runnable/09-19-PLAN.md | 170 +++++++++---------
1 file changed, 81 insertions(+), 89 deletions(-)
diff --git a/.planning/phases/09-minimum-browser-runnable/09-19-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-19-PLAN.md
index b4b3a08..6d2d7f0 100644
--- a/.planning/phases/09-minimum-browser-runnable/09-19-PLAN.md
+++ b/.planning/phases/09-minimum-browser-runnable/09-19-PLAN.md
@@ -28,10 +28,10 @@ estimate:
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 素材合并(U-9 / U-17 / OI-3)的技术核实结论与默认处置(默认保持 D-81 的 `npc0..5`=6 图 / 总数 42 = 单元格统一到 64×48;备选 A 按尺寸分组=`npc0..6`=7 图 / 总数 43、C 只出 32px=`npc0..3`=4 图 / 总数 40)、地图图层键口径、产出文件清单、消费接线不在本增量、静态门禁方式与 `open_items`(OI-1..OI-7)**,然后必须**停下等待**用户回复;**若回复未明确给出 OI-3 取值,视为未通过(本计划不因触发方式或 `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 素材**按尺寸分组**的技术核实结论与**已裁定处置(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` 自动放行)**。"
- "**基础字段范围(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,默认保持 `SPLIT-COUNT: 42`):** `src/content/texture/` 生成 42 张 `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`(2 帧列;默认 OI-3=B 单元格统一 64×48、来源 24 块 → 6×4)→ `npc0..5` 128×192(4 行)。**OI-3 备选(同样按 D-84 逐图裁剪):A 按尺寸分组 `npc0..6`(32px 组 14 块 → `npc0..2` 64×128 + `npc3` **64×64**;48px 组 10 块 → `npc4`/`npc5` 128×192 + `npc6` **128×96**;总数 43);C 只出 32px `npc0..3`(14 块 → `npc0..2` 64×128 + `npc3` **64×64**;总数 40,48px 变体延后)。** + 逐字节复制 `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`。**OI-3 以用户显式裁定为准并在 SUMMARY 记录(默认 B:`npc0..5`=6 图、总数 42)。**"
+ - "**贴图素材(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-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-70 / D-83):** 不把配置写入 store、不把 list 写入 `TextureManager`、不改 `packages*/` 与 `src/`(除 `src/content/**`)、不改 `public/project/**`;沿用 D-72③ 与「旧 `events`/`trigger` 等无对应目标字段者不移植」。"
@@ -57,25 +57,25 @@ must_haves:
- path: src/content/texture/.list.jsonc
provides: "D-80 已批准的 `ITextureListConfig`,覆盖拆分图 + 4 autotile + 1 tileset"
- path: src/content/texture/*.png
- provides: "42 张 D-72/D-84 拆分图(`terrains*`/`terrainsAnim*`/`items*`/`enemy0..20`/`npc0..5`;列数固定、行数 ≤4 且按每图实际裁剪——末图可为 128×32 / 64×32 等;OI-3=A 时 43 张 `npc0..6`;OI-3=C 时 40 张 `npc0..3`)+ 复制的 `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` 帧 32px、`npc4..6` 帧 64×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` 分配、素材产出、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` 的整数)—— OI-3 单元格统一与 D-84 逐图裁剪的技术依据(列数固定、行数 ≤4)"
+ - "`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/{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;OI-3=A/C 下 `npc3` = 64×64、A 下 `npc6` = 128×96)。拆分图**总数不变**(`SPLIT-COUNT: 42` 默认 B):A=3、B=8、C=4、D=21、E=6;本盘点的 §2「严格 4 行」描述**由 D-84 取代**(仅本迁移受影响,盘点文档不改)。"
- - "**48px 素材合并(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`**(`TextureRowSplitter` 按单一 `height` 切行),故 32×32 与 64×48 帧**无法共存于同一张 `rowAnimate` 图**。**按尺寸分组**会使 `npc` 由 D-81 的 6 张(`npc0..5`,`SPLIT-COUNT: 42`)变为 7 张(`npc0..6`,总数 43)。**默认处置 = 保持 D-81(6 张 / 42)**:将 `npc` 全部帧单元统一为 64×48(14 个 NPC 块(各 2 帧 32×32)以**透明留白**嵌入 64×48 单元,不做重采样),24 块 ÷ 4 块/图 = 6 张 `npc0..5.png`(24÷4=6 满图,均 4 行 → 均 128×192;此处置下 `npc` 每张恰为 4 行,故 128×192 成立)。该统一会改变小 NPC 的帧单元尺寸(其渲染端呈现影响不在本增量验证范围,D-83 接线延后);若用户不接受,须在 Task 0 明确改选 A(按尺寸分组、显式接受偏离 D-81)或 C(本增量只出 32px、`npc0..3`=4 图 / 总数 40)。`enemy` 无论按尺寸分组(19+2)或整组(81÷4 向上取整)均为 21 张,与 D-81 的 `enemy0..20` 一致。"
+ - "**贴图布局(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)** —— 留白会改变消费端读取的帧尺寸。"
- "**地图图层键语义待确认:** `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。"
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构断言」为准,不硬编码行号。"
@@ -84,8 +84,8 @@ must_haves:
- "**写入边界:** 只允许写本 `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`。"
- "**不移植边界:** §8.1 无目标字段**不得**出现在 `tile.jsonc`/`enemy.jsonc`/`item.jsonc`/`maps/*.jsonc` 中(只进 unmapped 文档);§8.2 待定字段**不得**移植、也**不得**进 unmapped 文档。各实体/地图条目以**精确键集断言**兜底(tile 恰 6 键 / enemy 恰 3 键 / item 恰 5 键 / map 顶层恰 5 键)。"
- - "**贴图尺寸不硬编码(D-84):** 贴图门禁必须按**每图实际行数(≤4)与裁剪后尺寸**断言(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`);**不得**假定每张图均为 4 行,**不得**硬编码 4 行尺寸(如 64×128 / 128×192)作为全体断言——仅当某图实际行数确为 4 时其 4 行尺寸才成立(如默认 OI-3=B 下 `npc0..5` 恰 4 行,128×192 成立;但 `terrains2`/`terrainsAnim7`/`enemy18` 必须按裁剪后的 1 行尺寸断言)。"
- - "**命名边界(AGENTS.md / D-73 / D-04):** 任何未批准的新命名(一次性脚本路径、unmapped 文档路径/命名、`source` 路径形态、地图图层键口径、48px 处理方式)**先入 `open_items`,由 Task 0 报备待批准**;D-80 已批准的类型/字段名可直接使用。"
+ - "**贴图尺寸不硬编码(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 帧放入同一张图,**不得**以「统一帧尺寸 + 透明留白」的方式混图。"
+ - "**命名边界(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` 零依赖方案。"
- "**不改既有注释 / jsDoc:** 不改 `src/content/**` 既有文件头 `// Type:` 注释行;所有生成文件头部 `// Type:` 前缀不得改动。"
@@ -96,21 +96,21 @@ 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 素材合并处理 = U-9 / U-17):** 实测 32×32 与 64×48 帧无法同图(`addRowAnimate` 单图单尺寸)。**默认 B(推荐)= 单元格统一到 64×48**:`npc0..5`(6 图,24 块 → 6×4 满图,每图 4 行 → 均 128×192),保持 D-81 的 `SPLIT-COUNT: 42`(14 个 NPC 块(各 2 帧 32×32)透明留白嵌入,不重采样;小 NPC 帧单元尺寸随之改变)。备选 **A = 按尺寸分组**:`npc0..6`(7 图;32px 组 14 块 4 图:`npc0..2` 4 行 64×128 + `npc3` 2 行 **64×64**;48px 组 10 块 3 图:`npc4`/`npc5` 4 行 128×192 + `npc6` 2 行 **128×96**;总数 43,**显式接受偏离 D-81**);备选 **C = 本增量只出 32px**(`npc0..3`,4 图:`npc0..2` 4 行 64×128 + `npc3` 2 行 **64×64**,总数 40,`npc48` 的 10 个 64×48 块延后)。**Task 0 须明确裁定 A/B/C(默认 B);各图行数与尺寸一律按 D-84 逐图裁剪。**"
+ - "**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-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「修改方式须预先声明」(内联 / 派发子代理)。"
---
-Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**:把 `public/project` 的**基础字段与数据**移植到 `src/content`(jsonc 数据 + PNG 素材),替换现有样例。
+Phase 9 · 任务 9 · 第 19 个计划(**revision 5**)—— **旧引擎数据基础移植**:把 `public/project` 的**基础字段与数据**移植到 `src/content`(jsonc 数据 + PNG 素材),替换现有样例。
**要解决的问题:** 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)。
-**大致解决方案(本计划 = 09-19):**
-- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含 PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按需裁剪)、`num` 策略 A 方案、48px 合并(OI-3)技术核实与默认处置、地图图层键等待裁定项),停下等待用户回复**并明确 OI-3 取值**。
+**大致解决方案(本计划 = 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** 拆分 42 张图(列数固定、行数 ≤4 且按每图实际块数裁剪;默认 OI-3=B 保持 D-81)并复制 autotile/tileset,据 Task 1 的新 `num` 写 `texture/.list.jsonc`。
+- **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`。
**为何分三个执行任务:** 数据依赖为「先定 `num`(Task 1)→ 贴图 list 复用该 `num`(Task 2)→ 地图把旧 `num` 重映射为新 `num`(Task 3)」;三类互不冲突、按依赖顺序最稳,且避免同一文件反复改写。
@@ -121,9 +121,9 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
**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);拆分图**总数不变**(`SPLIT-COUNT` 默认仍为 42)。§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 取代**(仅本迁移受影响,盘点文档不改)。
-**48px 合并可行性(本规划已核实):** `addRowAnimate` 要求单图内所有块共用同一帧尺寸,32×32 与 64×48 不能同图;默认按 OI-3=B 统一到 64×48 以保持 D-81 的 6 图(24 块 → 6×4 满图,均 4 行 → 均 128×192;备选 A=7 图 / 总数 43;备选 C=4 图 / 总数 40;A/C 下末图按 D-84 裁剪)。
+**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)** —— 留白会改变消费端读取的帧尺寸。
**API coverage:** `detected:false`。**`No external API integration: 本计划为本地数据/素材移植,不涉及任何外部 API。`**
@@ -147,24 +147,24 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
- Task 0: 预执行汇报关卡 —— 汇报「任务 9 · 旧引擎数据基础移植」的要做什么 / 为什么 / 大致怎么做 / 影响范围,等待用户回复并明确 OI-3 取值
+ Task 0: 预执行汇报关卡 —— 汇报「任务 9 · 旧引擎数据基础移植」的要做什么 / 为什么 / 大致怎么做 / 影响范围,等待用户回复「可以执行」
`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` 可用。
(只读汇报,不修改任何文件)
- - .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)
- - .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§1.2 / §1.5.1 / §2 / §3.1 / §4.2 / §5.1 / §6.2 / §7.1 / §8.1 / §8.2 / §9)
- - .planning/phases/09-minimum-browser-runnable/09-19-PLAN.md(本计划全文,含映射口径、OI-1..OI-7)
+ - .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-19-PLAN.md(本计划全文,含映射口径、OI-1/2/4/5/6/7 与**已裁定的 D-85**)
- .planning/phases/09-minimum-browser-runnable/09-18-SUMMARY.md(上游摘要,只读参考)
- 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(命名规则含「代码文件名=小驼峰」、注释规范、模块原则)
- **停下并向用户汇报,等待用户回复(须含 OI-3 取值)后才继续;汇报本身只读。**
+ **停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** `src/content` 目前只有样例(tile 1 条 / enemy 1 条 / item 1 条 / maps 仅 MT0 / texture list 空),无法驱动真实数据。本计划据只读台账 `09-DATA-MIGRATION-AUDIT.md`,把 `public/project` 的**基础字段**移植为 `src/content` 的 jsonc 数据与 PNG 素材,替换样例。
- ② **范围(六类,D-71 / D-77):** 图块 tile(235)、怪物 enemy(81)、道具 items(62)、NPC 图块(并入 tile 的 `Npc` 段)、地图 maps(5 楼层)、贴图素材 textures(42 张拆分图 + 复制 4 autotile + 1 tileset;OI-3=A 时 43 张、C 时 40 张)。
+ ② **范围(六类,D-71 / D-77):** 图块 tile(235)、怪物 enemy(81)、道具 items(62)、NPC 图块(并入 tile 的 `Npc` 段)、地图 maps(5 楼层)、贴图素材 textures(**43 张拆分图** + 复制 4 autotile + 1 tileset)。
③ **基础字段判定(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,43 +175,33 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
⑤ **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`(默认 OI-3=B,24 块 → `npc0..5` 均 128×192)。**门禁按实际行数断言、不硬编码 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 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 取代**(盘点文档不改)。
- ⑦ **48px 素材合并(U-9 / U-17 / OI-3)技术核实结论与默认处置:** 实测 `npcs.png` 单元格 32×32(14 个 NPC 块,各 2 帧 32×32)、`npc48.png` 单元格 64×48(10 个 NPC 块,各 2 帧 64×48)(`enemys.png` 32×32、`enemy48.png` 64×48)。`addRowAnimate`(`manager.ts`:158-175,配 `TextureRowSplitter`)要求**单张图内所有块共用同一帧 `width`/`height`**,故 32×32 与 64×48 帧**不能同图**。**按尺寸分组**会使 `npc` 由 D-81 的 6 张变 7 张(总数 43)。**默认处置 = 保持 D-81(6 张 / 42,`npc0..5`)**:将 `npc` 全部帧单元统一为 64×48(14 个 NPC 块(各 2 帧 32×32)以**透明留白**嵌入 64×48 单元,不做重采样)→ 24 块 ÷ 4 = 6 张(24÷4 满图,均 4 行 → 均 128×192)。该统一会改变小 NPC 帧单元尺寸(渲染影响不在本增量验证范围,D-83 接线延后)。备选 A = 按尺寸分组(`npc0..6`=7 图、总数 43,显式接受偏离 D-81;`npc0..2` 64×128 + `npc3` 64×64 + `npc4`/`npc5` 128×192 + `npc6` 128×96);备选 C = 本增量只出 32px(`npc0..3`=4 图、总数 40,`npc0..2` 64×128 + `npc3` 64×64,`npc48` 的 10 个 64×48 块延后)。`enemy` 无论按尺寸分组(19+2)或整组(81÷4 向上取整)均为 21 张,与 D-81 `enemy0..20` 一致。
+ ⑦ **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)已被用户明确否决**(留白会改变消费端读取的帧尺寸),本节不作为备选。
⑧ **地图图层键:** `mapState.fromRaw` 要求每层为整张展平网格(`length = width × height`),与台账 §6.2 候选「层键即行索引」不符;默认「旧二维 `map` 单张展平图层、键 `0`、`layerAlias` = `{}`」,额外图层(`bgmap` 等)属 §8.2 U-16b 不移植(OI-4)。
- ⑨ **产出文件:** `src/content/tile.jsonc` / `enemy.jsonc` / `item.jsonc`(替换样例);`src/content/maps/{empty,MT0,sample0,sample1,sample2}.jsonc` + `maps/.list.jsonc`;`src/content/texture/*.png`(42 拆分图 + 5 复制素材;A=43、C=40);`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`。
+ ⑨ **产出文件:** `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`。
⑩ **不在本增量(D-70 / D-83):** 不实现消费接线(写 store / 写 `TextureManager`);不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`;不移植 §8.2 待定与 §8.1 无目标字段。
- ⑪ **验证方式:** 静态门禁——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 等,不硬编码 4 行尺寸)**,`npc` 图数 `K` 按 OI-3 取值断言 `K ∈ {4,6,7}` 且按实际行数校验各图尺寸(B:24 块满图均 128×192;A:`npc3` 64×64、`npc6` 128×96;C:`npc3` 64×64);maps 5 楼层齐备、**顶层键集恰 5 键**、每层数组长度 = `width`×`height`、数值均为 `0` 或有效 `num`;unmapped 文档存在且仅含 §8.1。**不设全仓类型 / 构建门禁。**
+ ⑪ **验证方式:** 静态门禁——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。**不设全仓类型 / 构建门禁。**
⑫ **修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理执行**(`gsd-executor`),非内联执行。
- ⑬ **开放项:** OI-1(unmapped 文档路径/命名)、OI-2(脚本路径/命名,已纠正为小驼峰 `migrateProjectTextures.ts`)、OI-3(48px 合并处理,默认 B)、OI-4(地图图层键/`layerAlias`)、OI-5(`source` 路径形态)、OI-6(autotile/tileset 参数)、OI-7(写操作方式声明)。
+ ⑬ **开放项:** 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 取值(A/B/C),仅回「可以执行」而未含 OI-3 结论视为未通过;** 若用户就 OI-1..OI-6 给出指定,按指定执行。
+ ⑭ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过(OI-3 已由 D-85 裁定,无需在回复中再给出取值)。** 若用户就 OI-1/2/4/5/6 给出指定,按指定执行。
- 是否按本计划(09-19)执行「任务 9 · 旧引擎数据基础移植」——以 `num` 策略 A 写入 tile/enemy/item jsonc、按 D-72/D-81/D-84 生成贴图素材与 `texture/.list.jsonc`、写入 5 楼层与 `maps/.list.jsonc`、产出 unmapped 说明文档;基础字段全量、§8.2 待定与 §8.1 无目标字段不落数据文件(各实体/地图以精确键集断言兜底);不实现消费接线;**并明确 48px 合并(OI-3)的处置(默认 B = 单元格统一到 64×48 以保持 D-81 `npc0..5`=6 图 / 总数 42)**
+ 是否按本计划(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),无需在本关卡再裁定。**
- AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期为**写操作**(新增/替换 `src/content/**` 数据与 PNG、新增脚本与两份文档)。OI-3 为**必须显式裁定**项:默认处置会改变小 NPC 帧单元尺寸,故不得由「可以执行」隐含放行。执行期工作树存在用户并发改动(`AGENTS.md` / `09-CONTEXT.md`,D-56),提交时须逐条核对暂存面。贴图尺寸按 D-84 逐图实际行数(≤4)断言,不硬编码 4 行尺寸。行号为规划日实测、可能漂移,门禁以结构断言为准。
+ 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 尺寸分组断言。行号为规划日实测、可能漂移,门禁以结构断言为准。
-
-
- 回复「可以执行」**并明确 OI-3 取值(B(默认推荐)/ A / C)**;仅回「可以执行」而未含 OI-3 结论视为未通过。如需调整 OI-1..OI-6 的其它处置,请在回复中一并指明;修订意见请按 action ⑭ 处理。
+ 回复「可以执行」即可(OI-3 已由 D-85 裁定 = 按尺寸分组,无需在回复中再给出取值)。如需调整 OI-1/2/4/5/6 的其它处置,请在回复中一并指明;修订意见请按 action ⑭ 处理。
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')"
non-zero exit, 或 stderr 含 "PRESTATE FAILED"(被移植来源或参考文件缺失,或产出文件已存在,或 Node `zlib` 不可用)
- 用户在对话中明确回复「可以执行」**且明确给出 OI-3 取值(A/B/C)**(人工核对回复原文;用户若指定 OI-1..OI-6 的非默认处置,须在汇报与后续任务中据其指定执行)
+ 用户在对话中明确回复「可以执行」(OI-3 已由 D-85 裁定,无需再给出 OI-3 取值;用户若指定 OI-1/2/4/5/6 的非默认处置,须在汇报与后续任务中据其指定执行)
- - 用户回复中明确包含「可以执行」,**且明确给出 OI-3 取值(A/B/C)**
- - 汇报内容涵盖:要解决的问题、六类范围、三类基础字段判定(映射候选可确定 / §8.2 待定 / §8.1 无目标字段)、`num` 策略 A 具体方案、PNG 零依赖可行性结论、贴图布局规则(D-84:列数固定、行数 ≤4 且按每图实际块数裁剪)、48px 合并(OI-3)技术核实结论与默认处置(B)、地图图层键口径、产出文件清单、消费接线不在本增量、验证方式(静态门禁,无全仓门禁)、OI-1..OI-7
- - 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;不改 `packages*/`、`src/`(除 `src/content/**`)、`public/project/**`、`ROADMAP.md`/`STATE.md`/既有计划与台账)**、**不新增依赖**、**§8.1/§8.2 不落数据文件(精确键集断言兜底)**、**贴图尺寸按 D-84 逐图实际行数断言、不硬编码 4 行尺寸**、**命名待批准项先入 `open_items`**
- - 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;**回复须含 OI-3 取值**)
+ - 用户回复中明确包含「可以执行」(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`**
+ - 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;回复「可以执行」即通过,OI-3 已裁定无需再给取值)
- 用户明确回复且给出 OI-3 取值;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
+ 用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
Task 1: 建立全局 `id`↔`num` 基准并写三类实体 jsonc —— `tile.jsonc`(235)/ `enemy.jsonc`(81)/ `item.jsonc`(62)
- Task 0 已闭环:用户明确回复「可以执行」且给出 OI-3 取值(若用户就 OI-1..OI-6 另有指定,按指定执行)。
+ Task 0 已闭环:用户明确回复「可以执行」(OI-3 已由 D-85 裁定;若用户就 OI-1/2/4/5/6 另有指定,按指定执行)。
src/content/tile.jsonc, src/content/enemy.jsonc, src/content/item.jsonc
- .planning/phases/09-minimum-browser-runnable/09-DATA-MIGRATION-AUDIT.md(§1.1 `cls`→`TileType`、§1.2 tile 字段分类、§1.3 `num`/`id` 全清单、§1.5.1 `id` 全清单、§3.1 enemy 字段、§3.2 enemy 全清单、§4.1 `cls`→`ItemCategory`、§4.2 item 字段、§4.4 item 全清单)
@@ -290,13 +280,13 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
- Task 2: 一次性零依赖 PNG 脚本 + 按 D-72/D-84 生成贴图素材(列数固定、行数 ≤4 且按每图实际裁剪;默认 OI-3=B 保持 D-81)+ 写 `texture/.list.jsonc`
- Task 1 已完成(`tile.jsonc` 235 条与全局 `id`↔`num` 基准就位,可被本任务复用);Task 0 已就 OI-3 明确裁定(默认 B)。
+ Task 2: 一次性零依赖 PNG 脚本 + 按 D-72/D-84/D-85 生成 43 张贴图素材(列数固定、行数 ≤4 且按每图实际裁剪;npc 按尺寸分组 `npc0..6`)+ 写 `texture/.list.jsonc`
+ Task 1 已完成(`tile.jsonc` 235 条与全局 `id`↔`num` 基准就位,可被本任务复用);Task 0 已闭环(OI-3 = D-85 按尺寸分组,无需再裁定)。
script/migrateProjectTextures.ts, src/content/texture/.list.jsonc, src/content/texture/*.png
- - .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.2 npc 合并说明、§7 / §7.1 schema 与类型)—— **注意:§2.4「单帧 4×4 / 多帧 帧数列×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**)
- - 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)`)—— OI-3 单元格统一与 D-84 逐图裁剪依据
+ - .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-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/content/client.jsonc:9(`textureListDir: texture/.list.jsonc`、`textureDir: texture`)、src/content/texture/.list.jsonc(现状 `{}`)—— 落点与路径形态参考
- src/content/tile.jsonc(Task 1 产出:新 `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)
@@ -313,26 +303,26 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
- 生成 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①):源 `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 帧列。**按 Task 0 的 OI-3 裁定生成(各图行数按 D-84 逐图裁剪):**
- - **默认 B(保持 D-81)** = 单元格统一到 64×48(14 个 NPC 块(各 2 帧 32×32)以透明留白嵌入 64×48 单元格,不重采样;与 10 个 NPC 块(各 2 帧 64×48)同布局)→ 24 块 ÷ 4 = 6 图 `npc0..5.png`(24÷4=6 满图,每图 4 行 → 均 128×192,`SPLIT-COUNT` 42)。
- - **A(若用户裁定)** = 按尺寸分组:32px 组 14 块 → 4 图(`npc0..2` 各 4 行 64×128、**`npc3` 2 块 → 2 行 → 64×64**)、48px 组 10 块 → 3 图(`npc4`/`npc5` 各 4 行 128×192、**`npc6` 2 块 → 2 行 → 128×96**),共 7 图 `npc0..6.png`(总数 43,偏离 D-81,须记 SUMMARY)。
- - **C(若用户裁定)** = 只出 32px:14 块 → 4 图 `npc0..3.png`(`npc0..2` 各 4 行 64×128、**`npc3` 2 块 → 2 行 → 64×64**),`npc48` 的 10 个 64×48 块延后(总数 40,npc 素材不完整,记 SUMMARY)。
+ - 生成 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**。
+ - 合共 **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": <帧高> }`(默认 B 下 npc 条目 `width`/`height` = 64/48;A 下 32px 条目 = 32/32、48px 条目 = 64/48;C 下 npc 条目 = 32/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。
- autotile `{ "type": "autotile", "source": "autotile*.png", "identifier": <20|151|152|153 对应新 num>, "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` 的条目集合与拆分图数。**
+ - `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`)。**
(3) **收口:** 不写 §8.2/§8.1 内容;不改 `client.jsonc`/`defaults/**`;不新增依赖、不改 `package.json`;脚本运行后 `src/content/texture/` 内文件与 `.list.jsonc` 互一致。
新增脚本 + 生成 `src/content/texture/**`;可删除生成物或 `git checkout` 回退;未触及任何既有源码与注释。
- 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;i4)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) + copies; list covers all textured nums')"
- non-zero exit, 或 stderr 含 "TEXTURE GATE FAILED"(拆分图缺失/非 PNG/隔行/位深≠8/尺寸不等于按 D-84 逐图推导的「列数×帧宽 × 行数×帧高」(含任一 `enemy*`/`npc*`;末图可按实际行数裁剪,如 128×32 / 64×32)/ 出现多余拆分图 / `npc` 文件数 `K` ∉ {4,6,7} / 尺寸与 OI-3 取值不符(B=24 块满图均 128×192;A=`npc0..2` 64×128+`npc3` 64×64+`npc4`/`npc5` 128×192+`npc6` 128×96;C=`npc0..2` 64×128+`npc3` 64×64)/ `.list.jsonc` 标记缺失或解析失败 / `textures` 非数组 / 条目 `type` 未知 / `source` 文件在磁盘不存在 / `map` 或 autotile `identifier` 的 `num` 不在 `tile.jsonc` / `autotileType` 非数值 / tileset `identifier.alias` 缺失 / **存在未被任何 `map` 或 autotile `identifier` 覆盖的有贴图图块 `num`**)
+ 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;i4)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')"
+ 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`**)
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)')"
non-zero exit, 或 stderr 含 "COPY GATE FAILED"(autotile/tileset 复制缺失,或未逐字节复制而重编码)
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')"
@@ -340,11 +330,11 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
- `script/migrateProjectTextures.ts` 存在(小驼峰,符合 dev.md),仅用 Node `zlib` + `fs`(零外部依赖),未改 `package.json`
- - `src/content/texture/` 含拆分图(默认 OI-3=B:42 张 = `terrains0..2` / `terrainsAnim0..7` / `items0..3` / `enemy0..20` / `npc0..5`;OI-3=A 时 43 张 `npc0..6`;OI-3=C 时 40 张 `npc0..3`)与 5 张逐字节复制素材;**每张 PNG**(含全部 `enemy*`/`npc*`)均为非隔行 8 位、且尺寸 = **按 D-84 逐图推导**的「`列数 × 帧宽` × `行数 × 帧高`」(`1 ≤ 行数 ≤ 4`,列数固定;末图可裁剪,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32);`npc` 图数 `K ∈ {4,6,7}` 且尺寸与 OI-3 取值自洽(B=24 块满图均 128×192;A=`npc3` 64×64、`npc6` 128×96;C=`npc3` 64×64)
- - `texture/.list.jsonc` 为 D-80 已批准 schema(`textures` 数组,条目 `type` ∈ {`grid`,`rowAnimate`,`autotile`,`tileset`});每个 `source` 文件存在;`map`/`identifier` 的 `num` 全部来自 `tile.jsonc`;**`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`;npc 条目 `width`/`height` 按尺寸分组(`npc0..3` = 32/32、`npc4..6` = 64/48);**`map ∪ autotile identifier` 覆盖每一处有贴图的图块 `num`**
- 分隔标记后为严格 JSON;头部注释保留;未写 §8.1/§8.2 内容
- 拆分图(默认 B=42;A=43 `npc0..6`;C=40 `npc0..3`)+ 5 张复制素材生成;**每张图尺寸按 D-84 逐图实际行数(≤4)经门禁断言**(不硬编码 4 行尺寸);`texture/.list.jsonc` 落盘且 `source`/`map`/`identifier` 与 `tile.jsonc` 一致且覆盖全部有贴图图块;`npc` 图数按 OI-3 取值经门禁断言;零依赖门禁全绿
+ 43 张拆分图(含按尺寸分组的 `npc0..6`=7)+ 5 张复制素材生成;**每张图尺寸按 D-84 逐图实际行数(≤4)经门禁断言**(不硬编码 4 行尺寸);`npc` 图数 = 7 且按 D-85 尺寸分组经门禁断言;`texture/.list.jsonc` 落盘且 `source`/`map`/`identifier` 与 `tile.jsonc` 一致且覆盖全部有贴图图块;零依赖门禁全绿
@@ -353,7 +343,7 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
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
- .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)
+ - .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`,用于旧→新重映射)
@@ -372,7 +362,7 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
(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 条的 `""` 处置、素材产出(拆分图数量、**每张图的实际行数与尺寸(含裁剪末图,如 `terrains2`/`terrainsAnim7`=128×32、`enemy18`=64×32)**、**OI-3 最终处置**(默认 B=42 张 `npc0..5` 均 4 行 128×192;或 A=43 张 `npc0..6`(含 `npc3` 64×64、`npc6` 128×96);或 C=40 张 `npc0..3`(含 `npc3` 64×64);及 5 张复制素材;C 下需注明 48px 变体延后)、**D-84 布局口径(列数固定、行数 ≤4 且按每图实际裁剪)与「§2 严格 4 行读法已被 D-84 取代」说明**、地图图层键与旧→新重映射口径、消费接线未实现(D-70/D-83)、门禁结果(含 enemy/item/map 精确键集断言与 D-84 逐图尺寸断言)、`open_items`(OI-1..OI-7 的最终处置)。
+ (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 裁定」)。
(5) **收口:** 不改任何源码 / `public/project/**` / `ROADMAP.md` / `STATE.md` / 既有台账与计划;提交前 `git status` 逐条核对,仅暂存本计划 `files_modified` 条目(避开用户并发改动 D-56)。
@@ -388,7 +378,7 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
- 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 条的 `""` 处置、素材产出(含**每张图实际行数与裁剪末图尺寸**)与 **OI-3 处置**、**D-84 布局口径**、地图口径、门禁、`open_items`)
+ - `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 进数据文件(精确键集断言)
5 楼层 + `maps/.list.jsonc` + unmapped 文档 + SUMMARY 落盘;地图/作用域门禁全绿
@@ -406,10 +396,11 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
| 判定边界 | §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 图;不得以「统一帧尺寸 + 透明留白」混图 |
| 贴图覆盖边界 | `texture/.list.jsonc` 的 `map ∪ autotile identifier` 必须覆盖每一处有贴图的图块 `num` |
-| 决策边界 | OI-3(48px 合并)必须由用户显式裁定(默认 B 保持 D-81),不得由「可以执行」隐含放行 |
+| 决策边界 | OI-3(48px 合并)已由 **D-85** 裁定 = 按尺寸分组(`npc0..6`=7 图 / `SPLIT-COUNT`=43);执行须据此落地,不得改按别的口径 |
| 依赖边界 | 不得新增依赖;PNG 生成须零依赖 Node `zlib` |
-| 命名边界 | 未批准命名(脚本路径、unmapped 文档路径、`source` 形态、图层键、48px 方案)先入 `open_items` 待批准(D-73 / AGENTS.md) |
+| 命名边界 | 未批准命名(脚本路径、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` 及以下,并逐条给出缓解。
@@ -421,36 +412,36 @@ Phase 9 · 任务 9 · 第 19 个计划 —— **旧引擎数据基础移植**
| 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 方案或损坏(编造像素) | medium | mitigate | Task 2 `TEXTURE GATE` 从**来源 PNG 块数与列数**逐图推导期望 `width = 列数 × 帧宽`、`height = 行数 × 帧高`(`1 ≤ 行数 ≤ 4`),对**全部**产出图(含 `enemy*`/`npc*`)校验非隔行 8 位且尺寸相符;`npc` 图数 `K` 与 OI-3 取值联动断言;`COPY GATE` 校验 autotile/tileset 逐字节复制 |
+| 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 取值);命名候选入 `open_items`;Task 2 `DEP GATE` 禁依赖新增 |
-| T-09-19-08 | Elevation of Privilege | 48px 合并处置被「可以执行」隐含放行,导致静默偏离 D-81 | medium | mitigate | Task 0 明确要求 OI-3 取值(A/B/C),默认 B 保持 D-81 的 6 图;`TEXTURE GATE` 按取值断言 `K ∈ {4,6,7}` 及对应**实际行数尺寸**(B=24 块满图均 128×192;A=`npc3` 64×64、`npc6` 128×96;C=`npc3` 64×64) |
+| 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-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` 处置、OI-3 处置、D-84 布局口径与 `open_items` |
-| T-09-19-12 | Tampering | 门禁硬编码 4 行尺寸(64×128 / 128×192)导致裁剪末图被误判为缺失/失败 | medium | mitigate | Task 2 `TEXTURE GATE` 从来源块数**逐图推导**行数与尺寸(不硬编码 4 行);prohibitions 明列「贴图尺寸不硬编码(D-84)」 |
+| 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 报备获批 |
-- **Task 0**:预执行状态(台账 + 类型/地图管理器/切分器/加载器/源数据 + `node zlib` 就位;unmapped/summary 不存在);人工关卡(用户回复且明确 OI-3 取值)。
+- **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**:拆分图(默认 B=42;A=43;C=40)+ 复制素材存在、PNG `IHDR` 合法(8 位非隔行)、**尺寸按 D-84 从来源块数逐图推导**(`width = 列数 × 帧宽`、`height = 行数 × 帧高`,`1 ≤ 行数 ≤ 4`;含全部 `enemy*`/`npc*`;末图可裁剪为 128×32 / 64×32 等),`npc` 图数 `K` 与 OI-3 取值联动断言;`texture/.list.jsonc` 解析成功、`source` 存在、`map`/`identifier` `num` 均在 `tile.jsonc` 且 **覆盖全部有贴图图块 `num`**;autotile/tileset 逐字节复制;脚本零依赖、`package.json` 未变。
+- **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;OI-3 处置与 `npc` 图数 / 总数自洽(B=6/42、A=7/43、C=4/40);贴图尺寸与 D-84 实际行数自洽(不硬编码 4 行)。
+- **一致性**:五处 `id`↔`num` 全局一致(D-79);各实体/地图键集精确(§8.1/§8.2 键均不落数据文件);maps 5 楼层;unmapped 文档只含 §8.1;D-85 处置与 `npc` 图数 / 总数自洽(7 / 43);贴图尺寸与 D-84 实际行数自洽(不硬编码 4 行)。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
-- **人工复核**:结论写入 `09-19-SUMMARY.md`(含范围与映射口径、`num` 分配、`text` 处置、素材产出(含每图实际行数与裁剪末图尺寸)与 OI-3 处置、D-84 布局口径、地图口径、门禁结果、`open_items`)。
+- **人工复核**:结论写入 `09-19-SUMMARY.md`(含范围与映射口径、`num` 分配、`text` 处置、素材产出(含每图实际行数与裁剪末图尺寸、43 张总量)与 D-85 处置、D-84 布局口径、地图口径、门禁结果、`open_items`)。
- **实体(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):** 默认 42 张拆分图(OI-3=B,`npc0..5`)+ 4 autotile + 1 tileset 落 `src/content/texture/`(备选 A=43 `npc0..6`、C=40 `npc0..3`);`animates` 并入 `terrains`;零依赖生成;**每张 PNG 尺寸按 D-84 从来源块数逐图推导(列数固定、行数 ≤4 且裁剪),门禁按实际行数断言(不硬编码 4 行尺寸)**;`npc` 图数按 OI-3 取值断言;`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 且按 D-85 尺寸分组经门禁断言;`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 / OI-3):** 48px 合并处置由用户显式裁定;默认保持 D-81 的 `npc0..5`=6 图 / `SPLIT-COUNT` 42;备选 A=7/43、C=4/40;贴图布局按 D-84 逐图裁剪、门禁按实际行数断言(§2 严格 4 行读法已由 D-84 取代)。
-- **门禁:** 全部静态门禁(结构 / 标记 / 精确键集 / D-84 逐图尺寸 / 覆盖 / 一致性 / 作用域)全绿;产出经 Task 0 获批。
+- **决策(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 获批。