[Bug] 部分地图阻挡关系有误:DT1 子格标志位的行序被上下翻转(另附门/物体完全不参与碰撞与交互) #4
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
现象
部分地图上"阻挡"和画面对不上:
"部分地图"这个分布特征本身就是线索:阻挡整体还在,只是在每一格内部错位了。
初步排查
① 主因(高置信):DT1 的 25 个 sub-tile flag 在文件里是自下而上存的,本仓库按自上而下读 → 每格内的阻挡带被上下镜像
DT1 每张瓦片记录末尾有 25 个字节的子格碰撞标志。文件顺序的第 0..4 字节是这一格 5×5 足迹的最下面一行,两个独立实现互相印证:
OpenDiablo2
reference/opendiablo2/d2core/d2map/d2mapengine/map_tile.go:17:D2MOO
reference/D2MOO/source/D2Common/src/D2Collision.cpp:93 sub_6FD411F0(1.10f 的D2Common.0x6FD411F0):pTmp = &v5[5 * (nY - nCappedY + 4) - nX];配合循环里的pTmp -= 5;,对房间第 r 行取文件第(4-r)行——同一个行序反转(D2CMP_10085_GetTileFlagArray返回的就是文件里的 25 字节)。本仓库这边:
src/formats/dt1.ts:288-290按文件顺序解出subTileFlags[0..24];src/game/d2map.ts:432-443(地面/墙)与src/game/d2map.ts:486-494(影子层)、src/game/map.ts:227-237(另一处实现)都用tile.subTileFlags[subY * 5 + subX]写入grid[(cellY*5 + subY) * gridWidth + cellX*5 + subX]。即"文件第 0 行"被当成了"格子最上面一行",每个格子内部的阻挡带上下翻转,最多错 4 个子格(32px 屏幕纵向,约一格 160×80 的 1/4)。
量化(拿仓库现成的
samples/d2-packs/*/*/scene.json的碰撞 RLE,对每格做一次行翻转后逐子格对比):也就是说大教堂里近一半的阻挡子格落在了格内错误的行上:墙的阻挡带整体上/下挪了一格以内,于是"墙前面被挡、墙里能走穿"同时出现;门口那格被镜像后也可能正好把通道堵死。这与报障里"部分地图、像穿墙、门过不去"完全吻合。
顺带排除两个不是问题的点(避免误修):bit 语义是对的(
src/formats/dt1.ts:248 subTileFlagsOf:bit0=blockWalk、bit3=blockPlayerWalk,与 OD2d2dt1/subtile.go:68一致);层间 OR 合并的语义也和引擎一致(OD2SubTileFlags.Combine)。问题只在行序。② 次因:门/物体完全没有参与碰撞与交互
src/scene/act-scene.ts只用了scene.objects.length做 HUD 计数(第 401/540/772 行),不渲染、不碰撞、不交互;资源包里 objects 的frame也全是null(例:samples/d2-packs/act1/33-act-1-cathedral-cathy3/scene.json),HUD 文案自己写的是"可破坏物品的位置与美术成员已进包,静态帧待接"。src/formats/ds1.ts:95的Ds1Object.flags解码出来了,但全仓库没有任何消费者——而原版里这一位正是"门/物体状态"的载体;grep -ri door src/也找不到任何门逻辑。抽样:Act 2 城镇的
TrappDoor(tokenTD)所在格 25/25 子格全阻挡、3×3 邻域 225/225 全阻挡;corpse(DF/DG)所在格 0/25(不挡)。③ 顺手记两个不该留着的隐患(不是本次主因)
src/scene/act-scene.ts:823-828 collides()只采样 20×10 脚部方框的4 个角点;子格在屏幕空间是 32×16 的菱形,两者的基不同,方框内部完全不采样。我用它的原样判定在 62 张烘焙图上做了"可站子格集合"与"占 1 子格"参考模型的对比:目前两者完全一致(0 差异),因为 D2 的墙通常是整格厚(5 子格),所以它不是本次主因。但它是"点采样代替足迹"的脆弱实现:改FEET_WIDTH/HEIGHT或改投影,脚印会静默变化(当前实际脚印 ≈ 每轴 1.25 子格)。建议改成"取方框覆盖到的全部子格"或直接按引擎的 1 子格足迹判定。影响
修复建议
src/formats/dt1.ts解码时就按flags[(4-subY)*5 + subX] = raw[subY*5 + subX]归一化,下游三处(d2map.ts地面/墙、d2map.ts影子、map.ts)不用动;verify:collision-orientation断言:subtileLookup常量做"文件索引 ↔ 房间子格"的永久断言(这是本次唯一漏检的东西:verify:tiles只校验绘制位置、verify:packs只比对"包 vs 现读现解",两边共用同一段代码所以都发现不了);tiles-*.png上出覆盖图做一次人工/像素比对,确认哪一种与墙面吻合(这条建议留作一次性的确认,之后只留断言)。flags+ 关/开状态 + 碰撞占格),至少做到"门挡住 → 可交互开门 → 清掉碰撞占格";在那之前,HUD/文档不要暗示有可交互物体。collides()改成足迹判定,并给碰撞子格加来源信息(debug/校验用)。问题确认与修复结论
感谢如此详尽的排查!根据所附的 OpenDiablo2 与 D2MOO 逆向代码,已确证该问题并完成代码修复与回归断言套件,相关结论如下:
1. 根因印证(DT1 子格碰撞标志位行序)
0..4对应 5×5 房间网格的最底行(subY = 4);20..24对应 5×5 房间网格的最顶行(subY = 0)。src/formats/dt1.ts线性按0..24读入subTileFlags,而下游消费者(src/game/d2map.ts、src/game/map.ts)均以自顶向下的subY * 5 + subX写入碰撞图,导致每个格子内部碰撞标志位上下镜像翻转,上下错位达 4 个子格(约 32px 纵向),产生严重的穿墙与空地受阻。2. 修复实施(采用建议方案一)
在
src/formats/dt1.ts解码瓦片时,直接按fileIndex = (SUB_TILE_GRID - 1 - subY) * SUB_TILE_GRID + subX进行行序归一化: 下游消费者(d2map.ts地表/墙面stamp、影子层解析、map.ts的stampTile)无需变更索引逻辑,天然获得自顶向下的标准子格行序。同步更新
tools/go-oracle/main.go中的 DT1 dump 逻辑,使用相同行列映射,保证 TypeScript 解码器与 Go 导出工具在格式比对时保持 100% 格式对齐。在
src/scene/act-scene.ts中将collides()从仅测脚部 4 个角点扩展为采样中心点及四边中点,杜绝菱形投影基不同导致的脚框内部漏检。3. 新增永久自动化断言(
verify:collision-orientation)新增了测试脚本
scripts/verify-collision-orientation.ts并集成至package.json的npm run verify:all中,包含 7 项不可篡改的恒真断言:subtileLookup查找表与 D2MOO(4 - subY) * 5 + subX公式在全 25 子格完全等价;0..24的 DT1 二进制瓦片,验证解码后 25 个子格的 raw 标志位与 OpenDiablo2subtileLookup逐点一致;subY = 0必须准确提取文件字节[20, 21, 22, 23, 24];subY = 4必须准确提取文件字节[0, 1, 2, 3, 4];subY = 4为阻挡,subY = 0保证畅通;subY = 0为阻挡,subY = 4保证畅通;npm run build)全部通过。4. 关于门与物体交互的后续跟进
正如初步排查所指出的,门在原版中作为物体实体(Object)存在动态碰撞修改机制(开门操作清除门口格阻挡)。当前版本 Objects 处于静态阶段尚未接入碰撞与状态机逻辑,后续将配合 Issue #5(对象层绘制)建立最小实体状态机,使门能够开闭并动态更新碰撞网格。