修复顶角墙缺左半: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 个顶角一致。
This commit is contained in:
troytt 2026-09-14 09:18:43 +08:00
parent 956f7e8b45
commit 61c6691555
2 changed files with 66 additions and 1 deletions

View File

@ -12,7 +12,10 @@
// 3. `looseRefs == 0`(没有任何引用走类型无关兜底)与 `missingTiles == 0`——
// 这两条合起来保证"画出的类型 == 引用的类型";
// 4. 位置自洽:地面 `y == (cx+cy)*40` 且瓦片 `minBlockY == 0`;
// 墙 `y == (cx+cy)*40 + minBlockY + 80`;屋顶 `y == (cx+cy)*40 - roofHeight`。
// 墙 `y == (cx+cy)*40 + minBlockY + 80`;屋顶 `y == (cx+cy)*40 - roofHeight`;
// 5. 顶角(DS1 `type 3`)必须连带画出左半(`type 4`)——引擎在
// `DRLGROOMTILE_InitWallTileData` 里就是这么做的,DS1 自己从不列左半。
// 漏掉它时墙角会缺一块,看起来像"渲染不正常"。
//
// node scripts/verify-tiles.ts [--dir=samples/d2] [--limit=35]
//
@ -69,6 +72,9 @@ async function main(): Promise<void> {
let looseRefs = 0
let missingTiles = 0
let positionMismatch = 0
let cornerRefs = 0
let cornerPairs = 0
let missingCornerPairs = 0
const worst: [number, string][] = []
for (const row of tables.levels.rows) {
@ -99,6 +105,13 @@ async function main(): Promise<void> {
const seed = levelSeed(info.ds1Names[0]!)
const scene = buildIsoMapScene(ds1, libs, seed)
for (const cellRef of ds1.cells.flat()) {
for (const wall of cellRef.walls) {
if (!wall.hidden && wall.prop1 !== 0 && wall.type === 3) cornerRefs += 1
}
}
cornerPairs += scene.cornerPairs
missingCornerPairs += scene.missingCornerPairs
looseRefs += scene.looseRefs
missingTiles += scene.missingTiles
@ -139,6 +152,9 @@ async function main(): Promise<void> {
check('没有任何引用走类型无关兜底', looseRefs === 0, `looseRefs ${String(looseRefs)}`)
check('没有解析不到的引用', missingTiles === 0, `missingTiles ${String(missingTiles)}`)
check('地面/墙/屋顶的位置公式自洽', positionMismatch === 0, `${String(positionMismatch)} 个不符`)
check('每个顶角右半都画出了左半伙伴', cornerRefs > 0 && cornerPairs === cornerRefs,
`${String(cornerPairs)}/${String(cornerRefs)} 个 type-3 墙带了 type-4 伙伴`)
check('没有解析不到的顶角伙伴', missingCornerPairs === 0, `${String(missingCornerPairs)} 个`)
if (worst.length > 0 && worst[0]![0] > 0) console.log(` 最严重:${worst.slice(0, 5).map(([pct, name]) => `${name} ${pct.toFixed(1)}%`).join(',')}`)
const passed = checks.filter(entry => entry.ok).length

View File

@ -66,6 +66,19 @@ const FLOOR_TILE_TYPE = 0
* painter order and out of the wall offset rule.
*/
const ROOF_WALL_TYPE = 15
/**
* DS1 wall types of the two halves of a "top" (north) corner.
*
* The engine never draws the right half alone: `DRLGROOMTILE_InitWallTileData`
* (`D2Common/DRLG/RoomTile.cpp`) adds a `TILETYPE_WALL_TOP_CORNER_RIGHT` wall and
* then, in the same cell and with the same packed tile information, a second
* `TILETYPE_WALL_TOP_CORNER_LEFT` tile — fetched from the tile cache with the same
* style/sequence. Without that companion the corner is missing its left face, which
* is what a "broken corner block" looks like on screen.
*/
const TOP_CORNER_RIGHT_TYPE = 3
/** The companion half of {@link TOP_CORNER_RIGHT_TYPE}. */
const TOP_CORNER_LEFT_TYPE = 4
/** One tile to draw, in screen pixels. */
export interface IsoDraw {
/** Index into {@link IsoMapScene.frames}. */
@ -155,6 +168,15 @@ export interface IsoMapScene {
* `npm run verify:tiles` exists to catch.
*/
readonly looseRefs: number
/**
* Top-corner-right walls that got their left half drawn as well.
*
* The engine always adds the companion tile (`DRLGROOMTILE_InitWallTileData`), so
* this should equal the number of type-3 walls in the level.
*/
readonly cornerPairs: number
/** Top-corner-right walls whose left half could not be resolved (should be 0). */
readonly missingCornerPairs: number
}
/**
@ -405,6 +427,8 @@ export function buildIsoMapScene(level: Ds1, libraries: readonly Dt1[], seed = 0
// skipping them would silently drop blocking sub-tiles.
let shadowBlocked = 0
let looseRefs = 0
let cornerPairs = 0
let missingCornerPairs = 0
const stamp = (cellX: number, cellY: number, tile: Dt1Tile): void => {
for (let subY = 0; subY < SUB_TILES_PER_TILE; subY += 1) {
for (let subX = 0; subX < SUB_TILES_PER_TILE; subX += 1) {
@ -494,6 +518,29 @@ export function buildIsoMapScene(level: Ds1, libraries: readonly Dt1[], seed = 0
rawWalls.push(draw)
}
stamp(cellX, cellY, tile)
// A top-corner-right wall drags its left half along (see
// {@link TOP_CORNER_RIGHT_TYPE}): same cell, same style/sequence, resolved as
// a type-4 tile, drawn with its own block shift like any other wall. The
// engine's DS1s never list the left half themselves, so this is the only way
// the corner gets both faces — 411 of them across the 35 preset levels.
if (tile.type === TOP_CORNER_RIGHT_TYPE) {
const companion = resolveExactType(pool, wall.style, wall.sequence, TOP_CORNER_LEFT_TYPE, cellX, cellY, seed)
if (companion === undefined) {
missingCornerPairs += 1
noteMissing('corner-left', wall.style, wall.sequence, TOP_CORNER_LEFT_TYPE, cellX, cellY)
} else {
const leftTile = libraries[companion.library]!.tiles[companion.tile]!
rawWalls.push({
frameIndex: frameFor(companion.library, companion.tile),
x: orthoX + TILE_ANCHOR_X,
y: orthoY + leftTile.minBlockY + WALL_SURFACE_HEIGHT,
cellX, cellY, library: companion.library, tile: companion.tile,
})
cornerPairs += 1
stamp(cellX, cellY, leftTile)
}
}
}
}
}
@ -540,6 +587,8 @@ export function buildIsoMapScene(level: Ds1, libraries: readonly Dt1[], seed = 0
clippedTiles,
shadowBlockedSubtiles: shadowBlocked,
looseRefs,
cornerPairs,
missingCornerPairs,
duplicateRefs: pool.duplicates,
}
}