Merge branch 'fix/issue-549-minimap-level-switch' into main (Fixes #549)
This commit is contained in:
commit
47d18fce72
|
|
@ -288,6 +288,25 @@ export class AutomapView {
|
|||
return this.levelViews.get(areaId)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the active level (`currentAreaId` / `currentActId`) within the current game session
|
||||
* without clearing `levelViews` or `serverRevealedRooms` (#549).
|
||||
*/
|
||||
syncActiveLevel(areaId: number, existingLevelView?: LevelView): LevelView {
|
||||
if (areaId > 0) {
|
||||
this.currentAreaId = areaId
|
||||
this.currentActId = getActForArea(areaId)
|
||||
}
|
||||
const targetAreaId = this.currentAreaId
|
||||
if (existingLevelView && existingLevelView.areaId === targetAreaId) {
|
||||
this.levelViews.set(targetAreaId, existingLevelView)
|
||||
} else if (!this.levelViews.has(targetAreaId)) {
|
||||
this.ensureLevelView(targetAreaId)
|
||||
}
|
||||
this.applyRevealModeToViews()
|
||||
return this.levelViews.get(targetAreaId)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a server `0x07 AddMapRoom` event.
|
||||
*/
|
||||
|
|
@ -498,13 +517,38 @@ export class AutomapView {
|
|||
/**
|
||||
* Build a `MinimapLevel` descriptor suitable for `Minimap.draw(...)` (`src/ui/minimap.ts`).
|
||||
*/
|
||||
buildMinimapLevel(lang: TblLang = getTblLang()): MinimapLevel {
|
||||
buildMinimapLevel(lang: TblLang = getTblLang(), areaId?: number): MinimapLevel {
|
||||
if (areaId !== undefined && areaId > 0 && areaId !== this.currentAreaId) {
|
||||
this.syncActiveLevel(areaId)
|
||||
}
|
||||
const snap = this.buildSnapshot([], lang)
|
||||
const view = this.ensureLevelView(this.currentAreaId)
|
||||
const allRooms = view.rooms
|
||||
|
||||
let minCellX = 0
|
||||
let minCellY = 0
|
||||
let cellsX = 40
|
||||
let cellsY = 40
|
||||
let subW = 200
|
||||
let blocked: Uint8Array
|
||||
let originX = 0
|
||||
let originY = 0
|
||||
|
||||
if (
|
||||
view.cellsX > 0 &&
|
||||
view.cellsY > 0 &&
|
||||
view.blocked &&
|
||||
view.blocked.length === view.cellsX * 5 * view.cellsY * 5
|
||||
) {
|
||||
minCellX = view.tileOriginX ?? Math.floor((view.subOriginX ?? 0) / 5)
|
||||
minCellY = view.tileOriginY ?? Math.floor((view.subOriginY ?? 0) / 5)
|
||||
cellsX = view.cellsX
|
||||
cellsY = view.cellsY
|
||||
subW = view.gridWidth || cellsX * 5
|
||||
blocked = view.blocked
|
||||
originX = view.isoOriginX ?? (minCellX - minCellY) * 80
|
||||
originY = view.isoOriginY ?? (minCellX + minCellY) * 40
|
||||
} else {
|
||||
let maxCellX = 40
|
||||
let maxCellY = 40
|
||||
if (allRooms.length > 0) {
|
||||
|
|
@ -513,13 +557,11 @@ export class AutomapView {
|
|||
maxCellX = Math.max(...allRooms.map(r => r.tileX + r.tileW))
|
||||
maxCellY = Math.max(...allRooms.map(r => r.tileY + r.tileH))
|
||||
}
|
||||
|
||||
const cellsX = Math.max(1, maxCellX - minCellX)
|
||||
const cellsY = Math.max(1, maxCellY - minCellY)
|
||||
const subW = cellsX * 5
|
||||
cellsX = Math.max(1, maxCellX - minCellX)
|
||||
cellsY = Math.max(1, maxCellY - minCellY)
|
||||
subW = cellsX * 5
|
||||
const subH = cellsY * 5
|
||||
const blocked = new Uint8Array(subW * subH)
|
||||
|
||||
blocked = new Uint8Array(subW * subH)
|
||||
for (let sy = 0; sy < subH; sy++) {
|
||||
for (let sx = 0; sx < subW; sx++) {
|
||||
const absSubX = minCellX * 5 + sx
|
||||
|
|
@ -529,6 +571,9 @@ export class AutomapView {
|
|||
}
|
||||
}
|
||||
}
|
||||
originX = (minCellX - minCellY) * 80
|
||||
originY = (minCellX + minCellY) * 40
|
||||
}
|
||||
|
||||
const markers: MinimapMarker[] = snap.warpMarkers.map(wm => ({
|
||||
cellX: wm.cellX - minCellX,
|
||||
|
|
@ -538,6 +583,20 @@ export class AutomapView {
|
|||
targetName: wm.label,
|
||||
}))
|
||||
|
||||
if (Array.isArray(view.entrances)) {
|
||||
for (const ent of view.entrances) {
|
||||
if (ent.toLevelId > 0 && ent.toLevelId !== this.currentAreaId) {
|
||||
markers.push({
|
||||
cellX: ent.x,
|
||||
cellY: ent.y,
|
||||
kind: 'exit',
|
||||
toLevelId: ent.toLevelId,
|
||||
targetName: levelDisplayName(ent.toLevelId, lang),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: `automap_${this.mapSeed}_${this.difficulty}_${this.currentAreaId}_${this.mode}`,
|
||||
cellsX,
|
||||
|
|
@ -545,8 +604,8 @@ export class AutomapView {
|
|||
blocked,
|
||||
gridWidth: subW,
|
||||
markers,
|
||||
originX: (minCellX - minCellY) * 80,
|
||||
originY: (minCellX + minCellY) * 40,
|
||||
originX,
|
||||
originY,
|
||||
levelId: this.currentAreaId,
|
||||
levelName: levelDisplayName(this.currentAreaId, lang),
|
||||
...(view.automap !== undefined ? { automap: view.automap } : {}),
|
||||
|
|
|
|||
|
|
@ -897,13 +897,22 @@ export class CommandMapper {
|
|||
return this.lastPath
|
||||
}
|
||||
|
||||
const levelView =
|
||||
this.mapService?.getActiveLevelView() ?? this.automapView?.getLevelView() ?? null
|
||||
|
||||
if (levelView && levelView.walkable.length > 0) {
|
||||
const snap = this.predictor.getSnapshot()
|
||||
const sx = Math.round(snap.logicalX)
|
||||
const sy = Math.round(snap.logicalY)
|
||||
const levelView =
|
||||
(this.mapService && this.world.areaId > 0 && (sx !== 0 || sy !== 0)
|
||||
? this.mapService.findLevelViewAtWorldSubTile(sx, sy, [
|
||||
this.world.areaId,
|
||||
...this.world.getRevealedAreaIds(),
|
||||
])
|
||||
: null) ??
|
||||
(this.world.areaId > 0 ? this.automapView?.getLevelView(this.world.areaId) : undefined) ??
|
||||
this.mapService?.getActiveLevelView() ??
|
||||
this.automapView?.getLevelView() ??
|
||||
null
|
||||
|
||||
if (levelView && levelView.walkable.length > 0) {
|
||||
const ox = levelView.subOriginX ?? 0
|
||||
const oy = levelView.subOriginY ?? 0
|
||||
const inBounds =
|
||||
|
|
|
|||
|
|
@ -927,11 +927,19 @@ export function renderSceneSourceFrame(ctx: SceneSourceRenderContext): SceneFram
|
|||
ctx.minimap.visible = false
|
||||
} else {
|
||||
ctx.minimap.visible = true
|
||||
const baseMinimapLevel = ctx.automapView.buildMinimapLevel()
|
||||
if (levelView && levelView.areaId > 0) {
|
||||
ctx.automapView.syncActiveLevel(levelView.areaId, levelView)
|
||||
} else if (snapshot.levelId > 0 && ctx.automapView.getCurrentAreaId() !== snapshot.levelId) {
|
||||
ctx.automapView.syncActiveLevel(snapshot.levelId)
|
||||
}
|
||||
const baseMinimapLevel = ctx.automapView.buildMinimapLevel(
|
||||
undefined,
|
||||
levelView?.areaId ?? snapshot.levelId,
|
||||
)
|
||||
const effectiveMinimapLevel: MinimapLevel = {
|
||||
...baseMinimapLevel,
|
||||
originX: levelView?.isoOriginX ?? 0,
|
||||
originY: levelView?.isoOriginY ?? 0,
|
||||
originX: levelView?.isoOriginX ?? baseMinimapLevel.originX ?? 0,
|
||||
originY: levelView?.isoOriginY ?? baseMinimapLevel.originY ?? 0,
|
||||
preRevealTown: isTownLevel(snapshot.levelId),
|
||||
}
|
||||
const localSub = snapshot.playerLocalSubTile
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ export class OnlineSession {
|
|||
private disconnectReasonValue: string | null = null
|
||||
private pingMsValue = 0
|
||||
private tickCounter = 0
|
||||
private readonly knownAreaIds = new Set<number>()
|
||||
|
||||
private readonly outboundCommandsLog: ClientCommand[] = []
|
||||
private readonly stateListeners = new Set<(snapshot: OnlineSessionLobbySnapshot) => void>()
|
||||
|
|
@ -373,17 +374,11 @@ export class OnlineSession {
|
|||
// such as Blood Moor as a side effect of 0x07). The rendered level is the one whose DRLG
|
||||
// canvas contains the player's world sub-tile (1.13c: unit → pRoom → pLevel); until such a
|
||||
// level exists the scene reports not-ready.
|
||||
// #549: resolving the active levelView also keeps `world.areaId` and `automapView` in lockstep.
|
||||
const predSnap = this.predictor.getSnapshot()
|
||||
const playerSubX = predSnap.x !== 0 || predSnap.y !== 0 ? predSnap.x : this.world.self.x
|
||||
const playerSubY = predSnap.x !== 0 || predSnap.y !== 0 ? predSnap.y : this.world.self.y
|
||||
const levelView =
|
||||
this.levelView ??
|
||||
(this.mapService && this.world.areaId > 0 && (playerSubX !== 0 || playerSubY !== 0)
|
||||
? this.mapService.findLevelViewAtWorldSubTile(playerSubX, playerSubY, [
|
||||
this.world.areaId,
|
||||
...this.world.getRevealedAreaIds(),
|
||||
])
|
||||
: null)
|
||||
const levelView = this.syncActiveAreaFromPlayerPosition({ emitHudSync: true })
|
||||
const levelId = levelView?.areaId ?? (this.world.areaId > 0 ? this.world.areaId : 0)
|
||||
return {
|
||||
tick: this.tickCounter,
|
||||
|
|
@ -694,6 +689,7 @@ export class OnlineSession {
|
|||
const savedName = this.world.self.name
|
||||
const savedClass = this.world.self.charClass
|
||||
this.world.reset()
|
||||
this.knownAreaIds.clear()
|
||||
this.world.self.name = savedName
|
||||
this.world.self.charClass = savedClass
|
||||
this.predictor.setPosition(0, 0)
|
||||
|
|
@ -921,6 +917,66 @@ export class OnlineSession {
|
|||
}
|
||||
}
|
||||
|
||||
private getCandidateAreaIds(): number[] {
|
||||
const out: number[] = []
|
||||
if (this.world.areaId > 0) {
|
||||
out.push(this.world.areaId)
|
||||
}
|
||||
for (const id of this.world.getRevealedAreaIds()) {
|
||||
if (id > 0 && !out.includes(id)) {
|
||||
out.push(id)
|
||||
}
|
||||
}
|
||||
for (const id of this.knownAreaIds) {
|
||||
if (id > 0 && !out.includes(id)) {
|
||||
out.push(id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the `LevelView` containing the local player's current world sub-tile and synchronize
|
||||
* `world.areaId` and `automapView` whenever the player crosses into a different area (#549).
|
||||
*/
|
||||
private syncActiveAreaFromPlayerPosition(options?: {
|
||||
readonly emitHudSync?: boolean
|
||||
}): LevelView | null {
|
||||
const predSnap = this.predictor.getSnapshot()
|
||||
const playerSubX = predSnap.x !== 0 || predSnap.y !== 0 ? predSnap.x : this.world.self.x
|
||||
const playerSubY = predSnap.x !== 0 || predSnap.y !== 0 ? predSnap.y : this.world.self.y
|
||||
const candidateIds = this.getCandidateAreaIds()
|
||||
const levelView =
|
||||
this.levelView ??
|
||||
(this.mapService && candidateIds.length > 0 && (playerSubX !== 0 || playerSubY !== 0)
|
||||
? this.mapService.findLevelViewAtWorldSubTile(playerSubX, playerSubY, candidateIds)
|
||||
: null)
|
||||
|
||||
if (levelView && levelView.areaId > 0) {
|
||||
if (this.world.areaId > 0) {
|
||||
this.knownAreaIds.add(this.world.areaId)
|
||||
}
|
||||
this.knownAreaIds.add(levelView.areaId)
|
||||
const areaChanged = this.world.areaId !== levelView.areaId
|
||||
const automapOutOfSync =
|
||||
this.automapView.getCurrentAreaId() !== levelView.areaId ||
|
||||
this.automapView.getLevelView(levelView.areaId) === undefined
|
||||
if (areaChanged) {
|
||||
this.world.areaId = levelView.areaId
|
||||
}
|
||||
if (areaChanged || automapOutOfSync) {
|
||||
this.automapView.syncActiveLevel(levelView.areaId, levelView)
|
||||
}
|
||||
if (areaChanged) {
|
||||
void this.soundService.onEnterArea(levelView.areaId)
|
||||
if (options?.emitHudSync) {
|
||||
this.hudModel.syncFromWorld()
|
||||
}
|
||||
}
|
||||
}
|
||||
return levelView
|
||||
}
|
||||
|
||||
/**
|
||||
* `DUNGEON_IsRoomInTown` for the local player: whether the level containing its sub-tile is a
|
||||
* town level, or `null` while no loaded level contains it. The sub-tile is the one the gate
|
||||
|
|
@ -932,7 +988,7 @@ export class OnlineSession {
|
|||
const levelView = this.mapService.findLevelViewAtWorldSubTile(
|
||||
Math.round(snap.logicalX),
|
||||
Math.round(snap.logicalY),
|
||||
[this.world.areaId, ...this.world.getRevealedAreaIds()],
|
||||
this.getCandidateAreaIds(),
|
||||
)
|
||||
return levelView === null ? null : isTownLevel(levelView.areaId)
|
||||
}
|
||||
|
|
@ -1070,7 +1126,11 @@ export class OnlineSession {
|
|||
break
|
||||
}
|
||||
}
|
||||
const activeView = this.mapService?.getActiveLevelView() ?? undefined
|
||||
this.knownAreaIds.clear()
|
||||
this.knownAreaIds.add(ev.areaId)
|
||||
const activeView =
|
||||
this.mapService?.getActiveLevelView() ??
|
||||
(this.levelView && this.levelView.areaId === ev.areaId ? this.levelView : undefined)
|
||||
this.automapView.setContext(
|
||||
ev.mapSeed,
|
||||
this.world.difficulty,
|
||||
|
|
@ -1082,6 +1142,9 @@ export class OnlineSession {
|
|||
}
|
||||
|
||||
case 'RoomReveal': {
|
||||
if (ev.areaId > 0) {
|
||||
this.knownAreaIds.add(ev.areaId)
|
||||
}
|
||||
if (this.mapService) {
|
||||
try {
|
||||
this.mapService.onAddRoomDataPacket({
|
||||
|
|
@ -1271,6 +1334,9 @@ export class OnlineSession {
|
|||
}
|
||||
}
|
||||
|
||||
// Synchronize active area and AutomapView from current player position (#549)
|
||||
this.syncActiveAreaFromPlayerPosition({ emitHudSync: false })
|
||||
|
||||
// Synchronize HudManager via HudModel
|
||||
this.hudModel.syncFromWorld()
|
||||
|
||||
|
|
@ -1305,6 +1371,8 @@ export class OnlineSession {
|
|||
unit.direction = state.dir64
|
||||
}
|
||||
}
|
||||
|
||||
this.syncActiveAreaFromPlayerPosition({ emitHudSync: true })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1359,10 +1427,7 @@ export class OnlineSession {
|
|||
const activeLevelView =
|
||||
this.levelView ??
|
||||
(this.mapService && this.world.areaId > 0 && (selfX !== 0 || selfY !== 0)
|
||||
? this.mapService.findLevelViewAtWorldSubTile(selfX, selfY, [
|
||||
this.world.areaId,
|
||||
...this.world.getRevealedAreaIds(),
|
||||
])
|
||||
? this.mapService.findLevelViewAtWorldSubTile(selfX, selfY, this.getCandidateAreaIds())
|
||||
: null) ??
|
||||
this.mapService?.getActiveLevelView() ??
|
||||
this.automapView.getLevelView()
|
||||
|
|
|
|||
|
|
@ -498,6 +498,9 @@ export class Minimap {
|
|||
/** level key -> last player sub-unit coordinates where `AUTOMAP_UpdateVisibility` triggered. */
|
||||
private readonly lastRevealSubPos = new Map<string, { x: number; y: number }>()
|
||||
|
||||
/** Last drawn level key so manual pan resets when switching levels (#549). */
|
||||
private lastLevelKey: string | null = null
|
||||
|
||||
/** Diagnostic telemetry from the most recent `draw()` invocation. */
|
||||
readonly lastRenderStats: AutomapRenderTelemetry = {
|
||||
mode: 'minimap',
|
||||
|
|
@ -869,6 +872,11 @@ export class Minimap {
|
|||
viewport: { readonly width: number; readonly height: number },
|
||||
options?: MinimapDrawOptions,
|
||||
): void {
|
||||
if (this.lastLevelKey !== null && this.lastLevelKey !== level.key) {
|
||||
this.resetPan()
|
||||
}
|
||||
this.lastLevelKey = level.key
|
||||
|
||||
const screenW = Math.max(1, Math.trunc(viewport.width))
|
||||
const screenH = Math.max(1, Math.trunc(viewport.height))
|
||||
const vp = computeAutomapViewport(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue