diff --git a/scripts/verify-tiles.ts b/scripts/verify-tiles.ts index 3e77a6a..2b976bf 100644 --- a/scripts/verify-tiles.ts +++ b/scripts/verify-tiles.ts @@ -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 { 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 { 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 { 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 diff --git a/src/game/d2map.ts b/src/game/d2map.ts index a2ee84e..134c137 100644 --- a/src/game/d2map.ts +++ b/src/game/d2map.ts @@ -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, } }