159 lines
9.0 KiB
Markdown
159 lines
9.0 KiB
Markdown
# d2web
|
||
|
||
从零实现的**暗黑破坏神 II 引擎**(纯 TypeScript + Vite + WebGL2,**零运行时依赖**),在浏览器里读取
|
||
用户自备的经典版 D2 归档(MPQ),渲染等距地图、驱动角色走动,并支持把地图离线烘焙成 web 原生格式。
|
||
|
||
- **仓库不含任何暴雪资源**:没有 `.mpq` / `.dc6` / `.dt1` / 图集 PNG;原版数据要自己放进 `samples/d2/`。
|
||
- **代码可复现一切**:格式解码、地图构建、资源包烘焙、角色合成都在这份代码里,脚本可重跑。
|
||
- 实现细节、验证记录与未完成项在 [`HANDOVER.md`](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),
|
||
体积恒定在「一份快照」:
|
||
|
||
```bash
|
||
git clone https://git.projectdiablo2.cn/troytt/diablo2-web-assets.git samples/d2-packs
|
||
```
|
||
|
||
只想要一份压缩包(不需要 git):
|
||
|
||
```bash
|
||
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. 本地跑起来
|
||
|
||
```bash
|
||
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` 与重新烘焙不可用。
|
||
|
||
### 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. 部署到服务器
|
||
|
||
```bash
|
||
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`):
|
||
|
||
```nginx
|
||
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. 自检命令
|
||
|
||
```bash
|
||
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`](THIRD_PARTY_NOTICES.md));`samples/fixtures/`(本项目自产)与
|
||
`samples/d2-packs/`(派生数据)只作为 `/diablo2/assets/` 下的归档发布,不进 Git 历史。
|
||
图集包每次重烘都会让 Git 仓库再长 ~60 MB(PNG 无法 delta 压缩),所以不再入库。
|
||
- 使用本引擎需要你自己合法拥有的游戏数据。将来落地的生成类关卡(迷宫 / 野外)会在页面与文档中标注为
|
||
**"按官方参数表复现,非引擎原版布局"**(生成逻辑移植自 D2MOO)。
|