diablo2-web/scripts/verify-tiles.ts

150 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// verify-tiles.ts — 验收"每个槽位画的是不是它该有的那种瓦片"
//
// 这份脚本存在的原因是一次真实事故:地面引用在 `buildIsoMapScene()` 里以 `type = null` 走
// **类型无关**的兜底池,池子里混着地面、墙、柱子、影子、树、屋顶。加上"按 RarityFrameIndex
// 加权随机选变体"之后,地面槽位会随机挑到石墙/柱子瓦片,而地面绘制不做墙那套
// `minBlockY + 80` 补偿 —— 于是画面里出现"悬在地面上的暗色方块"(35 关实测 1,918/28,704
// 个地面槽位画错类型,修道院大教堂 28.2%、地下墓穴 4 层 41.5%)。
//
// 现在地面按 DT1 `type 0` 解析,这里把当年的普查变成永久断言:
// 1. 地面槽位画出的瓦片 `type` 必须是 0;
// 2. 墙/屋顶槽位不得画出地面瓦片(type 0);
// 3. `looseRefs == 0`(没有任何引用走类型无关兜底)与 `missingTiles == 0`——
// 这两条合起来保证"画出的类型 == 引用的类型";
// 4. 位置自洽:地面 `y == (cx+cy)*40` 且瓦片 `minBlockY == 0`;
// 墙 `y == (cx+cy)*40 + minBlockY + 80`;屋顶 `y == (cx+cy)*40 - roofHeight`。
//
// node scripts/verify-tiles.ts [--dir=samples/d2] [--limit=35]
//
// 退出码非 0 表示有断言没过。
export {}
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { decodeDs1 } from '../src/formats/ds1.ts'
import { decodeDt1 } from '../src/formats/dt1.ts'
import type { Dt1 } from '../src/formats/dt1.ts'
import { cell, loadActTables, resolveLevel, resolveLevelLibraries } from '../src/game/acts.ts'
import { buildIsoMapScene, levelSeed, ORTHO_CELL_HEIGHT } from '../src/game/d2map.ts'
/** 归档目录。 */
const DIR = process.argv.find(argument => argument.startsWith('--dir='))?.slice('--dir='.length) ?? 'samples/d2'
/** 最多走多少个预置关卡。 */
const LIMIT = Number(process.argv.find(argument => argument.startsWith('--limit='))?.slice('--limit='.length) ?? '35')
/** 断言结果。 */
const checks: { name: string; ok: boolean; detail: string }[] = []
/**
* 记录一条断言。
*
* @param name - what was checked.
* @param ok - whether it held.
* @param detail - evidence.
*/
function check(name: string, ok: boolean, detail: string): void {
checks.push({ name, ok, detail })
console.log(`${ok ? 'ok ' : 'FAIL'} ${name} — ${detail}`)
}
/**
* 主流程。
*/
async function main(): Promise<void> {
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`${DIR}/${name}`)))
}
const tables = await loadActTables(archives)
const libraryCache = new Map<string, Dt1>()
let levels = 0
let floors = 0
let walls = 0
let roofs = 0
let wrongFloorType = 0
let floorAsWall = 0
let looseRefs = 0
let missingTiles = 0
let positionMismatch = 0
const worst: [number, string][] = []
for (const row of tables.levels.rows) {
if (cell(tables.levels, row, 'DrlgType') !== '2') continue
const levelId = Number(cell(tables.levels, row, 'Id'))
let info
try {
info = resolveLevel(tables, levelId)
} catch { continue }
const { dt1Names } = resolveLevelLibraries(tables, levelId)
const libs: Dt1[] = []
for (const name of dt1Names) {
const cached = libraryCache.get(name)
if (cached !== undefined) { libs.push(cached); continue }
try {
const decoded = decodeDt1(await archives.read(name))
libraryCache.set(name, decoded)
libs.push(decoded)
} catch { /* 缺库时 verify:acts 会报 */ }
}
if (libs.length === 0) continue
let ds1
try {
ds1 = decodeDs1(await archives.read(info.ds1Names[0]!))
} catch { continue }
if (levels >= LIMIT) break
levels += 1
const seed = levelSeed(info.ds1Names[0]!)
const scene = buildIsoMapScene(ds1, libs, seed)
looseRefs += scene.looseRefs
missingTiles += scene.missingTiles
let badHere = 0
for (const draw of scene.floors) {
const tile = libs[draw.library]?.tiles[draw.tile]
if (tile === undefined) continue
floors += 1
// (1) 地面槽位只能是 type 0 的瓦片
if (tile.type !== 0) { wrongFloorType += 1; badHere += 1 }
// (4) 地面位置:不带动画块补偿,且地面瓦片本身不该有块位移
if (draw.y - scene.originY !== (draw.cellX + draw.cellY) * ORTHO_CELL_HEIGHT || tile.minBlockY !== 0) positionMismatch += 1
}
for (const draw of scene.walls) {
const tile = libs[draw.library]?.tiles[draw.tile]
if (tile === undefined) continue
walls += 1
// (2) 墙槽位不该画出地面瓦片
if (tile.type === 0) floorAsWall += 1
// (4) 墙位置 = 格子基线 + 该瓦片自己的块位移 + 一格高
if (draw.y - scene.originY !== (draw.cellX + draw.cellY) * ORTHO_CELL_HEIGHT + tile.minBlockY + 80) positionMismatch += 1
}
for (const draw of scene.roofs) {
const tile = libs[draw.library]?.tiles[draw.tile]
if (tile === undefined) continue
roofs += 1
if (draw.y - scene.originY !== (draw.cellX + draw.cellY) * ORTHO_CELL_HEIGHT - tile.roofHeight) positionMismatch += 1
}
if (scene.floors.length > 0) worst.push([100 * badHere / scene.floors.length, info.levelName])
}
worst.sort((left, right) => right[0] - left[0])
console.log(`\n走了 ${String(levels)} 个预置关卡:地面 ${String(floors)} 槽、墙 ${String(walls)} 槽、屋顶 ${String(roofs)} 槽`)
check('地面槽位画出的瓦片类型都是 0(无错配方块)', wrongFloorType === 0,
`${String(wrongFloorType)} 个错类型 / ${String(floors)} 个地面槽`)
check('墙/屋顶槽位没有画出地面瓦片', floorAsWall === 0, `${String(floorAsWall)} 个`)
check('没有任何引用走类型无关兜底', looseRefs === 0, `looseRefs ${String(looseRefs)}`)
check('没有解析不到的引用', missingTiles === 0, `missingTiles ${String(missingTiles)}`)
check('地面/墙/屋顶的位置公式自洽', positionMismatch === 0, `${String(positionMismatch)} 个不符`)
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
console.log(`\n${String(passed)}/${String(checks.length)} passed`)
if (passed !== checks.length) process.exitCode = 1
}
await main()