diablo2-web/tests/e2e-automap-113c/tier2-boundary-corner.test.ts

1202 lines
49 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.

/**
* Tier 2 — Boundary & Corner Cases E2E Tests (`tests/e2e-automap-113c/tier2-boundary-corner.test.ts`).
*
* Covers boundary conditions, edge cases, negative/fail-fast inputs, wildcard rows (`Style == -1`,
* `StartSequence == -1`), divider rows, hidden wall bit 0 (`flags & 1`), multi-tile `seq != 0`
* suppression, Level 4 portal mode `0` vs non-`0`, octile distance boundary `159` vs `160`,
* viewport edge clamping, and negative zero normalization (`>= 5` tests per feature, `65` tests total).
*/
import fs from 'node:fs'
import path from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { MpqArchive, memorySource } from '../../src/mpq/index.ts'
import {
type AutomapCell,
type AutomapLayerData,
type AutomapTable,
computeAutomapObjectPixelCoords,
computeAutomapTilePixelCoords,
deduplicateAutomapCells,
generateTownSavedExtras,
getAutomapCellId,
getAutomapCellWeight,
getAutomapLevelTypeId,
getAutomapTileType,
getCrosshairSegments,
parseAutomapTable,
resolveMonsterAutomapCellId,
resolveObjectAutomapCellId,
resolveTileAutomapCellId,
} from '../../src/formats/automap-table.ts'
import type { Ds1 } from '../../src/formats/ds1.ts'
import { actOfLevel, cell, hasOutdoorWaypoint, parseTable } from '../../src/game/acts.ts'
import { buildLevelAutomap } from '../../src/game/d2map.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 { D2FontRenderer } from '../../src/ui/font.ts'
import {
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 PACK_DIR = path.resolve(ROOT_DIR, 'samples/d2-packs')
let rawAutomapBytes: Uint8Array
let automapTable: AutomapTable
let packIndex: {
readonly levels: readonly {
readonly act: number
readonly levelId: number
readonly slug: string
readonly label: string
readonly levelName: string
readonly path: string
}[]
}
function createNoopRenderer(): SpriteRenderer {
return {
flush() {},
draw(_frame: AtlasFrame) {},
drawSolid() {},
drawSolidQuad() {},
} as unknown as SpriteRenderer
}
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)
}
}
beforeAll(async () => {
const expArchive = await MpqArchive.open(memorySource(new Uint8Array(fs.readFileSync(MPQ_EXP_PATH))))
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)
automapTable = parseAutomapTable(rawAutomapBytes)
packIndex = JSON.parse(fs.readFileSync(path.join(PACK_DIR, 'index.json'), 'utf8'))
})
describe('Tier 2 — Boundary & Corner Cases (F1–F13, >= 5 tests per feature)', () => {
// ============================================================================
// F1: AutoMap.txt 36 Level Names & 20 Tile Names — Boundary & Fail-Fast
// ============================================================================
describe('F1 Boundary: Level & Tile Name Lookup Edge Cases & Fail-Fast', () => {
it('F1.B1: maps empty string, "0", "None", and "Expansion" to levelTypeId 0', () => {
expect(getAutomapLevelTypeId('')).toBe(0)
expect(getAutomapLevelTypeId(' ')).toBe(0)
expect(getAutomapLevelTypeId('0')).toBe(0)
expect(getAutomapLevelTypeId('None')).toBe(0)
expect(getAutomapLevelTypeId('none')).toBe(0)
expect(getAutomapLevelTypeId('Expansion')).toBe(0)
expect(getAutomapLevelTypeId('EXPANSION')).toBe(0)
})
it('F1.B2: maps both "1 Monestary" (D2 binary typo) and "1 Monastery" / "Act 1 - Monastery" to index 5', () => {
expect(getAutomapLevelTypeId('1 Monestary')).toBe(5)
expect(getAutomapLevelTypeId('1 Monastery')).toBe(5)
expect(getAutomapLevelTypeId('Act 1 - Monastery')).toBe(5)
expect(getAutomapLevelTypeId(' act 1 - monastery ')).toBe(5)
})
it('F1.B3: throws fail-fast errors on out-of-range numeric levelTypeId (< 0, >= 36, NaN, float)', () => {
expect(() => getAutomapLevelTypeId(-1)).toThrow()
expect(() => getAutomapLevelTypeId(36)).toThrow()
expect(() => getAutomapLevelTypeId(NaN)).toThrow()
expect(() => getAutomapLevelTypeId(1.5)).toThrow()
expect(() => getAutomapLevelTypeId(Infinity)).toThrow()
})
it('F1.B4: throws fail-fast errors on unrecognized level name strings and non-string/number types', () => {
expect(() => getAutomapLevelTypeId('Act 6 - Heaven')).toThrow()
expect(() => getAutomapLevelTypeId('99 Unknown')).toThrow()
expect(() => getAutomapLevelTypeId(null as unknown as string)).toThrow()
expect(() => getAutomapLevelTypeId(undefined as unknown as string)).toThrow()
})
it('F1.B5: maps empty tileName to 0 ("fl") for divider rows and throws on unrecognized tile names', () => {
expect(getAutomapTileType('')).toBe(0)
expect(getAutomapTileType(' ')).toBe(0)
expect(() => getAutomapTileType('wall_invalid')).toThrow()
expect(() => getAutomapTileType('20')).toThrow()
expect(() => getAutomapTileType(null as unknown as string)).toThrow()
})
})
// ============================================================================
// F2: AutoMap.txt Parser & getAutomapCellId — Boundary & Wildcard Cases
// ============================================================================
describe('F2 Boundary: Parser Validation, Divider Rows, Wildcards & Cel Termination', () => {
it('F2.B1: fails fast when parseAutomapTable receives empty, null, or malformed headers', () => {
expect(() => parseAutomapTable('')).toThrow()
expect(() => parseAutomapTable(' \n ')).toThrow()
expect(() => parseAutomapTable(null as unknown as string)).toThrow()
expect(() => parseAutomapTable('BadHeader\tCol2')).toThrow()
})
it('F2.B2: stops counting celCount at the first -1 in Cel1..Cel4 per DATATBLS_LoadAutomapTxt (0x6fdbcca0)', () => {
const tsv = [
'LevelName\tTileName\tStyle\tStartSequence\tEndSequence\tType1\tCel1\tType2\tCel2\tType2\tCel3\tType4\tCel4',
'1 Town\tfl\t1\t0\t0\tfl\t10\t\t-1\t\t20\t\t30',
'1 Town\twl\t2\t0\t0\twl\t-1\t\t15\t\t25\t\t35',
].join('\n')
const table = parseAutomapTable(tsv)
expect(table.rows[0]!.celCount).toBe(1)
expect(table.rows[0]!.cels).toEqual([10])
expect(table.rows[1]!.celCount).toBe(0)
expect(table.rows[1]!.cels).toEqual([])
expect(getAutomapCellId(table, 1, 1, 2, 0)).toBe(-1)
})
it('F2.B3: matches StartSequence == -1 and Style == -1 as wildcards and respects inclusive [StartSequence..EndSequence] bounds', () => {
const tsv = [
'LevelName\tTileName\tStyle\tStartSequence\tEndSequence\tType1\tCel1\tType2\tCel2\tType2\tCel3\tType4\tCel4',
'1 Cave\tfl\t3\t5\t8\tfl\t111\t\t-1\t\t-1\t\t-1',
'1 Cave\tfl\t3\t-1\t-1\tfl\t222\t\t-1\t\t-1\t\t-1',
'1 Cave\tfl\t-1\t-1\t-1\tfl\t333\t\t-1\t\t-1\t\t-1',
].join('\n')
const table = parseAutomapTable(tsv)
// Exact boundary: sequence 5 and 8 match row 0 (111)
expect(getAutomapCellId(table, 3, 0, 3, 5)).toBe(111)
expect(getAutomapCellId(table, 3, 0, 3, 8)).toBe(111)
// Sequence 4 and 9 miss row 0, hit row 1 with StartSequence == -1 (222)
expect(getAutomapCellId(table, 3, 0, 3, 4)).toBe(222)
expect(getAutomapCellId(table, 3, 0, 3, 9)).toBe(222)
// Style 99 misses row 0 & 1, hits row 2 with Style == -1 (333)
expect(getAutomapCellId(table, 3, 0, 99, 99)).toBe(333)
})
it('F2.B4: returns -1 for levelTypeId === 0 (divider rows) even if bucket[0] contains rows', () => {
expect(getAutomapCellId(automapTable, 0, 0, 0, 0)).toBe(-1)
expect(getAutomapCellId(automapTable, 0, 1, -1, -1)).toBe(-1)
})
it('F2.B5: throws fail-fast errors in getAutomapCellId on invalid table, levelTypeId, tileType, style, or sequence', () => {
expect(() => getAutomapCellId(null as unknown as AutomapTable, 1, 0, 0, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, -1, 0, 0, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, 36, 0, 0, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, 1, -1, 0, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, 1, 20, 0, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, 1, 0, -2, 0)).toThrow()
expect(() => getAutomapCellId(automapTable, 1, 0, 0, -2)).toThrow()
})
})
// ============================================================================
// F3: 0x6fba1528 Priority Weight Table & Deduplication — Boundary Cases
// ============================================================================
describe('F3 Boundary: Weight Table Bounds & Same-Group / Cross-Slot Deduplication', () => {
it('F3.B1: returns -1 for negative, out-of-bounds (>= 2048), non-integer, and unmapped cellNos', () => {
expect(getAutomapCellWeight(-1)).toBe(-1)
expect(getAutomapCellWeight(-999)).toBe(-1)
expect(getAutomapCellWeight(2048)).toBe(-1)
expect(getAutomapCellWeight(99999)).toBe(-1)
expect(getAutomapCellWeight(1.5)).toBe(-1)
expect(getAutomapCellWeight(NaN)).toBe(-1)
expect(getAutomapCellWeight(4)).toBe(-1)
expect(getAutomapCellWeight(5)).toBe(-1)
})
it('F3.B2: retains the first cell when two cells at identical (xPixel, yPixel) belong to the exact same weight group', () => {
// Cells 46 and 49 both belong to weight group 6
const deduped = deduplicateAutomapCells([
{ cellNo: 49, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
{ cellNo: 46, xPixel: 16, yPixel: 8, cellX: 2, cellY: 0 },
])
expect(deduped).toHaveLength(1)
expect(deduped[0]!.cellNo).toBe(49)
expect(deduped[0]!.weight).toBe(6)
})
it('F3.B3: does not deduplicate cells across different fSaved atlas slots (0..3) at identical (xPixel, yPixel)', () => {
const deduped = deduplicateAutomapCells([
{ cellNo: 0, xPixel: 160, yPixel: 80, cellX: 20, cellY: 0, fSaved: 0 },
{ cellNo: 0, xPixel: 160, yPixel: 80, cellX: 20, cellY: 0, fSaved: 1 },
{ cellNo: 0, xPixel: 160, yPixel: 80, cellX: 20, cellY: 0, fSaved: 2 },
{ cellNo: 0, xPixel: 160, yPixel: 80, cellX: 20, cellY: 0, fSaved: 3 },
])
expect(deduped).toHaveLength(4)
})
it('F3.B4: normalizes -0 coordinates to +0 during deduplication and coordinate computation', () => {
const deduped = deduplicateAutomapCells([
{ cellNo: 6, xPixel: -0, yPixel: -0, cellX: 0, cellY: 0 },
{ cellNo: 534, xPixel: 0, yPixel: 0, cellX: 0, cellY: 0 },
])
expect(deduped).toHaveLength(1)
expect(Object.is(deduped[0]!.xPixel, 0)).toBe(true)
expect(Object.is(deduped[0]!.yPixel, 0)).toBe(true)
expect(deduped[0]!.cellNo).toBe(534)
})
it('F3.B5: handles empty arrays and throws fail-fast error on non-array input to deduplicateAutomapCells', () => {
expect(deduplicateAutomapCells([])).toEqual([])
expect(() => deduplicateAutomapCells(null as unknown as AutomapCell[])).toThrow()
})
})
// ============================================================================
// F4: 1.13c Special Tile, Object & Monster Overrides — Boundary Cases
// ============================================================================
describe('F4 Boundary: Hidden Wall Bit 0, Multi-Tile seq != 0 Suppression & Conditional Gates', () => {
it('F4.B1: skips wall tiles with bit 0 (flags & 1) set (InvisWal), but does NOT skip floor tiles (tileType === 0) with bit 0', () => {
// Wall tile (tileType=1) with flags=1 (bit 0) -> -1
expect(
resolveTileAutomapCellId(automapTable, {
levelId: 1,
levelTypeId: 1,
tileType: 1,
style: 0,
sequence: 0,
flags: 1,
}),
).toBe(-1)
// Wall tile with flags=2 (bit 1 only, bit 0 clear) -> not suppressed by bit 0
expect(
resolveTileAutomapCellId(automapTable, {
levelId: 1,
levelTypeId: 1,
tileType: 1,
style: 0,
sequence: 0,
flags: 2,
}),
).toBe(61)
// Floor tile (tileType=0) with flags=1 -> not suppressed by wall bit 0 check
expect(
resolveTileAutomapCellId(automapTable, {
levelId: 2,
levelTypeId: 2,
tileType: 0,
style: 8,
sequence: 0,
flags: 1,
}),
).toBe(405)
})
it('F4.B2: suppresses multi-tile duplicate icons when sequence != 0 for Blood Moor Campfire and Palace Cellar stairs', () => {
for (const seq of [1, 2, 3, 7, 15]) {
expect(
resolveTileAutomapCellId(automapTable, {
levelId: 2,
levelTypeId: 2,
tileType: 0,
style: 8,
sequence: seq,
}),
).toBe(-1)
expect(
resolveTileAutomapCellId(automapTable, {
levelId: 40,
levelTypeId: 12,
tileType: 8,
style: 25,
sequence: seq,
}),
).toBe(-1)
}
})
it('F4.B3: suppresses closed Red Portal (objectClassId=60, mode=0 -> -1) in Level 4 Stony Field while allowing mode != 0 (-> 300)', () => {
expect(resolveObjectAutomapCellId({ objectClassId: 60, autoMapCol: 306, levelId: 4, mode: 0 })).toBe(-1)
expect(resolveObjectAutomapCellId({ objectClassId: 60, autoMapCol: 306, levelId: 4, mode: 1 })).toBe(300)
expect(resolveObjectAutomapCellId({ objectClassId: 60, autoMapCol: 306, levelId: 4, mode: 2 })).toBe(300)
// Outside Level 4, Red Portal returns 306 even when mode === 0
expect(resolveObjectAutomapCellId({ objectClassId: 60, autoMapCol: 0, levelId: 109, mode: 0 })).toBe(306)
})
it('F4.B4: suppresses Compelling Orb (366) when mode !== 2 and Arcane Waypoint (402) when levelId !== 74', () => {
expect(resolveObjectAutomapCellId({ objectClassId: 366, autoMapCol: 308, mode: 0 })).toBe(-1)
expect(resolveObjectAutomapCellId({ objectClassId: 366, autoMapCol: 308, mode: 1 })).toBe(-1)
expect(resolveObjectAutomapCellId({ objectClassId: 366, autoMapCol: 308, mode: 2 })).toBe(308)
expect(resolveObjectAutomapCellId({ objectClassId: 402, autoMapCol: 318, levelId: 1 })).toBe(-1)
expect(resolveObjectAutomapCellId({ objectClassId: 402, autoMapCol: 318, levelId: 52 })).toBe(-1)
expect(resolveObjectAutomapCellId({ objectClassId: 402, autoMapCol: 318, levelId: 74 })).toBe(318)
})
it('F4.B5: suppresses dead monsters (isAlive=false) including Cain, and throws fail-fast on invalid override params', () => {
expect(
resolveMonsterAutomapCellId({
monsterClassId: 146, // Deckard Cain
automapCel: 267,
isAlive: false,
}),
).toBe(-1)
expect(
resolveMonsterAutomapCellId({
monsterClassId: 300,
automapCel: 0,
noMap: false,
isAlive: true,
}),
).toBe(-1)
expect(() => resolveObjectAutomapCellId({ objectClassId: -1, autoMapCol: 0 })).toThrow()
expect(() => resolveMonsterAutomapCellId({ monsterClassId: -1, automapCel: 0 })).toThrow()
expect(() =>
resolveTileAutomapCellId(automapTable, {
levelId: -1,
levelTypeId: 1,
tileType: 0,
style: 0,
sequence: 0,
}),
).toThrow()
})
})
// ============================================================================
// F5: Pre-Baked Town Base Map Chunk Generation — Boundary Cases
// ============================================================================
describe('F5 Boundary: Non-Chunk Towns, Custom Room Origins & Invalid Level IDs', () => {
it('F5.B1: returns empty array for Act 1 (levelId=1), Act 3 (levelId=75), and non-town levels', () => {
for (const id of [0, 1, 2, 39, 41, 74, 75, 102, 104, 108, 110, 136]) {
expect(generateTownSavedExtras(id)).toEqual([])
}
})
it('F5.B2: throws fail-fast error when generateTownSavedExtras receives negative or non-integer levelId', () => {
expect(() => generateTownSavedExtras(-1)).toThrow()
expect(() => generateTownSavedExtras(40.5)).toThrow()
expect(() => generateTownSavedExtras(NaN)).toThrow()
})
it('F5.B3: computes exact negative/zero-crossing coordinates for Act 2 (levelId=40) when roomOriginSubX == roomSubWidth', () => {
const cells = generateTownSavedExtras(40, 160, 0, 160)
expect(cells[0]).toMatchObject({ cellNo: 21, xPixel: 123, yPixel: 313, cellX: 46, cellY: 31 })
expect(cells[13]).toMatchObject({ cellNo: 38, xPixel: 443, yPixel: 613 })
})
it('F5.B4: computes exact coordinates for Act 4 (levelId=103) at origin (roomOriginSubX=45, roomOriginSubY=-20)', () => {
const cells = generateTownSavedExtras(103, 45, -20)
expect(cells[0]).toMatchObject({ cellNo: 0, xPixel: -53, yPixel: 96, fSaved: 2 })
expect(cells[3]).toMatchObject({ cellNo: 3, xPixel: 83, yPixel: 186, fSaved: 2 })
})
it('F5.B5: verifies all generated cellNo values for levels 40, 103, 109 are strictly within their sheet frameCount', () => {
for (const c of generateTownSavedExtras(40)) {
expect(c.cellNo).toBeGreaterThanOrEqual(0)
expect(c.cellNo).toBeLessThan(AUTOMAP_UI_MANIFEST.atlases.Act2Map.frameCount)
}
for (const c of generateTownSavedExtras(103)) {
expect(c.cellNo).toBeGreaterThanOrEqual(0)
expect(c.cellNo).toBeLessThan(AUTOMAP_UI_MANIFEST.atlases.Act4Map.frameCount)
}
for (const c of generateTownSavedExtras(109)) {
expect(c.cellNo).toBeGreaterThanOrEqual(0)
expect(c.cellNo).toBeLessThan(AUTOMAP_UI_MANIFEST.atlases.ExTnMap.frameCount)
}
})
})
// ============================================================================
// F6: Offline Packing of All 8 Automap DC6 Sprite Sheets — Boundary Cases
// ============================================================================
describe('F6 Boundary: First/Last Frame Bounds, Empty Frame Slots & Manifest Parity', () => {
it('F6.B1: verifies first (0) and last (frameCount - 1) frames exist in all 8 packed atlases', () => {
for (const key of Object.keys(AUTOMAP_UI_MANIFEST.atlases) as AutomapSpriteKey[]) {
const meta = AUTOMAP_UI_MANIFEST.atlases[key]
expect(meta.frames[0]).toBeDefined()
expect(meta.frames[meta.frameCount - 1]).toBeDefined()
expect(meta.frames[meta.frameCount]).toBeUndefined()
}
})
it('F6.B2: ensures public/assets/ui/automap/manifest.json and AUTOMAP_UI_MANIFEST are byte-for-byte consistent', () => {
const diskManifest = JSON.parse(
fs.readFileSync(path.join(ROOT_DIR, 'public/assets/ui/automap/manifest.json'), 'utf8'),
)
expect(diskManifest).toEqual(AUTOMAP_UI_MANIFEST)
})
it('F6.B3: verifies special override frames (267 Cain, 300 Red Portal, 305 Palace, 306/307 Portal, 318 WP, 319 Stash, 405 Campfire) have non-zero dimensions in MaxiMap & MaxiMapS', () => {
for (const cellNo of [267, 300, 305, 306, 307, 308, 309, 318, 319, 405]) {
const fullFrame = AUTOMAP_UI_MANIFEST.atlases.MaxiMap.frames[cellNo]!
const halfFrame = AUTOMAP_UI_MANIFEST.atlases.MaxiMapS.frames[cellNo]!
expect(fullFrame.width).toBeGreaterThan(0)
expect(fullFrame.height).toBeGreaterThan(0)
expect(halfFrame.width).toBeGreaterThan(0)
expect(halfFrame.height).toBeGreaterThan(0)
}
})
it('F6.B4: verifies Act2Map(S) frames 0..19, Act4Map(S) frames 0..3, and ExTnMap(S) frames 0..7 all have positive width & height', () => {
for (let i = 0; i < 20; i++) {
expect(AUTOMAP_UI_MANIFEST.atlases.Act2Map.frames[i]!.width).toBeGreaterThan(0)
expect(AUTOMAP_UI_MANIFEST.atlases.Act2MapS.frames[i]!.width).toBeGreaterThan(0)
}
for (let i = 0; i < 4; i++) {
expect(AUTOMAP_UI_MANIFEST.atlases.Act4Map.frames[i]!.width).toBeGreaterThan(0)
expect(AUTOMAP_UI_MANIFEST.atlases.Act4MapS.frames[i]!.width).toBeGreaterThan(0)
}
for (let i = 0; i < 8; i++) {
expect(AUTOMAP_UI_MANIFEST.atlases.ExTnMap.frames[i]!.width).toBeGreaterThan(0)
expect(AUTOMAP_UI_MANIFEST.atlases.ExTnMapS.frames[i]!.width).toBeGreaterThan(0)
}
})
it('F6.B5: verifies PNG binary headers (\x89PNG\r\n\x1a\n) for all 8 sheets in public/assets/ui/automap/ and public/ui/', () => {
const pngMagic = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
for (const key of Object.keys(AUTOMAP_UI_MANIFEST.atlases) as AutomapSpriteKey[]) {
const buf = fs.readFileSync(path.join(ROOT_DIR, 'public/assets/ui/automap', `${key}.png`))
expect(new Uint8Array(buf.buffer, buf.byteOffset, 8)).toEqual(pngMagic)
}
})
})
// ============================================================================
// F7: Live & Offline 4-Layer Automap Generation — Boundary Cases
// ============================================================================
describe('F7 Boundary: Lower Wall tileType=15 vs 16 Boundary, Truncation & Act Resolution', () => {
it('F7.B1: applies +24 yPixel offset strictly at tileType >= 16 (tileType 15 -> +0, tileType 16..19 -> +24)', () => {
const t15 = computeAutomapTilePixelCoords(5, 5, 15)
const t16 = computeAutomapTilePixelCoords(5, 5, 16)
const t17 = computeAutomapTilePixelCoords(5, 5, 17)
const t18 = computeAutomapTilePixelCoords(5, 5, 18)
const t19 = computeAutomapTilePixelCoords(5, 5, 19)
expect(t15.yPixel).toBe(40)
expect(t16.yPixel).toBe(64)
expect(t17.yPixel).toBe(64)
expect(t18.yPixel).toBe(64)
expect(t19.yPixel).toBe(64)
})
it('F7.B2: truncates negative xClient/yClient toward zero (Math.trunc) in computeAutomapObjectPixelCoords', () => {
// subX = 1, subY = 4 -> xClient = -3 * 16 = -48 -> Math.trunc(-48 / 10) + 1 = -4 + 1 = -3
// yClient = 5 * 8 = 40 -> Math.trunc(40 / 10) - 3 = 4 - 3 = 1
expect(computeAutomapObjectPixelCoords(1, 4)).toEqual({ xPixel: -3, yPixel: 1 })
// subX = 0, subY = 0 -> xPixel = 1, yPixel = -3
expect(computeAutomapObjectPixelCoords(0, 0)).toEqual({ xPixel: 1, yPixel: -3 })
})
it('F7.B3: resolves act boundaries (1..39 -> Act 1, 40..74 -> Act 2, 75..102 -> Act 3, 103..108 -> Act 4, 109..136 -> Act 5)', () => {
expect(actOfLevel(1)).toBe(1)
expect(actOfLevel(39)).toBe(1)
expect(actOfLevel(40)).toBe(2)
expect(actOfLevel(74)).toBe(2)
expect(actOfLevel(75)).toBe(3)
expect(actOfLevel(102)).toBe(3)
expect(actOfLevel(103)).toBe(4)
expect(actOfLevel(108)).toBe(4)
expect(actOfLevel(109)).toBe(5)
expect(actOfLevel(136)).toBe(5)
})
it('F7.B4: excludes town levels (1, 40, 75, 103, 109) from hasOutdoorWaypoint', () => {
for (const townId of [1, 40, 75, 103, 109]) {
expect(hasOutdoorWaypoint(townId)).toBe(false)
}
expect(hasOutdoorWaypoint(3)).toBe(true)
expect(hasOutdoorWaypoint(42)).toBe(true)
expect(hasOutdoorWaypoint(79)).toBe(true)
expect(hasOutdoorWaypoint(106)).toBe(true)
expect(hasOutdoorWaypoint(111)).toBe(true)
})
it('F7.B5: throws fail-fast error when parseTable is called with empty Uint8Array', () => {
expect(() => parseTable(new Uint8Array(0))).toThrow()
expect(cell({ header: ['Id', 'Name'], rows: [['1', 'Town']] }, ['1', 'Town'], 'MissingCol')).toBe('')
})
})
// ============================================================================
// F8: 4-Layer Isometric Sprite Renderer — Boundary Cases
// ============================================================================
describe('F8 Boundary: Invalid Cell Skipping, Out-of-Frustum Culling & Fallback Synthesis', () => {
it('F8.B1: skips cells with cellNo < 0 or cellNo >= sheetMeta.frames.length without crashing', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const coords = computeAutomapTilePixelCoords(4, 4, 0)
const level: MinimapLevel = {
key: 'f8-b1-invalid-cellno',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
automap: {
layerId: 1,
levelTypeId: 1,
floors: [
{ cellNo: -1, xPixel: coords.xPixel, yPixel: coords.yPixel, cellX: 4, cellY: 4, fSaved: 0 },
{ cellNo: 99999, xPixel: coords.xPixel, yPixel: coords.yPixel, cellX: 4, cellY: 4, fSaved: 0 },
{ cellNo: 5, xPixel: coords.xPixel, yPixel: coords.yPixel, cellX: 4, cellY: 4, fSaved: 0 },
],
walls: [],
objects: [],
extras: [],
},
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(createNoopRenderer(), level, 4, 4, { x: 0, y: 320, zoom: 1 }, { width: 800, height: 600 })
expect(minimap.spritesDrawnLastFrame.floors).toBe(1)
expect(minimap.spritesDrawnLastFrame.total).toBe(1)
})
it('F8.B2: frustum-culls cells projected outside the active viewport bounds', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'minimap' // 279x225 viewport
const near = computeAutomapTilePixelCoords(4, 4, 0)
const far = computeAutomapTilePixelCoords(200, 200, 0)
const level: MinimapLevel = {
key: 'f8-b2-frustum',
cellsX: 256,
cellsY: 256,
blocked: new Uint8Array(256 * 256 * 25),
gridWidth: 1280,
markers: [],
automap: {
layerId: 1,
levelTypeId: 1,
floors: [],
walls: [],
objects: [],
extras: [
{ cellNo: 309, xPixel: near.xPixel, yPixel: near.yPixel, cellX: 4, cellY: 4, fSaved: 0 },
{ cellNo: 309, xPixel: far.xPixel, yPixel: far.yPixel, cellX: 200, cellY: 200, fSaved: 0 },
],
},
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(createNoopRenderer(), level, 4, 4, { x: 0, y: 320, zoom: 1 }, { width: 800, height: 600 })
// Only the near extra is inside the 279x225 minimap box
expect(minimap.spritesDrawnLastFrame.extras).toBe(1)
})
it('F8.B3: synthesizes a fallback 4-layer AutomapLayerData when level.automap is omitted', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const blocked = new Uint8Array(10 * 10 * 25)
blocked[0] = 1
const level: MinimapLevel = {
key: 'f8-b3-synth',
cellsX: 10,
cellsY: 10,
blocked,
gridWidth: 50,
markers: [
{ cellX: 2, cellY: 2, kind: 'waypoint' },
{ cellX: 3, cellY: 3, kind: 'exit', targetName: 'Next Area' },
],
}
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
expect(minimap.lastRenderStats.cellCounts.total).toBe(100 + 2)
expect(minimap.spritesDrawnLastFrame.total).toBeGreaterThan(0)
})
it('F8.B4: skips all rendering when minimap.visible === false while resetting frame draw stats to 0', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.visible = false
const level: MinimapLevel = {
key: 'f8-b4-hidden',
cellsX: 8,
cellsY: 8,
blocked: new Uint8Array(8 * 8 * 25),
gridWidth: 40,
markers: [],
}
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
expect(minimap.spritesDrawnLastFrame.total).toBe(0)
expect(minimap.crosshairsDrawnLastFrame.total).toBe(0)
expect(minimap.hudTextLastFrame).toBeNull()
})
it('F8.B5: handles negative cell coordinates in automapPixelToCellCoords and projectAutomapCellScreenRect', () => {
const negPx = computeAutomapTilePixelCoords(2, 10, 0) // xPixel = -64, yPixel = 48
expect(negPx.xPixel).toBe(-64)
expect(negPx.yPixel).toBe(48)
expect(automapPixelToCellCoords(negPx.xPixel, negPx.yPixel)).toEqual({ cellX: 2, cellY: 10 })
const rect = projectAutomapCellScreenRect(negPx, { width: 16, height: 16, offsetX: 0, offsetY: 0 }, 0, 0, true)
expect(rect.screenX).toBe(-32)
expect(rect.screenY).toBe(24)
})
})
// ============================================================================
// F9: Viewport, Panel Shift & Hotkeys — Boundary Cases
// ============================================================================
describe('F9 Boundary: Both Panels Open Cancellation, Small Screen Clamping & Mode Reset', () => {
it('F9.B1: cancels horizontal center shift (centerShiftX === 0) when BOTH left and right HUD panels are open', () => {
const vpBoth = computeAutomapViewport(800, 600, false, true, true, 0, 0)
expect(vpBoth.centerShiftX).toBe(0)
expect(vpBoth.centerX).toBe(400)
})
it('F9.B2: clamps viewport dimensions safely when screenWidth or screenHeight is very small (<= 48px)', () => {
const vpTinyFull = computeAutomapViewport(0, 20, false, false, false, 0, 0)
expect(vpTinyFull.width).toBeGreaterThanOrEqual(1)
expect(vpTinyFull.height).toBeGreaterThanOrEqual(1)
const vpTinyMini = computeAutomapViewport(100, 100, true, false, false, 0, 0)
expect(vpTinyMini.x).toBeGreaterThanOrEqual(0)
expect(vpTinyMini.width).toBe(279)
expect(vpTinyMini.height).toBe(225)
})
it('F9.B3: ignores arrow-key panning when in Corner Minimap mode (bMinimap=true) or when hidden (visible=false)', () => {
const minimap = new Minimap()
expect(minimap.bMinimap).toBe(true)
expect(minimap.pan(16, -16)).toBe(false)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
minimap.mode = 'full'
minimap.visible = false
expect(minimap.pan(16, -16)).toBe(false)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
})
it('F9.B4: automatically resets panX and panY to (0, 0) when switching from Full Automap back to Corner Minimap or hiding with Tab', () => {
const minimap = new Minimap()
minimap.mode = 'full'
minimap.panBySteps(5, -4)
expect(minimap.panX).toBe(40)
expect(minimap.panY).toBe(-32)
minimap.toggleMode() // back to minimap
expect(minimap.bMinimap).toBe(true)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
minimap.mode = 'full'
minimap.panBySteps(3, 3)
minimap.toggleVisibility() // hide
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
})
it('F9.B5: wraps fadeState cleanly across multiple full cycles (0 -> 1 -> 2 -> 0 -> 1 -> 2 -> 0)', () => {
const minimap = new Minimap()
const sequence: number[] = []
for (let i = 0; i < 6; i++) {
sequence.push(minimap.cycleFade())
}
expect(sequence).toEqual([1, 2, 0, 1, 2, 0])
})
})
// ============================================================================
// F10: Room/Chunk Fog-of-War Exploration Reveal — Boundary Cases
// ============================================================================
describe('F10 Boundary: Octile Distance 159 vs 160 Threshold & Grid Edge Clamping', () => {
it('F10.B1: enforces exact octile distance boundary: 159 (< 160) does NOT reveal, 160 (>= 160) DOES reveal', () => {
// dx = 79, dy = 1 -> max(79,1)*2 + min(79,1) = 158 + 1 = 159
expect(computeAutomapRevealDistance(79, 1)).toBe(159)
expect(computeAutomapRevealDistance(79, 1) < AUTOMAP_REVEAL_DISTANCE_THRESHOLD).toBe(true)
// dx = 80, dy = 0 -> 80*2 + 0 = 160
expect(computeAutomapRevealDistance(80, 0)).toBe(160)
expect(computeAutomapRevealDistance(80, 0) >= AUTOMAP_REVEAL_DISTANCE_THRESHOLD).toBe(true)
const minimap = new Minimap()
const level: MinimapLevel = {
key: 'f10-b1-boundary',
cellsX: 32,
cellsY: 32,
blocked: new Uint8Array(32 * 32 * 25),
gridWidth: 160,
markers: [],
}
expect(minimap.reveal(level, 4, 4, 100, 100)).toBe(true)
// Move by (79, 1) -> octile = 159 -> skipped!
expect(minimap.reveal(level, 5, 4, 179, 101)).toBe(false)
// Move by (80, 0) from (100, 100) -> octile = 160 -> reveals!
expect(minimap.reveal(level, 5, 4, 180, 100)).toBe(true)
})
it('F10.B2: handles negative movement deltas symmetrically in computeAutomapRevealDistance', () => {
expect(computeAutomapRevealDistance(-80, 0)).toBe(160)
expect(computeAutomapRevealDistance(0, -80)).toBe(160)
expect(computeAutomapRevealDistance(-60, -40)).toBe(160)
expect(computeAutomapRevealDistance(-79, -1)).toBe(159)
})
it('F10.B3: clamps 3x3 room neighborhood at top-left corner room (0, 0) to 2x2 valid rooms (0..1, 0..1)', () => {
const minimap = new Minimap()
const level: MinimapLevel = {
key: 'f10-b3-corner',
cellsX: 32,
cellsY: 32,
blocked: new Uint8Array(32 * 32 * 25),
gridWidth: 160,
markers: [],
}
minimap.reveal(level, 0, 0, 0, 0, true)
expect(minimap.revealedRoomCount(level.key)).toBe(4)
expect(minimap.isRoomRevealed(level.key, 0, 0)).toBe(true)
expect(minimap.isRoomRevealed(level.key, 1, 0)).toBe(true)
expect(minimap.isRoomRevealed(level.key, 0, 1)).toBe(true)
expect(minimap.isRoomRevealed(level.key, 1, 1)).toBe(true)
expect(minimap.isRoomRevealed(level.key, -1, 0)).toBe(false)
})
it('F10.B4: returns false for out-of-bounds cell coordinates in isCellExplored', () => {
const minimap = new Minimap()
const level: MinimapLevel = {
key: 'f10-b4-oob',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
}
minimap.reveal(level, 8, 8, 640, 640, true)
expect(minimap.isCellExplored(level, -1, 8)).toBe(false)
expect(minimap.isCellExplored(level, 8, -1)).toBe(false)
expect(minimap.isCellExplored(level, 16, 8)).toBe(false)
expect(minimap.isCellExplored(level, 8, 16)).toBe(false)
})
it('F10.B5: returns 0 exploredFraction on an unrevealed or 0x0 empty level without division by zero', () => {
const minimap = new Minimap()
const emptyLevel: MinimapLevel = {
key: 'f10-b5-empty',
cellsX: 0,
cellsY: 0,
blocked: new Uint8Array(0),
gridWidth: 0,
markers: [],
}
expect(minimap.exploredFraction(emptyLevel)).toBe(0)
})
})
// ============================================================================
// F11: 12-Segment Crosshair & Dynamic Portals — Boundary Cases
// ============================================================================
describe('F11 Boundary: Dead Entities, Inactive Portals, Iron Golem Color & Offscreen Culling', () => {
it('F11.B1: omits dead entities (alive: false) from crosshair and label rendering', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f11-b1-dead',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(
createNoopRenderer(),
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
entities: [
{ x: 0, y: 320, kind: 'boss', name: 'DeadBoss', alive: false },
{ x: 0, y: 320, kind: 'npc', name: 'DeadNpc', alive: false },
],
},
)
expect(minimap.crosshairsDrawnLastFrame.bosses).toBe(0)
expect(minimap.crosshairsDrawnLastFrame.npcs).toBe(0)
expect(minimap.crosshairsDrawnLastFrame.player).toBe(1)
expect(minimap.lastRenderStats.entityLabels).toHaveLength(0)
})
it('F11.B2: omits inactive portals (active: false) from portal sprite and label rendering', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f11-b2-inactive-portal',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
automap: { layerId: 1, levelTypeId: 1, floors: [], walls: [], objects: [], extras: [] },
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(
createNoopRenderer(),
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
portals: [{ x: 0, y: 320, active: false, label: 'Closed Portal' }],
},
)
expect(minimap.spritesDrawnLastFrame.portals).toBe(0)
expect(minimap.warpLabelsLastFrame).toHaveLength(0)
})
it('F11.B3: renders Iron Golem (classId=291) and Valkyrie (classId=357) with white 0x1F (244/255) crosshair color', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const colors: (readonly [number, number, number, number])[] = []
const renderer = {
flush() {},
draw() {},
drawSolid() {},
drawSolidQuad(
_x0: number,
_y0: number,
_x1: number,
_y1: number,
_x2: number,
_y2: number,
_x3: number,
_y3: number,
color: readonly [number, number, number, number],
) {
colors.push(color)
},
} as unknown as SpriteRenderer
const level: MinimapLevel = {
key: 'f11-b3-golem',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(
renderer,
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
entities: [{ x: 0, y: 320, kind: 'summon', classId: 291 }],
},
)
expect(colors[0]?.[0]).toBeCloseTo(244 / 255, 4)
expect(colors[0]?.[1]).toBeCloseTo(244 / 255, 4)
expect(colors[0]?.[2]).toBeCloseTo(244 / 255, 4)
})
it('F11.B4: falls back to drawSolid when renderer does not implement drawSolidQuad', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
let solidCalls = 0
const fallbackRenderer = {
flush() {},
draw() {},
drawSolid() {
solidCalls++
},
} as unknown as SpriteRenderer
const level: MinimapLevel = {
key: 'f11-b4-solid-fallback',
cellsX: 8,
cellsY: 8,
blocked: new Uint8Array(8 * 8 * 25),
gridWidth: 40,
markers: [],
}
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(fallbackRenderer, level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
expect(solidCalls).toBe(12)
})
it('F11.B5: culls entity and player crosshairs when panned completely outside the Full Automap viewport', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
minimap.pan(5000, 5000)
const level: MinimapLevel = {
key: 'f11-b5-offscreen-crosshair',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(
createNoopRenderer(),
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
entities: [{ x: 0, y: 320, kind: 'boss', name: 'Boss' }],
},
)
expect(minimap.crosshairsDrawnLastFrame.player).toBe(0)
expect(minimap.crosshairsDrawnLastFrame.bosses).toBe(0)
expect(minimap.crosshairsDrawnLastFrame.total).toBe(0)
})
})
// ============================================================================
// F12: Entrance/Warp Labels & Top-Right Gold Font16 HUD Text — Boundary Cases
// ============================================================================
describe('F12 Boundary: Unrevealed Warp Suppression, Boss Name Suppression & Empty Level Name', () => {
it('F12.B1: suppresses entrance/warp labels for markers located in unrevealed rooms', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f12-b1-unrevealed-warp',
cellsX: 32,
cellsY: 32,
blocked: new Uint8Array(32 * 32 * 25),
gridWidth: 160,
markers: [{ cellX: 28, cellY: 28, kind: 'exit', targetName: 'Secret Exit' }],
}
// Reveal around (2, 2); (28, 28) remains unrevealed
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
expect(minimap.warpLabelsLastFrame).toHaveLength(0)
})
it('F12.B2: does NOT emit Font6 entity name labels for hostile bosses or mercenaries (only party & npc)', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f12-b2-no-boss-label',
cellsX: 16,
cellsY: 16,
blocked: new Uint8Array(16 * 16 * 25),
gridWidth: 80,
markers: [],
}
minimap.reveal(level, 4, 4, 0, 320, true)
minimap.draw(
createNoopRenderer(),
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
entities: [
{ x: 0, y: 320, kind: 'boss', name: 'Andariel' },
{ x: 0, y: 320, kind: 'mercenary', name: 'Emilio' },
],
},
)
expect(minimap.lastRenderStats.entityLabels).toHaveLength(0)
})
it('F12.B3: sets hudTextLastFrame to null when levelName is empty string', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f12-b3-empty-title',
cellsX: 8,
cellsY: 8,
blocked: new Uint8Array(8 * 8 * 25),
gridWidth: 40,
levelName: '',
markers: [],
}
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
expect(minimap.hudTextLastFrame).toBeNull()
})
it('F12.B4: defaults stash label to "Stash" when stash.label and stash.name are omitted', () => {
const minimap = new Minimap()
registerAtlases(minimap)
minimap.mode = 'full'
const level: MinimapLevel = {
key: 'f12-b4-default-stash',
cellsX: 8,
cellsY: 8,
blocked: new Uint8Array(8 * 8 * 25),
gridWidth: 40,
markers: [],
}
minimap.reveal(level, 2, 2, 0, 160, true)
minimap.draw(
createNoopRenderer(),
level,
2,
2,
{ x: 0, y: 160, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 160,
stashes: [{ x: 0, y: 160 }],
},
)
expect(minimap.warpLabelsLastFrame[0]?.text).toBe('Stash')
})
it('F12.B5: does not invoke font.drawText in drawHudOverlay when minimap.visible === false', () => {
const minimap = new Minimap()
minimap.visible = false
let textCount = 0
const mockFont = {
drawText() {
textCount++
},
} as unknown as D2FontRenderer
minimap.drawHudOverlay({} as CanvasRenderingContext2D, mockFont, 800, 600)
expect(textCount).toBe(0)
})
})
// ============================================================================
// F13: Full-Spectrum 136-Level Verification — Boundary & Special Levels
// ============================================================================
describe('F13 Boundary: Special Level IDs (Level 1, 38 Tristram, 39 Cow, 74 Arcane, 133..136 Uber/Baal)', () => {
it('F13.B1: verifies boundary LevelId=1 (Rogue Encampment) and LevelId=136 (Uber Tristram) have valid baked automaps', () => {
for (const levelId of [1, 136]) {
const entry = packIndex.levels.find(l => l.levelId === levelId)!
expect(entry).toBeDefined()
const scene = JSON.parse(
fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8'),
) as { automap: AutomapLayerData }
expect(
scene.automap.floors.length +
scene.automap.walls.length +
scene.automap.objects.length +
scene.automap.extras.length,
).toBeGreaterThan(0)
}
})
it('F13.B2: verifies special Act 1 levels Tristram (LevelId=38) and Moo Moo Farm (LevelId=39)', () => {
for (const levelId of [38, 39]) {
const entry = packIndex.levels.find(l => l.levelId === levelId)!
expect(entry).toBeDefined()
const scene = JSON.parse(
fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8'),
) as { automap: AutomapLayerData }
expect(scene.automap.floors.length + scene.automap.walls.length).toBeGreaterThan(0)
}
})
it('F13.B3: verifies Arcane Sanctuary (LevelId=74) variants and Arcane Waypoint icon (cellNo=318)', () => {
const entries74 = packIndex.levels.filter(l => l.levelId === 74)
expect(entries74.length).toBeGreaterThan(0)
let totalCells74 = 0
for (const entry of entries74) {
const scene = JSON.parse(
fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8'),
) as { automap: AutomapLayerData }
totalCells74 +=
scene.automap.floors.length +
scene.automap.walls.length +
scene.automap.objects.length +
scene.automap.extras.length
}
expect(totalCells74).toBeGreaterThan(0)
const emptyDs1 = {
version: 18,
width: 16,
height: 16,
act: 2,
substitutionType: 0,
files: [],
objects: [],
cells: [],
wallLayers: 1,
floorLayers: 1,
shadowLayers: 1,
substitutionLayers: 0,
npcPaths: [],
} as unknown as Ds1
const arcaneAutomap = buildLevelAutomap(emptyDs1, {
automapTable,
levelId: 74,
layerId: 74,
levelTypeId: 23,
act: 2,
waypoints: [{ x: 10, y: 10 }],
})
expect(arcaneAutomap.objects.some(c => c.cellNo === 318)).toBe(true)
})
it('F13.B4: verifies Act 5 Pandemonium Event / Uber levels (Matron Den 133, Forgotten Sands 134, Furnace of Pain 135, Tristram 136)', () => {
for (const levelId of [133, 134, 135, 136]) {
const entry = packIndex.levels.find(l => l.levelId === levelId)!
expect(entry).toBeDefined()
const scene = JSON.parse(
fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8'),
) as { automap: AutomapLayerData }
expect(
scene.automap.floors.length +
scene.automap.walls.length +
scene.automap.objects.length +
scene.automap.extras.length,
).toBeGreaterThan(0)
}
})
it('F13.B5: verifies all 365 scene variants across all 136 levels have zero negative cellNo or NaN pixel coordinates', () => {
for (const entry of packIndex.levels) {
const scene = JSON.parse(
fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8'),
) as { automap: AutomapLayerData }
for (const c of [
...scene.automap.floors,
...scene.automap.walls,
...scene.automap.objects,
...scene.automap.extras,
]) {
expect(c.cellNo).toBeGreaterThanOrEqual(0)
expect(Number.isFinite(c.xPixel)).toBe(true)
expect(Number.isFinite(c.yPixel)).toBe(true)
}
}
})
})
})