Go to file
troytt 29cfccaa37 feat(world): 全局世界连通、关卡无缝切换、传送点、传送门与小地图 (fixes #23)
实现《暗黑破坏神 II》全五幕(136 个关卡)的完整世界连通拓扑、烘焙期链接落位、
运行时无缝边界与楼梯渐变过渡、传送点网络、城镇传送门以及实时小地图。

1. 全局世界连通拓扑 (src/game/world-graph.ts)
   - 融合 Levels.txt (Vis0..7, Warp0..7)、28 对野外无缝相邻接缝 (SEAMLESS_ADJACENCY)、
     23 对双向传送门链接 (PORTAL_LINKS) 以及 LvlWarp.txt (88 行几何偏移)
     构建 136 关卡、288 条有向边连通图。
   - 实现确定性边界方向分配器 (assignGateSides),保证同一关卡的多条边不会冲突。

2. 楼梯瓦片识别与几何计算 (src/game/warp-tiles.ts, src/game/level-links.ts)
   - findWarpTiles: 扫描 DS1 墙体层 (type 10/11) 提取 style 0..7 对应 Warp0..7 瓦片。
   - level-links: 显式栈 4-连通分量洪水填充提取最大可行走主区域 (largestWalkableRegion),
     向内扫描边界缺口 (findBorderOpening),以及计算安全向内落脚点 (seamArrivalSpot)。

3. 野外道路与锚点导出 (src/game/wilderness.ts, src/game/acts.ts)
   - planAct1OutdoorLevel 导出城镇过渡口、野外边界门、洞穴入口与路边地标。
   - 实现泥土路网与 256 项 8 邻域位掩码地砖自动铺设。

4. 烘焙期流水线增强 (scripts/pack-act-assets.ts)
   - buildSceneLinks 5 级流水线:DS1 真实瓦片 -> 野外生成器锚点 -> 迷宫楼梯间 ->
     预设边界扫描与营地四朝向自适应 -> 兜底安全落脚点。
   - 全量重新烘焙 samples/d2-packs (365 张地图),输出 entrances, warps, waypoints
     与 world-graph.json。

5. 运行时交互与表现 (src/scene/act-scene.ts, src/scene/transition.ts, src/game/portal.ts, src/ui/minimap.ts)
   - 支持接触边界无缝切换 (90ms 淡入淡出) 与按 G 键 / 交互楼梯传送 (220ms 淡入淡出)。
   - swapLevel 保留玩家状态(位置、属性、背包、随从)并在对应配对出入口安全落地。
   - WaypointNetwork: 小站发现与跨图传送;TownPortalSlot: T 键回城与往返传送。
   - Minimap: Tab 键切换右上角小地图 / 全屏自动地图,绘制迷雾、玩家、出口、小站、回城门。

6. 验证护栏与测试
   - verify:world-graph: 40,003 项断言确认全 136 关卡全连通、边双向对称。
   - verify:world-walk: 模拟玩家徒步全图 338 张有出口地图,1046/1046 出入口 100% 互达。
   - 补齐 tests/world-graph.test.ts, tests/warp-tiles.test.ts, tests/wilderness-roads.test.ts。
2026-09-16 23:39:33 +00:00
.gitea/workflows test: 引入 Vitest 测试框架、覆盖率统计与 CI 流水线 (fixes #14) 2026-09-14 12:25:28 +00:00
docs/d2kb fix(monsters): 按 d2mods.info 官方规范修正 MonLvl 与 MonStats 等级/半径/稀有度逻辑 2026-09-15 08:57:15 +00:00
samples/fixtures fix(formats): 按官方哈希桶格式重写 tbl 解码器与编码器 (fixes #8) 2026-09-14 12:30:07 +00:00
scripts feat(world): 全局世界连通、关卡无缝切换、传送点、传送门与小地图 (fixes #23) 2026-09-16 23:39:33 +00:00
src feat(world): 全局世界连通、关卡无缝切换、传送点、传送门与小地图 (fixes #23) 2026-09-16 23:39:33 +00:00
tests feat(world): 全局世界连通、关卡无缝切换、传送点、传送门与小地图 (fixes #23) 2026-09-16 23:39:33 +00:00
tools/go-oracle fix(collision): 修复 DT1 碰撞网格方向与朝向对齐 (fixes #4) 2026-09-14 07:54:38 +00:00
.gitignore chore(git): 忽略 vitest 覆盖率报告输出目录 2026-09-14 13:27:18 +00:00
README.md M6:舍弃 map-scene.ts,收敛到 act-scene.ts 单轨 2026-09-15 07:40:56 +00:00
ROADMAP.md docs: 标记 M7 里程碑为已完成 2026-09-15 09:52:35 +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
net.html M6:舍弃 map-scene.ts,收敛到 act-scene.ts 单轨 2026-09-15 07:40:56 +00:00
package-lock.json chore(scripts): 使用 tsx 运行 TypeScript 脚本,解除对特定 Node 运行时的依赖 (fixes #12) 2026-09-14 11:44:43 +00:00
package.json feat(world): 全局世界连通、关卡无缝切换、传送点、传送门与小地图 (fixes #23) 2026-09-16 23:39:33 +00:00
tsconfig.json test: 引入 Vitest 测试框架、覆盖率统计与 CI 流水线 (fixes #14) 2026-09-14 12:25:28 +00:00
vite.config.ts M6:舍弃 map-scene.ts,收敛到 act-scene.ts 单轨 2026-09-15 07:40:56 +00: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/。
  • 代码可复现一切:格式解码、地图构建、资源包烘焙、角色合成都在这份代码里,脚本可重跑。
  • 路线图、实现细节、验证方法论与未完成项在 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 每次重烘都会让仓库再长一份), 按下面的「资源包从哪来」下载解包后,没有原版数据也能看地图。

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 资源检查器(头、存储标志、压缩掩码分布、成员列表、精灵预览)
/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)。