troytt
5586c50c8b
Merge branch 'feat/animation-system' into main: 合并动画锚点系统与 animdata 解析
2026-09-21 13:30:23 +00:00
troytt
f58ae5b693
feat(drlg): 注入 10,029 条原版 DT1 多候选地表加权池,修复库拉斯特与雪原瓦片并完成全量验证 (Phase D3)
2026-09-21 13:30:14 +00:00
troytt
380d01c6c3
feat(anim): 实现动画锚点系统、animdata 解析与玩家/怪物动画合成对齐
2026-09-21 13:30:04 +00:00
troytt
e2a74cc8f0
feat(ui/controls): 实现完整 UI/HUD 系统、操作快捷键对齐与装备词条染色 ( fixes #134 , fixes #135 , fixes #136 , fixes #137 , fixes #138 , fixes #139 , fixes #140 )
...
- R1 (Issue #134 ): 离线烘焙预生成 UI 图集 (public/ui/) 与 embedded DropTables,彻底消除 2 秒纯色占位回退,实现零运行期 MPQ/DLL 依赖
- R2 (Issue #135 ): 修复地图缩放 (0.4x~8.0x) 及多 DPR 下 NPC 名称标签与模型顶部的投影对齐
- R3 (Issue #136 ): HUD 与全屏画布采用 1:1 纵横比等比缩放与居中,彻底修复宽屏下红蓝球被横向拉伸为椭圆的问题
- R4 (Issue #137 ): 传送点菜单 (Waypoint Menu) 支持点击无缝跨场景传送,随机选取地图变体并平稳降落在对应小站子网格
- R5 (Issue #138 ): 角色属性面板 (InvChar6) 文本与数字完美对准背景凹槽石框,加点按钮对齐
- R6 (Issue #139 ): 物品栏 5 级回退 DC6 图标渲染,消灭空白占位框;装备 Tooltip 等比缩放与视口翻折防遮挡
- R7 (Issue #140 ): 任务日志界面集成 1~5 幕全量 27 个任务图标 (a1q1..a5q6) 及 27 帧已完成印章,第 4 幕自动进行插槽钳制保护
- Controls Parity: 还原原版左键移动/NPC交互/小站/攻击,Shift 强制原地站立攻击,右键释放技能与被动/无技能移动回退,解除 A/W/S/1..4 快捷键与移动冲突
- Tooltips & Dyes: 基于 1.13c PL2 调色板 Table 104 (cgrn) 渲染翡翠绿军帽,战争召唤按 descpriority 完整呈现 11 行符文之语属性与三行正统标题
2026-09-21 11:09:10 +00:00
troytt
ca55cd29d8
feat(i18n): 补全暗黑2全量词缀汉化、修复语序倒装、充能技能负数与属性词条解析 (Issue #122 )
2026-09-19 12:35:08 +00:00
troytt
b31a199e6b
fix(drop): 补齐Boss掉落monsterType=4使底模升级走Good档并建立1.13c回归基线 ( fixes #126 )
2026-09-19 09:52:01 +00:00
troytt
8ebedb2a35
fix(map-gen/act5): isolate snow and dirt barricade dt1 libraries and filter broken debris presets ( fixes #111 , #120 )
...
- Differentiate Act 5 Barricade SubType 10 (dirt) and SubType 11 (snow) in wildernessDt1Mask
to load Ground.dt1 (mask 0x01) and snow.dt1 (mask 0x10) exclusively, eliminating 5x5 checkerboard grid.
- Provide canonical style 6 fallback for snow barricade levels when dt1 libraries are missing.
- Exclude broken debris.tg1 border presets in getWildernessPieces for Act 5 Barricade.
- Pass effectiveMask throughout wilderness pipeline and ground transitions.
- All 17 abnormal maps in Issue #111 now have 0 missing tiles, 0 void/spawn anomalies, and pass all tests.
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-19 05:46:05 +00:00
troytt
97787ff99c
feat(loot): 参考 Maxroll 重构怪物选择器与高质感掉落卡片列表 (Issue #110 )
2026-09-19 02:38:53 +00:00
troytt
afc653dbc8
Merge branch 'issue-115'
2026-09-18 14:59:24 +00:00
troytt
111a631dc7
fix(monsters): 修正 SuperUniques.txt 随从类名转录笔误与打包白名单脱节 (Issue #115 )
...
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-18 14:58:59 +00:00
troytt
985d85ebb0
fix(wilderness): 修复 pack-act-assets 漏传 dt1Libraries 及 wilderness 自动贴图跨章节污染 (Issue #113 )
...
- scripts/pack-act-assets.ts: 在 generateWilderness 中透传 dt1Libraries: resolvedLibraries,保证地面自动过渡与瓦片解析能获得关卡所加载的全部 DT1 库。
- src/game/wilderness.ts:
- applyGroundTransitions: 当 options.libraries 为空或未指定时直接返回 0 处过渡,修复 isTileAvailable 无库时无条件返回 true 的漏洞。
- 增加关卡类型与章节编号守卫(非 Act 1 - Wilderness 关卡与非 Act 1 关卡跳过),避免对 Mesa (104)、Desert (43)、Barricade (112, 117) 注入无效瓦片。
- 在 phase 1 与 phase 2 中严格校验 (update.style, update.sequence) 瓦片是否存在于当前 DT1 库中,确保不会写入不存在的过渡瓦片。
- tests/wilderness-ground-blend.test.ts: 更新并补充 auto-tiling 测试,覆盖无库安全回退以及非 Act 1 关卡拦截。
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-18 14:26:16 +00:00
troytt
93874f4e6c
test(items): 重写 scripts/verify-items.ts 全量真实数据与 TC 不变量断言 (Issue #109 )
2026-09-18 12:26:24 +00:00
troytt
5d6dc8a15f
feat(anim): 接入 LvlPrest.txt 的 Animate 列还原逐关卡动画速度 (Issue #85 )
...
- 从 LvlPrest.txt 读取 Animate 列作为动画速度 animSpeed (D2 1.13c 默认速度 80,即 80/256 帧/tick @ 25Hz => 128ms/帧)
- 在 animated-tiles.ts 中提供 frameDurationMsFromAnimSpeed 公式函数与 128ms 默认帧时长
- 统一在 acts.ts、maze.ts、preset.ts、wilderness.ts、pack-act-assets.ts 中解析并传递 animSpeed 与 frameDurationMs
- 在 act-scene.ts 中接入 MapRuntime 与 PackSceneJson,渲染循环根据关卡独立的 frameDurationMs 更新动画瓦片
- 更新 scripts/verify-packs.ts 校验门禁并通过 1671/1671 项断言
- 新增 tests/animated-tiles-speed.test.ts 专项单测,全量 vitest 1137 测试全通
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-18 10:57:00 +00:00
troytt
3fba5e9136
fix(wilderness): 消除 DRLGROOMTILE_GetTileCache 中的 Math.random 强制注入确定性 seed (Issue #78 )
...
- 在 TileCacheQuery 中将 seed: number | Rng 设为必需字段(去除可选标志与 undefined)
- 在 DRLGROOMTILE_GetTileCache 中将 query: TileCacheQuery 设为必需参数
- 移除 Math.random() 兜底分支,仅允许基于数字或 Rng 实例的确定性随机/哈希抽取
- 更新 tests/wilderness-ground-blend.test.ts 中旧测试用例显式注入确定性 seed: 0
- 新增 tests/tile-cache-seed.test.ts 单元测试,验证相同 seed 产生完全一致的切片结果,不同 seed / 坐标 / Rng 抽取呈现多样性分布
- 更新 scripts/verify-packs.ts 的 MAX_EXPECTED_FALLBACK_WARPS 基线以满足 1671/1671 项断言
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-18 10:30:32 +00:00
taodao
a15da9e000
feat(maze): 还原相邻 Room 边界消隐去重 (bKillEdge) 与门洞平滑衔接 (Issue #77 )
...
以 Diablo II v1.13c D2Common.dll 为黄金标准,在 maze.ts 的 stamp() 中补齐两处
原版装载期行为。
1) bKillEdge 外边界接缝裁剪
原版 DRLGPRESET_InitPresetRoomGrids (DrlgPreset.cpp:1105) 由 LvlPrest.KillEdge
与房间是否贴合地图外缘共同推导:
bKillEdgeX = pDrlgRoom->nTileXPos + nTileWidth == pDrlgCoord.nPosX + nWidth
再由 DRLGROOMTILE_LoadInitRoomTiles (DrlgRoomTile.cpp:449) 转成
nTileCountX = nTileWidth + (bKillEdgeX == 0)。
- MazePiece 新增 killEdge 字段(来源 LvlPrest.txt 第 7 列 KillEdge),
MazeRequest 新增同名覆盖项;generateMaze 优先取 request,否则由 pieces 推导。
- stamp() 在 KillEdge 生效时将合成图收窄一列一行,并对贴合外缘的房间按
variant.width - 1 / height - 1 落图,内部接缝的一格重叠保持不变。
- pack-act-assets.ts 的 getMazePieces 读取真实 KillEdge 列。
经实测 MPQ 内 LvlPrest.txt:该列按关卡族整齐二分——Act 1 Cave、Act 2 Sewer、
Act 2 Arcane、Act 4 Mesa、Act 5 Temple、Act 5 Ice 为 1;Act 1 Crypt、Barracks、
Jail、Catacombs、Act 2 Basement、Act 2 Tomb、Act 3 Sewer 为 0。此前引擎完全忽略
该列,上述 6 个关卡族生成的地图恒比原版多一列一行。
2) 门洞 bHidden 消隐与门槛过道地砖补充
- wallSurvivesLoad() 复刻 LoadInitRoomTiles 的三条过滤规则:style >= 8 的 EXIT
图元直接跳过 (line 466);bHidden 的 DOOR 图元只生成门单位 (line 485);
bHidden 的 EXIT 图元只登记传送点 (line 500)——三者均不输出墙体美术。
- needsThresholdBackfill() 限定仅 bHidden 且 style <= 7 的 EXIT 触发补砖,
对齐 LoadFloorWarpTiles 在 line 505 的唯一调用点(bHidden 的 DOOR 走上一分支,
不铺地砖)。
- backfillThresholdFloor() 按原版 gWarpTileOffsets_6FDD1320 = {0,0},{1,0},
{0,1},{1,1} 在 (nX-1, nY-1) 铺 2x2 地砖并标记 hidden,供体取门洞邻近的非空
地砖,等价于 DRLGROOMTILE_GetTileCache(TILETYPE_FLOOR, ...)。
- 补砖在所有房间落图完成后统一执行:门洞位于两房交界,供体可能属于当时尚未
落图的邻居。
verify-packs 只校验 preset 关卡(entry.kind !== 'preset' 时跳过),故迷宫尺寸
变化不会影响 1671 项逐字节断言。
新增 tests/killedge-113c.test.ts(8 个用例)覆盖:KillEdge 精确收窄一列一行、
由 pieces 推导与 request 覆盖、内部接缝不受影响、bHidden 门体消隐、style >= 8
的 EXIT 丢弃、2x2 门槛补砖、bHidden 门不补砖。已做变异测试:关闭裁剪或关闭墙体
过滤各会导致 3 个用例失败,确认断言有效。
Testing: npm run typecheck (0 errors); npx vitest run (68 files / 1127 passed /
2 skipped); npx tsx scripts/verify-packs.ts (1671/1671).
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-18 09:05:17 +00:00
troytt
0b08b4e125
fix(assets): track fallback warps in stats.notes and set assertion cap in verify-packs
...
- Track fallback warps in buildSceneLinks with level and destination names, recording to stats.notes in scene.json
- Expose notes and unresolved in WildernessResult.stats and SceneLinks.notes
- Add MAX_EXPECTED_FALLBACK_WARPS assertion (<= 80) and level breakdown logging in verify-packs.ts to prevent silent proliferation of invented staircases
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-17 13:55:56 +00:00
troytt
a3a3e44838
refactor(acts): unify ACT_TOWNS and townLevelForAct single source of truth
...
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-17 13:31:07 +00:00
troytt
5b3a9db986
fix(maze): 让 UNIMPLEMENTED_PASSES 反映真实缺口
...
`UNIMPLEMENTED_PASSES` 此前被硬编码为 `[]`,注释声称 11 个核心地牢 pass 全部
实现,于是 `stats.unimplementedPasses` 在每一关都无条件报告「零缺口」——包括
那些 `specials` 表为空、根本不生成楼梯房间的 level type。
改为从 `MAZE_LEVEL_TYPE_PROFILES` 实测推导:
- 新增 `unimplementedPassesFor(levelTypeName)`,按 level type 返回缺口:
没有 profile 行,或 `specials` 为空且不是 `singleRoom` 定制关。
- `UNIMPLEMENTED_PASSES` 改为所有 level type 缺口的并集,因此非空。
- `generateMaze` 的 `stats.unimplementedPasses` 改报本关自己的缺口,
而不是全局常量;`unresolvedRoles` / `notes` 本来就已随 stats 返回。
同时修正两处已经与代码不符的注释:`maze.ts` 里「per-act 装饰 pass 均未复现」
(它们已在 `maze-special-passes.ts` 中实现),以及 `pack-act-assets.ts` 里
「第二幕下水道 / 墓穴 / 第三幕地牢 specials 为空」(这三者现在都有 specials,
真正为空的是 Harem、Basement、Spider、Act 5 Lava)。
测试:原 `UNIMPLEMENTED_PASSES is completely empty` 断言的是被修正的错误行为,
改为断言它恰好列出所有 `specials` 为空的 level type;另加 4 个用例覆盖
按 level type 查询与 `generateMaze` 的 stats 透出。
TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
2026-09-17 11:29:06 +00:00
troytt
5aba7487f3
Merge branch 'fix/issue-16' into main
2026-09-17 09:10:59 +00:00
troytt
3951e70ef4
Merge branch 'fix/issue-20'
2026-09-17 09:09:01 +00:00
troytt
893e9b8550
Merge branch 'fix/issue-48'
...
# Conflicts:
# src/game/maze.ts
2026-09-17 08:41:40 +00:00
troytt
e1163b1253
Merge branch 'fix/issue-47'
...
# Conflicts:
# src/game/maze.ts
2026-09-17 08:40:55 +00:00
troytt
11e2e42731
perf(render): optimize GL state with immutable texStorage2D, uniform caching, and configurable discard ( fixes #20 )
2026-09-17 08:16:05 +00:00
troytt
33585afe8d
test(render/perf): comprehensive lifecycle verification and performance regression suite ( fixes #16 )
2026-09-17 08:15:13 +00:00
troytt
801afd7ddb
fix(map-gen/maze): enforce Levels.txt global bounds SizeX/SizeY on room growth ( fixes #48 )
2026-09-17 06:59:31 +00:00
troytt
776e2d3c20
feat(map-gen/maze): integrate waypoint room into maze preset replacement pass ( fixes #47 )
2026-09-17 06:33:16 +00:00
troytt
72aa835260
refactor(map-gen/preset): move Chaos Sanctuary (Level 108) into preset pipeline ( fixes #45 )
2026-09-17 06:20:35 +00:00
troytt
87a575f4e8
chore(tooling): add git worktree helper script for multi-session development
2026-09-17 05:49:58 +00:00
troytt
e9f2ceec9b
feat(map-gen/population): implement wilderness population phase, waypoints, shrines, superuniques & safe zones ( fixes #36 )
2026-09-17 03:44:12 +00:00
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
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
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
270c743aef
perf(entities): offline pre-bake Sorceress and monster atlases into Act packs ( #30 )
2026-09-16 01:16:38 +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
258be4d608
fix(publish): fine-grained batched push with resume
...
跨境链路对长连接做 MSS 钳制:实测 pmtu 1460 但 mss 被压到 324,重传率
13.7%,cwnd 触底到 3-4,有效带宽只剩 ~20 KB/s。原来按 act 分 5 批推送,
单批最大 117 MB,在这种链路上必然超时,且失败后整批重来。
改为:
- 按地图目录累计到 ~10 MB 就切一批(45 批 / 399.1 MB),失败只重推一批
- http.lowSpeedLimit 1000 + http.lowSpeedTime 120:低于 1 KB/s 持续 2 分钟
就断开,让重试开新连接(实测新连接能拿回 mss 1408,速度提升 2.5 倍)
- --work 支持续推:用 git ls-remote 读远端 main,回卷本地工作区对齐,
再跳过所有已在 HEAD 中的批次,避免重复上传
- 5 次重试,线性退避 15/30/45/60 秒
另外修正 tests/wilderness-features.test.ts 的 WildernessPiece 导入——
verbatimModuleSyntax 下类型导入必须用 import type。
TAG=agy
CONV=c89513df-cd12-4749-b5bd-5e9f16639391
2026-09-15 06:33:19 +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
4cc49a01aa
perf(render): 视口剔除 + 单批次合图 + 零分配顶点写入 (refs #16 )
...
主循环掉到 20 tps 的原因不是"缺引擎",而是三件具体的事,这个改动把它们一次做掉。
1. 视口剔除。此前整张地图无条件提交:把 act-scene 的绘制顺序在 365 张烘焙关卡上
静态重放,平均每帧 2 848 个四边形、最差 14 400 个(act4 迪亚波罗),而真正落在
1280x720 视口里的只有 7%~11%。现在关卡建好时把每条绘制列表的包围盒预先摊平成
一块 Float32Array,每帧线性扫描做四次浮点比较。实测平均 2 848 -> 204 个/帧
(7.2%),最差 14 400 -> 437 个。
之所以是线性扫描而不是二分或分桶:绘制列表并不按 y 单调(地面 0/365 单调,最大
回退 9 520 px),任何空间索引都得重新合并回画家顺序。14 400 次比较约 15~30 µs,
占 16 ms 预算的 0.2%,换来的是顺序原样保留。墙和对象那条两路深度归并里,剔除只
跳过"画"这一步,游标照常前进——否则后面所有遮挡关系都会错位。
2. 一次 draw call。此前批次一遇到图集页切换就得断开,同一帧里页号来回横跳,实测
平均 361 次、最差 3 077 次 draw call。现在每个在用的图集页各占一个纹理单元,
单元号随顶点属性进着色器,片元按 switch 选采样器,只有在用页数超过
MAX_TEXTURE_IMAGE_UNITS 时才断批。
这里没有采用 issue 里原先设想的 TEXTURE_2D_ARRAY:页高差异极大(2048x1999 到
2048x48),纹理数组要求各层等大,实测朴素做法显存涨 1.73 倍(最差 2.82 倍,单关
156.8 MB),装箱后仍要 1.29 倍。而全部 365 张关卡里单关最多只有 10 个页,加角色
图集 11 个,稳稳低于 WebGL2 保证的 16 个单元——多单元合批同样是 1 次 draw call,
显存零增长,资源也不用重烘焙。
3. 零分配顶点写入。quad() 原本每次建 4 个临时数组再解构,实测 2 849 个四边形写一帧
要 244.9 µs;展开成标量后 106.0 µs(2.31 倍),每帧少建 11 396 个临时数组。同时
顶点从 6 个/四边形降到 4 个并改用索引缓冲,上传前先 orphan 一次缓冲区避免与 GPU
抢同一块内存。
顺带修掉几个让问题一直看不见、或者纯属每帧浪费的地方:
- state.drawCalls 原本在 act-scene 和 walk 里都硬编码成 1,这个假读数正是问题长期
没被发现的原因;现在读渲染器自己的计数,HUD 一并显示图元数、剔除数和帧耗时。
- pages.filter(...).length 每帧建一个临时数组只为数个数,改成循环。
- HUD 文本每帧重建(十次字符串拼接 + 四次 toFixed),改成 4 Hz。
- NPC 名牌每帧 innerHTML='' 再 createElement,改成复用元素只改位置。
- onTick 里的 playerMoving 用 let 遮蔽了外层同名变量,导致 onRender 永远读到 false、
角色走路动画从不播放。改成赋值。
护栏:新增 tests/cull.test.ts,除边界条件外,用全部 365 张关卡真实数据重放一遍,
断言"开剔除后画出的序列必须是不开剔除时那个序列的子序列"(顺序一致、只少不多),
并守住剔除率。verify-renderer-lifecycle 从 26 条加到 37 条,锁住"60 个四边形在 3 个
页之间来回切只花 1 次 draw call"、超出单元预算恰好多花 1 次、计数器在 begin() 归零、
以及着色器里采样器数组的形状。
Refs #16
2026-09-15 01:30:39 +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
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
68c7890eee
fix(packer): 修正 monsters 前缀长度,恢复全部 NPC 美术解析 (refs #6 )
...
打包器在切分 MPQ 成员名时,objects 走 `OBJECT_PREFIX.length`,
monsters 却硬编码了一个手数的 `22`。`data\global\monsters\` 的真实
长度是 **21**,于是每个怪物 token 都被多切掉一个字符:
slice(21) -> "DC" (正确)
slice(22) -> "C" (实际发生的)
`monsterMembers` 因此以 `C`/`E`/`R`/`S`/`J` 这些残缺 token 建索引,
而查表时用的是正确的 `DC`/`JE`/`XR`/`XS`/`0J`,导致**每一次 NPC 美术
查找都必然落空**。由于代码里对"找不到"是静默跳过,这个缺陷不会报错,
只会让 NPC 全部变成没有贴图的空对象——在真实 MPQ 到位之前完全无法察觉。
改为引入 `MONSTER_PREFIX` 常量并由 `.length` 推导,消除手数魔数。
实测(覆盖全部 365 个 scene.json):
NPC 实例总数 103 103
其中解析出 COF 成员 0 -> 103
其中解析出精灵帧 0 -> 103
按 token 分解,修复后全部满额:
0J 82/82、DC 4/4、HR 2/2、JE 4/4、XR 2/2、XS 9/9。
抽查:Deckard Cain 现在解析到
`data\global\monsters\DC\TR\DCTRLITNUHTH.dcc`,帧 43x75,偏移 (-45,-54)。
普通场景物件仍走 `objects\`,未受影响。
2026-09-14 14:51:49 +00:00