diablo2-web/tests/e2e-automap-113c/tier5-adversarial-coverage....

1345 lines
48 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.

/**
* @fileoverview Tier 5 — White-Box Adversarial & Differential Stress Tests (1.13c Automap & Minimap).
*
* Authored by `teamwork_preview_challenger_r1_1` following `test_coverage_audit` (Whitebox mode)
* and `solution_stress_testing` (Differential Oracle & Boundary/Permutation Fuzzing).
*
* Every test is prefixed with `adv_` to distinguish Tier 5 white-box adversarial tests from Tiers 1–4.
*
* White-box targets audited & stress-tested:
* - `src/formats/automap-table.ts` (lines 1–896)
* - `src/game/d2map.ts` (`buildLevelAutomap`, lines 587–833)
* - `src/ui/minimap.ts` (`Minimap`, lines 1–1452)
* - `src/ui/hud-manager.ts` (`HUDManager`, lines 1–1449)
*/
import fs from 'node:fs'
import path from 'node:path'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { MpqArchive, memorySource } from '../../src/mpq/index.ts'
import type { Ds1 } from '../../src/formats/ds1.ts'
import {
AUTOMAP_CELL_WEIGHT_PAIRS,
AUTOMAP_LEVEL_NAMES,
AUTOMAP_TILE_NAMES,
type AutomapAtlasSlot,
type AutomapCell,
type AutomapTable,
computeAutomapObjectPixelCoords,
computeAutomapTilePixelCoords,
deduplicateAutomapCells,
generateTownSavedExtras,
getAutomapCellId,
getAutomapCellWeight,
getAutomapLevelTypeId,
getAutomapTileType,
getCrosshairSegments,
parseAutomapTable,
resolveMonsterAutomapCellId,
resolveObjectAutomapCellId,
resolveTileAutomapCellId,
} from '../../src/formats/automap-table.ts'
import { cell, loadActTables, parseTable } from '../../src/game/acts.ts'
import { buildLevelAutomap } from '../../src/game/d2map.ts'
import {
loadObjectsTable,
type MonstersTable,
type ObjectsTable,
} from '../../src/game/objects.ts'
import { MountedArchives } from '../../src/mpq/mount.ts'
import type { AtlasFrame } from '../../src/render/atlas.ts'
import type { AtlasHandle, SpriteRenderer } from '../../src/render/renderer.ts'
import {
AUTOMAP_UI_MANIFEST,
type AutomapSpriteKey,
} from '../../src/ui/baked-ui-meta.ts'
import type { D2ColorCode, D2FontRenderer } from '../../src/ui/font.ts'
import {
AUTOMAP_CROSSHAIR_COLORS,
AUTOMAP_REVEAL_DISTANCE_THRESHOLD,
Minimap,
type MinimapLevel,
automapPixelToCellCoords,
computeAutomapRevealDistance,
computeAutomapViewport,
projectAutomapCellScreenRect,
resolveAutomapSheetName,
} from '../../src/ui/minimap.ts'
const ROOT_DIR = process.cwd()
const MPQ_EXP_PATH = path.resolve(ROOT_DIR, 'samples/d2/d2exp.mpq')
const MPQ_DATA_PATH = path.resolve(ROOT_DIR, 'samples/d2/d2data.mpq')
const MPQ_PATCH_PATH = path.resolve(ROOT_DIR, 'samples/d2/Patch_D2.mpq')
let expArchive: MpqArchive
let dataArchive: MpqArchive
let rawAutomapBytes: Uint8Array
let rawAutomapText: string
let automapTable: AutomapTable
let objectsTable: ObjectsTable
let monstersTable: MonstersTable
let monstats2ById: Map<string, { automapCel: number; noMap: boolean }>
beforeAll(async () => {
expArchive = await MpqArchive.open(memorySource(new Uint8Array(fs.readFileSync(MPQ_EXP_PATH))))
dataArchive = await MpqArchive.open(memorySource(new Uint8Array(fs.readFileSync(MPQ_DATA_PATH))))
const patchArchive = await MpqArchive.open(memorySource(new Uint8Array(fs.readFileSync(MPQ_PATCH_PATH))))
const archives = new MountedArchives()
archives.add('d2data.mpq', dataArchive)
archives.add('d2exp.mpq', expArchive)
archives.add('Patch_D2.mpq', patchArchive)
const autoMapEntry = expArchive.find('data\\global\\excel\\AutoMap.txt')
if (!autoMapEntry) throw new Error('AutoMap.txt missing in d2exp.mpq')
rawAutomapBytes = await expArchive.read(autoMapEntry)
rawAutomapText = Buffer.from(rawAutomapBytes).toString('latin1')
automapTable = parseAutomapTable(rawAutomapBytes)
objectsTable = await loadObjectsTable(archives)
const actTables = await loadActTables(archives)
const presetPlaceByAct = new Map<string, string[]>()
for (const row of actTables.monpreset.rows) {
const actStr = cell(actTables.monpreset, row, 'Act')
const placeStr = cell(actTables.monpreset, row, 'Place')
if (!placeStr) continue
let list = presetPlaceByAct.get(actStr)
if (!list) {
list = []
presetPlaceByAct.set(actStr, list)
}
list.push(placeStr)
}
const statsById = new Map<string, readonly string[]>()
for (const row of actTables.monstats.rows) {
const id = cell(actTables.monstats, row, 'Id')
if (id) statsById.set(id, row)
}
monstersTable = {
preset: actTables.monpreset,
stats: actTables.monstats,
presetPlaceByAct,
statsById,
}
const monstats2Table = parseTable(await archives.read('data\\global\\excel\\monstats2.txt'))
monstats2ById = new Map()
for (const row of monstats2Table.rows) {
const id = cell(monstats2Table, row, 'Id')
if (id) {
monstats2ById.set(id.toLowerCase(), {
noMap: Number(cell(monstats2Table, row, 'noMap')) !== 0,
automapCel: Number(cell(monstats2Table, row, 'automapCel')) || 0,
})
}
}
})
/**
* Brute-force reference oracle for `deduplicateAutomapCells` (`0x6fba1528` & `0x6fb11320`).
*/
function oracleDeduplicateAutomapCells(cells: readonly AutomapCell[]): AutomapCell[] {
const referenceWeights = new Map<number, number>(AUTOMAP_CELL_WEIGHT_PAIRS)
const isSpecialIcon = (cellNo: number): boolean =>
(cellNo >= 300 && cellNo <= 319) || cellNo === 405
const normZero = (n: number): number => (n === 0 ? 0 : n)
const byCoord = new Map<string, AutomapCell>()
for (const raw of cells) {
const weight =
raw.weight !== undefined
? raw.weight
: (referenceWeights.get(raw.cellNo) ?? -1)
const normalized: AutomapCell = {
...raw,
xPixel: normZero(raw.xPixel),
yPixel: normZero(raw.yPixel),
weight,
}
const key = `${String(normalized.fSaved ?? 0)}:${String(normalized.xPixel)}:${String(normalized.yPixel)}`
const existing = byCoord.get(key)
if (!existing) {
byCoord.set(key, normalized)
continue
}
const existWeight =
existing.weight !== undefined
? existing.weight
: (referenceWeights.get(existing.cellNo) ?? -1)
if (weight > existWeight) {
byCoord.set(key, normalized)
} else if (weight === existWeight && weight === -1) {
if (isSpecialIcon(normalized.cellNo) && !isSpecialIcon(existing.cellNo)) {
byCoord.set(key, normalized)
}
}
}
return [...byCoord.values()].sort(
(a, b) => a.yPixel - b.yPixel || a.xPixel - b.xPixel || a.cellNo - b.cellNo,
)
}
/**
* Deterministic PRNG (Mulberry32) for reproducible adversarial fuzzing.
*/
function createSeededRng(seed: number): () => number {
let state = seed >>> 0
return () => {
state = (state + 0x6d2b79f5) >>> 0
let t = Math.imul(state ^ (state >>> 15), 1 | state)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function shuffleInPlace<T>(arr: T[], rng: () => number): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1))
const tmp = arr[i]!
arr[i] = arr[j]!
arr[j] = tmp
}
return arr
}
function createRecordingRenderer(): {
renderer: SpriteRenderer
sprites: Array<{
frame: AtlasFrame
x: number
y: number
atlas?: AtlasHandle
width?: number
height?: number
tint?: readonly [number, number, number, number]
}>
quads: Array<{
x0: number
y0: number
x1: number
y1: number
x2: number
y2: number
x3: number
y3: number
color: readonly [number, number, number, number]
}>
scissors: Array<{ x: number; y: number; width: number; height: number } | null>
glCalls: { enableCount: number; disableCount: number }
} {
const sprites: Array<{
frame: AtlasFrame
x: number
y: number
atlas?: AtlasHandle
width?: number
height?: number
tint?: readonly [number, number, number, number]
}> = []
const quads: Array<{
x0: number
y0: number
x1: number
y1: number
x2: number
y2: number
x3: number
y3: number
color: readonly [number, number, number, number]
}> = []
const scissors: Array<{ x: number; y: number; width: number; height: number } | null> = []
const glCalls = { enableCount: 0, disableCount: 0 }
const mockGl = {
SCISSOR_TEST: 0x0c11,
enable(cap: number) {
if (cap === 0x0c11) glCalls.enableCount++
},
scissor(x: number, y: number, width: number, height: number) {
scissors.push({ x, y, width, height })
},
disable(cap: number) {
if (cap === 0x0c11) {
glCalls.disableCount++
scissors.push(null)
}
},
}
const renderer = {
gl: mockGl,
flush: vi.fn(),
addAtlas: vi.fn((_img: unknown, width: number, height: number) => ({
texture: 'mock-atlas-tex' as unknown as WebGLTexture,
width,
height,
})),
draw(
frame: AtlasFrame,
x: number,
y: number,
options?: {
atlas?: AtlasHandle
width?: number
height?: number
tint?: readonly [number, number, number, number]
},
) {
sprites.push({
frame,
x,
y,
...(options?.atlas ? { atlas: options.atlas } : {}),
...(options?.width !== undefined ? { width: options.width } : {}),
...(options?.height !== undefined ? { height: options.height } : {}),
...(options?.tint ? { tint: options.tint } : {}),
})
},
drawSolid: vi.fn(),
drawSolidQuad(
x0: number,
y0: number,
x1: number,
y1: number,
x2: number,
y2: number,
x3: number,
y3: number,
color: readonly [number, number, number, number],
) {
quads.push({ x0, y0, x1, y1, x2, y2, x3, y3, color })
},
} as unknown as SpriteRenderer
return { renderer, sprites, quads, scissors, glCalls }
}
function registerAtlases(minimap: Minimap): void {
const keys: readonly AutomapSpriteKey[] = [
'MaxiMap',
'MaxiMapS',
'Act2Map',
'Act2MapS',
'Act4Map',
'Act4MapS',
'ExTnMap',
'ExTnMapS',
]
for (const key of keys) {
const meta = minimap.getSheetMeta(key)
minimap.setAtlasHandle(key, {
texture: key as unknown as WebGLTexture,
width: meta.atlasWidth,
height: meta.atlasHeight,
} as AtlasHandle)
}
}
describe('Tier 5 — White-Box Adversarial & Differential Stress Coverage (1.13c Automap)', () => {
// =========================================================================
// Suite 1: F3 — Exhaustive 49-Entry 0x6fba1528 Weight Table & Differential Oracle
// =========================================================================
describe('Suite 1: F3 — 0x6fba1528 Weight Table & Differential Permutation Oracle', () => {
it('adv_f3_all_49_weight_pairs_exact_group_mapping_and_2048_bounds', () => {
expect(AUTOMAP_CELL_WEIGHT_PAIRS).toHaveLength(49)
const expectedMap = new Map<number, number>()
const seenGroups = new Set<number>()
for (const [cellId, groupId] of AUTOMAP_CELL_WEIGHT_PAIRS) {
expect(expectedMap.has(cellId)).toBe(false)
expectedMap.set(cellId, groupId)
seenGroups.add(groupId)
expect(getAutomapCellWeight(cellId)).toBe(groupId)
}
// Exactly 18 groups 0..17
expect(seenGroups.size).toBe(18)
for (let g = 0; g <= 17; g++) {
expect(seenGroups.has(g)).toBe(true)
}
// Every non-listed cellId across [-50, 2100] and non-integers must return -1
for (let id = -50; id <= 2100; id++) {
if (!expectedMap.has(id)) {
expect(getAutomapCellWeight(id)).toBe(-1)
}
}
expect(getAutomapCellWeight(1.5)).toBe(-1)
expect(getAutomapCellWeight(Number.NaN)).toBe(-1)
expect(getAutomapCellWeight(Number.POSITIVE_INFINITY)).toBe(-1)
})
it('adv_f3_differential_oracle_18_groups_adversarial_permutations', () => {
const rng = createSeededRng(0x113c_cafe)
// Pool containing all 49 weighted cells, 10 unweighted cells, and 6 special override icons
const pool: number[] = [
...AUTOMAP_CELL_WEIGHT_PAIRS.map(([cellId]) => cellId),
4, 5, 50, 77, 88, 99, 150, 200, 250, 299, // unweighted (-1)
300, 304, 306, 307, 318, 405, // special override icons
]
// Run 50 adversarial trials with random subsets, coordinates, fSaved slots, and permutations
for (let trial = 0; trial < 50; trial++) {
const input: AutomapCell[] = []
const coords = [
{ xPixel: 80, yPixel: 40, cellX: 10, cellY: 5 },
{ xPixel: -16, yPixel: 32, cellX: 1, cellY: 3 },
{ xPixel: -0, yPixel: -0, cellX: 0, cellY: 0 }, // tests -0 normalization
]
const shuffledPool = shuffleInPlace([...pool, ...pool.slice(0, 15)], rng)
for (let i = 0; i < shuffledPool.length; i++) {
const cellNo = shuffledPool[i]!
const c = coords[i % coords.length]!
const fSaved = (i % 4) as AutomapAtlasSlot
input.push({
cellNo,
xPixel: c.xPixel,
yPixel: c.yPixel,
cellX: c.cellX,
cellY: c.cellY,
fSaved,
})
}
const actual = deduplicateAutomapCells(input)
const expected = oracleDeduplicateAutomapCells(input)
expect(actual).toEqual(expected)
}
})
it('adv_f3_special_override_icons_vs_unweighted_and_weighted_matrix', () => {
const specialIcons = [
...Array.from({ length: 20 }, (_, i) => 300 + i),
405,
]
for (const specialCel of specialIcons) {
// Case A: unweighted tile (cellNo=50, weight=-1) first, then special icon -> special icon wins!
const caseA = deduplicateAutomapCells([
{ cellNo: 50, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
{ cellNo: specialCel, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
])
expect(caseA).toHaveLength(1)
expect(caseA[0]!.cellNo).toBe(specialCel)
// Case B: special icon first, then unweighted tile (cellNo=50) -> special icon is NOT overwritten!
const caseB = deduplicateAutomapCells([
{ cellNo: specialCel, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
{ cellNo: 50, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
])
expect(caseB).toHaveLength(1)
expect(caseB[0]!.cellNo).toBe(specialCel)
// Case C: two special icons at the same coordinate -> first special icon is preserved
const otherSpecial = specialCel === 300 ? 405 : 300
const caseC = deduplicateAutomapCells([
{ cellNo: specialCel, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
{ cellNo: otherSpecial, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
])
expect(caseC).toHaveLength(1)
expect(caseC[0]!.cellNo).toBe(specialCel)
}
})
it('adv_f3_explicit_cell_weight_override_precedence_and_fail_fast', () => {
// cellNo 50 (explicit weight 2) vs cellNo 60 (explicit weight 9) -> weight 9 wins
const result = deduplicateAutomapCells([
{ cellNo: 50, xPixel: 0, yPixel: 0, cellX: 0, cellY: 0, weight: 2 },
{ cellNo: 60, xPixel: 0, yPixel: 0, cellX: 0, cellY: 0, weight: 9 },
{ cellNo: 70, xPixel: 0, yPixel: 0, cellX: 0, cellY: 0, weight: 5 },
])
expect(result).toHaveLength(1)
expect(result[0]!.cellNo).toBe(60)
expect(result[0]!.weight).toBe(9)
// Fail-fast on non-array input
expect(() => deduplicateAutomapCells(null as unknown as AutomapCell[])).toThrow(
/expected array of AutomapCell/,
)
})
})
// =========================================================================
// Suite 2: F2 — parseAutomapTable, decodeLatin1, Frame Bounds & SplitMix64
// =========================================================================
describe('Suite 2: F2 — parseAutomapTable White-Box Edge Branches & SplitMix64', () => {
it('adv_f2_3287_line_trimmed_automap_txt_recovery_branch', () => {
// White-box test of src/formats/automap-table.ts:348-350:
// Passing rawAutomapText.trim() strips the trailing tab-only row 3288,
// leaving 3287 dataLines with 'Expansion' at index 2603.
const trimmedInput = rawAutomapText.trim()
const tableFromTrimmed = parseAutomapTable(trimmedInput)
expect(tableFromTrimmed.rows).toHaveLength(3288)
expect(tableFromTrimmed.rows[2603]!.levelName).toBe('Expansion')
expect(tableFromTrimmed.rows[3287]!.levelTypeId).toBe(0)
expect(tableFromTrimmed.rows.length).toBe(automapTable.rows.length)
})
it('adv_f2_decode_latin1_multi_chunk_uint8array_over_8192_bytes', () => {
// rawAutomapBytes is ~80KB (> 9 chunks of 8192 bytes in decodeLatin1)
expect(rawAutomapBytes.byteLength).toBeGreaterThan(8192 * 5)
const fromBytes = parseAutomapTable(rawAutomapBytes)
const fromString = parseAutomapTable(rawAutomapText)
expect(fromBytes.rows).toHaveLength(3288)
expect(fromBytes.rows[0]).toEqual(fromString.rows[0])
expect(fromBytes.rows[2602]).toEqual(fromString.rows[2602])
expect(fromBytes.rows[2603]).toEqual(fromString.rows[2603])
expect(fromBytes.rows[3286]).toEqual(fromString.rows[3286])
})
it('adv_f2_fail_fast_malformed_rows_and_non_integer_fields', () => {
const lines = rawAutomapText.split(/\r?\n/)
// 1. Empty TileName on non-divider row (white-box branch at line 377)
const emptyTileLines = [...lines]
const cols1 = emptyTileLines[1]!.split('\t')
cols1[1] = ' '
emptyTileLines[1] = cols1.join('\t')
expect(() => parseAutomapTable(emptyTileLines.join('\n'))).toThrow(/has empty TileName/)
// 2. Unknown LevelName on row 0
const badLevelLines = [...lines]
const cols2 = badLevelLines[1]!.split('\t')
cols2[0] = '99 Cow Level'
badLevelLines[1] = cols2.join('\t')
expect(() => parseAutomapTable(badLevelLines.join('\n'))).toThrow(
/unrecognized level name "99 Cow Level"/,
)
// 3. Unknown TileName on row 0
const badTileLines = [...lines]
const cols3 = badTileLines[1]!.split('\t')
cols3[1] = 'zz'
badTileLines[1] = cols3.join('\t')
expect(() => parseAutomapTable(badTileLines.join('\n'))).toThrow(
/unrecognized tile name "zz"/,
)
// 4. Non-integer Style field (white-box branch at line 298)
const nanStyleLines = [...lines]
const cols4 = nanStyleLines[1]!.split('\t')
cols4[2] = '12.5'
nanStyleLines[1] = cols4.join('\t')
expect(() => parseAutomapTable(nanStyleLines.join('\n'))).toThrow(
/invalid integer "12\.5" in Style at row 0/,
)
// 5. Header with only 1 header row and no data rows (white-box branch at line 353)
expect(() => parseAutomapTable(lines[0]!)).toThrow(/no data rows found/)
})
it('adv_f2_negative_cel_termination_and_maximap_frame_bounds', () => {
const maxiFramesCount = AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames.length
expect(maxiFramesCount).toBe(1499)
expect(AUTOMAP_UI_MANIFEST.atlases.MaxiMapS.frames.length).toBe(1499)
// Verify every single valid Cel entry in all 3,288 rows is within [0, 1498] (and within 2048 weight table)
for (const row of automapTable.rows) {
for (let i = 0; i < row.celCount; i++) {
const cel = row.cels[i]!
expect(cel).toBeGreaterThanOrEqual(0)
expect(cel).toBeLessThan(maxiFramesCount)
expect(cel & 0x7ff).toBe(cel)
}
}
// Also test custom Cel stopping at first negative value (e.g. -2) even if followed by positive Cel3
const lines = rawAutomapText.split(/\r?\n/)
const customLines = [...lines]
const cols = customLines[1]!.split('\t')
cols[6] = '42' // Cel1
cols[8] = '-2' // Cel2 (< 0 terminator)
cols[10] = '99' // Cel3 (must be ignored because Cel2 < 0)
cols[12] = '100' // Cel4
customLines[1] = cols.join('\t')
const customTable = parseAutomapTable(customLines.join('\n'))
expect(customTable.rows[0]!.celCount).toBe(1)
expect(customTable.rows[0]!.cels).toEqual([42])
})
it('adv_f2_splitmix64_coordinate_extremes_and_parameter_validation', () => {
const multiRow = automapTable.rows.find((r) => r.celCount >= 3 && r.levelTypeId > 0)
expect(multiRow).toBeDefined()
const extremeCoords: Array<[number, number, number]> = [
[0, 0, 0],
[-1, -1, -1],
[-2147483648, 2147483647, 0xffffffff],
[0x7fffffff, -0x80000000, 0xdeadbeef],
[123456, 654321, 42],
]
for (const [cx, cy, seed] of extremeCoords) {
const style = multiRow!.style === -1 ? 0 : multiRow!.style
const seq = multiRow!.startSequence === -1 ? 0 : multiRow!.startSequence
const id1 = getAutomapCellId(
automapTable,
multiRow!.levelTypeId,
multiRow!.tileType,
style,
seq,
cx,
cy,
seed,
)
const id2 = getAutomapCellId(
automapTable,
multiRow!.levelTypeId,
multiRow!.tileType,
style,
seq,
cx,
cy,
seed,
)
expect(id1).toBe(id2)
expect(multiRow!.cels).toContain(id1)
}
// LevelType 0 ("None") returns -1 without throwing
expect(getAutomapCellId(automapTable, 0, 0, 0, 0, 0, 0)).toBe(-1)
// Out-of-range parameters fail fast
expect(() => getAutomapCellId(automapTable, -1, 0, 0, 0)).toThrow(/invalid levelTypeId/)
expect(() => getAutomapCellId(automapTable, 36, 0, 0, 0)).toThrow(/invalid levelTypeId/)
expect(() => getAutomapCellId(automapTable, 1, -1, 0, 0)).toThrow(/invalid tileType/)
expect(() => getAutomapCellId(automapTable, 1, 20, 0, 0)).toThrow(/invalid tileType/)
expect(() => getAutomapCellId(automapTable, 1, 0, -2, 0)).toThrow(/invalid style/)
expect(() => getAutomapCellId(automapTable, 1, 0, 0, -2)).toThrow(/invalid sequence/)
})
})
// =========================================================================
// Suite 3: F4/F5/F6/F7 — buildLevelAutomap White-Box Branches in d2map.ts
// =========================================================================
describe('Suite 3: F7 — buildLevelAutomap White-Box Branches (d2map.ts)', () => {
it('adv_f7_top_corner_right_type3_synthesizes_companion_type4_tile', () => {
// Find a (levelTypeId, style, sequence) that has valid entries for BOTH tileType 3 ("wtlr") and tileType 4 ("wtll")
let chosenLevelType = 3
let chosenStyle = 0
let chosenSeq = 0
let expectedType3Cel = -1
let expectedType4Cel = -1
outer: for (let lt = 1; lt < 36; lt++) {
const b3 = automapTable.buckets[lt]![3]!
for (const r3 of b3) {
const st = r3.style === -1 ? 0 : r3.style
const sq = r3.startSequence === -1 ? 0 : r3.startSequence
const c3 = resolveTileAutomapCellId(automapTable, {
levelId: 5,
levelTypeId: lt,
tileType: 3,
style: st,
sequence: sq,
cellX: 0,
cellY: 0,
})
const c4 = resolveTileAutomapCellId(automapTable, {
levelId: 5,
levelTypeId: lt,
tileType: 4,
style: st,
sequence: sq,
cellX: 0,
cellY: 0,
})
if (c3 >= 0 && c4 >= 0 && c3 !== c4 && getAutomapCellWeight(c3) !== getAutomapCellWeight(c4)) {
chosenLevelType = lt
chosenStyle = st
chosenSeq = sq
expectedType3Cel = c3
expectedType4Cel = c4
break outer
}
}
}
expect(expectedType3Cel).toBeGreaterThanOrEqual(0)
expect(expectedType4Cel).toBeGreaterThanOrEqual(0)
// Construct a synthetic Ds1 with ONLY wall.type = 3 at cell (0, 0)
const makeDs1 = (): Ds1 => ({
version: 18,
width: 1,
height: 1,
act: 1,
substitutionType: 0,
wallLayers: 1,
floorLayers: 1,
objects: [],
npcPathOffset: null,
cells: [
[
{
floors: [],
shadows: [],
substitutions: [],
walls: [
{
prop1: 1,
sequence: chosenSeq,
unknown1: 0,
style: chosenStyle,
unknown2: 0,
hidden: false,
type: 3,
},
],
},
],
],
})
const automap = buildLevelAutomap(makeDs1(), {
automapTable,
levelId: 5,
layerId: 5,
levelTypeId: chosenLevelType,
act: 1,
})
const expectedWinner = deduplicateAutomapCells([
{
cellNo: expectedType3Cel,
xPixel: 0,
yPixel: 0,
cellX: 0,
cellY: 0,
fSaved: 0,
weight: getAutomapCellWeight(expectedType3Cel),
},
{
cellNo: expectedType4Cel,
xPixel: 0,
yPixel: 0,
cellX: 0,
cellY: 0,
fSaved: 0,
weight: getAutomapCellWeight(expectedType4Cel),
},
])
expect(automap.walls).toEqual(expectedWinner)
})
it('adv_f7_shadow_layer_tiletype13_and_prop1_zero_or_hidden_filtering', () => {
// White-box test of d2map.ts:645-670 (shadow layer routing into automap.walls with tileType = 13 "sh")
const customShadowTable = parseAutomapTable(
[
'LevelName\tTileName\tStyle\tStartSequence\tEndSequence\tType1\tCel1\tType2\tCel2\tType2\tCel3\tType4\tCel4',
'1 Town\tsh\t0\t0\t0\tsh\t77\t\t-1\t\t-1\t\t-1',
].join('\n'),
)
const ds1: Ds1 = {
version: 18,
width: 1,
height: 1,
act: 1,
substitutionType: 0,
wallLayers: 3,
floorLayers: 2,
objects: [],
npcPathOffset: null,
cells: [
[
{
substitutions: [],
floors: [
// prop1=0 floor -> skipped
{ prop1: 0, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: false },
// hidden=true floor -> skipped
{ prop1: 1, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: true },
],
shadows: [
// Valid shadow -> routed to automap.walls with tileType=13 -> cellNo=77
{ prop1: 1, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: false },
// Hidden shadow -> skipped
{ prop1: 1, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: true },
// prop1=0 shadow -> skipped
{ prop1: 0, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: false },
],
walls: [
// prop1=0 wall -> skipped
{ prop1: 0, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: false, type: 1 },
// hidden=true wall -> skipped
{ prop1: 1, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: true, type: 1 },
// out-of-range wall.type=25 -> skipped
{ prop1: 1, sequence: 0, unknown1: 0, style: 0, unknown2: 0, hidden: false, type: 25 },
],
},
],
],
}
const automap = buildLevelAutomap(ds1, {
automapTable: customShadowTable,
levelId: 1,
layerId: 1,
levelTypeId: 1,
act: 1,
})
expect(automap.floors).toHaveLength(0)
expect(automap.walls).toHaveLength(1)
expect(automap.walls[0]!.cellNo).toBe(77)
})
it('adv_f7_ds1_preset_monster_type1_and_object_type2_in_build_level_automap', () => {
// Find a DS1 object id in Act 1 for type 1 (preset monster/NPC, e.g. cain1) and type 2 (game object, e.g. Bank/Stash)
const act1Presets = monstersTable.presetPlaceByAct.get('1') ?? []
const cainIdx = act1Presets.findIndex((p) => p.toLowerCase().includes('cain'))
expect(cainIdx).toBeGreaterThanOrEqual(0)
const ds1: Ds1 = {
version: 18,
width: 20,
height: 20,
act: 1,
substitutionType: 0,
wallLayers: 0,
floorLayers: 0,
npcPathOffset: null,
cells: [],
objects: [
// Type 1: Deckard Cain preset in Act 1 -> resolves to cellNo 267
{ type: 1, id: cainIdx, x: 25, y: 25, flags: 0 },
// Type 1: Out-of-bounds preset id -> gracefully skipped (d2map.ts:736 / 769)
{ type: 1, id: 99999, x: 30, y: 30, flags: 0 },
// Type 2: Out-of-bounds object id -> gracefully skipped (d2map.ts:736)
{ type: 2, id: 99999, x: 35, y: 35, flags: 0 },
],
}
const automap = buildLevelAutomap(ds1, {
automapTable,
levelId: 1,
layerId: 1,
levelTypeId: 1,
act: 1,
objectsTable,
monstersTable,
monstats2ById,
})
expect(automap.objects.some((o) => o.cellNo === 267 && o.cellX === 5 && o.cellY === 5)).toBe(
true,
)
})
it('adv_f7_waypoint_deduplication_radius_2_vs_3_and_arcane_318', () => {
const emptyDs1: Ds1 = {
version: 18,
width: 20,
height: 20,
act: 1,
substitutionType: 0,
wallLayers: 0,
floorLayers: 0,
objects: [],
npcPathOffset: null,
cells: [],
}
// 1. Three waypoints: first at sub-tile (50, 50) -> cell (10, 10),
// second at sub-tile (60, 60) -> cell (12, 12) (|dx|=2, |dy|=2 -> deduplicated!),
// third at sub-tile (65, 50) -> cell (13, 10) (|dx|=3, |dy|=0 -> NOT deduplicated, added!).
const resNormal = buildLevelAutomap(emptyDs1, {
automapTable,
levelId: 2,
layerId: 2,
levelTypeId: 2,
act: 1,
waypoints: [
{ x: 50, y: 50 },
{ x: 60, y: 60 },
{ x: 65, y: 50 },
],
})
const wpCellsNormal = resNormal.objects.filter((c) => c.cellNo === 307)
expect(wpCellsNormal).toHaveLength(2)
expect(wpCellsNormal.map((c) => [c.cellX, c.cellY])).toEqual([
[10, 10],
[13, 10],
])
// 2. Level 74 (Arcane Sanctuary) emits cellNo = 318 for waypoints
const resArcane = buildLevelAutomap(emptyDs1, {
automapTable,
levelId: 74,
layerId: 74,
levelTypeId: 19,
act: 2,
waypoints: [{ x: 75, y: 75 }],
})
expect(resArcane.objects).toHaveLength(1)
expect(resArcane.objects[0]!.cellNo).toBe(318)
})
})
// =========================================================================
// Suite 4: F8/F9/F11/F12/F13 — Minimap Viewport Frustum, Sheet Routing & HUD
// =========================================================================
describe('Suite 4: F8/F9/F11/F12/F13 — Minimap Frustum Culling, Sheet Routing & HUD', () => {
it('adv_f8_f9_frustum_culling_exact_1px_boundaries_all_4_edges', () => {
const { renderer, sprites, glCalls, scissors } = createRecordingRenderer()
const minimap = new Minimap()
registerAtlases(minimap)
// Configure Corner Minimap (bMinimap = true, rightPanelOpen = false) on 800x600 screen
// Viewport is vpX=505, vpY=16, vpW=279, vpH=225 -> right=784, bottom=241, center=(644, 128)
minimap.bMinimap = true
minimap.visible = true
const maxiSMeta = minimap.getSheetMeta('MaxiMapS')
const frame0 = maxiSMeta.frames[0]!
expect(frame0.width).toBeGreaterThan(0)
expect(frame0.height).toBeGreaterThan(0)
const baseLeftAtZero = 644 + frame0.offsetX - 4
const baseTopAtZero = 128 - frame0.height + frame0.offsetY
// 1. Left edge: drawLeft + width === 504 (outside) vs 505 (inside)
const dxLeftOutside = 504 - frame0.width - baseLeftAtZero
const dxLeftInside = 505 - frame0.width - baseLeftAtZero
// 2. Right edge: drawLeft === 785 (outside) vs 784 (inside)
const dxRightOutside = 785 - baseLeftAtZero
const dxRightInside = 784 - baseLeftAtZero
// 3. Top edge: drawTop + height === 15 (outside) vs 16 (inside)
const dyTopOutside = 15 - frame0.height - baseTopAtZero
const dyTopInside = 16 - frame0.height - baseTopAtZero
// 4. Bottom edge: drawTop === 242 (outside) vs 241 (inside)
const dyBottomOutside = 242 - baseTopAtZero
const dyBottomInside = 241 - baseTopAtZero
const testCells: AutomapCell[] = [
{ cellNo: 0, xPixel: dxLeftOutside * 2, yPixel: 0, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: dxLeftInside * 2, yPixel: 0, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: dxRightOutside * 2, yPixel: 0, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: dxRightInside * 2, yPixel: 0, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: 0, yPixel: dyTopOutside * 2, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: 0, yPixel: dyTopInside * 2, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: 0, yPixel: dyBottomOutside * 2, cellX: 0, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: 0, yPixel: dyBottomInside * 2, cellX: 0, cellY: 0, fSaved: 0 },
]
const level: MinimapLevel = {
key: 'frustum-test',
cellsX: 10,
cellsY: 10,
blocked: new Uint8Array(2500),
gridWidth: 50,
markers: [],
automap: {
layerId: 1,
levelTypeId: 1,
floors: testCells,
walls: [],
objects: [],
extras: [],
},
}
minimap.reveal(level, 0, 0, 0, 0, true)
minimap.draw(
renderer,
level,
0,
0,
{ x: 400, y: 300, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: 0, playerWorldY: 0 },
)
// Hardware scissor must be enabled at (505, 600 - (16 + 225) = 359, 279, 225) and disabled after
expect(glCalls.enableCount).toBe(1)
expect(glCalls.disableCount).toBe(1)
expect(scissors[0]).toEqual({ x: 505, y: 359, width: 279, height: 225 })
expect(scissors[1]).toBeNull()
// Exactly the 4 inside cells must be drawn, and the 4 outside cells culled!
expect(sprites).toHaveLength(4)
expect(minimap.spritesDrawnLastFrame.floors).toBe(4)
})
it('adv_f8_f9_simultaneous_panel_states_pan_clamp_and_bminimap_toggle', () => {
// All 4 (leftPanelOpen, rightPanelOpen) states in Full Automap (bMinimap = false)
expect(computeAutomapViewport(800, 600, false, false, false)).toMatchObject({
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 400, centerY: 276, centerShiftX: 0, divisor: 10,
})
expect(computeAutomapViewport(800, 600, false, true, false)).toMatchObject({
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 560, centerY: 276, centerShiftX: 160, divisor: 10,
})
expect(computeAutomapViewport(800, 600, false, false, true)).toMatchObject({
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 240, centerY: 276, centerShiftX: -160, divisor: 10,
})
expect(computeAutomapViewport(800, 600, false, true, true)).toMatchObject({
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 400, centerY: 276, centerShiftX: 0, divisor: 10,
})
// All 4 (leftPanelOpen, rightPanelOpen) states in Corner Minimap (bMinimap = true)
expect(computeAutomapViewport(800, 600, true, false, false)).toMatchObject({
vpX: 505, vpY: 16, vpW: 279, vpH: 225, centerX: 644, centerY: 128, divisor: 20,
})
expect(computeAutomapViewport(800, 600, true, true, false)).toMatchObject({
vpX: 505, vpY: 16, vpW: 279, vpH: 225, centerX: 644, centerY: 128, divisor: 20,
})
expect(computeAutomapViewport(800, 600, true, false, true)).toMatchObject({
vpX: 16, vpY: 16, vpW: 279, vpH: 225, centerX: 155, centerY: 128, divisor: 20,
})
expect(computeAutomapViewport(800, 600, true, true, true)).toMatchObject({
vpX: 16, vpY: 16, vpW: 279, vpH: 225, centerX: 155, centerY: 128, divisor: 20,
})
// Pan behavior across mode switches
const minimap = new Minimap()
expect(minimap.bMinimap).toBe(true)
expect(minimap.panBySteps(2, -1)).toBe(false)
expect(minimap.panX).toBe(0)
minimap.toggleMode()
expect(minimap.bMinimap).toBe(false)
expect(minimap.panBySteps(5, -3)).toBe(true)
expect(minimap.panX).toBe(40)
expect(minimap.panY).toBe(-24)
minimap.toggleMode()
expect(minimap.bMinimap).toBe(true)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
})
it('adv_f8_resolve_automap_sheet_name_all_numeric_string_and_level_fallbacks', () => {
// Numeric slot 0..3
expect(resolveAutomapSheetName(0, false, 1)).toBe('MaxiMap')
expect(resolveAutomapSheetName(0, true, 1)).toBe('MaxiMapS')
expect(resolveAutomapSheetName(1, false, 1)).toBe('Act2Map')
expect(resolveAutomapSheetName(1, true, 1)).toBe('Act2MapS')
expect(resolveAutomapSheetName(2, false, 1)).toBe('Act4Map')
expect(resolveAutomapSheetName(2, true, 1)).toBe('Act4MapS')
expect(resolveAutomapSheetName(3, false, 1)).toBe('ExTnMap')
expect(resolveAutomapSheetName(3, true, 1)).toBe('ExTnMapS')
// String sheet key
expect(resolveAutomapSheetName('Act2Map', false, 1)).toBe('Act2Map')
expect(resolveAutomapSheetName('Act2Map', true, 1)).toBe('Act2MapS')
expect(resolveAutomapSheetName('Act4MapS', false, 1)).toBe('Act4Map')
expect(resolveAutomapSheetName('ExTnMapS', true, 1)).toBe('ExTnMapS')
// LevelId fallback when slot is 0 or undefined
expect(resolveAutomapSheetName(undefined, false, 47)).toBe('Act2Map')
expect(resolveAutomapSheetName(0, true, 48)).toBe('Act2MapS')
expect(resolveAutomapSheetName(0, false, 49)).toBe('Act2Map')
expect(resolveAutomapSheetName(0, false, 52)).toBe('ExTnMap')
expect(resolveAutomapSheetName(0, true, 109)).toBe('ExTnMapS')
expect(resolveAutomapSheetName(0, false, 136)).toBe('ExTnMap')
expect(resolveAutomapSheetName(0, false, 103)).toBe('Act4Map')
expect(resolveAutomapSheetName(0, true, 103)).toBe('Act4MapS')
expect(resolveAutomapSheetName(0, false, 2)).toBe('MaxiMap')
})
it('adv_f9_load_atlases_candidate_url_fallback_and_missing_sheet_resilience', async () => {
const { renderer } = createRecordingRenderer()
const minimap = new Minimap()
const origFetch = globalThis.fetch
const origCreateImageBitmap = globalThis.createImageBitmap
try {
const fetchedUrls: string[] = []
globalThis.fetch = vi.fn(async (urlInput: RequestInfo | URL) => {
const url = String(urlInput)
fetchedUrls.push(url)
if (!url.startsWith('/fallback-base/')) {
return { ok: false, status: 404 } as Response
}
return {
ok: true,
blob: async () => new Blob([new Uint8Array([1, 2, 3])]),
} as Response
})
globalThis.createImageBitmap = vi.fn(async () => ({
width: 64,
height: 64,
close: vi.fn(),
}) as unknown as ImageBitmap)
const loadedCount = await minimap.loadAtlases(renderer, '/fallback-base')
expect(loadedCount).toBe(8)
expect(minimap.atlasesLoadedCount).toBe(8)
expect(fetchedUrls.some((u) => u.startsWith('/fallback-base/'))).toBe(true)
} finally {
globalThis.fetch = origFetch
globalThis.createImageBitmap = origCreateImageBitmap
}
})
it('adv_f9_is_automap_cell_revealed_nan_cell_coord_fallback', () => {
const { renderer, sprites } = createRecordingRenderer()
const minimap = new Minimap()
registerAtlases(minimap)
// Cell with NaN cellX/cellY but valid xPixel=16, yPixel=8 -> automapPixelToCellCoords(16, 8) = (2, 0)
const nanCell: AutomapCell = {
cellNo: 0,
xPixel: 16,
yPixel: 8,
cellX: Number.NaN,
cellY: Number.NaN,
fSaved: 0,
}
expect(automapPixelToCellCoords(16, 8)).toEqual({ cellX: 2, cellY: 0 })
const level: MinimapLevel = {
key: 'nan-cell-fallback',
cellsX: 20,
cellsY: 20,
blocked: new Uint8Array(10000),
gridWidth: 100,
markers: [],
automap: {
layerId: 2,
levelTypeId: 2,
floors: [nanCell],
walls: [],
objects: [],
extras: [],
},
}
minimap.bMinimap = false
minimap.visible = true
minimap.draw(renderer, level, 0, 0, { x: 400, y: 300, zoom: 1 }, { width: 800, height: 600 })
expect(sprites).toHaveLength(0)
minimap.reveal(level, 0, 0)
minimap.draw(renderer, level, 0, 0, { x: 400, y: 300, zoom: 1 }, { width: 800, height: 600 })
expect(sprites).toHaveLength(1)
})
it('adv_f11_f12_crosshair_entity_colors_dead_monster_suppression_and_portal_300_306_307', () => {
const { renderer, quads, sprites } = createRecordingRenderer()
const minimap = new Minimap()
registerAtlases(minimap)
const level: MinimapLevel = {
key: 'crosshair-portal-test',
levelId: 4, // Stony Field -> Red portal uses frame 300
cellsX: 20,
cellsY: 20,
blocked: new Uint8Array(10000),
gridWidth: 100,
markers: [],
automap: {
layerId: 4,
levelTypeId: 2,
floors: [],
walls: [],
objects: [],
extras: [],
},
}
minimap.bMinimap = false
minimap.visible = true
minimap.showNames = true
minimap.reveal(level, 0, 0, 0, 0, true)
minimap.draw(
renderer,
level,
0,
0,
{ x: 400, y: 300, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 0,
levelId: 4,
portals: [
{ x: 0, y: 0, active: true, kind: 'red', label: 'Tristram' }, // frame 300 in Level 4
{ x: 0, y: 0, active: false, kind: 'blue' }, // inactive -> skipped!
],
entities: [
{ x: 0, y: 0, kind: 'summon', classId: 357, alive: true }, // Valkyrie -> white (0x1F)
{ x: 0, y: 0, kind: 'summon', classId: 291, alive: true }, // Iron Golem -> white (0x1F)
{ x: 0, y: 0, kind: 'mercenary', classId: 271, alive: true }, // Mercenary -> dark green (0x81)
{ x: 0, y: 0, kind: 'boss', alive: false }, // Dead boss -> skipped!
{ x: 0, y: 0, kind: 'boss', alive: true }, // Alive boss -> red (0x0A)
],
},
)
// 1 active portal drawn (frame 300 in Stony Field), 1 inactive skipped
expect(minimap.spritesDrawnLastFrame.portals).toBe(1)
expect(sprites).toHaveLength(1)
expect(sprites[0]!.frame).toEqual({
x: AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames[300]!.x,
y: AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames[300]!.y,
width: AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames[300]!.width,
height: AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames[300]!.height,
})
expect(minimap.warpLabelsLastFrame.some((l) => l.text === 'Tristram' && l.kind === 'portal')).toBe(true)
// Crosshairs drawn: 3 mercenaries/summons + 1 alive boss + 1 player = 5 crosshairs * 12 segments = 60 quads
expect(minimap.crosshairsDrawnLastFrame).toEqual({
player: 1,
party: 0,
npcs: 0,
mercenaries: 3,
bosses: 1,
total: 5,
})
expect(quads).toHaveLength(60)
expect(quads[0]!.color).toEqual(AUTOMAP_CROSSHAIR_COLORS.valkyrieOrIronGolem)
expect(quads[12]!.color).toEqual(AUTOMAP_CROSSHAIR_COLORS.valkyrieOrIronGolem)
expect(quads[24]!.color).toEqual(AUTOMAP_CROSSHAIR_COLORS.mercenary)
expect(quads[36]!.color).toEqual(AUTOMAP_CROSSHAIR_COLORS.boss)
expect(quads[48]!.color).toEqual(AUTOMAP_CROSSHAIR_COLORS.player)
})
it('adv_f13_hud_overlay_non_800x600_canvas_scaling_and_label_layout', () => {
const { renderer } = createRecordingRenderer()
const minimap = new Minimap()
registerAtlases(minimap)
const level: MinimapLevel = {
key: 'hud-scale-test',
cellsX: 20,
cellsY: 20,
blocked: new Uint8Array(10000),
gridWidth: 100,
markers: [
{ cellX: 2, cellY: 2, kind: 'exit', targetName: 'Den of Evil' },
],
levelName: 'Blood Moor',
difficulty: 'Hell',
}
minimap.bMinimap = false
minimap.visible = true
minimap.showNames = true
minimap.reveal(level, 2, 2)
minimap.draw(
renderer,
level,
2,
2,
{ x: 400, y: 300, zoom: 1 },
{ width: 800, height: 600 },
{
entities: [{ x: 160, y: 160, kind: 'npc', name: 'Flavie', alive: true }],
stashes: [{ x: 160, y: 160, label: 'Stash' }],
},
)
const drawnTexts: Array<{
text: string
x: number
y: number
font?: string
color?: D2ColorCode
align?: string
}> = []
const mockFont: D2FontRenderer = {
drawText(
_ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
opts?: { font?: string; color?: D2ColorCode; align?: string },
) {
drawnTexts.push({ text, x, y, ...opts })
},
} as unknown as D2FontRenderer
// Render onto a 1600x1200 logical canvas (scaleX = 2, scaleY = 2)
minimap.drawHudOverlay({} as CanvasRenderingContext2D, mockFont, 1600, 1200)
expect(drawnTexts.some((t) => t.text === 'Den of Evil' && t.font === 'font6' && t.color === 'white')).toBe(true)
expect(drawnTexts.some((t) => t.text === 'Stash' && t.font === 'font6' && t.color === 'gold')).toBe(true)
expect(drawnTexts.some((t) => t.text === 'Flavie' && t.font === 'font6' && t.color === 'gold')).toBe(true)
expect(
drawnTexts.some(
(t) => t.text === 'Blood Moor' && t.x === 1584 && t.y === 16 && t.font === 'font16' && t.align === 'right',
),
).toBe(true)
expect(
drawnTexts.some(
(t) => t.text === 'Hell' && t.x === 1584 && t.y === 34 && t.font === 'font16' && t.align === 'right',
),
).toBe(true)
})
})
// =========================================================================
// Suite 5: F10 — Exhaustive Octile Distance Locus & Level Isolation
// =========================================================================
describe('Suite 5: F10 — Exhaustive Octile Distance Locus & Level Isolation', () => {
it('adv_f10_exhaustive_octile_distance_locus_symmetry_and_159_160_boundary', () => {
let count159 = 0
let count160 = 0
// Exhaustively verify all (dx, dy) in [-100, 100] (40,401 integer coordinate pairs)
// against D2Client.dll 0x6fb12af0: max(|dx|, |dy|) * 2 + min(|dx|, |dy|) >= 160
for (let dx = -100; dx <= 100; dx++) {
for (let dy = -100; dy <= 100; dy++) {
const adx = Math.abs(dx)
const ady = Math.abs(dy)
const oracleDist = Math.max(adx, ady) * 2 + Math.min(adx, ady)
const dist = computeAutomapRevealDistance(dx, dy)
expect(dist).toBe(oracleDist)
if (oracleDist === 159 || oracleDist === 160) {
expect(computeAutomapRevealDistance(-dx, dy)).toBe(oracleDist)
expect(computeAutomapRevealDistance(dx, -dy)).toBe(oracleDist)
expect(computeAutomapRevealDistance(-dx, -dy)).toBe(oracleDist)
expect(computeAutomapRevealDistance(dy, dx)).toBe(oracleDist)
expect(computeAutomapRevealDistance(-dy, -dx)).toBe(oracleDist)
}
if (oracleDist === 159) count159++
if (oracleDist === 160) count160++
}
}
expect(count159).toBeGreaterThan(100)
expect(count160).toBeGreaterThan(100)
})
it('adv_f10_matrons_den_level_133_vs_non_133_octile_reveal_and_key_isolation', () => {
const minimap = new Minimap()
const makeLevel = (key: string, levelId: number): MinimapLevel => ({
key,
levelId,
cellsX: 80,
cellsY: 80,
blocked: new Uint8Array(160000),
gridWidth: 400,
markers: [],
})
const bloodMoor = makeLevel('level-2', 2)
const matronsDen = makeLevel('level-133', 133)
expect(minimap.reveal(bloodMoor, 0, 0, 0, 0)).toBe(true)
expect(computeAutomapRevealDistance(53, 53)).toBe(159)
expect(minimap.reveal(bloodMoor, 10, 10, 53, 53)).toBe(false)
expect(computeAutomapRevealDistance(53, 54)).toBe(161)
expect(minimap.reveal(bloodMoor, 10, 10, 53, 54)).toBe(true)
expect(minimap.revealedRoomCount('level-133')).toBe(0)
expect(minimap.reveal(matronsDen, 16, 16, 1280, 1280)).toBe(true)
expect(minimap.revealedRoomCount('level-133')).toBe(9)
})
})
})