[M6] 收敛到 act-scene 单轨:舍弃 map-scene.ts #21

Closed
opened 2026-09-15 06:35:57 +00:00 by troytt · 1 comment
Owner

里程碑 M6 · 重要程度 ★★★★★ · 风险 中 · 依赖:无(所有后续里程碑的前提)
完整路线图见 ROADMAP.md


⚠️ 方案已变更(2026-09-15)

本 issue 最初的方案是「抽出第三个共享层,两个页面都变薄」,并断言
act-scene.ts 无战斗 / 无物品 / 无技能 / 无存档。

经逐行核查,那个断言是错的。 下面是修正后的事实。

核查结论一:GameEngine 已经就是共享层

src/game/engine.ts(350 行)只依赖:

export interface WorldMapProvider {
  readonly widthPx: number
  readonly heightPx: number
  overlap(x: number, y: number): number
}

完全与地图实现无关。 它已经封装了战斗、物品/背包、技能/投射物、任务/NPC、
存读档、掉落/拾取的全部逻辑。

原计划要新建的 src/scene/shared/gameplay.ts 不需要了。

核查结论二:act-scene.ts 早就接入了玩法

位置 内容
act-scene.ts:34 import { GameEngine, syncEngineState }
act-scene.ts:777 new GameEngine(...)
act-scene.ts:820 engine.tick({...})
act-scene.ts:962 绘制 engine.world.monsters
act-scene.ts:968 绘制 engine.npcEntities
act-scene.ts:978 绘制 engine.projectiles
act-scene.ts:984 绘制 engine.ground
act-scene.ts:826-827 存读档经引擎:saving: input.saving, loading: input.loading
act-scene.ts:809-815 NPC 用真实 DS1 坐标,覆盖引擎的合成圆环兜底

之前 grep 不到是因为查的是 tickCombat / spawnMonsters 的直接导入,
而 act-scene 是通过 GameEngine 间接获得这些能力的。

「真地图上没有怪物」的全部原因只有一行 —— act-scene.ts:785:

stats: [],   // → combat.ts:376  if (stats.length === 0) return 0

核查结论三:map-scene.ts 的逻辑不正确

缺陷 1 · 孤儿世界 + 死代码

// 399-403 行:建了一个世界并往里刷怪
let world: CombatWorld = createWorld(spawn.x, spawn.y)
state.monstersSpawned = spawnMonsters(world, stats, 8, spawn, 260, terrain)
let player = world.player          // ← 此后再未使用

// 453 行:GameEngine 又建了自己的世界(engine.ts:82-83)
const engine = new GameEngine(terrain, {...})
state.monstersSpawned = engine.world.monsters.length   // ← 覆盖上面那行

前一个世界从头到尾没有被 tick 过。

缺陷 2 · 怪物动画冻结(真实 bug,不只是死代码)

// 572 行
const frame = group[moving ? (world.tick + monster.index * 3) % group.length : 0]

读的是孤儿世界的 tick。而 world.tick += 1 只在 combat.ts:518 的
tickCombat 里发生,map-scene 从不直接调用它(只调 engine.tick,那个 tick 的是
engine.world)。

world.tick 恒为 0,表达式退化为 group[(monster.index * 3) % group.length]
—— 每只怪一个固定帧。怪物在追击和攻击时动画是静止的。

缺陷 3 · 存读档绕过引擎

// 488-489 行:明确告诉引擎「不要存不要读」
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 后未清理外壳」留下的残骸。


修正后的范围

1. 打通 stats: []

act-scene.ts:785 改为接收真实怪物定义。真实数据接入属于 #22,
M6 只需打通链路并用占位数据验证。

2. 修复碰撞语义退化

// 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 恒成立 → 在墙里随机游走,能出来但不可靠、无方向性

  • [NEW] src/game/iso-terrain.ts —— 等距 CollisionGrid → 计数型 WorldMapProvider

⚠️ 不能直接搬 map.ts 的 blockedOverlap:它假设 sub-tile 是正方形,
而等距下是 16×8 各向异性(ORTHO_SUB_TILE_WIDTH=16 / ORTHO_SUB_TILE_HEIGHT=8)。

3. 删除 map-scene.ts 与 map.html

  • [DELETE] src/scene/map-scene.ts(1055 行)
  • [DELETE] map.html
  • [MODIFY] src/game/map.ts —— 保留仍被 d2map.ts 依赖的 SUB_TILES_PER_TILE 等

⚠️ 删除前必须安置 fixture 回归测试台。
map.html 是 scripts/browser/checks/map-save-load.js 等无头检查的落点,
verify:all 里的 verify-m4 / verify-m5 也依赖夹具路径。
这些检查跑得比真地图快得多,不能一起删。
方案:改为直接驱动 GameEngine + 一个夹具 WorldMapProvider(纯逻辑、无场景层)。

4. GameEngine 的硬编码表格化

  • engine.ts:83 —— spawnMonsters(this.world, opts.stats, 8, opts.spawn, 260, this.terrain)
    怪物数量写死 8、散布半径写死 260。改为由调用方传入
    (#22 会用 Levels.txt 的 NumMon / MonDen 填充)。
  • engine.ts:89-99 —— NPC 用 angle = attempt * 1.7 + 0.6、半径 56 的合成圆环放置。
    act-scene.ts:809-815 已用真实 DS1 坐标覆盖,但这段兜底应下沉为可选项,避免误用。

必须遵守的铁律

  • 怪物与投射物必须插入现有的等距深度排序(pushEntity),不能另起一层,否则穿墙穿屋顶
  • 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 基线
> **里程碑 M6** · 重要程度 ★★★★★ · 风险 中 · 依赖:无(所有后续里程碑的前提) > 完整路线图见 [`ROADMAP.md`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/branch/main/ROADMAP.md) --- ## ⚠️ 方案已变更(2026-09-15) 本 issue 最初的方案是「抽出第三个共享层,两个页面都变薄」,并断言 `act-scene.ts` 无战斗 / 无物品 / 无技能 / 无存档。 **经逐行核查,那个断言是错的。** 下面是修正后的事实。 ## 核查结论一:GameEngine 已经就是共享层 `src/game/engine.ts`(350 行)只依赖: ```ts export interface WorldMapProvider { readonly widthPx: number readonly heightPx: number overlap(x: number, y: number): number } ``` **完全与地图实现无关。** 它已经封装了战斗、物品/背包、技能/投射物、任务/NPC、 存读档、掉落/拾取的全部逻辑。 原计划要新建的 `src/scene/shared/gameplay.ts` **不需要了**。 ## 核查结论二:act-scene.ts 早就接入了玩法 | 位置 | 内容 | |---|---| | `act-scene.ts:34` | `import { GameEngine, syncEngineState }` | | `act-scene.ts:777` | `new GameEngine(...)` | | `act-scene.ts:820` | `engine.tick({...})` | | `act-scene.ts:962` | 绘制 `engine.world.monsters` | | `act-scene.ts:968` | 绘制 `engine.npcEntities` | | `act-scene.ts:978` | 绘制 `engine.projectiles` | | `act-scene.ts:984` | 绘制 `engine.ground` | | `act-scene.ts:826-827` | 存读档**经引擎**:`saving: input.saving, loading: input.loading` | | `act-scene.ts:809-815` | NPC 用**真实 DS1 坐标**,覆盖引擎的合成圆环兜底 | 之前 grep 不到是因为查的是 `tickCombat` / `spawnMonsters` 的**直接导入**, 而 act-scene 是通过 `GameEngine` **间接**获得这些能力的。 **「真地图上没有怪物」的全部原因只有一行** —— `act-scene.ts:785`: ```ts stats: [], // → combat.ts:376 if (stats.length === 0) return 0 ``` ## 核查结论三:map-scene.ts 的逻辑不正确 ### 缺陷 1 · 孤儿世界 + 死代码 ```ts // 399-403 行:建了一个世界并往里刷怪 let world: CombatWorld = createWorld(spawn.x, spawn.y) state.monstersSpawned = spawnMonsters(world, stats, 8, spawn, 260, terrain) let player = world.player // ← 此后再未使用 // 453 行:GameEngine 又建了自己的世界(engine.ts:82-83) const engine = new GameEngine(terrain, {...}) state.monstersSpawned = engine.world.monsters.length // ← 覆盖上面那行 ``` 前一个世界从头到尾**没有被 tick 过**。 ### 缺陷 2 · 怪物动画冻结(真实 bug,不只是死代码) ```ts // 572 行 const frame = group[moving ? (world.tick + monster.index * 3) % group.length : 0] ``` 读的是**孤儿世界**的 `tick`。而 `world.tick += 1` 只在 `combat.ts:518` 的 `tickCombat` 里发生,map-scene 从不直接调用它(只调 `engine.tick`,那个 tick 的是 `engine.world`)。 **`world.tick` 恒为 0**,表达式退化为 `group[(monster.index * 3) % group.length]` —— 每只怪一个固定帧。**怪物在追击和攻击时动画是静止的。** ### 缺陷 3 · 存读档绕过引擎 ```ts // 488-489 行:明确告诉引擎「不要存不要读」 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` 后未清理外壳」留下的残骸。 --- ## 修正后的范围 ### 1. 打通 `stats: []` `act-scene.ts:785` 改为接收真实怪物定义。真实数据接入属于 #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` 恒成立 → 在墙里**随机游走**,能出来但不可靠、无方向性 - **[NEW] `src/game/iso-terrain.ts`** —— 等距 `CollisionGrid` → 计数型 `WorldMapProvider` > ⚠️ 不能直接搬 `map.ts` 的 `blockedOverlap`:它假设 sub-tile 是正方形, > 而等距下是 **16×8 各向异性**(`ORTHO_SUB_TILE_WIDTH=16` / `ORTHO_SUB_TILE_HEIGHT=8`)。 ### 3. 删除 map-scene.ts 与 map.html - **[DELETE] `src/scene/map-scene.ts`**(1055 行) - **[DELETE] `map.html`** - **[MODIFY] `src/game/map.ts`** —— 保留仍被 `d2map.ts` 依赖的 `SUB_TILES_PER_TILE` 等 > ⚠️ **删除前必须安置 fixture 回归测试台。** > `map.html` 是 `scripts/browser/checks/map-save-load.js` 等无头检查的落点, > `verify:all` 里的 `verify-m4` / `verify-m5` 也依赖夹具路径。 > 这些检查跑得比真地图快得多,**不能一起删**。 > 方案:改为直接驱动 `GameEngine` + 一个夹具 `WorldMapProvider`(纯逻辑、无场景层)。 ### 4. GameEngine 的硬编码表格化 - `engine.ts:83` —— `spawnMonsters(this.world, opts.stats, 8, opts.spawn, 260, this.terrain)` 怪物数量写死 **8**、散布半径写死 **260**。改为由调用方传入 (#22 会用 `Levels.txt` 的 `NumMon` / `MonDen` 填充)。 - `engine.ts:89-99` —— NPC 用 `angle = attempt * 1.7 + 0.6`、半径 56 的**合成圆环**放置。 `act-scene.ts:809-815` 已用真实 DS1 坐标覆盖,但这段兜底应下沉为可选项,避免误用。 ## 必须遵守的铁律 - 怪物与投射物必须插入**现有的等距深度排序**(`pushEntity`),不能另起一层,否则穿墙穿屋顶 - `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 基线
troytt changed title from [M6] 两轨合流:抽出共享玩法层,让真实地图跑起玩法 to [M6] 收敛到 act-scene 单轨:舍弃 map-scene.ts 2026-09-15 06:52:29 +00:00
Author
Owner

已完成 · 提交 ae94755

真正的根因和原来写的不一样

真地图上一直没有玩法,原因不是「两轨没合流」。act-scene.ts 早就通过
GameEngine 接入了战斗、物品、技能、任务、存档。真正的原因只有一行:

// act-scene.ts,改之前
stats: [],

而 combat.ts 的 spawnMonsters 第一句就是 if (stats.length === 0) return 0。

所以这个里程碑没有新建共享层(原计划的 src/scene/shared/gameplay.ts 不需要,
GameEngine 本身就是那一层),而是直接把 act-scene.ts 接上,并删掉 map-scene.ts。

改了什么

1. 碰撞语义从布尔改为计数 —— 新增 src/game/iso-terrain.ts

combat.ts 的脱困规则是 if (overlap(next) <= current) 才走。原来传的是
collides(...) ? 1 : 0,在墙里读到的永远是 1,1 <= 1 恒真,角色在墙里随机游走。
改成计数之后,同一条规则变成梯度下降:陷进去 5 格深可以走到 3 格深,朝 7 格深的方向
会被拒绝。

不能复用 map.ts 的 blockedOverlap:那个函数遍历一个矩形的格索引区间,只有在
sub-tile 是屏幕矩形时才成立。等距下 sub-tile 是 16×8 的菱形,屏幕矩形映射过去是
错切且交错的,没有矩形索引区间可遍历。所以改为在包围盒上采样一个固定点阵,逐点问
isBlockedAt。点阵固定而非自适应 —— 计数必须是位置的纯函数,存档与 lockstep 都依赖这点。

2. 引擎硬编码下沉为选项 —— monsterCount(原写死 8)、monsterSpread(原写死 260)、
npcRingFallback(NPC 合成圆环;act-scene 本来就用真实 DS1 坐标覆盖掉了,现在干脆不构造)。
M7 会用 Levels.txt 的 NumMon / MonDen 填充,目前是占位的 12。

3. 顺带补上一个死接口 —— EngineInput.saving / loading

这两个字段声明了,但 tick() 从不读它们:存读档是 map-scene.ts 在 tick 之外自己
调 localStorage 做的。也就是说 act-scene.ts 虽然老老实实把 input.saving 传了进去,
K/L 其实什么也不会发生。

现在引擎新增 SnapshotStore 端口(read() / write()),在 tick 的最末尾按上升沿
处理。这样存档点永远是一个已结算的 tick;按住键不会每 tick 存一次;localStorage 仍留在
场景层,模拟层不碰宿主环境;配额满 / 隐私窗口 / 旧格式存档都记进 metrics.saveError。

4. 删除 map-scene.ts(1054 行)与 map.html

它的三个确凿缺陷(孤儿 world / 怪物动画帧恒为 0 / 存读档绕过引擎)加 12 个死 import,
不值得保留。scripts/browser/checks/map-save-load.js 改写为纯逻辑的
tests/engine-save-load.test.ts:同样的断言(走位砍怪 → 存档 → 继续跑 → 读档,比对
位置/击杀/金币/地面/等级),但不需要 headless Chromium 和 WebGL 上下文,而且顺带覆盖了
浏览器版从未走到的 in-tick 存读档路径
。verify-m4 / verify-m5 只依赖 src/game 与
src/net,原样保留、仍然全绿。

顺手修的工具缺陷

scripts/browser/inspect-page.mjs 按「第一个 type === 'page' 的目标」附着,而 Chrome
在打开命令行 URL 之前会先建一个空白标签页,200 ms 的轮询往往抢到空白页,之后的断言就
���「没有场景状态」—— 这和页面启动失败完全无法区分,很容易让人去应用代码里找根本不存在
的 bug。已改为等待 URL 匹配的目标。

验证

命令 结果
npx tsc --noEmit 0 错误
npm test 461 passed / 2 skipped(基线 442,新增 14,无回归)
npm run verify:all 全绿
npm run build 4 个入口,1.89s

新增测试:

  • tests/iso-terrain.test.ts(8 项)—— 空地为 0、完全陷入时饱和、越界视为阻挡、
    沿出墙方向单调递减且出现中间值(布尔谓词永远做不到这点)、身体边缘先于中心计入、
    y 轴采样密度是 x 轴的两倍(16×8 各向异性)
  • tests/engine-save-load.test.ts(6 项)—— 漂移后能恢复到存档点、按住键只存一次、
    按住读档键不会把角色钉住、空槽位不算错误、store 抛异常不掀翻主循环、不配 store 时输入惰性

验收对照

  • 角色从墙内出生时能沿梯度脱困(不是随机游走)
  • 新增 tests/iso-terrain.test.ts 验证等距碰撞计数的各向异性
  • map.html 及 map-scene.ts 已删除,且 verify:all 仍全绿
  • npm run typecheck 0 错误、npm test 不低于 442 passed 基线
  • acts.html 上能被怪物追、能打死怪、能捡掉落、能 K/L 存读档 —— 代码路径已接通,
    由 tests/engine-save-load.test.ts 覆盖

一处如实说明:最后一条的浏览器实测未能完成。当前环境里 headless Chrome
无法访问任何 HTTP 地址(包括 127.0.0.1),而同机 curl 正常。症状很有迷惑性:
CDP 目标列表会显示正确的 URL(Chrome 乐观地先报告待定导航),但页面里
location.href 仍是 about:blank、fetch() 报 Failed to fetch、
Page.navigate 直接挂住。换 chromium 二进制、换主机名、加 --no-proxy-server
都无效。判定方法已写进 scripts/browser/README.md。
这一步的检查脚本 scripts/browser/checks/act-gameplay.js 已经写好并入库,
换到能联网的环境直接跑即可。

下一步:#22(真实怪物 —— 从 Levels.txt / MonStats.txt / SuperUniques.txt 读取
怪物池、密度、精英怪与超级唯一怪)。

## 已完成 · 提交 `ae94755` ### 真正的根因和原来写的不一样 真地图上一直没有玩法,原因**不是**「两轨没合流」。`act-scene.ts` 早就通过 `GameEngine` 接入了战斗、物品、技能、任务、存档。真正的原因只有一行: ```ts // act-scene.ts,改之前 stats: [], ``` 而 `combat.ts` 的 `spawnMonsters` 第一句就是 `if (stats.length === 0) return 0`。 所以这个里程碑没有新建共享层(原计划的 `src/scene/shared/gameplay.ts` **不需要**, `GameEngine` 本身就是那一层),而是直接把 `act-scene.ts` 接上,并删掉 `map-scene.ts`。 ### 改了什么 **1. 碰撞语义从布尔改为计数** —— 新增 [`src/game/iso-terrain.ts`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/branch/main/src/game/iso-terrain.ts) `combat.ts` 的脱困规则是 `if (overlap(next) <= current) 才走`。原来传的是 `collides(...) ? 1 : 0`,在墙里读到的永远是 1,`1 <= 1` 恒真,角色在墙里**随机游走**。 改成计数之后,同一条规则变成梯度下降:陷进去 5 格深可以走到 3 格深,朝 7 格深的方向 会被拒绝。 不能复用 `map.ts` 的 `blockedOverlap`:那个函数遍历一个**矩形的格索引区间**,只有在 sub-tile 是屏幕矩形时才成立。等距下 sub-tile 是 **16×8 的菱形**,屏幕矩形映射过去是 错切且交错的,没有矩形索引区间可遍历。所以改为在包围盒上采样一个**固定点阵**,逐点问 `isBlockedAt`。点阵固定而非自适应 —— 计数必须是位置的纯函数,存档与 lockstep 都依赖这点。 **2. 引擎硬编码下沉为选项** —— `monsterCount`(原写死 8)、`monsterSpread`(原写死 260)、 `npcRingFallback`(NPC 合成圆环;act-scene 本来就用真实 DS1 坐标覆盖掉了,现在干脆不构造)。 M7 会用 `Levels.txt` 的 `NumMon` / `MonDen` 填充,目前是占位的 12。 **3. 顺带补上一个死接口** —— `EngineInput.saving` / `loading` 这两个字段声明了,但 `tick()` **从不读它们**:存读档是 `map-scene.ts` 在 tick 之外自己 调 `localStorage` 做的。也就是说 `act-scene.ts` 虽然老老实实把 `input.saving` 传了进去, **K/L 其实什么也不会发生**。 现在引擎新增 `SnapshotStore` 端口(`read()` / `write()`),在 tick 的**最末尾**按**上升沿** 处理。这样存档点永远是一个已结算的 tick;按住键不会每 tick 存一次;`localStorage` 仍留在 场景层,模拟层不碰宿主环境;配额满 / 隐私窗口 / 旧格式存档都记进 `metrics.saveError`。 **4. 删除 `map-scene.ts`(1054 行)与 `map.html`** 它的三个确凿缺陷(孤儿 world / 怪物动画帧恒为 0 / 存读档绕过引擎)加 12 个死 import, 不值得保留。`scripts/browser/checks/map-save-load.js` 改写为纯逻辑的 `tests/engine-save-load.test.ts`:同样的断言(走位砍怪 → 存档 → 继续跑 → 读档,比对 位置/击杀/金币/地面/等级),但不需要 headless Chromium 和 WebGL 上下文,而且**顺带覆盖了 浏览器版从未走到的 in-tick 存读档路径**。`verify-m4` / `verify-m5` 只依赖 `src/game` 与 `src/net`,原样保留、仍然全绿。 ### 顺手修的工具缺陷 `scripts/browser/inspect-page.mjs` 按「第一个 `type === 'page'` 的目标」附着,而 Chrome 在打开命令行 URL **之前**会先建一个空白标签页,200 ms 的轮询往往抢到空白页,之后的断言就 ���「没有场景状态」—— 这和页面启动失败**完全无法区分**,很容易让人去应用代码里找根本不存在 的 bug。已改为等待 URL 匹配的目标。 ### 验证 | 命令 | 结果 | | --- | --- | | `npx tsc --noEmit` | 0 错误 | | `npm test` | **461 passed / 2 skipped**(基线 442,新增 14,无回归) | | `npm run verify:all` | 全绿 | | `npm run build` | 4 个入口,1.89s | 新增测试: - `tests/iso-terrain.test.ts`(8 项)—— 空地为 0、完全陷入时饱和、越界视为阻挡、 **沿出墙方向单调递减且出现中间值**(布尔谓词永远做不到这点)、身体边缘先于中心计入、 **y 轴采样密度是 x 轴的两倍**(16×8 各向异性) - `tests/engine-save-load.test.ts`(6 项)—— 漂移后能恢复到存档点、按住键只存一次、 按住读档键不会把角色钉住、空槽位不算错误、store 抛异常不掀翻主循环、不配 store 时输入惰性 ### 验收对照 - [x] 角色从墙内出生时能沿梯度脱困(不是随机游走) - [x] 新增 `tests/iso-terrain.test.ts` 验证等距碰撞计数的各向异性 - [x] `map.html` 及 `map-scene.ts` 已删除,且 `verify:all` 仍全绿 - [x] `npm run typecheck` 0 错误、`npm test` 不低于 442 passed 基线 - [x] `acts.html` 上能被怪物追、能打死怪、能捡掉落、能 K/L 存读档 —— **代码路径已接通**, 由 `tests/engine-save-load.test.ts` 覆盖 > **一处如实说明**:最后一条的**浏览器实测未能完成**。当前环境里 headless Chrome > 无法访问任何 HTTP 地址(包括 `127.0.0.1`),而同机 `curl` 正常。症状很有迷惑性: > CDP 目标列表**会**显示正确的 URL(Chrome 乐观地先报告待定导航),但页面里 > `location.href` 仍是 `about:blank`、`fetch()` 报 `Failed to fetch`、 > `Page.navigate` 直接挂住。换 chromium 二进制、换主机名、加 `--no-proxy-server` > 都无效。判定方法已写进 `scripts/browser/README.md`。 > 这一步的检查脚本 `scripts/browser/checks/act-gameplay.js` 已经写好并入库, > 换到能联网的环境直接跑即可。 下一步:#22(真实怪物 —— 从 `Levels.txt` / `MonStats.txt` / `SuperUniques.txt` 读取 怪物池、密度、精英怪与超级唯一怪)。
Sign in to join this conversation.
No Label
No Milestone
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#21
No description provided.