diablo2-web/tests/automap-renderer.test.ts

600 lines
23 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
type AutomapLayerData,
computeAutomapTilePixelCoords,
getCrosshairSegments,
} from '../src/formats/automap-table.ts'
import type { AtlasFrame } from '../src/render/atlas.ts'
import type { AtlasHandle, SpriteRenderer } from '../src/render/renderer.ts'
import type { D2ColorCode, D2FontRenderer } from '../src/ui/font.ts'
import {
AUTOMAP_CROSSHAIR_COLORS,
AUTOMAP_FADE_ALPHAS,
AUTOMAP_PAN_STEP_PX,
AUTOMAP_REVEAL_DISTANCE_THRESHOLD,
Minimap,
type MinimapLevel,
automapPixelToCellCoords,
computeAutomapRevealDistance,
computeAutomapViewport,
projectAutomapCellScreenRect,
resolveAutomapSheetName,
} from '../src/ui/minimap.ts'
interface RecordedSpriteDraw {
readonly kind: 'sprite'
readonly frame: AtlasFrame
readonly x: number
readonly y: number
readonly atlas?: AtlasHandle
readonly width?: number
readonly height?: number
readonly tint?: readonly [number, number, number, number]
}
interface RecordedSolidDraw {
readonly kind: 'solid'
readonly x: number
readonly y: number
readonly width: number
readonly height: number
readonly color: readonly [number, number, number, number]
}
interface RecordedQuadDraw {
readonly kind: 'quad'
readonly x0: number
readonly y0: number
readonly x1: number
readonly y1: number
readonly x2: number
readonly y2: number
readonly x3: number
readonly y3: number
readonly color: readonly [number, number, number, number]
}
type RecordedDraw = RecordedSpriteDraw | RecordedSolidDraw | RecordedQuadDraw
function createMockRenderer(): {
renderer: SpriteRenderer
draws: RecordedDraw[]
scissors: ({ x: number; y: number; width: number; height: number } | null)[]
} {
const draws: RecordedDraw[] = []
const scissors: ({ x: number; y: number; width: number; height: number } | null)[] = []
let activeScissor: { x: number; y: number; width: number; height: number } | null = null
const mockGl = {
SCISSOR_TEST: 0x0c11,
enable(cap: number) {
if (cap === 0x0c11) {
activeScissor = { x: 0, y: 0, width: 0, height: 0 }
}
},
scissor(x: number, y: number, width: number, height: number) {
activeScissor = { x, y, width, height }
scissors.push(activeScissor)
},
disable(cap: number) {
if (cap === 0x0c11) {
activeScissor = null
scissors.push(null)
}
},
}
const renderer = {
gl: mockGl,
flush() {
// no-op in mock
},
draw(
frame: AtlasFrame,
x: number,
y: number,
options?: {
atlas?: AtlasHandle
width?: number
height?: number
tint?: readonly [number, number, number, number]
},
) {
draws.push({
kind: 'sprite',
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(
x: number,
y: number,
width: number,
height: number,
color: readonly [number, number, number, number],
) {
draws.push({ kind: 'solid', x, y, width, height, color })
},
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', x0, y0, x1, y1, x2, y2, x3, y3, color })
},
} as unknown as SpriteRenderer
return { renderer, draws, scissors }
}
function registerSyntheticAtlases(minimap: Minimap): void {
const keys = [
'MaxiMap',
'MaxiMapS',
'Act2Map',
'Act2MapS',
'Act4Map',
'Act4MapS',
'ExTnMap',
'ExTnMapS',
] as const
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,
})
}
}
function makeAutomapTestLevel(overrides?: Partial<MinimapLevel>): MinimapLevel {
const cellsX = 24
const cellsY = 24
const gridWidth = cellsX * 5
const blocked = new Uint8Array(gridWidth * cellsY * 5)
const nearCoords = computeAutomapTilePixelCoords(4, 4, 0)
const farCoords = computeAutomapTilePixelCoords(20, 20, 0)
const automap: AutomapLayerData = {
layerId: 1,
levelTypeId: 1,
floors: [
{ cellNo: 5, xPixel: nearCoords.xPixel, yPixel: nearCoords.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 },
{ cellNo: 6, xPixel: farCoords.xPixel, yPixel: farCoords.yPixel, cellX: 20, cellY: 20, fSaved: 0, weight: 0 }, // far unrevealed room (room 2,2)
],
walls: [
{ cellNo: 42, xPixel: nearCoords.xPixel, yPixel: nearCoords.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 },
{ cellNo: -1, xPixel: nearCoords.xPixel, yPixel: nearCoords.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 }, // cellNo < 0 must be skipped
],
objects: [
{ cellNo: 318, xPixel: nearCoords.xPixel, yPixel: nearCoords.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 },
],
extras: [
{ cellNo: 309, xPixel: nearCoords.xPixel, yPixel: nearCoords.yPixel, cellX: 4, cellY: 4, fSaved: 0, weight: 0 },
],
}
return {
key: 'act1-town',
cellsX,
cellsY,
blocked,
gridWidth,
originX: 0,
originY: 0,
automap,
levelId: 1,
levelName: '罗格营地',
difficulty: '普通',
markers: [
{ cellX: 5, cellY: 4, kind: 'exit', toLevelId: 2, targetName: '鲜血荒地' },
{ cellX: 4, cellY: 5, kind: 'waypoint' },
],
...overrides,
}
}
describe('R3 Authentic 1.13c Isometric Sprite Automap & Minimap UI', () => {
describe('8-Sheet Sprite Selection & Level Routing (resolveAutomapSheetName)', () => {
it('routes standard Act 1-4 levels to MaxiMap (full) and MaxiMapS (minimap)', () => {
expect(resolveAutomapSheetName(0, false, 1)).toBe('MaxiMap')
expect(resolveAutomapSheetName(0, true, 1)).toBe('MaxiMapS')
expect(resolveAutomapSheetName('MaxiMap', false, 1)).toBe('MaxiMap')
expect(resolveAutomapSheetName('MaxiMap', true, 1)).toBe('MaxiMapS')
expect(resolveAutomapSheetName(undefined, false, 75)).toBe('MaxiMap')
expect(resolveAutomapSheetName(undefined, true, 75)).toBe('MaxiMapS')
})
it('routes Act 2 Sewers (fSaved=1 or levels 47, 48, 49) to Act2Map and Act2MapS', () => {
expect(resolveAutomapSheetName(1, false)).toBe('Act2Map')
expect(resolveAutomapSheetName(1, true)).toBe('Act2MapS')
for (const levelId of [47, 48, 49]) {
expect(resolveAutomapSheetName('MaxiMap', false, levelId)).toBe('Act2Map')
expect(resolveAutomapSheetName('MaxiMap', true, levelId)).toBe('Act2MapS')
}
expect(resolveAutomapSheetName('Act2Map', false, 47)).toBe('Act2Map')
expect(resolveAutomapSheetName('Act2Map', true, 47)).toBe('Act2MapS')
})
it('routes Act 4 Pandemonium Fortress (fSaved=2 or level 103) to Act4Map and Act4MapS', () => {
expect(resolveAutomapSheetName(2, false)).toBe('Act4Map')
expect(resolveAutomapSheetName(2, true)).toBe('Act4MapS')
expect(resolveAutomapSheetName('MaxiMap', false, 103)).toBe('Act4Map')
expect(resolveAutomapSheetName('MaxiMap', true, 103)).toBe('Act4MapS')
expect(resolveAutomapSheetName('Act4Map', false, 103)).toBe('Act4Map')
expect(resolveAutomapSheetName('Act4Map', true, 103)).toBe('Act4MapS')
})
it('routes Act 2 Palace Cellar (level 52) and Act 5 Expansion (fSaved=3 or levels >= 109) to ExTnMap and ExTnMapS', () => {
expect(resolveAutomapSheetName(3, false)).toBe('ExTnMap')
expect(resolveAutomapSheetName(3, true)).toBe('ExTnMapS')
expect(resolveAutomapSheetName('MaxiMap', false, 52)).toBe('ExTnMap')
expect(resolveAutomapSheetName('MaxiMap', true, 52)).toBe('ExTnMapS')
expect(resolveAutomapSheetName('MaxiMap', false, 109)).toBe('ExTnMap')
expect(resolveAutomapSheetName('MaxiMap', true, 109)).toBe('ExTnMapS')
expect(resolveAutomapSheetName('ExTnMap', false, 136)).toBe('ExTnMap')
expect(resolveAutomapSheetName('ExTnMap', true, 136)).toBe('ExTnMapS')
})
})
describe('Isometric Projection & DC6 Baseline Offsets (D2Client.dll 0x6fb10480 / 0x6fb0f240)', () => {
it('projects pre-divided (xPixel, yPixel) cells using nDivisor=10 (full) and nDivisor=20 (minimap)', () => {
const tile = computeAutomapTilePixelCoords(2, 1, 0)
const recovered = automapPixelToCellCoords(tile.xPixel, tile.yPixel)
expect(recovered.cellX).toBe(2)
expect(recovered.cellY).toBe(1)
const frameFull = { width: 16, height: 24, offsetX: 0, offsetY: -3 }
const rectFull = projectAutomapCellScreenRect(tile, frameFull, -392, -264, false)
// screenX = Math.trunc((8 * 10) / 10) - (-392) = 400
// screenY = Math.trunc((12 * 10) / 10) - (-264) = 276
expect(rectFull.screenX).toBe(400)
expect(rectFull.screenY).toBe(276)
// drawLeft = screenX + offsetX - 8 = 392
// drawTop = screenY - height + offsetY = 276 - 24 - 3 = 249
expect(rectFull.drawLeft).toBe(392)
expect(rectFull.drawTop).toBe(249)
const frameHalf = { width: 8, height: 12, offsetX: 0, offsetY: -2 }
const rectHalf = projectAutomapCellScreenRect(tile, frameHalf, -640, -122, true)
// screenX = Math.trunc((8 * 10) / 20) - (-640) = 644
// screenY = Math.trunc((12 * 10) / 20) - (-122) = 128
expect(rectHalf.screenX).toBe(644)
expect(rectHalf.screenY).toBe(128)
// drawLeft = screenX + offsetX - 4 = 640
// drawTop = screenY - height + offsetY = 128 - 12 - 2 = 114
expect(rectHalf.drawLeft).toBe(640)
expect(rectHalf.drawTop).toBe(114)
})
})
describe('Dual Viewports & Panel Shifts (computeAutomapViewport)', () => {
it('computes 279x225 clipped Corner Minimap viewport and shifts to (16, 16) when right panel is open', () => {
const vpRightClosed = computeAutomapViewport(800, 600, true, false, false, 0, 0)
expect(vpRightClosed.bMinimap).toBe(true)
expect(vpRightClosed.divisor).toBe(20)
expect(vpRightClosed.x).toBe(505) // 800 - 295
expect(vpRightClosed.y).toBe(16)
expect(vpRightClosed.width).toBe(279)
expect(vpRightClosed.height).toBe(225)
expect(vpRightClosed.centerX).toBe(505 + 139)
expect(vpRightClosed.centerY).toBe(16 + 112)
const vpRightOpen = computeAutomapViewport(800, 600, true, false, true, 0, 0)
expect(vpRightOpen.x).toBe(16)
expect(vpRightOpen.y).toBe(16)
expect(vpRightOpen.width).toBe(279)
expect(vpRightOpen.height).toBe(225)
expect(vpRightOpen.centerX).toBe(16 + 139)
expect(vpRightOpen.centerY).toBe(16 + 112)
})
it('computes Full-Screen Automap viewport [0, 0, W, H-48] and shifts center +-160px for open side panels', () => {
const vpNeutral = computeAutomapViewport(800, 600, false, false, false, 0, 0)
expect(vpNeutral.bMinimap).toBe(false)
expect(vpNeutral.divisor).toBe(10)
expect(vpNeutral.x).toBe(0)
expect(vpNeutral.y).toBe(0)
expect(vpNeutral.width).toBe(800)
expect(vpNeutral.height).toBe(552) // 600 - 48
expect(vpNeutral.centerX).toBe(400)
expect(vpNeutral.centerY).toBe(276)
const vpLeftOpen = computeAutomapViewport(800, 600, false, true, false, 0, 0)
expect(vpLeftOpen.centerX).toBe(560) // 400 + 160
const vpRightOpen = computeAutomapViewport(800, 600, false, false, true, 0, 0)
expect(vpRightOpen.centerX).toBe(240) // 400 - 160
const vpBothOpen = computeAutomapViewport(800, 600, false, true, true, 0, 0)
expect(vpBothOpen.centerX).toBe(400)
})
})
describe('Strict 4-Layer Draw Order (floors -> walls -> objects -> extras) & Scissor Clipping', () => {
it('draws automap sprites in exact 1.13c layer order and skips cellNo < 0', () => {
const minimap = new Minimap()
registerSyntheticAtlases(minimap)
minimap.mode = 'full' // test Full Automap (bMinimap = false, MaxiMap)
const level = makeAutomapTestLevel()
// Player at cell (4, 4) -> isoX = 0, isoY = (4+4)*40 = 320
minimap.reveal(level, 4, 4, 0, 320, true)
const { renderer, draws, scissors } = createMockRenderer()
minimap.draw(
renderer,
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX: 0, playerWorldY: 320, levelId: 1 },
)
// Scissor was enabled with WebGL bottom-left coordinates (y = 600 - 552 = 48) and disabled afterwards
expect(scissors.length).toBe(2)
expect(scissors[0]).toEqual({ x: 0, y: 48, width: 800, height: 552 })
expect(scissors[1]).toBeNull()
const spriteDraws = draws.filter((d): d is RecordedSpriteDraw => d.kind === 'sprite')
// Cell (4,4) has 1 floor (cellNo 5), 1 valid wall (cellNo 42, cellNo -1 skipped), 1 object (cellNo 318), 1 extra (cellNo 309)
// Cell (20,20) is in room (2,2) which is unrevealed and must be culled by fog-of-war
expect(spriteDraws.length).toBe(4)
expect(minimap.spritesDrawnLastFrame.total).toBe(4)
expect(minimap.spritesDrawnLastFrame.floors).toBe(1)
expect(minimap.spritesDrawnLastFrame.walls).toBe(1)
expect(minimap.spritesDrawnLastFrame.objects).toBe(1)
expect(minimap.spritesDrawnLastFrame.extras).toBe(1)
const atlasMeta = minimap.getSheetMeta('MaxiMap')
expect(spriteDraws[0]!.frame.x).toBe(atlasMeta.frames[5]!.x)
expect(spriteDraws[0]!.frame.y).toBe(atlasMeta.frames[5]!.y)
expect(spriteDraws[1]!.frame.x).toBe(atlasMeta.frames[42]!.x)
expect(spriteDraws[1]!.frame.y).toBe(atlasMeta.frames[42]!.y)
expect(spriteDraws[2]!.frame.x).toBe(atlasMeta.frames[318]!.x)
expect(spriteDraws[2]!.frame.y).toBe(atlasMeta.frames[318]!.y)
expect(spriteDraws[3]!.frame.x).toBe(atlasMeta.frames[309]!.x)
expect(spriteDraws[3]!.frame.y).toBe(atlasMeta.frames[309]!.y)
})
})
describe('Fog-of-War Room/Chunk Reveal (D2Client.dll 0x6fb12af0 / 0x6fb12580)', () => {
it('evaluates octile distance max(|dx|,|dy|)*2 + min(|dx|,|dy|) >= 160 threshold', () => {
expect(computeAutomapRevealDistance(50, 0)).toBe(100)
expect(computeAutomapRevealDistance(50, 0) < AUTOMAP_REVEAL_DISTANCE_THRESHOLD).toBe(true)
expect(computeAutomapRevealDistance(80, 0)).toBe(160)
expect(computeAutomapRevealDistance(80, 0) >= AUTOMAP_REVEAL_DISTANCE_THRESHOLD).toBe(true)
expect(computeAutomapRevealDistance(60, 40)).toBe(60 * 2 + 40) // 160
})
it('reveals 3x3 room neighborhood around player and unlocks distant room only after player approaches', () => {
const minimap = new Minimap()
const level = makeAutomapTestLevel()
minimap.reveal(level, 4, 4, 0, 320, true)
// Room (0,0) and its immediate neighbors (0,1), (1,0), (1,1) are revealed
expect(minimap.isRoomRevealed(level.key, 0, 0)).toBe(true)
expect(minimap.isRoomRevealed(level.key, 1, 1)).toBe(true)
// Far room (2,2) containing cell (20,20) is unrevealed
expect(minimap.isRoomRevealed(level.key, 2, 2)).toBe(false)
// Moving < 160 sub-units in the same room does not reveal room (2,2)
expect(minimap.reveal(level, 5, 5, 0, 360)).toBe(false)
expect(minimap.isRoomRevealed(level.key, 2, 2)).toBe(false)
// Moving to cell (20, 20) (distance >= 160) reveals room (2,2)
expect(minimap.reveal(level, 20, 20, 0, 1600)).toBe(true)
expect(minimap.isRoomRevealed(level.key, 2, 2)).toBe(true)
})
})
describe('Controls: Tab, V (bMinimap toggle), F9 (Fade cycle), F10 (Names), Arrow Panning & Home Reset', () => {
it('defaults to Corner Minimap (bMinimap=true) and supports V mode toggle, F9 fade cycle, F10 name toggle, arrow panning, and Home reset', () => {
const minimap = new Minimap()
expect(minimap.visible).toBe(true)
expect(minimap.bMinimap).toBe(true)
expect(minimap.mode).toBe('minimap')
expect(minimap.divisor).toBe(20)
// Arrow panning is ignored in Corner Minimap
expect(minimap.panBySteps(1, 1)).toBe(false)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
// Toggle to Full Automap (V)
expect(minimap.toggleMinimapMode()).toBe(false)
expect(minimap.bMinimap).toBe(false)
expect(minimap.mode).toBe('full')
expect(minimap.divisor).toBe(10)
// Arrow panning in Full Automap (bMinimap = false)
expect(minimap.panBySteps(-1, 2)).toBe(true)
expect(minimap.panX).toBe(-AUTOMAP_PAN_STEP_PX)
expect(minimap.panY).toBe(2 * AUTOMAP_PAN_STEP_PX)
// Home resets pan
minimap.resetPan()
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
// Switching back to Corner Minimap resets pan automatically
minimap.panBySteps(3, 3)
expect(minimap.toggleMinimapMode()).toBe(true)
expect(minimap.bMinimap).toBe(true)
expect(minimap.panX).toBe(0)
expect(minimap.panY).toBe(0)
// F9 fade mode cycles 0 -> 1 -> 2 -> 0
expect(minimap.fadeMode).toBe(0)
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[0])
expect(minimap.cycleFade()).toBe(1)
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[1])
expect(minimap.cycleFade()).toBe(2)
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
expect(minimap.cycleFade()).toBe(0)
// F10 name toggle
expect(minimap.showNames).toBe(true)
expect(minimap.toggleNames()).toBe(false)
expect(minimap.toggleNames()).toBe(true)
})
})
describe('12-Segment Closed Directional Polygon Crosshair & Dynamic Portals (D2Client.dll 0x6fb107f0)', () => {
it('constructs a closed 12-segment polygon crosshair and renders player, NPC, boss, and portal overlays', () => {
const segmentsFull = getCrosshairSegments(400, 276, false)
expect(segmentsFull.length).toBe(12)
// Verify closed polygon loop: start of segment 0 equals end of segment 11
expect(segmentsFull[0]!.x0).toBe(segmentsFull[11]!.x1)
expect(segmentsFull[0]!.y0).toBe(segmentsFull[11]!.y1)
const segmentsMini = getCrosshairSegments(400, 276, true)
expect(segmentsMini.length).toBe(12)
// Verify (-1, +5) center shift when bMinimap === true
expect(segmentsMini[0]!.x0).toBe(segmentsFull[0]!.x0 - 1)
expect(segmentsMini[0]!.y0).toBe(segmentsFull[0]!.y0 + 5)
const minimap = new Minimap()
registerSyntheticAtlases(minimap)
minimap.mode = 'full'
const level = makeAutomapTestLevel()
minimap.reveal(level, 4, 4, 0, 320, true)
const { renderer, draws } = createMockRenderer()
minimap.draw(
renderer,
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
levelId: 1,
entities: [
{ x: 16, y: 320, kind: 'npc', name: '阿卡拉' },
{ x: -16, y: 320, kind: 'boss', name: '尸体发火' },
],
portals: [
{ x: 32, y: 320, kind: 'blue', label: '传送门 -> 鲜血荒地' },
],
stashes: [
{ x: 0, y: 336, label: '储物箱' },
],
},
)
// 3 entities (player + npc + boss) -> 3 crosshairs drawn (3 * 12 = 36 quad segments)
expect(minimap.crosshairsDrawnLastFrame.total).toBe(3)
expect(minimap.crosshairsDrawnLastFrame.player).toBe(1)
expect(minimap.crosshairsDrawnLastFrame.npcs).toBe(1)
expect(minimap.crosshairsDrawnLastFrame.bosses).toBe(1)
const quadDraws = draws.filter((d): d is RecordedQuadDraw => d.kind === 'quad')
expect(quadDraws.length).toBe(3 * 12)
expect(quadDraws.some(d => d.color[2] === AUTOMAP_CROSSHAIR_COLORS.player[2])).toBe(true)
expect(quadDraws.some(d => d.color[1] === AUTOMAP_CROSSHAIR_COLORS.npc[1])).toBe(true)
expect(quadDraws.some(d => d.color[0] === AUTOMAP_CROSSHAIR_COLORS.boss[0])).toBe(true)
// 4 static cell sprites + 1 blue portal sprite (cellNo 306)
expect(minimap.spritesDrawnLastFrame.total).toBe(5)
expect(minimap.spritesDrawnLastFrame.portals).toBe(1)
expect(minimap.warpLabelsLastFrame.some(l => l.text === '鲜血荒地' && l.kind === 'warp')).toBe(true)
expect(minimap.warpLabelsLastFrame.some(l => l.text === '储物箱' && l.kind === 'stash')).toBe(true)
expect(minimap.warpLabelsLastFrame.some(l => l.text === '传送门 -> 鲜血荒地' && l.kind === 'portal')).toBe(true)
})
})
describe('HUD Text Overlay (drawHudOverlay: Font6 warp/stash labels & Font16 top-right Level Name + Difficulty)', () => {
it('renders Font6 warp/stash labels and right-aligned gold Font16 Level Name + Difficulty at (W-16, 16) and (W-16, 34)', () => {
const minimap = new Minimap()
registerSyntheticAtlases(minimap)
minimap.mode = 'full'
const level = makeAutomapTestLevel()
minimap.reveal(level, 4, 4, 0, 320, true)
const { renderer } = createMockRenderer()
minimap.draw(
renderer,
level,
4,
4,
{ x: 0, y: 320, zoom: 1 },
{ width: 800, height: 600 },
{
playerWorldX: 0,
playerWorldY: 320,
levelId: 1,
stashes: [{ x: 0, y: 336, label: '储物箱' }],
},
)
const drawnTexts: {
text: string
x: number
y: number
font?: string
color?: D2ColorCode
align?: string
}[] = []
const mockFont = {
drawText(
_ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
opts?: Parameters<D2FontRenderer['drawText']>[4],
) {
drawnTexts.push({
text,
x,
y,
...(opts?.font ? { font: opts.font } : {}),
...(opts?.color ? { color: opts.color } : {}),
...(opts?.align ? { align: opts.align } : {}),
})
},
} as unknown as D2FontRenderer
minimap.drawHudOverlay({} as CanvasRenderingContext2D, mockFont, 800, 600)
const levelTitle = drawnTexts.find(t => t.text === '罗格营地')
const diffTitle = drawnTexts.find(t => t.text === '普通')
expect(levelTitle).toBeDefined()
expect(levelTitle!.font).toBe('font16')
expect(levelTitle!.color).toBe('gold')
expect(levelTitle!.align).toBe('right')
expect(levelTitle!.x).toBe(800 - 16)
expect(levelTitle!.y).toBe(16)
expect(diffTitle).toBeDefined()
expect(diffTitle!.font).toBe('font16')
expect(diffTitle!.color).toBe('gold')
expect(diffTitle!.align).toBe('right')
expect(diffTitle!.x).toBe(800 - 16)
expect(diffTitle!.y).toBe(34)
const warpText = drawnTexts.find(t => t.text === '鲜血荒地')
const stashText = drawnTexts.find(t => t.text === '储物箱')
expect(warpText).toBeDefined()
expect(warpText!.font).toBe('font6')
expect(warpText!.align).toBe('center')
expect(stashText).toBeDefined()
expect(stashText!.font).toBe('font6')
expect(stashText!.color).toBe('gold')
})
})
})