真地图上一直没有玩法,原因不是「两轨没合流」——act-scene.ts 早就通过 GameEngine 接入了战斗、物品、技能、任务、存档。真正的原因只有一行:act-scene.ts 给引擎传的是 `stats: []`,而 combat.ts 的 spawnMonsters 在 stats 为空时直接 return 0。 同时 map-scene.ts 被查出三个确凿缺陷,不值得保留: 1. 它建了一个从不被 tick 的孤儿 world,`let player = world.player` 之后再没用过 2. 怪物动画取帧读的是那个孤儿 world 的 tick,而 world.tick 只在 tickCombat 里自增, map-scene 从不直接调它,于是 tick 恒为 0,每只怪只显示一个固定帧——追击和攻击时 怪物是静止的 3. 存读档绕过引擎,在 tick 之外直接读写 localStorage 外加 12 个死 import。 本次改动: 碰撞语义从布尔改为计数(src/game/iso-terrain.ts) combat.ts 的脱困规则是 `if (overlap(next) <= current) 才走`。原来 act-scene 传的是 `collides(...) ? 1 : 0`,在墙里读到的永远是 1,`1 <= 1` 恒真,于是角色在墙里随机 游走。改成计数之后同一条规则变成了梯度下降:陷进去 5 格深可以走到 3 格深,朝 7 格 深的方向会被拒绝。 不能直接复用 map.ts 的 blockedOverlap:那个函数遍历一个矩形的格索引区间,只有在 sub-tile 是屏幕矩形时才成立;等距下 sub-tile 是 16×8 的菱形,屏幕矩形映射过去是 错切且交错的,没有矩形索引区间可遍历。所以这里改为在包围盒上采样一个固定点阵, 逐点问 isBlockedAt。点阵固定而非自适应,保证计数是位置的纯函数——存档与 lockstep 都依赖这一点。 引擎硬编码下沉为选项(src/game/engine.ts) monsterCount(原写死 8)、monsterSpread(原写死 260)、npcRingFallback(NPC 合成 圆环,act-scene 本来就用真实 DS1 坐标覆盖掉了,现在干脆不构造)。 补上一个死接口:EngineInput.saving / loading 这两个字段声明了但 tick() 从不读,也就是说 act-scene 的 K/L 其实什么也不做。 现在引擎新增 SnapshotStore 端口(read/write),在 tick 末尾按上升沿处理: 存档点永远是一个已结算的 tick;按住键不会每 tick 存一次;localStorage 仍留在场景 层,模拟层不碰宿主环境。配额满、隐私窗口、旧格式存档都记进 metrics.saveError。 删除 map-scene.ts 与 map.html map-save-load.js 这个无头检查改写为纯逻辑的 tests/engine-save-load.test.ts: 同样的断言(走位砍怪 → 存档 → 继续跑 → 读档,比对位置/击杀/金币/地面/等级), 但不需要 headless Chromium 和 WebGL 上下文,而且顺带覆盖了浏览器版从未走到的 in-tick 存读档路径。verify-m4 / verify-m5 只依赖 src/game 与 src/net,原样保留。 顺带修了 inspect-page.mjs 的一个目标选择缺陷:它按「第一个 type 为 page 的目标」 附着,而 Chrome 在打开命令行 URL 之前会先建一个空白标签页,轮询往往抢到空白页, 之后的断言就报「没有场景状态」——和页面启动失败完全无法区分。改为等待 URL 匹配的 目标。(注:当前沙盒里 headless Chrome 完全没有网络,浏览器实测仍待补,已在 scripts/browser/README.md 记录判定方法。) 验证: npx tsc --noEmit 0 错误 npm test 461 passed / 2 skipped(基线 442,新增 14,无回归) npm run verify:all 全绿 npm run build 4 个入口,1.89s Fixes #21 TAG=agy CONV=c89513df-cd12-4749-b5bd-5e9f16639391 |
||
|---|---|---|
| .. | ||
| checks | ||
| README.md | ||
| inspect-page.mjs | ||
| many-pages.mjs | ||
README.md
浏览器验证工具(零依赖)
这些脚本用 Node 内置的 WebSocket 直接讲 CDP(Chrome DevTools Protocol),
不需要 puppeteer/playwright 这类依赖。目的只有一个:给"页面里到底发生了什么"留下证据,
因为像素和截图的判断在这套环境里不可靠(模型读不了图,readPixels 在合成后会返回清空缓冲)。
inspect-page.mjs —— 单页检查
CHROME=/root/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome
tsx scripts/browser/inspect-page.mjs "$CHROME" \
"http://127.0.0.1:5173/acts.html" \
scripts/browser/checks/act-state.js --screenshot /tmp/act.png
它会启动一个 headless Chromium(SwiftShader 软件 WebGL2),打开 URL,等待页面加载, 把 check 脚本的返回值打印成 JSON,可选截图。
many-pages.mjs —— N 页同时跑(联机用)
tsx scripts/browser/many-pages.mjs "$CHROME" \
"http://127.0.0.1:5173/net.html?data=samples/fixtures&ws=ws://127.0.0.1:8787&peers=3&peer=0" \
"http://127.0.0.1:5173/net.html?data=samples/fixtures&ws=ws://127.0.0.1:8787&peers=3&peer=1" \
"http://127.0.0.1:5173/net.html?data=samples/fixtures&ws=ws://127.0.0.1:8787&peers=3&peer=2" \
--seconds 20 --shot /tmp/coop
每一页是一个独立的 Chromium 实例(标签页级复用在这套 CDP 流程里会把两次导航混在一起,
曾经导致"两页都跑成了 peer 0"这种假象)。它按页分发不同的方向键,让每个角色只受本页按键驱动,
结束时打印每页的 window.__d2web* 状态。判定靠状态字段,不靠看图。
checks/ —— 页面内脚本
| 文件 | 目标页面 | 做什么 |
|---|---|---|
walk-state.js |
walk.html |
读真实 MPQ(暗黑1 spawn.mpq)后的就绪状态、tick、drawCalls、glError |
net-state.js |
net.html |
单机模式的状态:世界 tick、角色数、怪物数、tps |
act-state.js |
acts.html(?live=1) |
读归档路径:归档是否读入、DS1 象限、格数、瓦片引用缺失数、图集尺寸,以及按键后的位移与朝向 |
act-pack-state.js |
acts.html(默认) |
资源包路径:是否走 pack、首帧装了几页(证明懒加载)、出画面毫秒数、按键位移与朝向 |
已知限制:沙盒里 Chrome 没有网络
在某些受限环境(例如代理后的自动化沙盒)里,headless Chrome 无法访问任何 HTTP 地址,
包括 127.0.0.1。症状很有迷惑性:
- CDP 的
/json目标列表会显示你要的 URL(Chrome 乐观地先报告待定导航) - 但页面里
location.href仍是about:blank、document.readyState是complete - 页面内
fetch()报TypeError: Failed to fetch Page.navigate直接挂住不返回- 同一台机器上
curl一切正常
这和「场景启动失败」长得一模一样,很容易让人去应用代码里找根本不存在的 bug。
判定方法:如果 location.href === 'about:blank' 而目标列表里的 URL 是对的,
那就是环境问题,不是页面问题。
此时请改用纯逻辑测试(tests/ 下的 vitest),它们覆盖了同样的断言且不需要浏览器。
tests/engine-save-load.test.ts 就是这样从退役的 map-save-load.js 迁移过来的。