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 { 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[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') }) }) })