docs(roadmap): correct M6 — discard map-scene.ts instead of merging
原诊断断言 act-scene.ts「无战斗/无物品/无技能/无存档」,据此把 M6 定为
「两轨合流、抽出第三个共享层」。逐行核查后发现这个断言是错的。
act-scene.ts:777 就实例化了 GameEngine,820 行 tick 它,962/968/978/984 行
分别绘制怪物、NPC、投射物、地面掉落,826-827 行的存读档也是经引擎走的。
之前 grep 不到是因为查的是 tickCombat/spawnMonsters 的直接导入,而
act-scene 是通过 GameEngine 间接获得这些能力的。
真相是 GameEngine(engine.ts,350 行)本身就已经是地图无关的共享层——
它只依赖 WorldMapProvider { widthPx, heightPx, overlap() }。原计划要新建的
src/scene/shared/gameplay.ts 不需要了。
「真地图上没有怪物」的全部原因是 act-scene.ts:785 的 stats: [],
因为 combat.ts:376 是 if (stats.length === 0) return 0。
反过来 map-scene.ts 查出三个确凿缺陷:
1. 孤儿世界:399-403 行 createWorld + spawnMonsters 建的世界从未被 tick,
453 行的 GameEngine 又建了自己的世界;let player = world.player 是死变量。
2. 怪物动画冻结(真实 bug):572 行读孤儿世界的 world.tick,而 world.tick += 1
只在 combat.ts:518 的 tickCombat 里发生,map-scene 从不直接调它。world.tick
恒为 0,表达式退化成每只怪一个固定帧,追击/攻击时动画静止。
3. 存读档绕过引擎:488-489 行传 saving:false/loading:false,然后在 492-511 行
于引擎外部读写 localStorage。在 tick 外做存读档对确定性是隐患。
另有 12 个死导入(tickCombat/rebindPlayer/damageMonster/castSkill/
tickProjectiles/QuestLog/npcDialog/captureSnapshot/parseSnapshot/
restoreSnapshot/serializeSnapshot/Rng),是逻辑搬进 GameEngine 后未清理的外壳。
因此 M6 改为:舍弃 map-scene.ts 与 map.html,收敛到 act-scene.ts 单轨。
范围收窄为三件事——打通 stats、修复 overlap 布尔化导致的脱困梯度丢失、
删除 map-scene 并把夹具回归检查改为直接驱动 GameEngine。风险由「高」下调为「中」。
TAG=agy
CONV=c89513df-cd12-4749-b5bd-5e9f16639391
This commit is contained in:
parent
b9671b015f
commit
b28ce37e8a
168
ROADMAP.md
168
ROADMAP.md
|
|
@ -25,35 +25,65 @@
|
|||
| DRLG 随机地图生成器(迷宫 70 + 野外 31) | `verify-generators` **788/788 断言**,缺瓦片 0.00%,可达率 100% |
|
||||
| 战斗 / 物品 / 技能 / 任务 / 存档 / 联机骨架 | `verify:all`(combat 38 + items 58 + m4 56 + m5 33 + net 143) |
|
||||
|
||||
但存在一个**根本性的架构断层**——这是整个路线图的排序依据:
|
||||
> [!IMPORTANT]
|
||||
> **2026-09-15 修正**:本节最初的判断是「两条轨道从未合流,acts.html 无战斗/无物品/无技能/无存档」。
|
||||
> 经逐行核查,**这个判断是错的**。`act-scene.ts` 第 777 行就实例化了 `GameEngine`,
|
||||
> 第 820 行 tick 它,第 962 / 968 / 978 / 984 行分别绘制怪物、NPC、投射物、地面掉落。
|
||||
> 之前的结论来自 grep `tickCombat` / `spawnMonsters` 的直接导入——而 act-scene 是
|
||||
> **通过 `GameEngine` 间接获得全部玩法的**。下文是修正后的事实。
|
||||
|
||||
真实的架构是**三层**,而不是两条平行轨道:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph A["轨道 A:acts.html(真地图轨)"]
|
||||
A1["act-scene.ts (1437 行)"]
|
||||
A2["✅ buildIsoMapScene 真实等距 DS1/DT1"]
|
||||
A3["✅ loadCharacterSheet 真实法师 16 层 × 8 向"]
|
||||
A4["✅ CollisionGrid 真实碰撞"]
|
||||
A5["❌ 无战斗 ❌ 无物品 ❌ 无技能<br/>❌ 无存档 ❌ 无小地图/传送点"]
|
||||
subgraph E["玩法层:src/game/engine.ts (350 行)"]
|
||||
E1["GameEngine —— 已经是地图无关的共享层"]
|
||||
E2["只依赖 WorldMapProvider { widthPx, heightPx, overlap() }"]
|
||||
E3["✅ 战斗 ✅ 物品/背包 ✅ 技能/投射物<br/>✅ 任务/NPC ✅ 存读档 ✅ 掉落/拾取"]
|
||||
end
|
||||
subgraph B["轨道 B:map.html(玩法轨)"]
|
||||
B1["map-scene.ts (1054 行)"]
|
||||
B2["❌ 正交 fixture 地图,非真实 D2"]
|
||||
B3["✅ tickCombat / spawnMonsters"]
|
||||
B4["✅ Inventory / rollDrop"]
|
||||
B5["✅ castSkill / tickProjectiles"]
|
||||
B6["✅ 存读档 / 任务 / NPC"]
|
||||
B7["⚠️ 全部跑在 demo-data.ts 的<br/>3 怪 / 3 物 / 2 词缀 / 2 技能 / 5 级经验上"]
|
||||
subgraph A["表现层 A:act-scene.ts (1437 行) → acts.html"]
|
||||
A1["✅ 真实等距 DS1/DT1 + 屋顶 + 深度排序"]
|
||||
A2["✅ 真·女法师 DCC+COF 16 层 × 8 向"]
|
||||
A3["✅ 已接 GameEngine,已画怪物/NPC/投射物/掉落"]
|
||||
A4["✅ NPC 用真实 DS1 坐标"]
|
||||
A5["✅ 存读档经引擎(saving: input.saving)"]
|
||||
A6["❌ stats: [] —— 唯一的「没有怪物」原因"]
|
||||
A7["⚠️ overlap 被布尔化,丢失重叠计数梯度"]
|
||||
A8["❌ 无背包 UI / 无血蓝球(属 M12)"]
|
||||
end
|
||||
subgraph C["轨道 C:net.html(联机轨)"]
|
||||
C1["net-scene.ts (653 行) 25Hz 锁步"]
|
||||
C2["⚠️ 背包/任务不进同步哈希"]
|
||||
subgraph B["表现层 B:map-scene.ts (1055 行) → map.html"]
|
||||
B1["❌ 正交 fixture 地图,非真实 D2"]
|
||||
B2["🐛 孤儿 world + 12 个死导入"]
|
||||
B3["🐛 怪物动画冻结(读未 tick 的 world.tick)"]
|
||||
B4["🐛 存读档绕过引擎"]
|
||||
B5["✅ 背包 UI + 血蓝球(唯一优势,属 M12)"]
|
||||
end
|
||||
A -.->|"两条轨道从未合流<br/>这是最大的阻塞点"| B
|
||||
B -.-> C
|
||||
E --> A
|
||||
E --> B
|
||||
B -.->|"逻辑不正确,M6 决定舍弃"| X["🗑️"]
|
||||
```
|
||||
|
||||
`acts.html` 的现状可以概括为:**在一张漂亮的真地图上,操纵一个真法师,永远独自行走。**
|
||||
**结论:`GameEngine` 本身就是共享层,不需要再抽一层。**
|
||||
`acts.html` 现状可概括为:**玩法已经接好,但怪物定义是空数组。**
|
||||
|
||||
### `map-scene.ts` 的三个确凿缺陷(M6 决定舍弃它的依据)
|
||||
|
||||
1. **孤儿世界**:第 399–403 行 `createWorld` + `spawnMonsters` 建了一个世界,
|
||||
但第 453 行的 `GameEngine` 又建了自己的世界(`engine.ts:82-83`)。前者从未被 tick。
|
||||
`let player = world.player`(403 行)此后再未使用。
|
||||
2. **怪物动画冻结(真实 bug)**:第 572 行
|
||||
`group[moving ? (world.tick + monster.index * 3) % group.length : 0]`
|
||||
读的是孤儿世界的 `tick`。而 `world.tick += 1` 只在 `combat.ts:518` 的 `tickCombat` 里发生,
|
||||
map-scene 从不直接调它。**`world.tick` 恒为 0**,表达式退化成每只怪一个固定帧——
|
||||
怪物在追击/攻击时动画是静止的。
|
||||
3. **存读档绕过引擎**:第 488–489 行向 `engine.tick()` 传 `saving: false, loading: false`,
|
||||
然后在 492–511 行于引擎外部自行读写 `localStorage`。
|
||||
act-scene 的做法(826–827 行传入真实标志)才是对的;在 tick 外做存读档对确定性是隐患。
|
||||
|
||||
另有 **12 个死导入**(`tickCombat`、`rebindPlayer`、`damageMonster`、`castSkill`、
|
||||
`tickProjectiles`、`QuestLog`、`npcDialog`、`captureSnapshot`、`parseSnapshot`、
|
||||
`restoreSnapshot`、`serializeSnapshot`、`Rng`)——全部 import 但零使用,
|
||||
是「逻辑搬进 `GameEngine` 后未清理外壳」留下的残骸。
|
||||
|
||||
### demo 数据与真实 D2 的差距
|
||||
|
||||
|
|
@ -72,7 +102,7 @@ graph TB
|
|||
|
||||
| 议题 | 裁决 |
|
||||
| --- | --- |
|
||||
| M6 合流方向 | **抽出第三个共享层**,两个页面都变薄 |
|
||||
| M6 合流方向 | ~~抽出第三个共享层~~ → **舍弃 `map-scene.ts`,直接在 `act-scene.ts` 上做**(核查后变更,见 M6) |
|
||||
| 联机 | **不作为目标**,只需单机;**保留扩展可能**(模拟层纯函数约束必须继续遵守) |
|
||||
| 怪物精灵包体 | **接受按幕分包 + 按需懒加载** |
|
||||
| 音频 | 解码器保留,**暂不接入场景层** |
|
||||
|
|
@ -84,7 +114,7 @@ graph TB
|
|||
|
||||
| # | 里程碑 | Issue | 重要程度 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| M6 | 两轨合流(共享玩法层) | [#21](https://git.projectdiablo2.cn/troytt/diablo2-web/issues/21) | ★★★★★ | 高 | 待开始 |
|
||||
| M6 | 收敛到 act-scene 单轨(舍弃 map-scene) | [#21](https://git.projectdiablo2.cn/troytt/diablo2-web/issues/21) | ★★★★★ | 中 | 待开始 |
|
||||
| M7 | 真实怪物 | [#22](https://git.projectdiablo2.cn/troytt/diablo2-web/issues/22) | ★★★★★ | 中 | 待开始 |
|
||||
| M8 | 世界连通 | [#23](https://git.projectdiablo2.cn/troytt/diablo2-web/issues/23) | ★★★★★ | 中高 | 待开始 |
|
||||
| M9 | 真实战斗数学 | [#24](https://git.projectdiablo2.cn/troytt/diablo2-web/issues/24) | ★★★★☆ | 中 | 待开始 |
|
||||
|
|
@ -115,28 +145,86 @@ graph LR
|
|||
|
||||
---
|
||||
|
||||
## M6 · 两轨合流(共享玩法层)
|
||||
## M6 · 舍弃 map-scene.ts,收敛到 act-scene.ts 单轨
|
||||
|
||||
**★★★★★ 风险:高 全部后续里程碑的前提**
|
||||
**★★★★★ 风险:中(原评估为「高」,核查后下调) 全部后续里程碑的前提**
|
||||
|
||||
把 `map-scene.ts` 上跑通的整套玩法抽成与地图实现无关的共享层,两个页面共同依赖;
|
||||
核心技术点是让 `CombatWorld` 绑定到 `d2map.ts` 的 `CollisionGrid` 而不是 `map.ts` 的正交网格。
|
||||
> [!IMPORTANT]
|
||||
> **2026-09-15 方案变更(Master 裁决)**:原方案是「抽出第三个共享层,两个页面都变薄」。
|
||||
> 核查发现 **`GameEngine` 本身就已经是那个共享层**(只依赖 `WorldMapProvider`,地图无关),
|
||||
> 而 `act-scene.ts` 早已接入它。同时 `map-scene.ts` 被查出三个确凿缺陷(见上文 §0)。
|
||||
> 因此改为:**舍弃 `map-scene.ts`,直接在 `act-scene.ts` 上继续做,不做合流。**
|
||||
|
||||
`combat.ts` 的 `CombatTerrain` 是**重叠计数谓词**而非布尔值(身体能从坏出生点逃出),
|
||||
而 `d2map.ts` 提供的是 `isBlockedAt(scene, x, y): boolean` + `cellAt` + `cellCentre` + `findIsoSpawn`。
|
||||
需要一个适配器把后者升格成前者,并处理等距坐标下 sub-tile **16×8 的各向异性**
|
||||
(`ORTHO_SUB_TILE_WIDTH = 16` / `ORTHO_SUB_TILE_HEIGHT = 8`)。
|
||||
### 范围(比原计划小得多)
|
||||
|
||||
- **[NEW] `src/scene/shared/gameplay.ts`** —— 与地图实现无关的玩法编排:世界创建、tick 循环、
|
||||
事件流消费、掉落、拾取、存读档钩子。
|
||||
- **[NEW] `src/game/iso-terrain.ts`** —— `CollisionGrid` → `CombatTerrain` 适配器。
|
||||
**最容易出 bug 的地方:正交轨假设 sub-tile 是正方形。**
|
||||
- **[MODIFY] `src/scene/act-scene.ts`** —— 接入共享层。怪物与投射物必须插入**现有的等距深度排序**
|
||||
(`IsoDraw` 列表),不能另起一层,否则会穿墙穿屋顶;屋顶层(DS1 wall type 15)仍最后画。
|
||||
- **[MODIFY] `src/scene/map-scene.ts`** —— 改为消费共享层,保留 fixture 地图作为**快速回归测试台**
|
||||
(它比真地图快得多,单元测试仍应打在这条轨上)。
|
||||
原计划要新建的 `src/scene/shared/gameplay.ts` **不需要了**——`src/game/engine.ts` 就是它。
|
||||
|
||||
**验收**:在 `acts.html` 的第一幕荒野上,能被怪物追、能打死怪、能捡到掉落、能 K/L 存读档。
|
||||
实际只有三件事:
|
||||
|
||||
#### 1. `stats: []` → 真实怪物定义
|
||||
|
||||
`act-scene.ts:785` 传的是空数组,而 `combat.ts:376` 是 `if (stats.length === 0) return 0`。
|
||||
**这一行就是「真地图上没有怪物」的全部原因。** 怪物的绘制、tick、AI 早就接好了。
|
||||
|
||||
真实怪物数据的接入属于 M7(#22),M6 只需把这个口子打通并用占位数据验证链路。
|
||||
|
||||
#### 2. 修复碰撞语义退化(真实缺陷)
|
||||
|
||||
```ts
|
||||
// act-scene.ts:781 —— 布尔化,丢失重叠计数
|
||||
overlap: (x, y) => (collides(runtime.grid, x, y) ? 1 : 0)
|
||||
```
|
||||
|
||||
`collides`(`act-scene.ts:1120`)对脚底盒采样 9 个点,任一阻挡即返回 `true`。
|
||||
但 `combat.ts` 的脱困规则是 `terrain.overlap(x + dx, y) <= current`:
|
||||
|
||||
- **有计数时**:陷在墙里 5 格深 → 向 3 格深移动被允许(沿梯度爬出),向 7 格深被拒绝
|
||||
- **布尔化后**:`1 <= 1` 恒成立 → 在墙里**随机游走**,能出来但不可靠、无方向性
|
||||
|
||||
需要一个返回**真实阻挡子格计数**的 `overlap`,并处理等距坐标下 sub-tile
|
||||
**16×8 的各向异性**(`ORTHO_SUB_TILE_WIDTH = 16` / `ORTHO_SUB_TILE_HEIGHT = 8`)——
|
||||
`map.ts` 的 `blockedOverlap` 假设 sub-tile 是正方形,不能直接搬。
|
||||
|
||||
- **[NEW] `src/game/iso-terrain.ts`** —— 等距 `CollisionGrid` → 计数型 `WorldMapProvider`
|
||||
|
||||
#### 3. 删除 map-scene.ts 与 map.html
|
||||
|
||||
- **[DELETE] `src/scene/map-scene.ts`**(1055 行)
|
||||
- **[DELETE] `map.html`**
|
||||
- **[MODIFY] `src/game/map.ts`** —— 仅保留仍被引用的部分(`SUB_TILES_PER_TILE` 被 `d2map.ts` 依赖)
|
||||
|
||||
> [!WARNING]
|
||||
> 删除前必须确认 **fixture 回归测试台的去向**。`map.html` 现在是
|
||||
> `scripts/browser/checks/map-save-load.js` 等无头检查的落点,`verify:all` 链条里
|
||||
> `verify-m4` / `verify-m5` 也依赖夹具数据路径。
|
||||
> 这些检查**不能一起删**——它们跑得比真地图快得多,是快速回归的价值所在。
|
||||
> 方案:把它们改为直接驱动 `GameEngine` + 一个夹具 `WorldMapProvider`(纯逻辑,无场景层),
|
||||
> 而不是驱动一个完整页面。
|
||||
|
||||
### GameEngine 里需要一并表格化的硬编码
|
||||
|
||||
- `engine.ts:83` —— `spawnMonsters(this.world, opts.stats, 8, opts.spawn, 260, this.terrain)`
|
||||
怪物数量写死 8、散布半径写死 260。应改为由调用方传入(M7 会用 `Levels.txt` 的
|
||||
`NumMon` / `MonDen` 填充)。
|
||||
- `engine.ts:89-99` —— NPC 用 `angle = attempt * 1.7 + 0.6`、半径 56 的**合成圆环**放置。
|
||||
`act-scene.ts:809-815` 已经用真实 DS1 坐标覆盖了它,但这段兜底逻辑应下沉为可选项,
|
||||
避免将来有人误用。
|
||||
|
||||
### 必须遵守的铁律
|
||||
|
||||
- 怪物与投射物必须插入**现有的等距深度排序**(`pushEntity`),不能另起一层,
|
||||
否则会穿墙穿屋顶;屋顶层(DS1 wall type 15)仍最后画
|
||||
- `CombatWorld.player` 与 `CombatWorld.players[i]` **必须是同一个对象**(用 `rebindPlayer()`)
|
||||
- 渲染后**必须 `renderer.flush()`**
|
||||
- 模拟层保持纯函数:禁 `Date.now()` / `Math.random()`
|
||||
|
||||
### 验收
|
||||
|
||||
- [ ] `acts.html` 第一幕荒野上能被怪物追、能打死怪、能捡掉落、能 K/L 存读档
|
||||
- [ ] 角色从墙内出生时能**沿梯度**脱困(不是随机游走)
|
||||
- [ ] 新增 `tests/iso-terrain.test.ts` 验证等距碰撞计数的各向异性
|
||||
- [ ] `map.html` 及 `map-scene.ts` 已删除,且 `verify:all` 仍全绿
|
||||
- [ ] `npm run typecheck` 0 错误、`npm test` 不低于 442 passed 基线
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue