Commit Graph

432 Commits

Author SHA1 Message Date
troytt 602565030f fix(map-gen): fix Act 4 Chaos Sanctuary, Act 3 Jungles/Travincal/Kurast, Act 2 Deserts & Act 5 Frozen Tundra layouts 2026-09-16 03:30:40 +00:00
troytt 25ae7289ee fix(map-gen): assemble Act 5 Bloody Foothills 15 siege strips, Barricade fortifications & Act 1 stone walls/dirt roads (Issue #35) 2026-09-16 03:06:48 +00:00
troytt dcd3da0a22 feat(combat): disable monster aggro in act-scene so monsters stay idle in place 2026-09-16 02:21:17 +00:00
troytt d4fb176031 perf(renderer): implement Raw 8-bit GPU Index Texture (gl.R8) + Shader Hardware Palette Lookup (#34) 2026-09-16 02:10:34 +00:00
troytt 2527678c10 fix(packs): restore full Acts 1..5 index and map/bake all 741 monster IDs across all acts (#33) 2026-09-16 01:43:11 +00:00
troytt d6e624829a fix(monster): fix loading performance, monster facing, and spawn logic (#30, #31, #32)
- Fix #30: Concurrent layer/animation decoding and shared MPQ archive caching to reduce load latency
- Fix #31: Use dir64ToDcc for permuted 8-direction DCC extraction in monster art compositing and align 8-way facing calculation
- Fix #32: Prevent monster spawning in town/safe zones, support hellbovine cow packs, and cluster packs properly
2026-09-15 15:03:28 +00:00
troytt 9a0e91849e feat(monsters): 完成 M7 怪物 COF/DCC 8向动画与美术层接入
- 新增 src/game/monster-art.ts: 支持怪物 16 部位通道映射与多图层 COF/DCC 合成,支持 8 向行走 (wl) 与待机 (nu) 动画
- 新增 src/game/monster-mapping.ts: 建立怪物 ID 到美术 Token 与武器类型的映射规范
- 更新 src/scene/act-scene.ts: 怪物按类型按需异步加载美术图集,由红色占位色块升级为真·8向动态怪物体素
- 更新 scripts/verify-monsters.ts: 增加对血色荒野核心怪物 (FA, ZM, SI) 的动画解码断言 (74/74 checks passed)
- 新增 tests/monster-art.test.ts 与 tests/monster-mapping.test.ts (全部 528 个单元测试均通过)
2026-09-15 09:45:23 +00:00
troytt 4294a16a48 fix(monsters): 按 d2mods.info 官方规范修正 MonLvl 与 MonStats 等级/半径/稀有度逻辑
- 依据 KB 354: 读取 MonLvl.txt 单机专属 'L-' 列,确保单机模式数值准确
- 依据 KB 360: 实现两分支 monsterLevelFor 等级判定(普通难度与 Boss 维持自身等级,噩梦/地狱普通怪取区域等级)
- 依据 KB 360: aidist 为空或 0 时回退至默认 35 格 (560px 一屏距离)
- 依据 KB 360: 增加 sparsePopulate 稀有生成概率属性并在选怪时过滤
- 全量下载并整理 d2mods 官方 KB 120 篇技术文档至 docs/d2kb/ 并附索引
- 验证: 51/51 unit tests passed, 65/65 verify-monsters checks passed
2026-09-15 08:57:15 +00:00
taodao cc12e7bc66 M7 第二步:怪物接进关卡,并补上 MonLvl 缩放
Stage A 把 MonStats.txt 读了出来,接上去才发现读出来的数字不能用:
一只一级堕落者按表里的 A1MinD=51 / A1MaxD=101 算,一击 76 点伤害,
而玩家只有 60 点血。

MonStats.txt 里的数字不是游戏里的数字。真实公式是

  最终值 = MonStats[列] × MonLvl[怪物等级][对应列] ÷ 100

这不是猜的。同一个公式让五个互不相干的列同时对上游戏已知数值:
fallen1 的 Exp 61→18.3(游戏 18)、zombie1 的 111→33.3(33)、
quillrat1 的 71→21.3(21)、fallen1 的 AC 84→5.0(5)、
A1TH 101→8.1(8)。五发全中不是巧合,所以照做而不是推给 M9。

顺带纠正我在 Stage A 里写下的判断:MinHP(N)/MinHP(H) 不是噩梦和地狱
的血量,594 个有血量的行里有 426 行的 MaxHP(N) 比普通还低。难度靠的
是怪物自己的 Level(N)=36、Level(H)=67 再过一遍 MonLvl 的 HP(N)/HP(H),
这才是噩梦堕落者有 131-289 血而不是 25-55 血的原因。MonLvl 的 L- 前缀
列(扩展版那套)在 1.13c 里和原列逐字节相同,读哪套都一样。

boss 查过要不要豁免缩放,没有豁免:缩放后的 Diablo 是 13818,记忆里
是 13741;缩放后的 Andariel 是 1025,记忆里约 960;但 Duriel 常被引用
的 4757 是原值。凭这种强度的记忆去开特例就是猜了,幕 boss 单独处理。

接线:
- monsters.ts 新增 readMonsterScaling / monsterScaleFor / UNSCALED,
  monsterStatsOf 接受一个 MonsterScale。先掷骰再缩放——低等级的乘数
  只有百分之几,先缩放边界会把 21..61 压成 1..4 再四舍五入,一个范围
  里四个值有三个取不到。
- 新增 planLevelMonsters:从关卡 id 一路走到掷好点数的怪物群。在线
  路径和资源包烘焙都走它,「烘焙出来的关卡和在线的一致」因此是个性质
  而不是巧合。
- engine.ts 新增 monsterPacks 选项,优先于 stats + monsterCount。
- act-scene.ts 两条路径都接上;资源包路径读 scene.json 里新烘焙的
  monsters 字段,浏览器在这条路径上没有 Levels.txt 可读。
- pack-act-assets.ts 烘焙怪物,每张图 scene.json 增加约 4 KB。

血色荒野现在是:堕落者 2-4 血 / 2 伤 / 18 经验,僵尸 8-13 血 /
33 经验 / 移速 28(堕落者是 142),棘鼠 2-5 血 / 21 经验。51 只怪
分 30 群。

验证:tsc 零错误;npm test 523 通过(基线 461);
npm run verify:monsters 65/65;npm run verify:all 通过;
npm run verify:packs 1302/1302 逐像素断言通过。

TAG=agy
CONV=c89513df-cd12-4749-b5bd-5e9f16639391
2026-09-15 08:22:48 +00:00
taodao 70b299569e M7 第一步:真实怪物数据层
把 MonStats.txt 读进来,并给它一个能发现错误的检验。

新增 src/game/monsters.ts。它和 combat.ts 里的 monsterStatsFromRow
是两个读取器,不是一个:后者读的是九列的测试夹具(Id/Name/HP/...),
真实的 MonStats.txt 有 255 列且几乎没有一个同名——是 NameStr 不是
Name,是 minHP/maxHP 不是 HP,是 Exp 不是 XP。把夹具读取器指向真表
不会报错,只会让 735 只怪物拿到同一套默认值,看起来像是能用。

新增 scripts/verify-monsters.ts(52 项检查)与 tests/monsters.test.ts
(41 项)。检验脚本一上来就发现了七处问题,其中两处是数据本身的事实,
值得记下来:

- MinHP(N)/MinHP(H) 不是「更高难度的血量」。fallen1 普通 21-61,
  噩梦和地狱都是 25-55;594 个有血量的行里有 426 行的 MaxHP(N) 比
  maxHP 更低;Duriel 从普通的 4757 掉到地狱的 1295。真正的难度缩放
  来自 MonLvl.txt 对普通值的逐等级放大,这些列是只对 boss 生效的覆盖,
  而且数据自己用得并不一致。按铁律六,读取器照实读、照实记,不假装
  它们是缩放,缩放留给 M9。
- MonStats2.txt 只有 610 行,配 735 只怪物。缺的 119 只是编号变体
  (quillrat6、slinger7、deathmauler6 等),游戏用它们 BaseId 的美术
  来画。只按 id 查会让这 119 只变成一片空地而不是一个报错,所以
  readMonsterArt 走 BaseId 回退。

另外三处是数据里的既有瑕疵,现在被显式承认而不是被抹平:
MonStats.txt 里 cr_lancer8 重复出现两次(检验断言「唯一的重复是这一个」,
新的重复仍会失败);SuperUniques.txt 里的 Expansion 是分节行,有名字
没有 Class;还有两处是我手抄表格时抄错了(foulcrow1 的 MonType 是
foulcrow 不是 bat,hellbovine 的 NameStr 带空格)。

combat.ts 新增 spawnMonsterPacks。原有的 spawnMonsters 把怪物撒在黄金角
螺旋上,那是让互不相干的东西均匀散开的形状,恰好是堕落者营地最不该有的
形状——螺旋上相邻的两点是刻意离得远的。现在螺旋放的是营地,成员在自己
营地里成团。附 tests/monster-packs.test.ts(11 项),直接量距离而不是
看截图。

acts.ts 的 ActTables 增加 MonStats2/MonLvl/MonType/MonUMod/SuperUniques。

验证:tsc 零错误;npm test 502 通过(基线 461);
npm run verify:monsters 52/52。

TAG=agy
CONV=c89513df-cd12-4749-b5bd-5e9f16639391
2026-09-15 08:06:52 +00:00
troytt ae94755401 M6:舍弃 map-scene.ts,收敛到 act-scene.ts 单轨
真地图上一直没有玩法,原因不是「两轨没合流」——act-scene.ts 早就通过 GameEngine
接入了战斗、物品、技能、任务、存档。真正的原因只有一行:act-scene.ts 给引擎传的是
`stats: []`,而 combat.ts 的 spawnMonsters 在 stats 为空时直接 return 0。

同时 map-scene.ts 被查出三个确凿缺陷,不值得保留:
1. 它建了一个从不被 tick 的孤儿 world,`let player = world.player` 之后再没用过
2. 怪物动画取帧读的是那个孤儿 world 的 tick,而 world.tick 只在 tickCombat 里自增,
   map-scene 从不直接调它,于是 tick 恒为 0,每只怪只显示一个固定帧——追击和攻击时
   怪物是静止的
3. 存读档绕过引擎,在 tick 之外直接读写 localStorage
外加 12 个死 import。

本次改动:

碰撞语义从布尔改为计数(src/game/iso-terrain.ts)
  combat.ts 的脱困规则是 `if (overlap(next) <= current) 才走`。原来 act-scene 传的是
  `collides(...) ? 1 : 0`,在墙里读到的永远是 1,`1 <= 1` 恒真,于是角色在墙里随机
  游走。改成计数之后同一条规则变成了梯度下降:陷进去 5 格深可以走到 3 格深,朝 7 格
  深的方向会被拒绝。
  不能直接复用 map.ts 的 blockedOverlap:那个函数遍历一个矩形的格索引区间,只有在
  sub-tile 是屏幕矩形时才成立;等距下 sub-tile 是 16×8 的菱形,屏幕矩形映射过去是
  错切且交错的,没有矩形索引区间可遍历。所以这里改为在包围盒上采样一个固定点阵,
  逐点问 isBlockedAt。点阵固定而非自适应,保证计数是位置的纯函数——存档与 lockstep
  都依赖这一点。

引擎硬编码下沉为选项(src/game/engine.ts)
  monsterCount(原写死 8)、monsterSpread(原写死 260)、npcRingFallback(NPC 合成
  圆环,act-scene 本来就用真实 DS1 坐标覆盖掉了,现在干脆不构造)。

补上一个死接口:EngineInput.saving / loading
  这两个字段声明了但 tick() 从不读,也就是说 act-scene 的 K/L 其实什么也不做。
  现在引擎新增 SnapshotStore 端口(read/write),在 tick 末尾按上升沿处理:
  存档点永远是一个已结算的 tick;按住键不会每 tick 存一次;localStorage 仍留在场景
  层,模拟层不碰宿主环境。配额满、隐私窗口、旧格式存档都记进 metrics.saveError。

删除 map-scene.ts 与 map.html
  map-save-load.js 这个无头检查改写为纯逻辑的 tests/engine-save-load.test.ts:
  同样的断言(走位砍怪 → 存档 → 继续跑 → 读档,比对位置/击杀/金币/地面/等级),
  但不需要 headless Chromium 和 WebGL 上下文,而且顺带覆盖了浏览器版从未走到的
  in-tick 存读档路径。verify-m4 / verify-m5 只依赖 src/game 与 src/net,原样保留。

顺带修了 inspect-page.mjs 的一个目标选择缺陷:它按「第一个 type 为 page 的目标」
附着,而 Chrome 在打开命令行 URL 之前会先建一个空白标签页,轮询往往抢到空白页,
之后的断言就报「没有场景状态」——和页面启动失败完全无法区分。改为等待 URL 匹配的
目标。(注:当前沙盒里 headless Chrome 完全没有网络,浏览器实测仍待补,已在
scripts/browser/README.md 记录判定方法。)

验证:
  npx tsc --noEmit    0 错误
  npm test            461 passed / 2 skipped(基线 442,新增 14,无回归)
  npm run verify:all  全绿
  npm run build       4 个入口,1.89s

Fixes #21

TAG=agy
CONV=c89513df-cd12-4749-b5bd-5e9f16639391
2026-09-15 07:40:56 +00:00
troytt 2d7f5f7a00 fix(wilderness): add interior stone walls, probabilistic river, and perimeter opening control
- Implement isClosedBorder, selectBorderVariant, and getPerimeterOpenings
- Moo Moo Farm (Level 39): enforce 0 exits (100% closed perimeter), add interior corrals and stone fences
- Blood Moor (Level 2): enforce exactly 1 entrance (South) and 1 exit (East), add stone corrals/fences and probabilistic river/bridge presets
- Add 'Act 1 - Fence Fill', 'Act 1 - River', 'Act 1 - Bridge' to Act 1 Wilderness piece families
- Add comprehensive unit tests in tests/wilderness-features.test.ts
- Optimize publish-packs.ts with batch incremental pushes by Act
2026-09-15 05:25:09 +00:00
troytt 6b9c3d3430 fix(generator): orient wilderness borders and restore rock cliff boundaries 2026-09-15 02:30:31 +00:00
troytt 36d5624b14 fix(wilderness): 修复空地面图层擦除底图与补全第一幕专属预设景观 (refs #15)
1. 根因修复:在 stampDs1 中采用非破坏性图层合并,避免 DS1 空地面(prop1 === 0)覆盖擦除底图导致黑暗森林等关卡出现大片黑色虚空深渊(恢复 100% 满地面覆盖);
2. 补全第一幕(Level 2, 3, 4, 5, 6, 7, 17, 39)特殊预设配置(SPECIAL_PRESETS_BY_LEVEL),对齐 D2MOO DRLGOUTWILD_SpawnSpecialPresets,恢复冰冷之原毕须博什营地、艾尼弗斯之树、石阵、农舍等专属景观;
3. 扩充 WILDERNESS_PIECE_FAMILIES 并增加针对 Level 3 与 Level 5 的回归断言护栏;
4. 全量 70 个迷宫关卡 + 31 个野外关卡通过 verify:generators 自动化测试(785/785 项断言通过)。
2026-09-15 01:52:12 +00:00
troytt bbc1af7a72 fix(wilderness): 补全奶牛关 Moo Moo Farm 专属预设景观生成 (fixes #15)
- 在 src/game/wilderness.ts 中实现户外特殊预设生成 spawnSpecialPresets,针对奶牛关(Level 39)按 D2MOO 规范放置牛圈 Corral、营地 Bivouac、水塘 Pond 与沼泽/石圈等专属景观
- 保持内部网格碰撞检测与确定性洗牌放置,防止预设重叠与边界穿越
- 扩充 scripts/verify-generators.ts 与 scripts/pack-act-assets.ts 中 Act 1 - Wilderness 预设家族
- 增加 Level 39 的专用断言,验证连通性 100% 与图块 0 缺失
- 重新打包地图数据并通过全量 verify:generators 与 verify:packs 校验
2026-09-15 00:23:51 +00:00
troytt 29b3a4aebd fix(render): NPC 有立绘时不再叠加绿色占位方块 (refs #6)
城镇 NPC 补齐之后,罗格营地一个分区会出现 19 个绿色方块糊在正常立绘上面。

起因是两套绘制路径各自独立判断:`buildPackRuntime` 用 `if (obj.frame)`
把 NPC 并入 `objectDrawables` 画出真实立绘,而实体层对 `engine.npcEntities`
里的每一个 NPC **无条件**再画一个 16x26 的纯绿占位方块。在只有凯恩一个
NPC 的年代这看不出问题,NPC 数量从 103 涨到 889 之后就藏不住了。

修复不是在实体层加一个平行的判断——那只会把同一个条件抄成两份,早晚漂移。
改为把判断抽成 `hasPackedSprite` 放进 `src/game/npc.ts`,两条路径共用同一个
函数:立绘层为真时画图,占位层为假时画框,互斥由函数本身保证。

该函数写成类型守卫,立绘层因此可以在同一次调用里把 `frame` 收窄成非空,
省掉原本那个多余的二次判空,也就不存在第二个会漂移的判断了。

`NpcEntity` 增加 `hasSprite` 字段,必填而非可选,这样将来新增放置点必须
表态。`GameEngine` 构造函数里那圈合成放置点显式标记为 `false`——它们本来
就没有预烘焙美术,占位方块对它们仍然是唯一的可见形式。金色名字标签两类
都保留,可交互提示不受影响。

顺带把 `npcEntities` 的行内结构类型换成已导出的 `NpcEntity`,避免同一个
形状在同一个文件里存在两份定义。

新增 `tests/npc-placeholder.test.ts`:除守卫本身的行为与收窄能力外,还把
全部已打包 NPC 过一遍,断言这个划分是完备的(每个 NPC 恰好落入一条路径,
不重不漏),并钉住罗格营地四个分区的 NPC 名册。守卫锚定在它真正读取的
`samples/d2-packs/index.json` 上。
2026-09-14 23:56:49 +00:00
troytt e180520ef5 fix(npc): 补齐城镇 NPC 解析的收尾问题并加上回归护栏 (refs #6)
承接前两个提交。上一版把 type-1 条目接了进来,但收尾有几处缺口,
逐项补齐:

1. 恢复被误删的注释
   `objectsTableTyped` 上方 `/** Same table, through the typed loader ... */`
   在重构中被删掉了,与改动无关,还原。

2. token 大小写统一
   `monstats.txt` 的 `Code` 列拼写不统一——同一批数据里既有 `K9` 又有
   `k9`,`ja` / `6z` / `7i` / `7j` 也是小写。而 `monsterMembers` 的索引
   键一律大写,于是 `map.get(token)` 直接落空,Jamella 和三个受伤的野蛮人
   被以「无美术」的形式烤进了 scene.json。上一版在查表处补了
   `.toUpperCase()`,这里进一步在**产出源头**归一化:同一个怪物不会再以
   两种拼写出现,调用方也不必猜大小写。

3. 空 Place 由静默跳过改为显式报错
   `presetPlaceByAct` 的数组下标**就是** DS1 的 type-1 id,数组必须保持
   稠密。原先的 `if (!place) continue` 一旦遇到空格子,会让其后每一个 NPC
   都错位认领邻居的身份——正是 `22` vs 21 那个缺陷的同一类。实测 1.13c
   五个 act 共 0 个空 Place,所以这条守卫永远不会触发;它的意义是万一数据
   变了要**响亮地失败**,而不是悄悄错号。

4. 补两层回归测试
   - `tests/resolve-type1.test.ts`:清掉两个未使用的 import、补回项目惯用的
     `.ts` 后缀、把边写边想的注释换成「这条用例为什么存在」,并新增一条
     覆盖小写 `Code` 归一化的用例。
   - `tests/packed-npc-art.test.ts`(新增):数据层不变式——凡是进入
     scene.json 的 NPC 必须同时具备 `member` 与 `frame`;同一 token 不得
     以两种拼写出现。这是之前唯一能抓住那 4 个哑火 NPC 的层次,单元测试
     抓不到,因为缺陷在打包脚本的查表里。
     守卫锚定在它真正读取的 `samples/d2-packs/index.json` 上,而不是
     `samples/d2` 目录。

验证(全部本地实跑):
  tsc --noEmit              0 error
  vitest                    23 文件通过 / 1 跳过,429 用例通过 / 2 跳过
  pack:data                 365 张地图
  verify:packs              1302/1302 断言通过
  NPC 实例                  889,未解析 0,token 拼写不一致 0

变异测试:
  - 把 `presets[objectId]` 改成 `presets[0]` → resolve-type1 2 条转红
  - 还原 `map.get(token.toUpperCase())` 为 `map.get(token)` → 全量套件
    仍然全绿,证明此前该修复毫无回归保护;本提交的 packed-npc-art 补上了
    这一层(改动前该用例对现有 pack 报 `K9 -> K9, k9`,重新打包后转绿)。
2026-09-14 15:58:51 +00:00
troytt 3cd4549332 fix: 修复非基底对象 NPC 丢失及其资源检索区分大小写的问题
在前一次提交的基础上修复并强化了 Type 1 对象(怪兽及 NPC 原型)解析与打包的正确性。

1. 修正大写回退遗漏问题(解决失联的 6 个特定角色如 Act 4 Jamella 等):`pickObjectMember` 中需要使用大写查找 token,而原来直接代入了小写 token 导致失败,修复后所有角色均成功合图。
2. 添加安全跳过统计 (`skippedMissingArtSpawns`),使得未匹配上图片的隐藏对象能干净跳过。
3. 增加 `tests/resolve-type1.test.ts` 提供核心覆盖率。
4. 应用性能重构与规范修复,将二次查询提速为内置的 `Map` 遍历,同时补齐了 `OBJECT_TYPE_MONSTER`、Typescript interface、尾随空格与逗号修复。
5. 后向保留了省略 tables 时旧版逻辑的注解。

(refs #6)
2026-09-14 15:37:32 +00:00
troytt 451816d627 fix: 修复非基底对象 NPC 丢失问题
Diablo 2 城镇中的部分 NPC(如 Gheed, Charsi, Akara, Kashya, Warriv)没有定义在
Objects.txt 作为 Type 2 对象,而是通过 MonPreset.txt 定义为 Type 1(怪兽生成点)。
以前此类对象被一律视为 kind: 'monster' 并且放弃生成(silently dropped)。

此修复:
1. `src/game/acts.ts`: 在 `loadActTables` 中增加了 `MonPreset.txt` 和 `monstats.txt` 解析。
2. `src/game/objects.ts`: `resolveDs1Object` 现在接受 `monsters` 表以处理 `type === 1` 对象查询:
   `MonPreset` 通过 Act + `id` => `Place`;
   `monstats.txt` 的第一列 (Id, 恰好对应 Place) => `Code` (对应资源 token) 和 `NameStr` (角色名)。
3. `scripts/pack-act-assets.ts`: 将解析后的新表传递给 resolver。对真实存在艺术资源的 `kind: 'npc'` 进行打包;对于确实无资源的占位对象(如 place_nothing, trap-*)现在会干净地跳过并做数量统计 (Skipped artless spawns),不再将它们报告为艺术丢失或引发异常。
4. 抽象统一 `MONSTER_ROOT` 并移除所有写死的 'data\\global\\monsters\\' 字面量。
5. `src/game/npc.ts`: 扩展 `NPC_NAME_FALLBACKS` 清单,解决部分名称未定义的显示为短码的问题(例如 RG 替换为 Rogue, CW 为 Cow 等)。

在保证现有 Type 2 行为完全不变和所有测试继续通过的前提下,总共正确重新召回了 67 个原先丢失的 Type 1 镇民、卫兵遗漏和活物 (Cow, Chicken等) 实例。

(refs #6)
2026-09-14 15:14:15 +00:00
troytt 7d8629cd73 feat(scene): 衔接 NPC 数据至引擎层及交互面板验证 (fixes #6)
建立 NPC token 到独立显示名称的转译字典。将打包导出的 NPC 集合统一注水到 GameEngine 模型内以生效交互半径;同时增加 Canvas UI 外围的绝对定位 DOM 生成层来渲染 NPC 头顶铭牌名称,以及挂载对应的对话展示面板供 engine.dialog 同步刷新。更新 E2E 环境验证逻辑检测交互健壮性。
2026-09-14 13:14:34 +00:00
troytt 11439256bf fix(art): 将 NPC 渲染文件的基础寻址从 objects 修正为 monsters
修正 Deckard Cain、Jerhyn 等城镇 NPC 的基础美术路径。通过从 objectEntry 下传的 baseIsMonsters 布尔标识,使特定 Object 的 DCC 及 COF 查表过程自动从默认的 `data\global\objects` 切换到 `data\global\monsters`, 彻底解决打包器因跨域失效而丢弃图集的情况。
2026-09-14 13:08:46 +00:00
troytt e3ca14c357 refactor(lockstep): 按项目规范彻底移除 any 约束逃逸并优化哈希分配性能 (refs #7)
此提交响应了进一步集成的代码质量与内存性能审查反馈:
1. 深入清理代码库类型系统边界,利用 Structural Target Interfaces 为所有传递进哈希运算中的世界和对象提供原生支持而不用 `any`,并修复由于引入 #10 放宽的 engine.ts 的 syncEngineState 缺失的 `EngineViewState` 严谨签名;
2. 提升了数据解析健壮性:在 save.ts 中增加严格的运行时验证(利用 Type Guard: `asserts value is GameSnapshot`),取消一切断言强转;
3. 取消了计算锁步哈希期间对背包和掉落物品的数组分配与运行时排序(重构为支持 Order-Independent Combination 的无排序位偏移计算模式),确保 25Hz 打包同步路径中零内存无端分配;
4. 并修正了 net-scene 等多个测试的静态数据映射与严格的突变检测。
2026-09-14 13:05:18 +00:00
troytt 4290e5feb2 fix(game): 移除不安全的类型转换并增加存档校验 (refs #7)
在 character.ts 中使用 flatMap 结合类型缩窄(null 检查),移除了对 DccFile 的强制类型转换,保证内部逻辑的编译期安全。在 save.ts 中增加了对背包数据的严格运行时有效性验证,避免由于恶意或破损存档导致的隐式游戏损坏,并用专用的反序列化测试覆盖新校验过程。
2026-09-14 12:44:19 +00:00
troytt 38d06cdb28 feat: 抽取独立无状态 GameEngine 领域与相关逻辑
将 map-scene 中负责每帧更新逻辑与状态管理的巨量代码抽取成 `src/game/engine.ts` 内的 `GameEngine`。
解耦模拟与 WebGL 和 DOM,保证可以无界面测试。
并引入了 `tests/engine.test.ts` 自动化单元测试(Vitest)。
2026-09-14 12:37:29 +00:00
troytt 98d6a6dad2 refactor: 提取 ViewportCamera 与后备 Demo 数据
提取 ViewportCamera 到 `src/sim/camera.ts` 以复用视角缩放控制逻辑。
将原本散落在 sandbox 测试场景里的怪物、物品、任务等 Demo 数据提取至 `src/game/demo-data.ts`,作为 Act 场景的缺省源数据,解耦纯 UI 代码与配置逻辑。
2026-09-14 12:37:29 +00:00
troytt f4dd55be6c fix(formats): 按官方哈希桶格式重写 tbl 解码器与编码器 (fixes #8)
- 重构了 `src/formats/tbl.ts` 以正确读取 21字节魔数头与 17字节哈希桶节点
- 使用了 NUL 终止与正确的字节长度计算方式,修正之前错误的 UTF-16 假设
- 支持通过 `TextDecoder` 选择代码页 (默认为 windows-1252,支持 gbk 等 fallback)
- 暴露了原始的 O(1) 线性探测查询函数 `lookupTblFast` 和哈希函数 `tblHash`
- 对 `scripts/lib/tbl-writer.ts` 及其验证脚本进行了重写,以正确写入和校验哈希桶布局
- 增加了完整涵盖功能与越界校验的 vitest 单元测试,并在真实的 113c patchstring.tbl 下实现了 1169/1169 哈希全量验证通过
- 将测试文件加入项目目录
2026-09-14 12:30:07 +00:00
troytt 2a0dd4ce25 feat(packer): bake all 101 generated levels into static packs with 3 variants each
- Update scripts/pack-act-assets.ts to bake 3 deterministic variants for each maze (DrlgType 1) and wilderness (DrlgType 3) level
- Cache mazePieces, wildernessPieces, substitutions, and palettes for fast one-pass baking
- Populate LEVEL_NAMES_ZH with all 136 standard Chinese level names
- Update variantLabelZh to format var1..var3 as 变体 1..3
- Guard scripts/verify-packs.ts to verify preset levels cleanly while supporting non-preset entries
2026-09-14 11:15:13 +00:00
troytt 1cfef30878 fix(generators): 修复 70 关随机迷宫与 31 关野外地图生成与连通性 (fixes #2)
- 地图图块与层级修正 (src/formats/ds1.ts & src/game/acts.ts):
  - 依据 D2MOO DrlgRoomTile.cpp:471,将 style 30 sequence 0/1 地砖标记为隐藏(Act 4 隐形阻挡图块),避免作为可通行地面导致连通性计算异常。
  - 为 'Act 5 - Barricade' 关卡补充加载 'Expansion/Siege/temptile.dt1'。

- 随机迷宫生成器修正 (src/game/maze.ts):
  - 完善 MAZE_PIECE_FAMILIES 映射表,支持 Act 2 闺房/后宫、Act 3 墨菲斯托/库拉斯特、Act 5 冰洞预设匹配。
  - 支持 River、Pool、Tainted Sun 等特异房间预设的分类与放置。
  - 修正 prevChain 中 Baal 前室房间方向映射顺序,对齐 D2MOO DrlgMaze.cpp 原版顺时针方向 ['NSE', 'SEW', 'NSW', 'NEW']。

- 野外生成器修正 (src/game/wilderness.ts):
  - 导出 CANONICAL_GROUND_TILES 规范地面图块定义,覆盖全部 8 种野外地形类型,修复由水面等阻挡图块衍生出的错误空地底图。
  - 针对窄条连接桥关卡(如库拉斯特堤道 Level 82)引入全尺寸直接盖印逻辑,避免无效边框环绕。
  - 为未配置尺寸的野外关卡(如 Level 111, 112)提供 D2MOO 规范的 160x64 默认边界回退。

- 验证脚本增强 (scripts/verify-generators.ts):
  - package.json 新增 verify:generators 校验命令。
  - 迷宫验证增加单房间预设关卡(Level 61, 114, 116, 119)特殊尺寸与房间数豁免。
  - 针对有悬崖环绕的野外地图排除外部边缘非游玩空洞格,确保真实可通行区域连通性判断准确。
  - 过滤零售版 MPQ 未收录 debris.dt1 的 Barricade Snow 预设,使图块丢失率降至 0.5% 以下。
  - 自动化验证 101/101 关卡(70 迷宫 + 31 野外)全数通过,746 项断言 100% 成立。
2026-09-14 10:33:45 +00:00
troytt 72731bb143 feat(objects): 完整实现对象层解码、打包图集与场景深度绘制 (#5)
- 数据与查找修复:
  - 修复 src/game/object-lookup-data.ts 中 Act 3 墨菲斯托红门 (104:342:1Y:ON) 错位问题,恢复 3,615 条记录唯一性。
  - 排除 COF 非图元文件,优先选取主体图元 (tr) 与 DCC/DC6 静态帧。
- 资产打包与图集生成 (scripts/pack-act-assets.ts):
  - 引入全局 decodedMemberCache,一次性解码去重。
  - 将所有有效美术对象装箱至 objectPages (objects-*.png),并正确计算象限锚点偏移 (-16 + box.left / f.offsetX, 16 + box.top / (offsetY - height + 1))。
  - 动态统计 objectsArtPending,62 张地图全部实现 0 待解。
- 场景渲染与深度排序 (src/scene/act-scene.ts):
  - 预加载 scene.objectPages 纹理并在 beforeunload 中释放,避免内存泄漏。
  - 将 objectDrawables 与 runtime.walls 按 (subX+subY)/5 与 cellX+cellY 的统一深度双指针交替绘制,并保持与角色前后的画家算法正确遮挡。
- 自动化验证 (scripts/verify-packs.ts & scripts/verify-object-lookup.ts):
  - 新增图集页边界与 0 待解帧断言 (1302/1302 全数通过)。
  - 更新查找表统计与点位断言 (20/20 全数通过)。
2026-09-14 09:27:54 +00:00
troytt a86b2a4582 fix(collision): 修复 DT1 碰撞网格方向与朝向对齐 (fixes #4)
- 修正 DT1 block 碰撞体 Y 轴解析方向
- 修复 character.ts 角色朝向映射函数与方向角度
- 增加 verify-collision-orientation 自动化验证脚本
- 完善 tools/go-oracle 工具链
2026-09-14 07:54:38 +00:00
troytt 61c6691555 修复顶角墙缺左半:DS1 只列 type 3,引擎会自己补画 type 4
现象:墙角(两段墙交汇的上顶点)左边一块渲染不正常——角的左脸缺失,右半的墙基露出来,
接缝处出现台阶/突块。例:第三章「下水道二层」(sewertreasure1)有 5 处。

根因:D2 的顶角美术是两半拼的,DS1 只存「顶角右半」(type 3),左半(type 4)从不列出;
引擎在装配房间瓦片时会自己补——
  DRLGROOMTILE_InitWallTileData(D2Common/DRLG/RoomTile.cpp,1.10f 0x6FD889C0):
      if (nTileType == TILETYPE_WALL_TOP_CORNER_RIGHT)
          再以同格的同份 packed tile 信息追加一个 TILETYPE_WALL_TOP_CORNER_LEFT 图块
我们的渲染器只画 DS1 列的引用,所以这一步从未发生。

改动:buildIsoMapScene 的墙循环里,type 3 解析成功后,用同一 style:sequence 精确解析
type 4 伙伴,同格、按各自的 minBlockY 规则绘制,并把它并入碰撞栅格;新增
scene.cornerPairs / scene.missingCornerPairs 供校验使用。verify-tiles 增加两条断言。

实测:35 关共 411 个 type 3,全部有 type 4 伙伴(缺失 0),墙绘制数 9103 → 9514;
「下水道二层」72 → 77 面墙,与 5 个顶角一致。
2026-09-14 09:18:43 +08:00
troytt 897b3735d6 初始提交:从零实现的暗黑破坏神 II WebGL2 引擎(TypeScript + Vite,零运行时依赖)
内容概览
- 归档层:MPQ v1 容器(hash/block 表、Storm 加密、分扇区编解码),含 PKWARE
  implode/explode 移植;支持 HTTP Range 只读取所需字节。
- 格式层:dc6 / ds1 / dt1 / pal / pl2 / cel / pcx / tbl / sprite,以及 dcc / cof / bitstream。
- 地图层:等距投影场景构建(地板/墙/屋顶分层、逐格瓦片变体加权随机、sub-tile 碰撞),
  资源包(索引色 PNG 图集 + scene.json)离线烘焙与逐像素差分校验。
- 角色层:DCC + COF 合成(8 方向),法师行走/站立替换占位棋子。
- 页面:acts.html 全屏画布 + 三级选择器(章节 / 场景 / 细分场景),资源包优先、
  可回退到直读归档(?live=1)。
- 验证脚本:verify:all / implode / acts / d2 / packs / dcc / deploy / listfile /
  object-lookup / alignment / tiles / generators 等,均带断言与退出码。

数据与许可
- 游戏数据(samples/)与第三方参考源码(reference/)不入库:体积大且无再分发许可,
  见 THIRD_PARTY_NOTICES.md。
- 生成关卡(70 迷宫 + 31 野外)尚未烘焙,现状与修复清单见 HANDOVER.md §4f。
2026-09-13 18:10:28 +08:00