diablo2-web/tests/e2e-automap-113c/tier4-real-world-scenarios....

897 lines
30 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 4 — Real-World E2E Scenarios (`tests/e2e-automap-113c/tier4-real-world-scenarios.test.ts`).
*
* Covers 12 realistic end-to-end gameplay and full-spectrum 136-level verification scenarios
* (`>= 10` required) exercising the complete Diablo II 1.13c Isometric Automap & Minimap pipeline.
*/
import fs from 'node:fs'
import path from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { MpqArchive, memorySource } from '../../src/common/mpq/index.ts'
import {
type AutomapLayerData,
type AutomapTable,
computeAutomapTilePixelCoords,
parseAutomapTable,
} from '../../src/common/formats/automap-table.ts'
import type { AtlasFrame } from '../../src/client/render/atlas.ts'
import type { AtlasHandle, SpriteRenderer } from '../../src/client/render/renderer.ts'
import { AUTOMAP_UI_MANIFEST, type AutomapSpriteKey } from '../../src/common/items/baked-ui-meta.ts'
import {
AUTOMAP_CROSSHAIR_COLORS,
AUTOMAP_FADE_ALPHAS,
AUTOMAP_PAN_STEP_PX,
type AutomapEntityMarker,
Minimap,
type MinimapLevel,
resolveAutomapSheetName,
} from '../../src/client/ui/minimap.ts'
const ROOT_DIR = process.cwd()
const PACK_DIR = path.resolve(ROOT_DIR, 'samples/d2-packs')
const MPQ_EXP_PATH = path.resolve(ROOT_DIR, 'samples/d2/d2exp.mpq')
interface PackIndexEntry {
readonly act: number
readonly levelId: number
readonly slug: string
readonly label: string
readonly levelName: string
readonly path: string
readonly kind?: string
}
interface PackedSceneJson {
readonly act: number
readonly levelId: number
readonly levelName: string
readonly cellsX: number
readonly cellsY: number
readonly originX: number
readonly originY: number
readonly widthPx: number
readonly heightPx: number
readonly collision: {
readonly width: number
readonly height: number
readonly runs: readonly (readonly number[])[]
}
readonly spawn: readonly number[] | null
readonly entrances?: readonly { readonly x: number; readonly y: number; readonly toLevelId?: number }[]
readonly warps?: readonly { readonly x: number; readonly y: number; readonly toLevelId?: number }[]
readonly waypoints?: readonly { readonly x: number; readonly y: number; readonly waypointId?: number }[]
readonly automap?: AutomapLayerData
}
interface RecordedDraw {
readonly kind: 'sprite' | 'quad'
readonly frame?: AtlasFrame
readonly atlas?: AtlasHandle
readonly x?: number
readonly y?: number
readonly tint?: readonly [number, number, number, number]
readonly color?: readonly [number, number, number, number]
}
function createScenarioRenderer(): {
renderer: SpriteRenderer
draws: RecordedDraw[]
} {
const draws: RecordedDraw[] = []
const renderer = {
flush() {},
draw(
frame: AtlasFrame,
x: number,
y: number,
options?: {
atlas?: AtlasHandle
tint?: readonly [number, number, number, number]
},
) {
draws.push({
kind: 'sprite',
frame,
x,
y,
...(options?.atlas ? { atlas: options.atlas } : {}),
...(options?.tint ? { tint: options.tint } : {}),
})
},
drawSolid() {},
drawSolidQuad(
x0: number,
y0: number,
_x1: number,
_y1: number,
_x2: number,
_y2: number,
_x3: number,
_y3: number,
color: readonly [number, number, number, number],
) {
draws.push({ kind: 'quad', x: x0, y: y0, color })
},
} as unknown as SpriteRenderer
return { renderer, draws }
}
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)
}
}
let packIndex: { readonly levels: readonly PackIndexEntry[] }
let entriesByLevelId: Map<number, PackIndexEntry[]>
let automapTable: AutomapTable
function loadSceneByLevelId(levelId: number, variantIndex = 0): {
entry: PackIndexEntry
scene: PackedSceneJson
levelModel: MinimapLevel
playerCellX: number
playerCellY: number
playerWorldX: number
playerWorldY: number
} {
const entries = entriesByLevelId.get(levelId) ?? []
if (entries.length === 0) {
throw new Error(`No baked scene found for LevelId ${String(levelId)}`)
}
const entry = entries[Math.min(variantIndex, entries.length - 1)]!
const sceneRaw = fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8')
const scene = JSON.parse(sceneRaw) as PackedSceneJson
const automap = scene.automap!
const refCell =
automap.floors[0] ?? automap.walls[0] ?? automap.objects[0] ?? automap.extras[0]!
const playerCellX = Math.max(
0,
Math.min(scene.cellsX - 1, refCell.cellX ?? Math.floor(scene.cellsX / 2)),
)
const playerCellY = Math.max(
0,
Math.min(scene.cellsY - 1, refCell.cellY ?? Math.floor(scene.cellsY / 2)),
)
const playerWorldX = scene.originX + (playerCellX - playerCellY) * 80
const playerWorldY = scene.originY + (playerCellX + playerCellY) * 40
const levelModel: MinimapLevel = {
key: `e2e:${entry.path}`,
cellsX: scene.cellsX,
cellsY: scene.cellsY,
blocked: new Uint8Array(scene.collision.width * scene.collision.height),
gridWidth: scene.collision.width,
originX: scene.originX,
originY: scene.originY,
automap,
levelId,
levelName: scene.levelName,
difficulty: 'Normal',
markers: [
...(scene.warps ?? []).map(w => ({
cellX: Math.floor(w.x / 5),
cellY: Math.floor(w.y / 5),
kind: 'exit' as const,
...(w.toLevelId !== undefined ? { toLevelId: w.toLevelId } : {}),
targetName: `Level ${String(w.toLevelId ?? 0)}`,
})),
],
}
return {
entry,
scene,
levelModel,
playerCellX,
playerCellY,
playerWorldX,
playerWorldY,
}
}
beforeAll(async () => {
const indexRaw = fs.readFileSync(path.join(PACK_DIR, 'index.json'), 'utf8')
packIndex = JSON.parse(indexRaw) as { readonly levels: readonly PackIndexEntry[] }
entriesByLevelId = new Map<number, PackIndexEntry[]>()
for (const entry of packIndex.levels) {
const list = entriesByLevelId.get(entry.levelId) ?? []
list.push(entry)
entriesByLevelId.set(entry.levelId, list)
}
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')
automapTable = parseAutomapTable(await expArchive.read(autoMapEntry))
})
describe('Tier 4 — Real-World E2E Scenarios (12 end-to-end scenarios across Acts 1–5 & all 136 Level IDs)', () => {
it('S1: Act 1 Rogue Encampment (LevelId=1) to Blood Moor (LevelId=2) — town icons, MaxiMap/MaxiMapS sheets, and mode switch', () => {
expect(automapTable.rows.length).toBeGreaterThan(3000)
const town = loadSceneByLevelId(1)
const bloodMoor = loadSceneByLevelId(2)
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer } = createScenarioRenderer()
// Reveal Act 1 Town and render in Full Automap (bMinimap=false, nDivisor=10)
minimap.mode = 'full'
minimap.reveal(
town.levelModel,
town.playerCellX,
town.playerCellY,
town.playerWorldX,
town.playerWorldY,
true,
)
minimap.draw(
renderer,
town.levelModel,
town.playerCellX,
town.playerCellY,
{ x: town.playerWorldX, y: town.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: town.playerWorldX, playerWorldY: town.playerWorldY, levelId: 1 },
)
expect(minimap.lastRenderStats.divisor).toBe(10)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(20)
expect(minimap.lastRenderStats.crosshairsDrawn.player).toBe(1)
expect(minimap.lastRenderStats.hudText?.levelName).toBe(town.scene.levelName)
// Switch to Blood Moor in Corner Minimap (bMinimap=true, nDivisor=20)
minimap.mode = 'minimap'
minimap.reveal(
bloodMoor.levelModel,
bloodMoor.playerCellX,
bloodMoor.playerCellY,
bloodMoor.playerWorldX,
bloodMoor.playerWorldY,
true,
)
minimap.draw(
renderer,
bloodMoor.levelModel,
bloodMoor.playerCellX,
bloodMoor.playerCellY,
{ x: bloodMoor.playerWorldX, y: bloodMoor.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: bloodMoor.playerWorldX,
playerWorldY: bloodMoor.playerWorldY,
levelId: 2,
},
)
expect(minimap.lastRenderStats.divisor).toBe(20)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
expect(minimap.lastRenderStats.crosshairsDrawn.player).toBe(1)
})
it('S2: Act 2 Lut Gholein (LevelId=40) to Sewers Level 1 (LevelId=47) & Palace Cellar (LevelId=50) — fSaved=1 town extras & Act2Map routing', () => {
const lutGholein = loadSceneByLevelId(40)
const sewers1 = loadSceneByLevelId(47)
const palaceCellar1 = loadSceneByLevelId(50)
// Lut Gholein has pre-revealed fSaved=1 Act2Map town extras
const act2Extras = lutGholein.scene.automap!.extras.filter(c => c.fSaved === 1)
expect(act2Extras.length).toBeGreaterThan(0)
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer, draws } = createScenarioRenderer()
// Even before revealing the player's room, pre-revealed fSaved=1 extras in Lut Gholein are drawn
// when camera is centered near them
const firstExtra = act2Extras[0]!
const extraCellX = firstExtra.cellX ?? 10
const extraCellY = firstExtra.cellY ?? 10
const extraWorldX = lutGholein.scene.originX + (extraCellX - extraCellY) * 80
const extraWorldY = lutGholein.scene.originY + (extraCellX + extraCellY) * 40
minimap.mode = 'full'
minimap.draw(
renderer,
lutGholein.levelModel,
extraCellX,
extraCellY,
{ x: extraWorldX, y: extraWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: extraWorldX, playerWorldY: extraWorldY, levelId: 40 },
)
expect(minimap.lastRenderStats.spritesDrawn.extras).toBeGreaterThan(0)
expect(draws.some(d => d.kind === 'sprite' && (d.atlas?.texture as unknown) === 'Act2Map')).toBe(
true,
)
// Transition to Sewers Level 1 (LevelId=47) and Palace Cellar Level 1 (LevelId=50)
for (const target of [sewers1, palaceCellar1]) {
draws.length = 0
minimap.mode = 'minimap'
minimap.reveal(
target.levelModel,
target.playerCellX,
target.playerCellY,
target.playerWorldX,
target.playerWorldY,
true,
)
minimap.draw(
renderer,
target.levelModel,
target.playerCellX,
target.playerCellY,
{ x: target.playerWorldX, y: target.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: target.playerWorldX,
playerWorldY: target.playerWorldY,
levelId: target.scene.levelId,
},
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
}
})
it('S3: Act 3 Kurast Docks (LevelId=75) & Travincal (LevelId=83) — multi-layer pass ordering & HUD overlay', () => {
const kurast = loadSceneByLevelId(75)
const travincal = loadSceneByLevelId(83)
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer } = createScenarioRenderer()
minimap.mode = 'full'
minimap.reveal(
kurast.levelModel,
kurast.playerCellX,
kurast.playerCellY,
kurast.playerWorldX,
kurast.playerWorldY,
true,
)
minimap.draw(
renderer,
kurast.levelModel,
kurast.playerCellX,
kurast.playerCellY,
{ x: kurast.playerWorldX, y: kurast.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: kurast.playerWorldX,
playerWorldY: kurast.playerWorldY,
levelId: 75,
difficulty: 'Nightmare',
},
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
expect(minimap.lastRenderStats.hudText?.levelName).toBe(kurast.scene.levelName)
expect(minimap.lastRenderStats.hudText?.difficulty).toBe('Nightmare')
minimap.reveal(
travincal.levelModel,
travincal.playerCellX,
travincal.playerCellY,
travincal.playerWorldX,
travincal.playerWorldY,
true,
)
minimap.draw(
renderer,
travincal.levelModel,
travincal.playerCellX,
travincal.playerCellY,
{ x: travincal.playerWorldX, y: travincal.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: travincal.playerWorldX,
playerWorldY: travincal.playerWorldY,
levelId: 83,
difficulty: 'Hell',
},
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
expect(minimap.lastRenderStats.hudText?.difficulty).toBe('Hell')
})
it('S4: Act 4 Pandemonium Fortress (LevelId=103) to Chaos Sanctuary (LevelId=108) — fSaved=2 Act4Map extras & Chaos Sanctuary rendering', () => {
const fortress = loadSceneByLevelId(103)
const chaos = loadSceneByLevelId(108)
const act4Extras = fortress.scene.automap!.extras.filter(c => c.fSaved === 2)
expect(act4Extras.length).toBe(4)
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer, draws } = createScenarioRenderer()
minimap.mode = 'full'
minimap.reveal(
fortress.levelModel,
fortress.playerCellX,
fortress.playerCellY,
fortress.playerWorldX,
fortress.playerWorldY,
true,
)
minimap.draw(
renderer,
fortress.levelModel,
fortress.playerCellX,
fortress.playerCellY,
{ x: fortress.playerWorldX, y: fortress.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: fortress.playerWorldX, playerWorldY: fortress.playerWorldY, levelId: 103 },
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
expect(draws.some(d => d.kind === 'sprite' && (d.atlas?.texture as unknown) === 'Act4Map')).toBe(
true,
)
minimap.reveal(
chaos.levelModel,
chaos.playerCellX,
chaos.playerCellY,
chaos.playerWorldX,
chaos.playerWorldY,
true,
)
minimap.draw(
renderer,
chaos.levelModel,
chaos.playerCellX,
chaos.playerCellY,
{ x: chaos.playerWorldX, y: chaos.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: chaos.playerWorldX, playerWorldY: chaos.playerWorldY, levelId: 108 },
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
})
it('S5: Act 5 Harrogath (LevelId=109) to Worldstone Chamber (LevelId=132) & Uber Tristram (LevelId=136) — fSaved=3 ExTnMap extras & expansion levels', () => {
const harrogath = loadSceneByLevelId(109)
const worldstone = loadSceneByLevelId(132)
const uberTristram = loadSceneByLevelId(136)
const act5Extras = harrogath.scene.automap!.extras.filter(c => c.fSaved === 3)
expect(act5Extras.length).toBe(6)
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer, draws } = createScenarioRenderer()
minimap.mode = 'full'
minimap.reveal(
harrogath.levelModel,
harrogath.playerCellX,
harrogath.playerCellY,
harrogath.playerWorldX,
harrogath.playerWorldY,
true,
)
minimap.draw(
renderer,
harrogath.levelModel,
harrogath.playerCellX,
harrogath.playerCellY,
{ x: harrogath.playerWorldX, y: harrogath.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: harrogath.playerWorldX,
playerWorldY: harrogath.playerWorldY,
levelId: 109,
},
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
expect(draws.some(d => d.kind === 'sprite' && (d.atlas?.texture as unknown) === 'ExTnMap')).toBe(
true,
)
for (const target of [worldstone, uberTristram]) {
minimap.reveal(
target.levelModel,
target.playerCellX,
target.playerCellY,
target.playerWorldX,
target.playerWorldY,
true,
)
minimap.draw(
renderer,
target.levelModel,
target.playerCellX,
target.playerCellY,
{ x: target.playerWorldX, y: target.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: target.playerWorldX,
playerWorldY: target.playerWorldY,
levelId: target.scene.levelId,
},
)
expect(minimap.lastRenderStats.spritesDrawn.total).toBeGreaterThan(0)
}
})
it('S6: Progressive Fog-of-War Dungeon Exploration Walk — step-by-step 160px threshold room discovery & irreversible fog persistence', () => {
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer } = createScenarioRenderer()
minimap.mode = 'full'
// Construct a 3-room dungeon corridor: Room (0,0) -> Room (2,0) -> Room (4,0)
const r0 = computeAutomapTilePixelCoords(4, 4, 0)
const r2 = computeAutomapTilePixelCoords(20, 4, 0)
const r4 = computeAutomapTilePixelCoords(36, 4, 0)
const dungeon: MinimapLevel = {
key: 'e2e-progressive-walk',
cellsX: 48,
cellsY: 16,
blocked: new Uint8Array(48 * 5 * 16 * 5),
gridWidth: 48 * 5,
originX: 0,
originY: 0,
levelId: 8,
levelName: 'Den of Evil',
difficulty: 'Normal',
automap: {
layerId: 8,
levelTypeId: 3,
floors: [
{ cellNo: 5, xPixel: r0.xPixel, yPixel: r0.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 },
{ cellNo: 5, xPixel: r2.xPixel, yPixel: r2.yPixel, cellX: 20, cellY: 4, fSaved: 0, weight: 0 },
{ cellNo: 5, xPixel: r4.xPixel, yPixel: r4.yPixel, cellX: 36, cellY: 4, fSaved: 0, weight: 0 },
],
walls: [],
objects: [],
extras: [],
},
markers: [
{ cellX: 36, cellY: 4, kind: 'exit', toLevelId: 2, targetName: 'Blood Moor' },
],
}
// Step 1: Spawn at Room (0,0) — only 1 floor cell visible, exit in Room (4,0) hidden
minimap.reveal(dungeon, 4, 4, 0, 0, true)
minimap.draw(renderer, dungeon, 4, 4, { x: 0, y: 0, zoom: 1 }, { width: 1600, height: 1200 }, {
playerWorldX: 0,
playerWorldY: 0,
})
expect(minimap.lastRenderStats.spritesDrawn.floors).toBe(1)
expect(minimap.lastRenderStats.warpLabels).toHaveLength(0)
// Step 2: Walk < 160 sub-units in octile distance — reveal suppressed until 160 threshold
expect(minimap.reveal(dungeon, 20, 4, 50, 0, false)).toBe(false)
expect(minimap.isCellExplored(dungeon, 20, 4)).toBe(false)
// Step 3: Cross >= 160 sub-units into Room (2,0) — Room (2,0) reveals (now 2 floors visible)
expect(minimap.reveal(dungeon, 20, 4, 200, 0, false)).toBe(true)
expect(minimap.isCellExplored(dungeon, 20, 4)).toBe(true)
minimap.draw(
renderer,
dungeon,
20,
4,
{ x: 200, y: 0, zoom: 1 },
{ width: 1600, height: 1200 },
{ playerWorldX: 200, playerWorldY: 0 },
)
expect(minimap.lastRenderStats.spritesDrawn.floors).toBe(2)
// Step 4: Walk >= 160 sub-units into Room (4,0) — all 3 floors and the exit label are now visible
expect(minimap.reveal(dungeon, 36, 4, 400, 0, false)).toBe(true)
expect(minimap.isCellExplored(dungeon, 36, 4)).toBe(true)
expect(minimap.isCellExplored(dungeon, 4, 4)).toBe(true) // Room (0,0) remains irreversibly explored
minimap.draw(
renderer,
dungeon,
36,
4,
{ x: 400, y: 0, zoom: 1 },
{ width: 1600, height: 1200 },
{ playerWorldX: 400, playerWorldY: 0 },
)
expect(minimap.lastRenderStats.spritesDrawn.floors).toBe(3)
expect(minimap.lastRenderStats.warpLabels).toHaveLength(1)
expect(minimap.lastRenderStats.warpLabels[0]!.text).toBe('Blood Moor')
})
it('S7: Live Combat Encounter on Automap — player, mercenary, party ally, town portal, SuperUnique boss, and dead corpses', () => {
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer, draws } = createScenarioRenderer()
minimap.mode = 'full'
const c = computeAutomapTilePixelCoords(8, 8, 0)
const level: MinimapLevel = {
key: 'e2e-combat-encounter',
cellsX: 32,
cellsY: 32,
blocked: new Uint8Array(32 * 5 * 32 * 5),
gridWidth: 32 * 5,
originX: 0,
originY: 0,
automap: {
layerId: 2,
levelTypeId: 2,
floors: [
{ cellNo: 5, xPixel: c.xPixel, yPixel: c.yPixel, cellX: 8, cellY: 8, fSaved: 0, weight: 0 },
],
walls: [],
objects: [],
extras: [],
},
markers: [],
}
minimap.reveal(level, 8, 8, 0, 0, true)
const entities: AutomapEntityMarker[] = [
{ x: 20, y: 20, kind: 'mercenary', alive: true },
{ x: 30, y: -20, kind: 'party', name: 'Waheed', alive: true },
{ x: 50, y: 40, kind: 'unique', alive: true },
{ x: 60, y: 50, kind: 'superunique', alive: true },
{ x: 70, y: 60, kind: 'boss', alive: false }, // dead corpse — must be skipped
]
minimap.draw(renderer, level, 8, 8, { x: 0, y: 0, zoom: 1 }, { width: 800, height: 600 }, {
playerWorldX: 0,
playerWorldY: 0,
entities,
portals: [{ x: -30, y: 10, active: true, isLocalPlayerOwner: true }],
})
// 1 player + 1 party + 1 mercenary + 2 bosses (unique + superunique) = 5 crosshairs (60 quads)
expect(minimap.lastRenderStats.crosshairsDrawn.player).toBe(1)
expect(minimap.lastRenderStats.crosshairsDrawn.party).toBe(1)
expect(minimap.lastRenderStats.crosshairsDrawn.mercenaries).toBe(1)
expect(minimap.lastRenderStats.crosshairsDrawn.bosses).toBe(2)
expect(minimap.lastRenderStats.crosshairsDrawn.total).toBe(5)
expect(minimap.lastRenderStats.spritesDrawn.portals).toBe(1)
const quads = draws.filter(d => d.kind === 'quad')
expect(quads).toHaveLength(5 * 12)
expect(quads.filter(q => q.color === AUTOMAP_CROSSHAIR_COLORS.player)).toHaveLength(12)
expect(quads.filter(q => q.color === AUTOMAP_CROSSHAIR_COLORS.party)).toHaveLength(12)
expect(quads.filter(q => q.color === AUTOMAP_CROSSHAIR_COLORS.mercenary)).toHaveLength(12)
expect(quads.filter(q => q.color === AUTOMAP_CROSSHAIR_COLORS.boss)).toHaveLength(24)
})
it('S8: Complete Player UI Interaction Workflow — Tab, V, F9, F10, Arrow Panning, Home Re-center, and Side Panel Shifts', () => {
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer, draws } = createScenarioRenderer()
const town = loadSceneByLevelId(1)
minimap.reveal(
town.levelModel,
town.playerCellX,
town.playerCellY,
town.playerWorldX,
town.playerWorldY,
true,
)
// 1. Default mode is Corner Minimap ('minimap'); Tab toggles visibility off and back on
expect(minimap.mode).toBe('minimap')
expect(minimap.visible).toBe(true)
expect(minimap.toggle()).toBe(false)
expect(minimap.visible).toBe(false)
expect(minimap.toggle()).toBe(true)
expect(minimap.visible).toBe(true)
// 2. V switches between Corner Minimap (div=20) and Full Automap (div=10)
expect(minimap.toggleMode()).toBe('full')
expect(minimap.mode).toBe('full')
expect(minimap.toggleMode()).toBe('minimap')
expect(minimap.toggleMode()).toBe('full')
// 3. F9 cycles fadeState through 0 -> 1 -> 2 -> 0
expect(minimap.fadeState).toBe(0)
expect(minimap.cycleFade()).toBe(1)
expect(minimap.cycleFade()).toBe(2)
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
// 4. F10 toggles showNames
expect(minimap.showNames).toBe(true)
expect(minimap.toggleShowNames()).toBe(false)
expect(minimap.toggleShowNames()).toBe(true)
// 5. Arrow keys pan by AUTOMAP_PAN_STEP_PX (8px) and Home resets to (0,0)
expect(minimap.panBySteps(1, -2)).toBe(true)
expect(minimap.panX).toBe(AUTOMAP_PAN_STEP_PX)
expect(minimap.panY).toBe(-2 * AUTOMAP_PAN_STEP_PX)
minimap.resetPan()
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
// 6. Left panel open (+160 shift) with fadeState=2 (alpha=0.5)
draws.length = 0
minimap.draw(
renderer,
town.levelModel,
town.playerCellX,
town.playerCellY,
{ x: town.playerWorldX, y: town.playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: town.playerWorldX,
playerWorldY: town.playerWorldY,
leftPanelOpen: true,
rightPanelOpen: false,
},
)
expect(minimap.lastRenderStats.viewport.centerShiftX).toBe(160)
expect(minimap.lastRenderStats.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
})
it('S9: Multi-Floor Dungeon Traversal — independent per-level fog-of-war state across Catacombs Level 1 (34) & Level 2 (35)', () => {
const catacombs1 = loadSceneByLevelId(34)
const catacombs2 = loadSceneByLevelId(35)
const minimap = new Minimap()
registerAtlases(minimap)
// Reveal starting room in Catacombs Level 1
minimap.reveal(
catacombs1.levelModel,
catacombs1.playerCellX,
catacombs1.playerCellY,
catacombs1.playerWorldX,
catacombs1.playerWorldY,
true,
)
expect(
minimap.isCellExplored(catacombs1.levelModel, catacombs1.playerCellX, catacombs1.playerCellY),
).toBe(true)
expect(
minimap.isCellExplored(catacombs2.levelModel, catacombs2.playerCellX, catacombs2.playerCellY),
).toBe(false)
// Descend to Catacombs Level 2 and reveal its starting room
minimap.reveal(
catacombs2.levelModel,
catacombs2.playerCellX,
catacombs2.playerCellY,
catacombs2.playerWorldX,
catacombs2.playerWorldY,
true,
)
expect(
minimap.isCellExplored(catacombs2.levelModel, catacombs2.playerCellX, catacombs2.playerCellY),
).toBe(true)
// Return to Catacombs Level 1 — its previously revealed room is still explored
expect(
minimap.isCellExplored(catacombs1.levelModel, catacombs1.playerCellX, catacombs1.playerCellY),
).toBe(true)
})
it('S10: Full-Spectrum 136-Level (1..136) Offline Pack Integrity Audit — all 365 baked scenes have non-empty automap cells within valid sheet bounds', () => {
let totalScenesVerified = 0
let totalCellsVerified = 0
for (let levelId = 1; levelId <= 136; levelId += 1) {
const entries = entriesByLevelId.get(levelId) ?? []
expect(entries.length, `LevelId ${String(levelId)} missing in index.json`).toBeGreaterThan(0)
for (const entry of entries) {
const sceneRaw = fs.readFileSync(path.join(PACK_DIR, entry.path, 'scene.json'), 'utf8')
const scene = JSON.parse(sceneRaw) as PackedSceneJson
const automap = scene.automap
expect(automap, `${entry.path} missing scene.automap`).toBeDefined()
if (!automap) continue
const allCells = [
...automap.floors,
...automap.walls,
...automap.objects,
...automap.extras,
]
expect(
allCells.length,
`${entry.path} (LevelId=${String(levelId)}) has 0 automap cells`,
).toBeGreaterThan(0)
for (const cell of allCells) {
const sheetFull = resolveAutomapSheetName(cell.fSaved ?? 0, false)
const sheetHalf = resolveAutomapSheetName(cell.fSaved ?? 0, true)
const metaFull = AUTOMAP_UI_MANIFEST.atlases[sheetFull]
const metaHalf = AUTOMAP_UI_MANIFEST.atlases[sheetHalf]
expect(cell.cellNo).toBeGreaterThanOrEqual(0)
expect(cell.cellNo).toBeLessThan(metaFull.frames.length)
expect(cell.cellNo).toBeLessThan(metaHalf.frames.length)
}
totalScenesVerified += 1
totalCellsVerified += allCells.length
}
}
expect(totalScenesVerified).toBe(136)
expect(totalCellsVerified).toBeGreaterThan(100000)
})
it('S11: Full-Spectrum 136-Level (1..136) Corner Minimap (bMinimap=true, nDivisor=20) Render Pipeline Audit', () => {
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer } = createScenarioRenderer()
minimap.mode = 'minimap'
for (let levelId = 1; levelId <= 136; levelId += 1) {
const { scene, levelModel, playerCellX, playerCellY, playerWorldX, playerWorldY } =
loadSceneByLevelId(levelId)
minimap.reveal(levelModel, playerCellX, playerCellY, playerWorldX, playerWorldY, true)
minimap.draw(
renderer,
levelModel,
playerCellX,
playerCellY,
{ x: playerWorldX, y: playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX, playerWorldY, levelId },
)
expect(minimap.lastRenderStats.divisor).toBe(20)
expect(
minimap.lastRenderStats.spritesDrawn.total,
`LevelId ${String(levelId)} (${scene.levelName}) drew 0 sprites in Corner Minimap mode`,
).toBeGreaterThan(0)
expect(minimap.lastRenderStats.crosshairsDrawn.player).toBe(1)
expect(minimap.lastRenderStats.hudText?.levelName).toBe(scene.levelName)
}
})
it('S12: Full-Spectrum 136-Level (1..136) Full Automap (bMinimap=false, nDivisor=10) Render Pipeline Audit', () => {
const minimap = new Minimap()
registerAtlases(minimap)
const { renderer } = createScenarioRenderer()
minimap.mode = 'full'
for (let levelId = 1; levelId <= 136; levelId += 1) {
const { scene, levelModel, playerCellX, playerCellY, playerWorldX, playerWorldY } =
loadSceneByLevelId(levelId)
minimap.reveal(levelModel, playerCellX, playerCellY, playerWorldX, playerWorldY, true)
minimap.draw(
renderer,
levelModel,
playerCellX,
playerCellY,
{ x: playerWorldX, y: playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX, playerWorldY, levelId },
)
expect(minimap.lastRenderStats.divisor).toBe(10)
expect(
minimap.lastRenderStats.spritesDrawn.total,
`LevelId ${String(levelId)} (${scene.levelName}) drew 0 sprites in Full Automap mode`,
).toBeGreaterThan(0)
expect(minimap.lastRenderStats.crosshairsDrawn.player).toBe(1)
expect(minimap.lastRenderStats.hudText?.levelName).toBe(scene.levelName)
}
})
})