245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
/**
|
|
* Issue #549 regression: Minimap / Automap must switch synchronously with the main map
|
|
* when the player transitions between levels/areas within an Act (outdoor border crossing,
|
|
* dungeon warp, town portal, or waypoint) without a new `0x03 LoadAct` packet.
|
|
*/
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
import '../../tools/lib/node-map-loader.ts'
|
|
import { AutomapView } from '../../src/client/automap/automap-view.ts'
|
|
import { MapService } from '../../src/client/map/map-service.ts'
|
|
import { renderSceneSourceFrame } from '../../src/client/scene/world-renderer.ts'
|
|
import { OnlineSession } from '../../src/client/session/online-session.ts'
|
|
import { createSettingsStore } from '../../src/client/settings/settings-store.ts'
|
|
import { Minimap } from '../../src/client/ui/minimap.ts'
|
|
import { levelDisplayName } from '../../src/common/world/level-names-zh.ts'
|
|
|
|
const FIXTURE_DIR = resolve('tests/fixtures/netproto')
|
|
const ROGUE_ENCAMPMENT = 1
|
|
const BLOOD_MOOR = 2
|
|
const DEN_OF_EVIL = 8
|
|
|
|
function createNoopSpriteRenderer() {
|
|
return {
|
|
begin() {},
|
|
draw() {},
|
|
drawSolidQuad() {},
|
|
setLighting() {},
|
|
setPalette() {},
|
|
flush() {},
|
|
getGl() {
|
|
return null
|
|
},
|
|
}
|
|
}
|
|
|
|
function createFakeTileGpuCache() {
|
|
return {
|
|
loadedPageCount: 1,
|
|
ensureActPalette() {},
|
|
getPageHandle() {
|
|
return { index: 1, width: 2048, height: 1932 }
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('Issue #549: Minimap / Automap switches synchronously with main map on area transitions', () => {
|
|
let mapService: MapService
|
|
|
|
beforeAll(async () => {
|
|
mapService = await MapService.fromPackedDir(resolve('public/act-packs/tiles'))
|
|
})
|
|
|
|
it('AutomapView.syncActiveLevel switches currentAreaId without wiping serverRevealedRooms across the Act', () => {
|
|
const automap = new AutomapView({ mapService, initialMode: 'off' })
|
|
const townView = automap.setContext(0x52e4e6ff, 0, ROGUE_ENCAMPMENT)
|
|
const townRoom = townView.rooms[0]!
|
|
automap.onServerAddRoom(townRoom.tileX, townRoom.tileY, ROGUE_ENCAMPMENT)
|
|
expect(automap.getCurrentAreaId()).toBe(ROGUE_ENCAMPMENT)
|
|
expect(automap.buildSnapshot().serverRevealedRoomCount).toBe(1)
|
|
|
|
const bloodMoorView = mapService.getLevelView(BLOOD_MOOR, 'all')
|
|
const bmRoom = bloodMoorView.rooms[0]!
|
|
automap.onServerAddRoom(bmRoom.tileX, bmRoom.tileY, BLOOD_MOOR)
|
|
|
|
// Switch to Blood Moor via syncActiveLevel: preserves revealed rooms in both Area 1 and Area 2
|
|
automap.syncActiveLevel(BLOOD_MOOR, bloodMoorView)
|
|
expect(automap.getCurrentAreaId()).toBe(BLOOD_MOOR)
|
|
expect(automap.buildSnapshot().currentAreaId).toBe(BLOOD_MOOR)
|
|
expect(automap.buildSnapshot().serverRevealedRoomCount).toBe(1)
|
|
|
|
const bmMinimapLevel = automap.buildMinimapLevel('ENG')
|
|
expect(bmMinimapLevel.levelId).toBe(BLOOD_MOOR)
|
|
expect(bmMinimapLevel.levelName).toBe(levelDisplayName(BLOOD_MOOR, 'ENG'))
|
|
expect(bmMinimapLevel.cellsX).toBe(bloodMoorView.cellsX)
|
|
expect(bmMinimapLevel.cellsY).toBe(bloodMoorView.cellsY)
|
|
expect(bmMinimapLevel.markers.some((m) => m.kind === 'exit')).toBe(true)
|
|
|
|
// Switch back to Rogue Encampment: Area 1 revealed rooms are still intact
|
|
automap.syncActiveLevel(ROGUE_ENCAMPMENT, townView)
|
|
expect(automap.getCurrentAreaId()).toBe(ROGUE_ENCAMPMENT)
|
|
expect(automap.buildSnapshot().serverRevealedRoomCount).toBe(1)
|
|
})
|
|
|
|
it('OnlineSession keeps automapView and world.areaId in lockstep across Area 1 -> Area 2 -> Area 8 -> Area 1 transitions', () => {
|
|
const text = readFileSync(resolve(FIXTURE_DIR, '04-d2gs-idle-60s.d2cap'), 'utf8')
|
|
const settingsStore = createSettingsStore()
|
|
settingsStore.set({ automapReveal: 'level' })
|
|
const session = new OnlineSession({ settingsStore, mapService })
|
|
session.loadD2CapReplay(text)
|
|
session.replayAllD2Cap()
|
|
|
|
// Initial state: standing in Rogue Encampment (Area 1), even though Area 2 rooms were pre-revealed by 0x07
|
|
expect(session.world.getRevealedAreaIds()).toContain(BLOOD_MOOR)
|
|
expect(session.sceneSource.getLevelView()?.areaId).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.world.areaId).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.automapView.getCurrentAreaId()).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelId).toBe(ROGUE_ENCAMPMENT)
|
|
|
|
// 1. Player walks/moves across border into Blood Moor (Area 2) without a new LoadAct packet
|
|
const bloodMoorView = mapService.getLevelView(BLOOD_MOOR, 'all')
|
|
const bmSubX = bloodMoorView.subOriginX + bloodMoorView.spawnX * 5 + 2
|
|
const bmSubY = bloodMoorView.subOriginY + bloodMoorView.spawnY * 5 + 2
|
|
session.handleServerEvent({
|
|
type: 'UnitReassign',
|
|
unitType: 'player',
|
|
id: session.world.selfId,
|
|
at: { x: bmSubX, y: bmSubY },
|
|
snap: true,
|
|
})
|
|
|
|
expect(session.sceneSource.getLevelView()?.areaId).toBe(BLOOD_MOOR)
|
|
expect(session.sceneSource.buildFrameSnapshot().levelId).toBe(BLOOD_MOOR)
|
|
expect(session.world.areaId).toBe(BLOOD_MOOR)
|
|
expect(session.automapView.getCurrentAreaId()).toBe(BLOOD_MOOR)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelId).toBe(BLOOD_MOOR)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelName).toBe(
|
|
levelDisplayName(BLOOD_MOOR, 'ENG'),
|
|
)
|
|
|
|
// 2. Player enters Den of Evil (Area 8): server reveals Area 8 room via 0x07 and reassigns player
|
|
const denView = mapService.getLevelView(DEN_OF_EVIL, 'all')
|
|
const denRoom = denView.rooms[0]!
|
|
session.handleServerEvent({
|
|
type: 'RoomReveal',
|
|
areaId: DEN_OF_EVIL,
|
|
tileX: denRoom.tileX,
|
|
tileY: denRoom.tileY,
|
|
})
|
|
const denSubX = denView.subOriginX + denView.spawnX * 5 + 2
|
|
const denSubY = denView.subOriginY + denView.spawnY * 5 + 2
|
|
session.handleServerEvent({
|
|
type: 'UnitReassign',
|
|
unitType: 'player',
|
|
id: session.world.selfId,
|
|
at: { x: denSubX, y: denSubY },
|
|
snap: true,
|
|
})
|
|
|
|
expect(session.sceneSource.getLevelView()?.areaId).toBe(DEN_OF_EVIL)
|
|
expect(session.sceneSource.buildFrameSnapshot().levelId).toBe(DEN_OF_EVIL)
|
|
expect(session.world.areaId).toBe(DEN_OF_EVIL)
|
|
expect(session.automapView.getCurrentAreaId()).toBe(DEN_OF_EVIL)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelId).toBe(DEN_OF_EVIL)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelName).toBe(
|
|
levelDisplayName(DEN_OF_EVIL, 'ENG'),
|
|
)
|
|
|
|
// 3. Player returns to Rogue Encampment (Area 1) via Town Portal / walk back
|
|
const townView = mapService.getLevelView(ROGUE_ENCAMPMENT, 'all')
|
|
const townSubX = townView.subOriginX + townView.spawnX * 5 + 2
|
|
const townSubY = townView.subOriginY + townView.spawnY * 5 + 2
|
|
session.handleServerEvent({
|
|
type: 'UnitReassign',
|
|
unitType: 'player',
|
|
id: session.world.selfId,
|
|
at: { x: townSubX, y: townSubY },
|
|
snap: true,
|
|
})
|
|
|
|
expect(session.sceneSource.getLevelView()?.areaId).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.sceneSource.buildFrameSnapshot().levelId).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.world.areaId).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.automapView.getCurrentAreaId()).toBe(ROGUE_ENCAMPMENT)
|
|
expect(session.automapView.buildMinimapLevel('ENG').levelId).toBe(ROGUE_ENCAMPMENT)
|
|
})
|
|
|
|
it('renderSceneSourceFrame + Minimap switches level geometry, HUD title, and resets manual pan when moving across areas', () => {
|
|
const text = readFileSync(resolve(FIXTURE_DIR, '04-d2gs-idle-60s.d2cap'), 'utf8')
|
|
const settingsStore = createSettingsStore()
|
|
settingsStore.set({ automapReveal: 'level' })
|
|
const session = new OnlineSession({ settingsStore, mapService })
|
|
session.loadD2CapReplay(text)
|
|
session.replayAllD2Cap()
|
|
|
|
const minimap = new Minimap()
|
|
minimap.mode = 'full'
|
|
const renderer = createNoopSpriteRenderer()
|
|
const tileGpuCache = createFakeTileGpuCache()
|
|
|
|
// Frame 1: in Rogue Encampment (Area 1)
|
|
renderSceneSourceFrame({
|
|
source: session.sceneSource,
|
|
lightingPreset: 'fullbright',
|
|
renderer: renderer as never,
|
|
tileGpuCache: tileGpuCache as never,
|
|
minimap,
|
|
automapView: session.automapView,
|
|
})
|
|
expect(minimap.hudTextLastFrame?.levelName).toBe(levelDisplayName(ROGUE_ENCAMPMENT))
|
|
|
|
// User manually pans the full automap in Area 1
|
|
minimap.pan(32, -24)
|
|
expect(minimap.panX).toBe(32)
|
|
expect(minimap.panY).toBe(-24)
|
|
|
|
// Local predictor moves player into Blood Moor (Area 2) via session.tick()
|
|
const bloodMoorView = mapService.getLevelView(BLOOD_MOOR, 'all')
|
|
const bmSubX = bloodMoorView.subOriginX + bloodMoorView.spawnX * 5 + 2
|
|
const bmSubY = bloodMoorView.subOriginY + bloodMoorView.spawnY * 5 + 2
|
|
session.predictor.setPosition(bmSubX, bmSubY)
|
|
session.tick(40, 2000)
|
|
|
|
// Frame 2: in Blood Moor (Area 2)
|
|
const snap2 = renderSceneSourceFrame({
|
|
source: session.sceneSource,
|
|
lightingPreset: 'fullbright',
|
|
renderer: renderer as never,
|
|
tileGpuCache: tileGpuCache as never,
|
|
minimap,
|
|
automapView: session.automapView,
|
|
})
|
|
expect(snap2.levelId).toBe(BLOOD_MOOR)
|
|
expect(session.automapView.getCurrentAreaId()).toBe(BLOOD_MOOR)
|
|
expect(minimap.hudTextLastFrame?.levelName).toBe(levelDisplayName(BLOOD_MOOR))
|
|
// Manual pan from Area 1 is automatically reset to (0, 0) upon switching to Area 2
|
|
expect(minimap.panX).toBe(0)
|
|
expect(minimap.panY).toBe(0)
|
|
expect(minimap.revealedRoomsCount).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('CommandMapper resolves local pathfinding against the level containing the player even when 0x07 revealed a neighbouring level', () => {
|
|
const text = readFileSync(resolve(FIXTURE_DIR, '04-d2gs-idle-60s.d2cap'), 'utf8')
|
|
const settingsStore = createSettingsStore()
|
|
settingsStore.set({ automapReveal: 'level' })
|
|
const session = new OnlineSession({ settingsStore, mapService })
|
|
session.loadD2CapReplay(text)
|
|
session.replayAllD2Cap()
|
|
|
|
// Precondition: MapService.getActiveLevelView() was switched to Area 2 by 0x07 RoomReveal,
|
|
// while player is standing in Area 1 (Rogue Encampment).
|
|
expect(mapService.getActiveLevelView()?.areaId).toBe(BLOOD_MOOR)
|
|
expect(session.world.areaId).toBe(ROGUE_ENCAMPMENT)
|
|
|
|
const playerX = Math.round(session.predictor.getSnapshot().logicalX)
|
|
const playerY = Math.round(session.predictor.getSnapshot().logicalY)
|
|
const targetScreen = session.camera.subtileToScreen(playerX + 3, playerY)
|
|
session.commandMapper.handlePointerDown(targetScreen.x, targetScreen.y, 0, 5000)
|
|
session.commandMapper.handlePointerUp(0)
|
|
|
|
const predAfterClick = session.predictor.getSnapshot()
|
|
expect(predAfterClick.moving).toBe(true)
|
|
})
|
|
})
|