9.3 KiB
d2web
从零实现的暗黑破坏神 II 引擎(纯 TypeScript + Vite + WebGL2,零运行时依赖),在浏览器里读取 用户自备的经典版 D2 归档(MPQ),渲染等距地图、驱动角色走动,并支持把地图离线烘焙成 web 原生格式。
- 仓库不含任何暴雪资源:没有
.mpq/.dc6/.dt1/ 图集 PNG;原版数据要自己放进samples/d2/。 - 代码可复现一切:格式解码、地图构建、资源包烘焙、角色合成都在这份代码里,脚本可重跑。
- 路线图、实现细节、验证方法论与未完成项在
ROADMAP.md(README 只做总览与部署使用)。
总览
| 能力 | 说明 |
|---|---|
| 等距地图渲染 | DS1 + DT1 投影(2:1 dimetric,格 80×40 px),地板 / 墙 / 屋顶 三层分别绘制;逐格按 RarityFrameIndex 加权随机选瓦片变体;画家序排序 |
| 碰撞 | 每格 5×5 个 sub-tile 标志位(一层内所有层做 OR 合并,含 type 13 影子层),角色按 sub-tile 判定走位 |
| 真实关卡 | 已烘焙 365 张地图,覆盖 5 个 act:35 个预置关卡(罗格营地、修道院、大教堂、地下墓穴、鲁高因、库拉斯特码头、哈洛加斯…)+ 70 个随机迷宫 + 31 个野外关卡(生成逻辑移植自 D2MOO,按官方参数表复现) |
| 真·女法师 | 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 |
尚未实现:真实地图上的怪物还是占位配置——怪物种类、密度、精英怪、超级唯一怪都尚未从
Levels.txt / MonStats.txt / SuperUniques.txt 读取(见 M7);关卡之间还不连通(见 M8);
战斗数值不是暗黑2 的公式(见 M9);可破坏物品的美术(位置与美术成员已进包,DCC 帧未接)。
完整的里程碑排期见 ROADMAP.md。
代码结构
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/——它不在 Git 里(63 MB 的索引 PNG 每次重烘都会让仓库再长一份),
按下面的「资源包从哪来」下载解包后,没有原版数据也能看地图。
samples/d2/ 通常做成软链接,指向一个磁盘上的持久目录。切勿链到 /tmp——它是内存盘,
重启即清空,链接会变死链而角色/怪物退化成占位方块。本机的实际摆放、清单与逆向参考用的原版
DLL 见 docs/LOCAL_DATA.md。
2. 资源包从哪来(不在 Git 里)
图集包(62 个地图块 / 35 关,索引色 PNG + scene.json)不在本仓库里——它是 ~66 MB 的索引色
PNG,每次重烘几乎全部文件都变,而 PNG 无法被 git 的 delta 压缩,随仓库分发会让每烘焙一次仓库再长
~60 MB。它放在图库仓库,那里刻意只保留一个提交(每次重烘 orphan 提交 + force push),
体积恒定在「一份快照」:
git clone https://git.projectdiablo2.cn/troytt/diablo2-web-assets.git samples/d2-packs
只想要一份压缩包(不需要 git):
curl -O https://www.laiseek.xyz/diablo2/assets/d2-packs-2026-09-14.tar.xz
sha256sum d2-packs-2026-09-14.tar.xz # 应为 9cdfd0c19ac68de842c6725070b65d056e25f5bcc77f36d6d9a29cd9f1b431e8
tar xJf d2-packs-2026-09-14.tar.xz # 解出 samples/d2-packs/
仓库里只保留 .gitignore 规则与本文档;要自己生成包就照第 3 步 npm run pack:data 从原版 MPQ
烘焙,再用 npm run publish:packs 发布到图库仓库(脚本会重建单提交历史)。
3. 本地跑起来
npm install # 仅 vite / typescript / @types/node
tsx scripts/make-fixtures.ts samples/fixtures # 夹具:DC6 + 调色板 + 文本表
tsx 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 与重新烘焙不可用。
4. 页面与操作
| 页面 | 用途 |
|---|---|
/acts.html(或访问根路径 /) |
主页面:全屏画布 + 三级选择器(章节 / 场景 / 细分场景)+ HUD |
/arena.html |
战斗测试竞技场:攻防数值数学模型、技能加成、伤害计算器(1.13c 原版公式验证) |
主页面查询参数:
?act=1..5 章节
?level=<地图块 label> 直接指定细分场景,如 33-act-1-cathedral-cathy3
?quadrant=<DS1 名> 直读归档模式下指定该关卡的某个 DS1
?live=1 不走资源包,直接按 HTTP Range 读 MPQ 解码(慢)
?pack=<目录> 换一个资源包目录 ?base=<目录> 换一个归档目录
操作:WASD / 方向键移动(按屏幕方向),滚轮 或 + / - 缩放,0 复位;画布随窗口自适应并铺满视口。
5. 部署到服务器
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 自检。
6. 自检命令
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/(派生数据)只作为/diablo2/assets/下的归档发布,不进 Git 历史。 图集包每次重烘都会让 Git 仓库再长 ~60 MB(PNG 无法 delta 压缩),所以不再入库。 - 使用本引擎需要你自己合法拥有的游戏数据。将来落地的生成类关卡(迷宫 / 野外)会在页面与文档中标注为 "按官方参数表复现,非引擎原版布局"(生成逻辑移植自 D2MOO)。