Go to file
troytt f52edd85f9 test(formats): 增加针对损坏资产的覆盖率和回归测试 (fixes #11)
Implements regression limits testing bounds limits, invalid header mappings and stream truncations for binary formats, verifying the decoder engine halts cleanly without exception masking or propagation leaks. Ensures zero false positives and safely covers buffer boundary rules.
2026-09-14 13:24:25 +00:00
.gitea/workflows test: 引入 Vitest 测试框架、覆盖率统计与 CI 流水线 (fixes #14) 2026-09-14 12:25:28 +00:00
samples/fixtures fix(formats): 按官方哈希桶格式重写 tbl 解码器与编码器 (fixes #8) 2026-09-14 12:30:07 +00:00
scripts test(formats): 增加针对损坏资产的覆盖率和回归测试 (fixes #11) 2026-09-14 13:24:25 +00:00
src fix(formats): 替换原始缓冲区的未检查读取 2026-09-14 13:19:01 +00:00
tests test(formats): 增加针对损坏资产的覆盖率和回归测试 (fixes #11) 2026-09-14 13:24:25 +00:00
tools/go-oracle fix(collision): 修复 DT1 碰撞网格方向与朝向对齐 (fixes #4) 2026-09-14 07:54:38 +00:00
.gitignore chore: 在 .gitignore 中忽略 Diablo II*/ 用户数据目录 2026-09-14 08:55:08 +00:00
HANDOVER.md chore(scripts): 使用 tsx 运行 TypeScript 脚本,解除对特定 Node 运行时的依赖 (fixes #12) 2026-09-14 11:44:43 +00:00
README.md chore(scripts): 使用 tsx 运行 TypeScript 脚本,解除对特定 Node 运行时的依赖 (fixes #12) 2026-09-14 11:44:43 +00:00
THIRD_PARTY_NOTICES.md 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
acts.html feat(scene): 衔接 NPC 数据至引擎层及交互面板验证 (fixes #6) 2026-09-14 13:14:34 +00: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 chore(scripts): 使用 tsx 运行 TypeScript 脚本,解除对特定 Node 运行时的依赖 (fixes #12) 2026-09-14 11:44:43 +00:00
package.json test(formats): 增加针对损坏资产的覆盖率和回归测试 (fixes #11) 2026-09-14 13:24:25 +00:00
tsconfig.json test: 引入 Vitest 测试框架、覆盖率统计与 CI 流水线 (fixes #14) 2026-09-14 12:25:28 +00:00
vite.config.ts 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖) 2026-09-13 18:10:28 +08:00
vitest.config.ts test: 引入 Vitest 测试框架、覆盖率统计与 CI 流水线 (fixes #14) 2026-09-14 12:25:28 +00: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/——它不在 Git 里(63 MB 的索引 PNG 每次重烘都会让仓库再长一份), 按下面的「资源包从哪来」下载解包后,没有原版数据也能看地图。

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
/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 复位;画布随窗口自适应并铺满视口。

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)。