Go to file
troytt 46218f4280 verify-tiles: 给 DT1 库缓存加上限,避免跑全量时内存涨到数 GB
解码出来的 DT1 库很占内存(每个瓦片的块像素是 width×bitmapHeight 的 Uint8Array,一个库几十 MB),
原来无限缓存跑完 35 关会到 GB 级。与打包器一致地限制为同时 16 个库(比任何单一关卡类型用到的都多,
所以一关之内不会重复解码)。实测:全量跑 35 关峰值 RSS ≈ 1.9 GB,堆上限 2 GB,结束后立即回落。

顺带记录本地实测结果(7/7 通过):
  地面 28704 槽 / 墙 9514 槽 / 屋顶 492 槽
  地面槽类型全为 0(0 个错配)、墙/屋顶没画地面瓦片、looseRefs 0、missingTiles 0、位置公式 0 不符、
  411/411 个顶角右半都带了 type-4 伙伴、缺失伙伴 0
2026-09-14 09:28:07 +08:00
samples/fixtures 加入自产夹具美术(samples/fixtures,492 KB) 2026-09-13 18:27:16 +08:00
scripts verify-tiles: 给 DT1 库缓存加上限,避免跑全量时内存涨到数 GB 2026-09-14 09:28:07 +08:00
src 修复顶角墙缺左半:DS1 只列 type 3,引擎会自己补画 type 4 2026-09-14 09:18:43 +08:00
tools/go-oracle 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
.gitignore 加入烘焙图集包(samples/d2-packs,66 MB / 275 个文件) 2026-09-13 18:29:28 +08:00
HANDOVER.md README 精简为「项目总览 + 部署与使用」 2026-09-13 18:38:04 +08:00
README.md README 精简为「项目总览 + 部署与使用」 2026-09-13 18:38:04 +08:00
THIRD_PARTY_NOTICES.md 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
acts.html 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
index.html 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
map.html 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
net.html 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
package-lock.json 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
package.json 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
tsconfig.json 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
vite.config.ts 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
walk.html 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00

README.md

d2web

从零实现的暗黑破坏神 II 引擎(纯 TypeScript + Vite + WebGL2,零运行时依赖),在浏览器里读取 用户自备的经典版 D2 归档(MPQ),渲染等距地图、驱动角色走动,并支持把地图离线烘焙成 web 原生格式。

  • 仓库不含任何暴雪资源:没有 .mpq / .dc6 / .dt1 / 图集 PNG;原版数据要自己放进 samples/d2/。
  • 代码可复现一切:格式解码、地图构建、资源包烘焙、角色合成都在这份代码里,脚本可重跑。
  • 实现细节、验证记录与未完成项在 HANDOVER.md(README 只做总览与部署使用)。

总览

能力 说明
等距地图渲染 DS1 + DT1 投影(2:1 dimetric,格 80×40 px),地板 / 墙 / 屋顶 三层分别绘制;逐格按 RarityFrameIndex 加权随机选瓦片变体;画家序排序
碰撞 每格 5×5 个 sub-tile 标志位(一层内所有层做 OR 合并,含 type 13 影子层),角色按 sub-tile 判定走位
真实关卡 已烘焙 35 个预置关卡 / 62 个地图块,覆盖 5 个 act(罗格营地、修道院、大教堂、地下墓穴、鲁高因、库拉斯特码头、哈洛加斯…)
真·女法师 d2char.mpq 的 DCC + COF 逐层合成(8 方向;行走 SOWLHTH、站立 SONUHTH),不是占位棋子
资源包(离线烘焙) 索引色 PNG 图集页(≤2048²,PLTE + tRNS 取自该 act 的 pal.pl2)+ scene.json(绘制列表、碰撞 RLE、出生点、对象元数据);首屏只加载视口需要的页,其余后台补
按需读归档 也可不烘焙,直接用 HTTP Range 只拉需要的 MPQ 字节当场解码(?live=1,慢,作为回退路径)
三级关卡选择器 章节 → 场景 → 细分场景(如 第一章 / 罗格营地 / 北);中文场景名见 src/game/level-names-zh.ts

尚未实现:怪物与可破坏物品的美术(对象的位置与美术成员已进包,静态帧未接);70 个随机迷宫 + 31 个野外关卡(鲜血荒野等)尚未生成 —— 现状与修复清单见 HANDOVER.md §4f。

代码结构

src/mpq/       MPQ v1 容器:hash/block 表、Storm 加密、分扇区编解码、PKWARE implode/explode、Range 读取
src/formats/   dc6 / ds1 / dt1 / pal / pl2 / cel / pcx / tbl / sprite / dcc / cof / bitstream
src/game/      等距场景与碰撞、关卡表解析、DCC+COF 角色合成、对象与物品解析、确定性 RNG
src/render/    WebGL2 渲染器(多图集;纹理切换时 flush 以保画家序)
src/scene/     页面级装配(资源包优先,失败回退到直读归档)
src/net/ src/sim/   联机中继与确定性锁步(夹具地图上的 2–4 人合作)
scripts/       资源包烘焙、格式/数据/部署校验、无头浏览器检查、夹具生成
*.html         五个入口页面:acts / index / map / walk / net

部署与使用

1. 前置:自备游戏数据

把你自己合法拥有的经典版暗黑破坏神 II(1.13c) 数据放进 samples/d2/,至少需要 d2data.mpq、d2char.mpq、d2exp.mpq、Patch_D2.mpq。资源包模式(页面默认)只需要 samples/d2-packs/——本仓库已附带 62 个地图块,所以没有原版数据也能看地图。

2. 本地跑起来

npm install                                          # 仅 vite / typescript / @types/node
node scripts/make-fixtures.ts     samples/fixtures    # 夹具:DC6 + 调色板 + 文本表
node scripts/make-map-fixtures.ts samples/fixtures    # 夹具:DT1 + DS1 + 夹具 MPQ
npm run pack:data                                    # 从 samples/d2/ 烘焙 62 个地图块 → samples/d2-packs
npm run dev                                          # http://127.0.0.1:5173/acts.html

pack:data 需要原版 MPQ(约 2 分钟);只用仓库自带的 samples/d2-packs/ 可以跳过它, 代价是 ?live=1 与重新烘焙不可用。

3. 页面与操作

页面 用途
/acts.html 主页面:全屏画布 + 三级选择器(章节 / 场景 / 细分场景)+ HUD
/index.html MPQ 资源检查器(头、存储标志、压缩掩码分布、成员列表、精灵预览)
/map.html?data=samples/fixtures 单人战役沙盒(走路 / 战斗 / 掉落 / 拾取 / 技能 / 任务 / 存读档)
/walk.html?sample=<你的.mpq> 可行走演示(D1 试玩版 spawn.mpq 已验证)
/net.html?data=samples/fixtures&ws=ws://127.0.0.1:8787&peers=3&peer=0 2–4 人联机(先 npm run net-server)

主页面查询参数:

?act=1..5                章节
?level=<地图块 label>     直接指定细分场景,如 33-act-1-cathedral-cathy3
?quadrant=<DS1 名>       直读归档模式下指定该关卡的某个 DS1
?live=1                  不走资源包,直接按 HTTP Range 读 MPQ 解码(慢)
?pack=<目录>             换一个资源包目录      ?base=<目录>   换一个归档目录

操作:WASD / 方向键移动(按屏幕方向),滚轮 或 + / - 缩放,0 复位;画布随窗口自适应并铺满视口。

4. 部署到服务器

npm run build:game                # → dist-game/(base=/diablo2/)
rm -rf /var/www/d2web   && mkdir -p /var/www/d2web   && cp -r dist-game/.       /var/www/d2web/
rm -rf /var/www/d2packs && mkdir -p /var/www/d2packs && cp -r samples/d2-packs/. /var/www/d2packs/
mkdir -p /var/www/d2data && ln -f samples/d2/*.mpq /var/www/d2data/     # 只有 ?live=1 用得到

nginx(/etc/nginx/sites-enabled/default):

location = /diablo2  { return 301 /diablo2/; }
location = /diablo2/ { rewrite ^ /diablo2/acts.html last; }
location /diablo2/   { alias /var/www/d2web/; autoindex off; add_header Cache-Control 'no-store'; }

location /diablo2/packs/ {                       # 资源包:可缓存
    alias /var/www/d2packs/;
    autoindex off;
    add_header Cache-Control 'public, max-age=3600';
}

location ~ ^/diablo2/data/(?<d2file>[A-Za-z0-9_.-]+\.mpq)$ {   # 只暴露 .mpq,不列目录
    alias /var/www/d2data/$d2file;
    autoindex off;
    add_header Accept-Ranges bytes;
    add_header Cache-Control 'public, max-age=3600';
}

要点:归档必须支持 Range(否则页面会退化成整文件下载);/root 是 0700,所以用硬链接暴露给 nginx。 部署后跑 npm run verify:deploy 自检。

5. 自检命令

npm run verify:deploy         # 线上:页面 / 资源包索引 / 图集 / Range 206 / 旧入口 410
npm run verify:packs          # 资源包与"现读现解"逐项比对(绘制序、碰撞栅格逐字节、每帧像素哈希)
npm run verify:tiles          # 每个槽位画的是不是该有的瓦片类型(地面=type 0)+ 位置公式
npm run verify:alignment      # 墙 / 地面基线、屋顶偏移与引擎公式一致
npm run verify:object-lookup  # DS1 对象 id → token/mode 与 Objects.txt 交叉核对
npm run verify:acts           # 关卡表 → DS1 → DT1 → 等距场景 + 碰撞
npm run verify:d2             # 真 MPQ 数据端到端解码
npm run verify:implode        # PKWARE implode 解码器(全成员)
npm run typecheck             # tsc --noEmit

数据与许可

  • 仓库不含暴雪美术:原版 MPQ、reference/ 下的第三方参考源码都不入库(规则写在 .gitignore 与 THIRD_PARTY_NOTICES.md);samples/fixtures/(本项目自产)与 samples/d2-packs/(本仓库内的派生数据)仅在本私有仓库内流通。
  • 使用本引擎需要你自己合法拥有的游戏数据。将来落地的生成类关卡(迷宫 / 野外)会在页面与文档中标注为 "按官方参数表复现,非引擎原版布局"(生成逻辑移植自 D2MOO)。