[M15] 地图与怪物生成逻辑 hardcode / fallback 全量审计(追踪) #64

Closed
opened 2026-09-17 11:13:59 +00:00 by troytt · 1 comment
Owner

概述

对 main 分支(origin/main = 4c2e399,审计时 src/ 与远端逐字节一致)的地图与怪物生成逻辑做了一次全量 hardcode / fallback 审计。

审计方式:三个并行只读子代理逐行通读全部源文件 + 模式扫描(TODO|FIXME|HACK|placeholder|fallback|approximat|heuristic|stub|synthetic|unimplemented + ?? / || / catch)。

覆盖范围:约 15,000 行
src/game/wilderness{,-acts,-jungles,-siege}.ts、src/game/maze{,-objects,-special-passes}.ts、src/game/world-graph.ts、src/game/level-links.ts、src/game/preset.ts、src/game/monster{s,-mapping,-rooms,-streaming,-art}.ts、src/scene/act-scene.ts


总览

层 硬编码处 Fallback 处 数据真实性
野外生成器 ~60 ~45 约 900 行手写常量表代替 MPQ 数据;多数「地形 pass」是凭空发明的程序化合成,无 D2 对应物
迷宫 / 世界连通层 ~45 ~48 布局 profile 手写;世界连通图 69 条边手打;LvlWarp.txt 几何是真解析的(唯一亮点)
怪物生成层 ~40 ~35 统计数值层是真数据驱动的;但美术层、超独层、词缀层、放置层全是手写

合计:约 145 处硬编码 + 约 128 处 fallback + 约 2,200 行本应解析自 MPQ 的手写常量表。


三个核心发现

1. 最严重的问题不是任何单点 bug,而是「静默降级」设计

三层全部采用:数据缺失时不报错、不中断,而是替换成合成值继续生成。

结果是:一个数据完全损坏的世界仍然能「成功」生成,并通过全部 885 个测试。

最典型的例子:src/game/maze.ts:856 声明 UNIMPLEMENTED_PASSES = [] 并注释「11 个核心 pass 均已实现」,但同文件 :594-598 与 scripts/pack-act-assets.ts:733-740 都明说墓穴 / 下水道 / 第三幕地牢的楼梯 pass 尚未转写。设计好的预警通道恒报「无缺失」。

2. 修复成本远低于表面规模 —— 真解析器已写好但从未被调用

已解析却被弃用 位置 被什么取代
readSuperUniques() monsters.ts:605-644 344 行 CANONICAL_SUPER_UNIQUES_BY_LEVEL
readEliteModifiers() monsters.ts:660-682 13 行 CANONICAL_ELITE_MODIFIERS + 内联数值
MonsterKind.code monsters.ts:171-172 741 行 MONSTER_ART_MAP
MonsterArt.meleeRange monsters.ts:376-389 常量 DEFAULT_REACH_PX = 40
LevelRow.offsetX/offsetY world-graph.ts:399-400 46 对手打 SEAMLESS_ADJACENCY

删表大多只需改接线,不需要写新解析器。

3. 部分「地形生成」是凭空发明,还会破坏真实数据

  • 第四幕熔岩裂谷是字面上的正弦波 y(x) = H*(0.35+0.3t) + sin(2πt)*4
  • 库拉斯特运河把关卡 82 完全重绘,覆盖刚贴上的 Act 3 - Bridge DS1
  • stampThemedRoads 会 walls.length = 0 删除格子内所有墙层
  • 血腥丘陵是 19 次调用约 60 个字面坐标的手绘固定地图,每个种子都一样

子 Issue

  • #58 · P0 · fix(diag) 消除静默降级 —— 让数据缺失响亮失败
  • #59 · P0 · refactor(monsters) 用 MPQ 表取代约 1500 行手写怪物数据
  • #60 · P0 · fix(world) 消除不可达关卡、编造的楼梯位置与传送无限弹射
  • #61 · P1 · refactor(map) 用 LvlPrest/Levels/Objects 表取代约 1100 ���手写地图数据
  • #62 · P2 · refactor(map) 处理凭空发明的程序化地形 pass
  • #63 · P2 · chore 消除重复真相源、死代码与形同虚设的逻辑规则

修复路线图

flowchart TD
    P0["P0 · 让失败变响亮"] --> P0a["#58 修复 UNIMPLEMENTED_PASSES 谎报"]
    P0 --> P0b["#58 修复 maze-objects.ts:238 的 || true 死代码"]
    P0 --> P0c["#58 美术失败上报到 ActSceneState 与 HUD"]
    P0 --> P0d["#58 MonLvl 缺行时告警而非静默 UNSCALED"]
    P0 --> P0e["#58 删除 DEMO_MONSTERS 伪造刷怪路径"]

    P0 --> P1["P1 · 消除最大的手写表"]
    P1 --> P1a["#59 MONSTER_ART_MAP → MonStats.Code + MonStats2.BaseW"]
    P1 --> P1b["#59 CANONICAL_SUPER_UNIQUES → readSuperUniques"]
    P1 --> P1c["#59 CANONICAL_ELITE_MODIFIERS → MonUMod.txt"]
    P1 --> P1d["#59 超级独特怪 ID 接入 loadMonsterArtMap"]

    P1 --> P2["P2 · 世界完整性"]
    P2 --> P2a["#60 getPerimeterOpenings → Levels.txt Vis/Warp"]
    P2 --> P2b["#60 fallback warp 数量超阈值时打包失败"]
    P2 --> P2c["#60 修复 level-links.ts:409 无限弹射"]
    P2 --> P2d["#60 SEAMLESS_ADJACENCY → OffsetX/OffsetY = -1"]

    P2 --> P3["P3 · 数据表与地形保真"]
    P3 --> P3a["#61 地图数据表迁移到 MPQ"]
    P3 --> P3b["#62 转写真实 DRLG pass"]
    P3 --> P3c["#63 清理重复源与死代码"]

值得称赞、不要动的部分

  1. LvlWarp.txt 几何是真解析的 —— parseWarpGeometry(world-graph.ts:755-780)正确按 (Id, Direction) 读取全部列。全树无任何手打 warp 几何。
  2. LvlMaze.txt 输入是表驱动的 —— pack-act-assets.ts:1557-1563 逐关读取 SizeX/SizeY/Rooms/Merge。
  3. 怪物统计缩放正确 —— MonStats.txt × MonLvl.txt ÷ 100,含五列校验与完整推理文档(monsters.ts:32-71)。
  4. Levels.txt mon 列是真读的 —— monsters.ts:536-543 遍历 mon/nmon/umon,含 Rarity 加权。
  5. BaseId 回退解析 —— 正确处理 60 个无 MonStats2 行的怪物(monsters.ts:350-390)。
  6. warp-tiles.ts 的 DS1 特殊瓦片语义 —— 有文档且经 10 个关卡验证。
  7. assignGateSides 失败时抛异常 —— world-graph.ts:576-645 是全树少数响亮失败的地方。
  8. 拒绝在阻挡格生成怪物而非把怪物推进墙里(combat.ts:380-385)。

总体验收标准

  • 三层不再存在「数据损坏但生成成功」的路径;有测试构造损坏数据并断言失败
  • stats 中所有字段反映真实计数,无伪造值
  • 约 2,200 行手写常量表中,至少 P0/P1 部分(约 1,500 行)改为解析自 MPQ
  • 遍历全部 136 关的连通性 / 可达性回归测试
  • npm run typecheck 零错误,npx vitest run 全绿

TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1

## 概述 对 `main` 分支(`origin/main` = `4c2e399`,审计时 `src/` 与远端逐字节一致)的地图与怪物生成逻辑做了一次全量 hardcode / fallback 审计。 **审计方式**:三个并行只读子代理逐行通读全部源文件 + 模式扫描(`TODO|FIXME|HACK|placeholder|fallback|approximat|heuristic|stub|synthetic|unimplemented` + `??` / `||` / `catch`)。 **覆盖范围**:约 15,000 行 `src/game/wilderness{,-acts,-jungles,-siege}.ts`、`src/game/maze{,-objects,-special-passes}.ts`、`src/game/world-graph.ts`、`src/game/level-links.ts`、`src/game/preset.ts`、`src/game/monster{s,-mapping,-rooms,-streaming,-art}.ts`、`src/scene/act-scene.ts` --- ## 总览 | 层 | 硬编码处 | Fallback 处 | 数据真实性 | | :--- | ---: | ---: | :--- | | **野外生成器** | ~60 | ~45 | 约 900 行手写常量表代替 MPQ 数据;多数「地形 pass」是**凭空发明的程序化合成**,无 D2 对应物 | | **迷宫 / 世界连通层** | ~45 | ~48 | 布局 profile 手写;世界连通图 69 条边手打;`LvlWarp.txt` 几何**是真解析的**(唯一亮点) | | **怪物生成层** | ~40 | ~35 | **统计数值层是真数据驱动的**;但美术层、超独层、词缀层、放置层全是手写 | **合计:约 145 处硬编码 + 约 128 处 fallback + 约 2,200 行本应解析自 MPQ 的手写常量表。** --- ## 三个核心发现 ### 1. 最严重的问题不是任何单点 bug,而是「静默降级」设计 三层全部采用:数据缺失时不报错、不中断,而是替换成合成值继续生成。 **结果是:一个数据完全损坏的世界仍然能「成功」生成,并通过全部 885 个测试。** 最典型的例子:`src/game/maze.ts:856` 声明 `UNIMPLEMENTED_PASSES = []` 并注释「11 个核心 pass 均已实现」,但同文件 `:594-598` 与 `scripts/pack-act-assets.ts:733-740` 都明说墓穴 / 下水道 / 第三幕地牢的楼梯 pass **尚未转写**。设计好的预警通道恒报「无缺失」。 ### 2. 修复成本远低于表面规模 —— 真解析器已写好但从未被调用 | 已解析却被弃用 | 位置 | 被什么取代 | | :--- | :--- | :--- | | `readSuperUniques()` | `monsters.ts:605-644` | 344 行 `CANONICAL_SUPER_UNIQUES_BY_LEVEL` | | `readEliteModifiers()` | `monsters.ts:660-682` | 13 行 `CANONICAL_ELITE_MODIFIERS` + 内联数值 | | `MonsterKind.code` | `monsters.ts:171-172` | 741 行 `MONSTER_ART_MAP` | | `MonsterArt.meleeRange` | `monsters.ts:376-389` | 常量 `DEFAULT_REACH_PX = 40` | | `LevelRow.offsetX/offsetY` | `world-graph.ts:399-400` | 46 对手打 `SEAMLESS_ADJACENCY` | 删表大多只需改接线,不需要写新解析器。 ### 3. 部分「地形生成」是凭空发明,还会破坏真实数据 - 第四幕熔岩裂谷是**字面上的正弦波** `y(x) = H*(0.35+0.3t) + sin(2πt)*4` - 库拉斯特运河把关卡 82 **完全重绘**,覆盖刚贴上的 `Act 3 - Bridge` DS1 - `stampThemedRoads` 会 `walls.length = 0` **删除格子内所有墙层** - 血腥丘陵是 19 次调用约 60 个字面坐标的**手绘固定地图**,每个种子都一样 --- ## 子 Issue - [ ] #58 · **P0** · `fix(diag)` 消除静默降级 —— 让数据缺失响亮失败 - [ ] #59 · **P0** · `refactor(monsters)` 用 MPQ 表取代约 1500 行手写怪物数据 - [ ] #60 · **P0** · `fix(world)` 消除不可达关卡、编造的楼梯位置与传送无限弹射 - [ ] #61 · **P1** · `refactor(map)` 用 LvlPrest/Levels/Objects 表取代约 1100 ���手写地图数据 - [ ] #62 · **P2** · `refactor(map)` 处理凭空发明的程序化地形 pass - [ ] #63 · **P2** · `chore` 消除重复真相源、死代码与形同虚设的逻辑规则 --- ## 修复路线图 ```mermaid flowchart TD P0["P0 · 让失败变响亮"] --> P0a["#58 修复 UNIMPLEMENTED_PASSES 谎报"] P0 --> P0b["#58 修复 maze-objects.ts:238 的 || true 死代码"] P0 --> P0c["#58 美术失败上报到 ActSceneState 与 HUD"] P0 --> P0d["#58 MonLvl 缺行时告警而非静默 UNSCALED"] P0 --> P0e["#58 删除 DEMO_MONSTERS 伪造刷怪路径"] P0 --> P1["P1 · 消除最大的手写表"] P1 --> P1a["#59 MONSTER_ART_MAP → MonStats.Code + MonStats2.BaseW"] P1 --> P1b["#59 CANONICAL_SUPER_UNIQUES → readSuperUniques"] P1 --> P1c["#59 CANONICAL_ELITE_MODIFIERS → MonUMod.txt"] P1 --> P1d["#59 超级独特怪 ID 接入 loadMonsterArtMap"] P1 --> P2["P2 · 世界完整性"] P2 --> P2a["#60 getPerimeterOpenings → Levels.txt Vis/Warp"] P2 --> P2b["#60 fallback warp 数量超阈值时打包失败"] P2 --> P2c["#60 修复 level-links.ts:409 无限弹射"] P2 --> P2d["#60 SEAMLESS_ADJACENCY → OffsetX/OffsetY = -1"] P2 --> P3["P3 · 数据表与地形保真"] P3 --> P3a["#61 地图数据表迁移到 MPQ"] P3 --> P3b["#62 转写真实 DRLG pass"] P3 --> P3c["#63 清理重复源与死代码"] ``` --- ## 值得称赞、不要动的部分 1. **`LvlWarp.txt` 几何是真解析的** —— `parseWarpGeometry`(`world-graph.ts:755-780`)正确按 `(Id, Direction)` 读取全部列。**全树无任何手打 warp 几何**。 2. **`LvlMaze.txt` 输入是表驱动的** —— `pack-act-assets.ts:1557-1563` 逐关读取 `SizeX/SizeY/Rooms/Merge`。 3. **怪物统计缩放正确** —— `MonStats.txt × MonLvl.txt ÷ 100`,含五列校验与完整推理文档(`monsters.ts:32-71`)。 4. **`Levels.txt` mon 列是真读的** —— `monsters.ts:536-543` 遍历 `mon`/`nmon`/`umon`,含 `Rarity` 加权。 5. **`BaseId` 回退解析** —— 正确处理 60 个无 MonStats2 行的怪物(`monsters.ts:350-390`)。 6. **`warp-tiles.ts` 的 DS1 特殊瓦片语义** —— 有文档且经 10 个关卡验证。 7. **`assignGateSides` 失败时抛异常** —— `world-graph.ts:576-645` 是全树少数**响亮失败**的地方。 8. **拒绝在阻挡格生成怪物**而非把怪物推进墙里(`combat.ts:380-385`)。 --- ## 总体验收标准 - [ ] 三层不再存在「数据损坏但生成成功」的路径;有测试构造损坏数据并断言失败 - [ ] `stats` 中所有字段反映真实计数,无伪造值 - [ ] 约 2,200 行手写常量表中,至少 P0/P1 部分(约 1,500 行)改为解析自 MPQ - [ ] 遍历全部 136 关的连通性 / 可达性回归测试 - [ ] `npm run typecheck` 零错误,`npx vitest run` 全绿 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
troytt added this to the [M15] 生成器数据保真度:消除 hardcode 与静默降级 milestone 2026-09-17 11:13:59 +00:00
Author
Owner

🎉 [M15] 地图与怪物生成逻辑 hardcode / fallback 全量消除 —— 圆满完成!

Milestone 6 (M15) 下规划的 6 个子 Issue 已全部按高保真规范串行修复、独立交叉复验通过,并合入主线分支 main:

子 Issue 核心成果 提交哈希 状态
#58 消除静默降级 暴露未实现 pass 遥测;消除 ` true` 假地板;上报怪物流式加载解析错误;移除假测试刷怪器;MonLvl 缺失显式告警
#59 用 MPQ 表取代 1500 行手写怪物数据 读取 SuperUniques.txt / MonStats2.txt / MonUMod.txt;修复奶牛等怪物动画 Token;消灭红框占位符;实装攻击距离与属性缩放 5f484e5 ✅ 已关闭
#60 消除不可达关卡与无限弹射 根治接缝传送无限弹射死循环;统一城镇 ID 与幕归属;消除 PRNG 假随机与第5幕神殿不可达 Bug;开放非第一幕野外边界开口;硬约束兜底楼梯上限 955e9ed ✅ 已关闭
#61 用 MPQ 表取代 1100 行手写地图数据 统一 SUB_TILES_PER_TILE;消除血腥丘陵假步长;动态解析 string.tbl 关卡名;消灭模块循环依赖;校验 DS1 Type 2 物件池 710efa0 ✅ 已关闭
#62 处理凭空发明的程序化地形 pass 清除 generateAct3Jungle 死代码;保护真实 DS1 预设不被道路/桥梁破坏性抹除;程序化地形 Pass 全指标上报;赋予血腥丘陵障碍物种子多样性 c377e8e ✅ 已关闭
#63 消除重复真相源、死代码与形同虚设规则 修复 5 处恒真恒假条件;统一血腥荒地预设与 PACK_RADIUS_PX;修复等距瓦片默认高度;支持 mon25 动态列;普通怪依据 Rarity 加权选型;实装精英词缀数值机制 a7e526d ✅ 已关闭

全局验证汇总 (HEAD = a7e526d)

  • npm run typecheck: 0 错误
  • npx vitest run: 61 test files passed, 1 skipped; 1018 passed (+95 个新增专项测试), 2 skipped (全绿)
  • npx tsx scripts/verify-packs.ts: 1671/1671 项断言通过(逐像素比对了 280.2 MB 的索引数据)
  • 所有临时 worktree 与开发分支已干净清除。

TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1

# 🎉 [M15] 地图与怪物生成逻辑 hardcode / fallback 全量消除 —— 圆满完成! Milestone 6 (M15) 下规划的 6 个子 Issue 已全部按高保真规范串行修复、独立交叉复验通过,并合入主线分支 `main`: | 子 Issue | 核心成果 | 提交哈希 | 状态 | | :--- | :--- | :---: | :---: | | **#58 消除静默降级** | 暴露未实现 pass 遥测;消除 `|| true` 假地板;上报怪物流式加载解析错误;移除假测试刷怪器;MonLvl 缺失显式告警 | `9d568ad` | ✅ 已关闭 | | **#59 用 MPQ 表取代 1500 行手写怪物数据** | 读取 `SuperUniques.txt` / `MonStats2.txt` / `MonUMod.txt`;修复奶牛等怪物动画 Token;消灭红框占位符;实装攻击距离与属性缩放 | `5f484e5` | ✅ 已关闭 | | **#60 消除不可达关卡与无限弹射** | 根治接缝传送无限弹射死循环;统一城镇 ID 与幕归属;消除 PRNG 假随机与第5幕神殿不可达 Bug;开放非第一幕野外边界开口;硬约束兜底楼梯上限 | `955e9ed` | ✅ 已关闭 | | **#61 用 MPQ 表取代 1100 行手写地图数据** | 统一 `SUB_TILES_PER_TILE`;消除血腥丘陵假步长;动态解析 `string.tbl` 关卡名;消灭模块循环依赖;校验 DS1 Type 2 物件池 | `710efa0` | ✅ 已关闭 | | **#62 处理凭空发明的程序化地形 pass** | 清除 `generateAct3Jungle` 死代码;保护真实 DS1 预设不被道路/桥梁破坏性抹除;程序化地形 Pass 全指标上报;赋予血腥丘陵障碍物种子多样性 | `c377e8e` | ✅ 已关闭 | | **#63 消除重复真相源、死代码与形同虚设规则** | 修复 5 处恒真恒假条件;统一血腥荒地预设与 `PACK_RADIUS_PX`;修复等距瓦片默认高度;支持 `mon25` 动态列;普通怪依据 Rarity 加权选型;实装精英词缀数值机制 | `a7e526d` | ✅ 已关闭 | --- ### 全局验证汇总 (HEAD = `a7e526d`) - `npm run typecheck`: **0 错误** - `npx vitest run`: **61 test files passed, 1 skipped; 1018 passed (+95 个新增专项测试), 2 skipped (全绿)** - `npx tsx scripts/verify-packs.ts`: **1671/1671 项断言通过(逐像素比对了 280.2 MB 的索引数据)** - 所有临时 worktree 与开发分支已干净清除。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
Sign in to join this conversation.
No Label
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#64
No description provided.