Fix automap exploration, viewport geometry, markers, and fail-fast loading (Closes #624, #625, #626, #627, #628, #629, #630, #631, #632, #635)
This commit is contained in:
parent
f1ca3dfc2b
commit
036d36bccb
|
|
@ -1,15 +1,20 @@
|
||||||
|
import { DRLG_GetActNoFromLevelId } from '../../common/drlg/drlg-drlg.ts'
|
||||||
|
import type {
|
||||||
|
PackedDrlgBundleManifest,
|
||||||
|
PackedTileCatalogIndex,
|
||||||
|
} from '../../common/pack-contract/index.ts'
|
||||||
|
import { getMonsterKindByClassId } from '../../common/units/monsters-data.ts'
|
||||||
import { levelDisplayName } from '../../common/world/level-names-zh.ts'
|
import { levelDisplayName } from '../../common/world/level-names-zh.ts'
|
||||||
import { getTblLang, type TblLang } from '../i18n/lang.ts'
|
import { getTblLang, lookupTbl, type TblLang } from '../i18n/lang.ts'
|
||||||
import type {
|
import type {
|
||||||
MinimapLevel,
|
MinimapLevel,
|
||||||
MinimapMarker,
|
MinimapMarker,
|
||||||
} from '../ui/minimap.ts'
|
} from '../ui/minimap.ts'
|
||||||
import {
|
import {
|
||||||
isWalkableWorldSubTile,
|
|
||||||
type LevelRoomView,
|
type LevelRoomView,
|
||||||
type LevelView,
|
type LevelView,
|
||||||
} from '../map/level-view.ts'
|
} from '../map/level-view.ts'
|
||||||
import type { MapService } from '../map/map-service.ts'
|
import { MapService } from '../map/map-service.ts'
|
||||||
import type { AutomapRevealMode } from '../settings/client-settings.ts'
|
import type { AutomapRevealMode } from '../settings/client-settings.ts'
|
||||||
import type { RenderableUnitInput } from '../view/unit-drawables.ts'
|
import type { RenderableUnitInput } from '../view/unit-drawables.ts'
|
||||||
|
|
||||||
|
|
@ -34,12 +39,41 @@ export interface MinimapStashIcon {
|
||||||
export type ActId = 1 | 2 | 3 | 4 | 5
|
export type ActId = 1 | 2 | 3 | 4 | 5
|
||||||
export type DifficultyId = 0 | 1 | 2
|
export type DifficultyId = 0 | 1 | 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the 1-based Act ID (1..5) for a given level areaId using canonical DRLG table logic
|
||||||
|
* (D2Common.0x6FD75300 #10001 DRLG_GetActNoFromLevelId).
|
||||||
|
*/
|
||||||
export function getActForArea(areaId: number): ActId {
|
export function getActForArea(areaId: number): ActId {
|
||||||
if (areaId <= 39) return 1
|
const act0 = DRLG_GetActNoFromLevelId(areaId)
|
||||||
if (areaId <= 74) return 2
|
return (act0 + 1) as ActId
|
||||||
if (areaId <= 102) return 3
|
}
|
||||||
if (areaId <= 108) return 4
|
|
||||||
return 5
|
/**
|
||||||
|
* Localized difficulty string ('Normal' | 'Nightmare' | 'Hell' / '普通' | '惡夢' | '地獄')
|
||||||
|
* wired to .tbl string keys (strNormal, strNightmare, strHell) per Issue #628.
|
||||||
|
*/
|
||||||
|
export function getLocalizedDifficulty(
|
||||||
|
difficulty: DifficultyId,
|
||||||
|
lang: TblLang = getTblLang(),
|
||||||
|
): string {
|
||||||
|
const isZh = lang === 'CHI'
|
||||||
|
if (difficulty === 1) {
|
||||||
|
return lookupTbl('strNightmare', lang) ?? (isZh ? '惡夢' : 'Nightmare')
|
||||||
|
}
|
||||||
|
if (difficulty === 2) {
|
||||||
|
return lookupTbl('strHell', lang) ?? (isZh ? '地獄' : 'Hell')
|
||||||
|
}
|
||||||
|
return lookupTbl('strNormal', lang) ?? (isZh ? '普通' : 'Normal')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical preset town levels with AutoMap=1 in LvlPrest.txt (DrlgPreset.cpp:1794-1823):
|
||||||
|
* 1 (Rogue Encampment), 40 (Lut Gholein), 75 (Kurast Docks), 103 (Pandemonium Fortress), 109 (Harrogath).
|
||||||
|
*/
|
||||||
|
export const TOWN_LEVEL_IDS: ReadonlySet<number> = new Set([1, 40, 75, 103, 109])
|
||||||
|
|
||||||
|
export function isTownLevel(levelId: number): boolean {
|
||||||
|
return TOWN_LEVEL_IDS.has(levelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,106 +128,66 @@ function makeRoomKey(areaId: number, roomX: number, roomY: number): string {
|
||||||
return `${areaId}:${roomX},${roomY}`
|
return `${areaId}:${roomX},${roomY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFallbackLevelView(
|
interface NodeFsLike {
|
||||||
seed: number,
|
readFileSync(path: string, encoding: string): string
|
||||||
difficulty: DifficultyId,
|
readFileSync(path: string): { buffer: ArrayBuffer; byteOffset: number; byteLength: number }
|
||||||
areaId: number,
|
existsSync(path: string): boolean
|
||||||
): LevelView {
|
|
||||||
const actIndex = getActForArea(areaId) - 1
|
|
||||||
const baseTileX = (areaId * 32) + ((seed >>> 0) % 16)
|
|
||||||
const baseTileY = (areaId * 24) + (((seed >>> 4) >>> 0) % 16)
|
|
||||||
const roomGrid = 3
|
|
||||||
const roomTileW = 8
|
|
||||||
const roomTileH = 8
|
|
||||||
const rooms: LevelRoomView[] = []
|
|
||||||
let idx = 0
|
|
||||||
for (let ry = 0; ry < roomGrid; ry++) {
|
|
||||||
for (let rx = 0; rx < roomGrid; rx++) {
|
|
||||||
const localTileX = rx * roomTileW
|
|
||||||
const localTileY = ry * roomTileH
|
|
||||||
const tileX = baseTileX + localTileX
|
|
||||||
const tileY = baseTileY + localTileY
|
|
||||||
rooms.push({
|
|
||||||
index: idx++,
|
|
||||||
areaId,
|
|
||||||
tileX,
|
|
||||||
tileY,
|
|
||||||
tileW: roomTileW,
|
|
||||||
tileH: roomTileH,
|
|
||||||
localTileX,
|
|
||||||
localTileY,
|
|
||||||
subX: localTileX * 5,
|
|
||||||
subY: localTileY * 5,
|
|
||||||
subW: roomTileW * 5,
|
|
||||||
subH: roomTileH * 5,
|
|
||||||
worldSubX: tileX * 5,
|
|
||||||
worldSubY: tileY * 5,
|
|
||||||
revealed: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tileWidth = roomGrid * roomTileW
|
interface NodePathLike {
|
||||||
const tileHeight = roomGrid * roomTileH
|
resolve(...args: string[]): string
|
||||||
const cellsX = tileWidth + 1
|
join(...args: string[]): string
|
||||||
const cellsY = tileHeight + 1
|
}
|
||||||
const subWidth = cellsX * 5
|
|
||||||
const subHeight = cellsY * 5
|
|
||||||
const totalSub = subWidth * subHeight
|
|
||||||
const collision = new Uint16Array(totalSub)
|
|
||||||
const blocked = new Uint8Array(totalSub)
|
|
||||||
const walkable = new Uint8Array(totalSub)
|
|
||||||
walkable.fill(1)
|
|
||||||
|
|
||||||
const originX = baseTileX * 5
|
/**
|
||||||
const originY = baseTileY * 5
|
* Synchronously loads the packed tile bundle in Node/test environments when MapService
|
||||||
|
* was not injected via AutomapViewOptions.
|
||||||
return {
|
*/
|
||||||
id: areaId,
|
function tryLoadPackedBundleNodeSync(): MapService | null {
|
||||||
areaId,
|
const proc = (
|
||||||
levelId: areaId,
|
globalThis as unknown as {
|
||||||
act: actIndex,
|
process?: { versions?: { node?: string }; cwd?: () => string }
|
||||||
seed: seed >>> 0,
|
}
|
||||||
difficulty,
|
).process
|
||||||
originX,
|
if (!proc || !proc.versions?.node) {
|
||||||
originY,
|
return null
|
||||||
subOriginX: originX,
|
}
|
||||||
subOriginY: originY,
|
try {
|
||||||
width: subWidth,
|
const req =
|
||||||
height: subHeight,
|
typeof (globalThis as unknown as { require?: unknown }).require === 'function'
|
||||||
subWidth,
|
? (globalThis as unknown as { require: (id: string) => unknown }).require
|
||||||
subHeight,
|
: null
|
||||||
tileOriginX: baseTileX,
|
if (!req) return null
|
||||||
tileOriginY: baseTileY,
|
const fs = req('node:fs') as NodeFsLike | undefined
|
||||||
tileWidth,
|
const path = req('node:path') as NodePathLike | undefined
|
||||||
tileHeight,
|
if (!fs || !path) return null
|
||||||
cellsX,
|
const cwd = typeof proc.cwd === 'function' ? proc.cwd() : '.'
|
||||||
cellsY,
|
const candidates = [
|
||||||
gridWidth: subWidth,
|
path.resolve(cwd, 'public/act-packs/tiles'),
|
||||||
gridHeight: subHeight,
|
path.resolve(cwd, 'public/assets/act-packs/tiles'),
|
||||||
spawnX: Math.floor(tileWidth / 2),
|
path.resolve('/usr/local/google/home/taodao/diablo2-web/public/act-packs/tiles'),
|
||||||
spawnY: Math.floor(tileHeight / 2),
|
]
|
||||||
isoOriginX: cellsY * 80,
|
let resolvedDir: string | null = null
|
||||||
isoOriginY: 0,
|
for (const dir of candidates) {
|
||||||
widthPx: (cellsX + cellsY) * 80,
|
if (fs.existsSync(path.join(dir, 'drlg-bundle.json'))) {
|
||||||
heightPx: (cellsX + cellsY) * 40,
|
resolvedDir = dir
|
||||||
collision,
|
break
|
||||||
collisionMasks: collision,
|
}
|
||||||
blocked,
|
}
|
||||||
walkable,
|
if (!resolvedDir) return null
|
||||||
walkableCellCount: totalSub,
|
const indexJson = JSON.parse(
|
||||||
dt1Names: [],
|
fs.readFileSync(path.join(resolvedDir, 'index.json'), 'utf8'),
|
||||||
floors: [],
|
) as PackedTileCatalogIndex
|
||||||
shadows: [],
|
const manifest = JSON.parse(
|
||||||
walls: [],
|
fs.readFileSync(path.join(resolvedDir, 'drlg-bundle.json'), 'utf8'),
|
||||||
roofs: [],
|
) as PackedDrlgBundleManifest
|
||||||
frames: [],
|
const buf = fs.readFileSync(path.join(resolvedDir, 'drlg-bundle.bin'))
|
||||||
ds1Objects: [],
|
const bundleBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||||
presetUnits: [],
|
return MapService.fromPackedBundle(manifest, bundleBytes, indexJson)
|
||||||
entrances: [],
|
} catch (err) {
|
||||||
warps: [],
|
throw new Error(
|
||||||
landmarks: [],
|
`[AutomapView] Failed to synchronously load packed tile bundle from public/act-packs/tiles: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
rooms,
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,7 +200,7 @@ function buildFallbackLevelView(
|
||||||
* - `'act'`: All DRLG levels and rooms in the active Act are generated and revealed with cross-level warp markers.
|
* - `'act'`: All DRLG levels and rooms in the active Act are generated and revealed with cross-level warp markers.
|
||||||
*/
|
*/
|
||||||
export class AutomapView {
|
export class AutomapView {
|
||||||
private readonly mapService: MapService | null
|
private mapService: MapService | null
|
||||||
private readonly actRevealLevelIds: Readonly<Record<ActId, readonly number[]>>
|
private readonly actRevealLevelIds: Readonly<Record<ActId, readonly number[]>>
|
||||||
private mode: AutomapRevealMode
|
private mode: AutomapRevealMode
|
||||||
private mapSeed = 0
|
private mapSeed = 0
|
||||||
|
|
@ -223,6 +217,9 @@ export class AutomapView {
|
||||||
this.mapService = options?.mapService ?? null
|
this.mapService = options?.mapService ?? null
|
||||||
this.mode = options?.initialMode ?? 'off'
|
this.mode = options?.initialMode ?? 'off'
|
||||||
this.actRevealLevelIds = options?.actRevealLevelIds ?? ACT_PRIMARY_LEVEL_IDS
|
this.actRevealLevelIds = options?.actRevealLevelIds ?? ACT_PRIMARY_LEVEL_IDS
|
||||||
|
if (!this.mapService) {
|
||||||
|
this.mapService = tryLoadPackedBundleNodeSync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMode(): AutomapRevealMode {
|
getMode(): AutomapRevealMode {
|
||||||
|
|
@ -260,8 +257,10 @@ export class AutomapView {
|
||||||
if (seedChanged) {
|
if (seedChanged) {
|
||||||
this.levelViews.clear()
|
this.levelViews.clear()
|
||||||
this.serverRevealedRooms.clear()
|
this.serverRevealedRooms.clear()
|
||||||
|
if (!this.mapService) {
|
||||||
|
this.mapService = tryLoadPackedBundleNodeSync()
|
||||||
|
}
|
||||||
if (this.mapService) {
|
if (this.mapService) {
|
||||||
try {
|
|
||||||
const loaded = this.mapService.loadAct({
|
const loaded = this.mapService.loadAct({
|
||||||
act: this.currentActId - 1,
|
act: this.currentActId - 1,
|
||||||
seed: this.mapSeed,
|
seed: this.mapSeed,
|
||||||
|
|
@ -272,9 +271,6 @@ export class AutomapView {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
this.levelViews.set(areaId, loaded)
|
this.levelViews.set(areaId, loaded)
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Fallback to deterministic synthetic LevelView if MapService has no worker/engine
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,15 +313,11 @@ export class AutomapView {
|
||||||
roomX,
|
roomX,
|
||||||
roomY,
|
roomY,
|
||||||
})
|
})
|
||||||
if (this.mapService) {
|
if (this.mapService && typeof this.mapService.onAddRoomDataPacket === 'function') {
|
||||||
try {
|
|
||||||
this.mapService.onAddRoomDataPacket(
|
this.mapService.onAddRoomDataPacket(
|
||||||
{ areaId, roomX, roomY },
|
{ areaId, roomX, roomY },
|
||||||
{ rebuildView: false },
|
{ rebuildView: false },
|
||||||
)
|
)
|
||||||
} catch {
|
|
||||||
// Ignore if MapService is not backed by an in-process engine
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,15 +326,11 @@ export class AutomapView {
|
||||||
*/
|
*/
|
||||||
onServerRemoveRoom(roomX: number, roomY: number, areaId: number = this.currentAreaId): void {
|
onServerRemoveRoom(roomX: number, roomY: number, areaId: number = this.currentAreaId): void {
|
||||||
this.serverRevealedRooms.delete(makeRoomKey(areaId, roomX, roomY))
|
this.serverRevealedRooms.delete(makeRoomKey(areaId, roomX, roomY))
|
||||||
if (this.mapService) {
|
if (this.mapService && typeof this.mapService.onRemoveRoomDataPacket === 'function') {
|
||||||
try {
|
|
||||||
this.mapService.onRemoveRoomDataPacket(
|
this.mapService.onRemoveRoomDataPacket(
|
||||||
{ areaId, roomX, roomY },
|
{ areaId, roomX, roomY },
|
||||||
{ rebuildView: false },
|
{ rebuildView: false },
|
||||||
)
|
)
|
||||||
} catch {
|
|
||||||
// Ignore if MapService is not backed by an in-process engine
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,8 +341,14 @@ export class AutomapView {
|
||||||
private ensureLevelView(areaId: number): LevelView {
|
private ensureLevelView(areaId: number): LevelView {
|
||||||
let view = this.levelViews.get(areaId)
|
let view = this.levelViews.get(areaId)
|
||||||
if (!view) {
|
if (!view) {
|
||||||
if (this.mapService) {
|
if (!this.mapService) {
|
||||||
try {
|
this.mapService = tryLoadPackedBundleNodeSync()
|
||||||
|
}
|
||||||
|
if (!this.mapService) {
|
||||||
|
throw new Error(
|
||||||
|
`[AutomapView] Failed to load LevelView for area ${areaId} (seed: ${this.mapSeed}, difficulty: ${this.difficulty}): mapService is not initialized and packed tile bundle is unavailable`,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (this.mapService.getActiveAct() !== getActForArea(areaId) - 1) {
|
if (this.mapService.getActiveAct() !== getActForArea(areaId) - 1) {
|
||||||
this.mapService.loadAct({
|
this.mapService.loadAct({
|
||||||
act: getActForArea(areaId) - 1,
|
act: getActForArea(areaId) - 1,
|
||||||
|
|
@ -365,11 +359,10 @@ export class AutomapView {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
view = this.mapService.getLevelView(areaId, 'all')
|
view = this.mapService.getLevelView(areaId, 'all')
|
||||||
} catch {
|
if (!view) {
|
||||||
view = buildFallbackLevelView(this.mapSeed, this.difficulty, areaId)
|
throw new Error(
|
||||||
}
|
`[AutomapView] MapService failed to produce LevelView for area ${areaId} (seed: ${this.mapSeed}, difficulty: ${this.difficulty})`,
|
||||||
} else {
|
)
|
||||||
view = buildFallbackLevelView(this.mapSeed, this.difficulty, areaId)
|
|
||||||
}
|
}
|
||||||
this.levelViews.set(areaId, view)
|
this.levelViews.set(areaId, view)
|
||||||
}
|
}
|
||||||
|
|
@ -396,8 +389,10 @@ export class AutomapView {
|
||||||
for (const rev of this.serverRevealedRooms.values()) {
|
for (const rev of this.serverRevealedRooms.values()) {
|
||||||
if (rev.areaId !== areaId) continue
|
if (rev.areaId !== areaId) continue
|
||||||
if (
|
if (
|
||||||
(rev.roomX >= room.tileX && rev.roomX < room.tileX + room.tileW && rev.roomY >= room.tileY && rev.roomY < room.tileY + room.tileH) ||
|
rev.roomX >= room.tileX &&
|
||||||
(rev.roomX >= room.worldSubX && rev.roomX < room.worldSubX + room.subW && rev.roomY >= room.worldSubY && rev.roomY < room.worldSubY + room.subH)
|
rev.roomX < room.tileX + room.tileW &&
|
||||||
|
rev.roomY >= room.tileY &&
|
||||||
|
rev.roomY < room.tileY + room.tileH
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -482,14 +477,33 @@ export class AutomapView {
|
||||||
name: u.name,
|
name: u.name,
|
||||||
})
|
})
|
||||||
} else if (u.kind === 'monster') {
|
} else if (u.kind === 'monster') {
|
||||||
|
// Diablo II v1.13c Parity: NEVER draw hostile monsters or bosses on the automap!
|
||||||
|
// Only player corpses and friendly interactable NPCs (interact=1, excluding ancient statues 537..539) are drawn.
|
||||||
const isDead = Boolean(u.isCorpse) || (u.lifePct !== undefined && u.lifePct <= 0)
|
const isDead = Boolean(u.isCorpse) || (u.lifePct !== undefined && u.lifePct <= 0)
|
||||||
|
if (isDead) {
|
||||||
|
if (u.isCorpse) {
|
||||||
entityDots.push({
|
entityDots.push({
|
||||||
worldX,
|
worldX,
|
||||||
worldY,
|
worldY,
|
||||||
kind: isDead ? 'corpse' : 'monster',
|
kind: 'corpse',
|
||||||
name: u.name,
|
name: u.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const cid = u.classId ?? 0
|
||||||
|
const isAncientStatue = cid >= 537 && cid <= 539
|
||||||
|
const mk = typeof u.classId === 'number' ? getMonsterKindByClassId(u.classId) : undefined
|
||||||
|
const isInteract = mk ? mk.interact : Boolean((u as { isNpc?: boolean }).isNpc)
|
||||||
|
if (isInteract && !isAncientStatue) {
|
||||||
|
entityDots.push({
|
||||||
|
worldX,
|
||||||
|
worldY,
|
||||||
|
kind: 'npc',
|
||||||
|
name: u.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverRevealedCount = 0
|
let serverRevealedCount = 0
|
||||||
|
|
@ -523,58 +537,27 @@ export class AutomapView {
|
||||||
}
|
}
|
||||||
const snap = this.buildSnapshot([], lang)
|
const snap = this.buildSnapshot([], lang)
|
||||||
const view = this.ensureLevelView(this.currentAreaId)
|
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 (
|
if (
|
||||||
view.cellsX > 0 &&
|
view.cellsX <= 0 ||
|
||||||
view.cellsY > 0 &&
|
view.cellsY <= 0 ||
|
||||||
view.blocked &&
|
!view.blocked ||
|
||||||
view.blocked.length === view.cellsX * 5 * view.cellsY * 5
|
view.blocked.length !== view.cellsX * 5 * view.cellsY * 5
|
||||||
) {
|
) {
|
||||||
minCellX = view.tileOriginX ?? Math.floor((view.subOriginX ?? 0) / 5)
|
throw new Error(
|
||||||
minCellY = view.tileOriginY ?? Math.floor((view.subOriginY ?? 0) / 5)
|
`[AutomapView] Invalid LevelView for area ${this.currentAreaId}: cellsX=${view.cellsX}, cellsY=${view.cellsY}, blockedLength=${view.blocked?.length ?? 0} (expected ${view.cellsX * 5 * view.cellsY * 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) {
|
|
||||||
minCellX = Math.min(...allRooms.map(r => r.tileX))
|
|
||||||
minCellY = Math.min(...allRooms.map(r => r.tileY))
|
|
||||||
maxCellX = Math.max(...allRooms.map(r => r.tileX + r.tileW))
|
|
||||||
maxCellY = Math.max(...allRooms.map(r => r.tileY + r.tileH))
|
|
||||||
}
|
|
||||||
cellsX = Math.max(1, maxCellX - minCellX)
|
|
||||||
cellsY = Math.max(1, maxCellY - minCellY)
|
|
||||||
subW = cellsX * 5
|
|
||||||
const subH = cellsY * 5
|
|
||||||
blocked = new Uint8Array(subW * subH)
|
|
||||||
for (let sy = 0; sy < subH; sy++) {
|
|
||||||
for (let sx = 0; sx < subW; sx++) {
|
|
||||||
const absSubX = minCellX * 5 + sx
|
|
||||||
const absSubY = minCellY * 5 + sy
|
|
||||||
if (!isWalkableWorldSubTile(view, absSubX, absSubY)) {
|
|
||||||
blocked[sy * subW + sx] = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
originX = (minCellX - minCellY) * 80
|
|
||||||
originY = (minCellX + minCellY) * 40
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minCellX = view.tileOriginX ?? Math.floor((view.subOriginX ?? 0) / 5)
|
||||||
|
const minCellY = view.tileOriginY ?? Math.floor((view.subOriginY ?? 0) / 5)
|
||||||
|
const cellsX = view.cellsX
|
||||||
|
const cellsY = view.cellsY
|
||||||
|
const subW = view.gridWidth || cellsX * 5
|
||||||
|
const blocked = view.blocked
|
||||||
|
const originX = view.isoOriginX ?? (minCellX - minCellY) * 80
|
||||||
|
const originY = view.isoOriginY ?? (minCellX + minCellY) * 40
|
||||||
|
|
||||||
const markers: MinimapMarker[] = snap.warpMarkers.map(wm => ({
|
const markers: MinimapMarker[] = snap.warpMarkers.map(wm => ({
|
||||||
cellX: wm.cellX - minCellX,
|
cellX: wm.cellX - minCellX,
|
||||||
cellY: wm.cellY - minCellY,
|
cellY: wm.cellY - minCellY,
|
||||||
|
|
@ -598,7 +581,7 @@ export class AutomapView {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: `automap_${this.mapSeed}_${this.difficulty}_${this.currentAreaId}_${this.mode}`,
|
key: `automap_${this.mapSeed}_${this.difficulty}_${this.currentAreaId}`,
|
||||||
cellsX,
|
cellsX,
|
||||||
cellsY,
|
cellsY,
|
||||||
blocked,
|
blocked,
|
||||||
|
|
@ -608,6 +591,8 @@ export class AutomapView {
|
||||||
originY,
|
originY,
|
||||||
levelId: this.currentAreaId,
|
levelId: this.currentAreaId,
|
||||||
levelName: levelDisplayName(this.currentAreaId, lang),
|
levelName: levelDisplayName(this.currentAreaId, lang),
|
||||||
|
difficulty: getLocalizedDifficulty(this.difficulty, lang),
|
||||||
|
preRevealTown: isTownLevel(this.currentAreaId),
|
||||||
...(view.automap !== undefined ? { automap: view.automap } : {}),
|
...(view.automap !== undefined ? { automap: view.automap } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,26 @@
|
||||||
* - `AUTOMAP_LoadSprites` (`0x6fb11cc0`) & `AUTOMAP_DrawCell` (`0x6fb0f240`):
|
* - `AUTOMAP_LoadSprites` (`0x6fb11cc0`) & `AUTOMAP_DrawCell` (`0x6fb0f240`):
|
||||||
* 8 DC6 sprite sheets (`MaxiMap`, `MaxiMapS`, `Act2Map`, `Act2MapS`, `Act4Map`, `Act4MapS`,
|
* 8 DC6 sprite sheets (`MaxiMap`, `MaxiMapS`, `Act2Map`, `Act2MapS`, `Act4Map`, `Act4MapS`,
|
||||||
* `ExTnMap`, `ExTnMapS`) indexed by `AutomapAtlasSlot` (`0..3`) and `bMinimap` (`false`/`true`).
|
* `ExTnMap`, `ExTnMapS`) indexed by `AutomapAtlasSlot` (`0..3`) and `bMinimap` (`false`/`true`).
|
||||||
* - `AUTOMAP_SetupViewportAndRender` (`0x6fb102a0`) & `AUTOMAP_RenderLayer` (`0x6fb10480`):
|
* - `AUTOMAP_SetupViewportAndRender` (`0x6fae9e20`) & `AUTOMAP_RenderLayer` (`0x6fb10489`):
|
||||||
* Isometric projection with `nDivisor = bMinimap ? 20 : 10`, DC6 baseline anchor
|
* Isometric projection with `nDivisor = bMinimap ? 20 : 10`, DC6 baseline anchor
|
||||||
* `drawLeft = screenX + frame.offsetX - (bMinimap ? 4 : 8)`,
|
* `drawLeft = screenX + frame.offsetX - (bMinimap ? 4 : 8)`,
|
||||||
* `drawTop = screenY - frame.height + frame.offsetY`,
|
* `drawTop = screenY - frame.height + frame.offsetY`,
|
||||||
* rendered in strict 4-layer order: `floors -> walls -> objects -> extras`.
|
* rendered in strict 4-layer order: `floors -> walls -> objects -> extras`.
|
||||||
* - Dual Viewport Modes (`0x6fb102a0..0x6fb10378`):
|
* - Dual Viewport Modes (`0x6fae9e20..0x6fb104d1`):
|
||||||
* - Corner Minimap (`bMinimap = true`, `nDivisor = 20`, default on start):
|
* - Corner Minimap (`bMinimap = true`, `nDivisor = 20`, default on start):
|
||||||
* Clipped `279×225` viewport at top-right `(screenWidth - 295, 16)`, shifting to
|
* Clipped `279×225` viewport at top-right `(screenWidth - 281, 57)`, shifting to
|
||||||
* top-left `(16, 16)` when a right-dock panel (Inventory/Skill Tree) is open.
|
* top-left `(0, 75)` when a right-dock panel (Inventory/Skill Tree) is open.
|
||||||
* - Full-Screen Automap (`bMinimap = false`, `nDivisor = 10`):
|
* - Full-Screen Automap (`bMinimap = false`, `nDivisor = 10`):
|
||||||
* Covers `[0, 0, screenWidth, screenHeight - 48]`, shifts center by `+160px` when left
|
* Covers `[0, 0, screenWidth, screenHeight]`, shifts center by `+(screenWidth >> 2)` when left
|
||||||
* panel is open or `-160px` when right panel is open, and supports arrow-key panning
|
* panel is open or `-(screenWidth >> 2)` when right panel is open, and supports arrow-key panning
|
||||||
* (`±8px` per step) + `Home` reset.
|
* (`±8px` per step) + `Home` reset.
|
||||||
* - `AUTOMAP_UpdateVisibility` (`0x6fb12af0`) & `AUTOMAP_RevealRoom` (`0x6fb12580`):
|
* - `AUTOMAP_UpdateVisibility` (`0x6fb12af0`) & `AUTOMAP_RevealRoom` (`0x6fb12580`):
|
||||||
* Octile distance threshold `max(|dx|, |dy|) * 2 + min(|dx|, |dy|) >= 160` sub-units,
|
* Octile distance threshold `max(|dx|, |dy|) * 2 + min(|dx|, |dy|) >= 160` sub-units,
|
||||||
* revealing the player's current room and adjacent 3×3 rooms into fog-of-war state.
|
* revealing the player's current room and adjacent 3×3 rooms into fog-of-war state.
|
||||||
* - `AUTOMAP_DrawCrosshair` (`0x6fb107f0`) & `AUTOMAP_RenderEntitiesAndPortals` (`0x6fb10dc5`):
|
* - `AUTOMAP_DrawCrosshair` (`0x6fb11490`) & `AUTOMAP_CrosshairColors` (`0x6fabd390`):
|
||||||
* 12-segment closed directional polygon crosshairs for Player, Party/NPCs, Mercenaries/Summons,
|
* 12-segment closed directional polygon crosshairs for Player (0x210), Party (0x213),
|
||||||
* and Bosses/SuperUniques, plus active town portal sprites (`306`/`307`/`300`), warp destination
|
* Pets/Mercenaries (0x214/0x215), Friendly NPCs (0x216), and Player Corpses (0x212),
|
||||||
|
* plus active town portal sprites (`306`/`307`/`300`), warp destination
|
||||||
* `Font6` labels, and top-right right-aligned gold `Font16` Level Name + Difficulty readout.
|
* `Font6` labels, and top-right right-aligned gold `Font16` Level Name + Difficulty readout.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -43,6 +44,8 @@ import {
|
||||||
computeAutomapTilePixelCoords,
|
computeAutomapTilePixelCoords,
|
||||||
getCrosshairSegments,
|
getCrosshairSegments,
|
||||||
} from '../../common/formats/automap-table.ts'
|
} from '../../common/formats/automap-table.ts'
|
||||||
|
import { lookupTbl } from '../i18n/lang.ts'
|
||||||
|
import { reportMissingArt } from '../scene/world-renderer.ts'
|
||||||
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
||||||
|
|
||||||
export type AutomapCrosshairSegment = {
|
export type AutomapCrosshairSegment = {
|
||||||
|
|
@ -67,25 +70,47 @@ export const AUTOMAP_REVEAL_DISTANCE_THRESHOLD = 160
|
||||||
/** Arrow-key pan step in screen pixels when Full Automap (`bMinimap = false`) is open. */
|
/** Arrow-key pan step in screen pixels when Full Automap (`bMinimap = false`) is open. */
|
||||||
export const AUTOMAP_PAN_STEP_PX = 8
|
export const AUTOMAP_PAN_STEP_PX = 8
|
||||||
|
|
||||||
/** 3-state fade alpha cycle (`F9`: `0 -> 1 -> 2 -> 0`). */
|
/** 4-state fade alpha cycle (`F9`: `0 -> 1 -> 2 -> 3 -> 0`). */
|
||||||
export const AUTOMAP_FADE_ALPHAS = [1.0, 0.75, 0.5] as const
|
export const AUTOMAP_FADE_ALPHAS = [1.0, 0.75, 0.5, 0.25] as const
|
||||||
|
|
||||||
|
const COLOR_PLAYER = [0.0, 0.0, 1.0, 1.0] as const
|
||||||
|
const COLOR_RED = [1.0, 0.0, 0.0, 1.0] as const
|
||||||
|
const COLOR_CORPSE = [1.0, 0.0, 1.0, 1.0] as const
|
||||||
|
const COLOR_PARTY = [0.0, 1.0, 0.0, 1.0] as const
|
||||||
|
const COLOR_OWN_PET = [68 / 255, 112 / 255, 116 / 255, 1.0] as const
|
||||||
|
const COLOR_PARTY_PET = [72 / 255, 160 / 255, 52 / 255, 1.0] as const
|
||||||
|
const COLOR_NPC = [244 / 255, 244 / 255, 244 / 255, 1.0] as const
|
||||||
|
const COLOR_NEUTRAL = [128 / 255, 128 / 255, 128 / 255, 1.0] as const
|
||||||
|
const COLOR_PORTAL = [244 / 255, 244 / 255, 0.0, 1.0] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical 1.13c crosshair palette colors (`AUTOMAP_DrawCrosshair` @ `0x6fb107f0`, `0x6fb10dc5`, `0x6fb11490`).
|
* Canonical 1.13c crosshair palette colors (`AUTOMAP_DrawCrosshair` @ `0x6fb11490`, `0x6fabd390`).
|
||||||
*/
|
*/
|
||||||
export const AUTOMAP_CROSSHAIR_COLORS = {
|
export const AUTOMAP_CROSSHAIR_COLORS = {
|
||||||
/** Player crosshair: Palette 0x97 (`RGB(0, 0, 255)` / `#0080ff`). */
|
/** Player crosshair: Palette 0x210 (`RGB(0, 0, 255)`). */
|
||||||
player: [0.0, 0.5, 1.0, 1.0] as const,
|
player: COLOR_PLAYER,
|
||||||
/** Party member / Ally crosshair: Palette 0x7D / 0x84 (`RGB(0, 255, 0)`). */
|
/** Non-party player crosshair: Palette 0x211 (`RGB(255, 0, 0)`). */
|
||||||
party: [0.0, 1.0, 0.0, 1.0] as const,
|
nonPartyPlayer: COLOR_RED,
|
||||||
/** Friendly Town/Quest NPC crosshair: `0x6fbcc216` (`RGB(244, 244, 244)` in `0x6fb11490`). */
|
/** Player corpse crosshair: Palette 0x212 (`RGB(255, 0, 255)`). */
|
||||||
npc: [244 / 255, 244 / 255, 244 / 255, 1.0] as const,
|
corpse: COLOR_CORPSE,
|
||||||
/** Mercenary / Summon crosshair: Palette 0x81 (`RGB(72, 160, 52)`). */
|
/** Party member / Ally crosshair: Palette 0x213 (`RGB(0, 255, 0)`). */
|
||||||
mercenary: [72 / 255, 160 / 255, 52 / 255, 1.0] as const,
|
party: COLOR_PARTY,
|
||||||
/** Valkyrie (357) / Iron Golem (291) crosshair: Palette 0x1F (`RGB(244, 244, 244)`). */
|
/** Own pet / Mercenary crosshair: Palette 0x214 (`RGB(68, 112, 116)`). */
|
||||||
valkyrieOrIronGolem: [244 / 255, 244 / 255, 244 / 255, 1.0] as const,
|
ownPet: COLOR_OWN_PET,
|
||||||
/** Boss / SuperUnique / Unique monster crosshair: Palette 0x0A (`RGB(255, 0, 0)`). */
|
/** Mercenary crosshair alias. */
|
||||||
boss: [1.0, 0.0, 0.0, 1.0] as const,
|
mercenary: COLOR_OWN_PET,
|
||||||
|
/** Party member pet crosshair: Palette 0x215 (`RGB(72, 160, 52)`). */
|
||||||
|
partyPet: COLOR_PARTY_PET,
|
||||||
|
/** Friendly Town/Quest NPC crosshair: Palette 0x216 (`RGB(244, 244, 244)`). */
|
||||||
|
npc: COLOR_NPC,
|
||||||
|
/** Neutral crosshair: Palette 0x217 (`RGB(128, 128, 128)`). */
|
||||||
|
neutral: COLOR_NEUTRAL,
|
||||||
|
/** Portal crosshair: Palette 0x218 (`RGB(244, 244, 0)`). */
|
||||||
|
portal: COLOR_PORTAL,
|
||||||
|
/** Valkyrie (357) / Iron Golem (291) crosshair: Palette 0x216 (`RGB(244, 244, 244)`). */
|
||||||
|
valkyrieOrIronGolem: COLOR_NPC,
|
||||||
|
/** Boss crosshair retained for test backward compatibility: Palette 0x211 (`RGB(255, 0, 0)`). */
|
||||||
|
boss: COLOR_RED,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Full-scale (`bMinimap = false`) and half-scale (`bMinimap = true`) sheet names per slot `0..3`. */
|
/** Full-scale (`bMinimap = false`) and half-scale (`bMinimap = true`) sheet names per slot `0..3`. */
|
||||||
|
|
@ -171,7 +196,7 @@ export interface AutomapViewportMetrics {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes viewport bounds, divisor, panel shift, and center anchor per
|
* Computes viewport bounds, divisor, panel shift, and center anchor per
|
||||||
* `AUTOMAP_SetupViewportAndRender` (`0x6fb102a0..0x6fb10378`).
|
* `AUTOMAP_SetupViewportAndRender` (`0x6fae9e20..0x6fb104d1`).
|
||||||
*/
|
*/
|
||||||
export function computeAutomapViewport(
|
export function computeAutomapViewport(
|
||||||
screenWidth: number,
|
screenWidth: number,
|
||||||
|
|
@ -188,8 +213,10 @@ export function computeAutomapViewport(
|
||||||
if (bMinimap) {
|
if (bMinimap) {
|
||||||
const vpW = 279
|
const vpW = 279
|
||||||
const vpH = 225
|
const vpH = 225
|
||||||
const vpX = rightPanelOpen ? 16 : Math.max(0, safeW - 295)
|
const vpX = rightPanelOpen ? 0 : Math.max(0, safeW - 281)
|
||||||
const vpY = 16
|
const vpY = rightPanelOpen ? 75 : 57
|
||||||
|
const centerX = rightPanelOpen ? 139 : Math.max(139, safeW - 142)
|
||||||
|
const centerY = rightPanelOpen ? 187 : 169
|
||||||
return {
|
return {
|
||||||
bMinimap: true,
|
bMinimap: true,
|
||||||
divisor: 20,
|
divisor: 20,
|
||||||
|
|
@ -203,8 +230,8 @@ export function computeAutomapViewport(
|
||||||
y: vpY,
|
y: vpY,
|
||||||
width: vpW,
|
width: vpW,
|
||||||
height: vpH,
|
height: vpH,
|
||||||
centerX: vpX + 139,
|
centerX,
|
||||||
centerY: vpY + 112,
|
centerY,
|
||||||
centerShiftX: 0,
|
centerShiftX: 0,
|
||||||
effectivePanX: 0,
|
effectivePanX: 0,
|
||||||
effectivePanY: 0,
|
effectivePanY: 0,
|
||||||
|
|
@ -216,11 +243,16 @@ export function computeAutomapViewport(
|
||||||
const vpX = 0
|
const vpX = 0
|
||||||
const vpY = 0
|
const vpY = 0
|
||||||
const vpW = safeW
|
const vpW = safeW
|
||||||
const vpH = Math.max(1, safeH - 48)
|
const vpH = safeH
|
||||||
|
const shiftAmount = safeW >> 2
|
||||||
const centerShiftX =
|
const centerShiftX =
|
||||||
leftPanelOpen && !rightPanelOpen ? 160 : rightPanelOpen && !leftPanelOpen ? -160 : 0
|
leftPanelOpen && !rightPanelOpen
|
||||||
const centerX = Math.trunc(vpW / 2) + centerShiftX
|
? shiftAmount
|
||||||
const centerY = Math.trunc(vpH / 2)
|
: rightPanelOpen && !leftPanelOpen
|
||||||
|
? -shiftAmount
|
||||||
|
: 0
|
||||||
|
const centerX = Math.trunc(safeW / 2) + centerShiftX
|
||||||
|
const centerY = Math.trunc((safeH - 40) / 2)
|
||||||
const effPanX = Math.trunc(panX)
|
const effPanX = Math.trunc(panX)
|
||||||
const effPanY = Math.trunc(panY)
|
const effPanY = Math.trunc(panY)
|
||||||
|
|
||||||
|
|
@ -319,23 +351,35 @@ export interface MinimapMarker {
|
||||||
readonly toLevelId?: number
|
readonly toLevelId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dynamic entity marker drawn on the automap (`0x6fb10dc5`). */
|
/** Dynamic entity marker drawn on the automap (`0x6fb11490` & `0x6fabd390`). */
|
||||||
export interface AutomapEntityMarker {
|
export interface AutomapEntityMarker {
|
||||||
/** World X coordinate (or relative to grid origin if `isRelativeToOrigin` is true). */
|
/** World X coordinate (or relative to grid origin if `isRelativeToOrigin` is true). */
|
||||||
readonly x: number
|
readonly x: number
|
||||||
/** World Y coordinate. */
|
/** World Y coordinate. */
|
||||||
readonly y: number
|
readonly y: number
|
||||||
/** Entity classification for crosshair color and name label rules. */
|
/** Entity classification for crosshair color and name label rules. */
|
||||||
readonly kind: 'party' | 'npc' | 'mercenary' | 'summon' | 'boss' | 'superunique' | 'unique'
|
readonly kind:
|
||||||
|
| 'party'
|
||||||
|
| 'npc'
|
||||||
|
| 'mercenary'
|
||||||
|
| 'summon'
|
||||||
|
| 'boss'
|
||||||
|
| 'superunique'
|
||||||
|
| 'unique'
|
||||||
|
| 'player'
|
||||||
|
| 'corpse'
|
||||||
|
| 'ownPet'
|
||||||
|
| 'partyPet'
|
||||||
|
| 'nonPartyPlayer'
|
||||||
/** Optional display name (drawn in `Font6` when `showNames` is true for allies/NPCs). */
|
/** Optional display name (drawn in `Font6` when `showNames` is true for allies/NPCs). */
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
/** Optional monster class ID (e.g. `357` Valkyrie, `291` Iron Golem -> white `0x1F`). */
|
/** Optional monster class ID (e.g. `357` Valkyrie, `291` Iron Golem -> white `0x216`). */
|
||||||
readonly classId?: number
|
readonly classId?: number
|
||||||
/** Whether the entity is alive (dead monsters are omitted from crosshair rendering). */
|
/** Whether the entity is alive (dead monsters are omitted from crosshair rendering). */
|
||||||
readonly alive?: boolean
|
readonly alive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Active Town Portal descriptor for dynamic portal rendering (`0x6fb10dc5`). */
|
/** Active Town Portal descriptor for dynamic portal rendering (`0x6fb11490`). */
|
||||||
export interface AutomapPortalDescriptor {
|
export interface AutomapPortalDescriptor {
|
||||||
readonly x: number
|
readonly x: number
|
||||||
readonly y: number
|
readonly y: number
|
||||||
|
|
@ -417,7 +461,7 @@ export interface AutomapRenderTelemetry {
|
||||||
mode: 'minimap' | 'full'
|
mode: 'minimap' | 'full'
|
||||||
bMinimap: boolean
|
bMinimap: boolean
|
||||||
divisor: 10 | 20
|
divisor: 10 | 20
|
||||||
fadeState: 0 | 1 | 2
|
fadeState: 0 | 1 | 2 | 3
|
||||||
fadeAlpha: number
|
fadeAlpha: number
|
||||||
showNames: boolean
|
showNames: boolean
|
||||||
panX: number
|
panX: number
|
||||||
|
|
@ -467,12 +511,12 @@ export class Minimap {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `true` = Corner Minimap (`nDivisor = 20`, `*S.dc6` half-scale sheets, `279×225` clipped viewport).
|
* `true` = Corner Minimap (`nDivisor = 20`, `*S.dc6` half-scale sheets, `279×225` clipped viewport).
|
||||||
* `false` = Full-Screen Automap (`nDivisor = 10`, full-scale sheets, `[0, 0, screenW, screenH - 48]`).
|
* `false` = Full-Screen Automap (`nDivisor = 10`, full-scale sheets, `[0, 0, screenW, screenH]`).
|
||||||
*/
|
*/
|
||||||
bMinimap = true
|
bMinimap = true
|
||||||
|
|
||||||
/** 3-state fade index (`0` = 100%, `1` = 75%, `2` = 50%), cycled by `F9`. */
|
/** 4-state fade index (`0` = 100%, `1` = 75%/distance, `2` = 50%, `3` = 25%), cycled by `F9`. */
|
||||||
fadeState: 0 | 1 | 2 = 0
|
fadeState: 0 | 1 | 2 | 3 = 0
|
||||||
|
|
||||||
/** Whether ally/NPC names and labels are displayed (`F10` toggles). */
|
/** Whether ally/NPC names and labels are displayed (`F10` toggles). */
|
||||||
showNames = true
|
showNames = true
|
||||||
|
|
@ -521,6 +565,32 @@ export class Minimap {
|
||||||
hudText: null,
|
hudText: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('d2_automap_fade')
|
||||||
|
if (saved !== null) {
|
||||||
|
const val = parseInt(saved, 10)
|
||||||
|
if (val >= 0 && val <= 3) {
|
||||||
|
this.fadeState = val as 0 | 1 | 2 | 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private persistFadeState(): void {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('d2_automap_fade', String(this.fadeState))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Current mode (`'minimap'` when `bMinimap === true`, `'full'` when `bMinimap === false`). */
|
/** Current mode (`'minimap'` when `bMinimap === true`, `'full'` when `bMinimap === false`). */
|
||||||
get mode(): 'minimap' | 'full' {
|
get mode(): 'minimap' | 'full' {
|
||||||
return this.bMinimap ? 'minimap' : 'full'
|
return this.bMinimap ? 'minimap' : 'full'
|
||||||
|
|
@ -549,7 +619,7 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias for `fadeState`. */
|
/** Alias for `fadeState`. */
|
||||||
get fadeMode(): 0 | 1 | 2 {
|
get fadeMode(): 0 | 1 | 2 | 3 {
|
||||||
return this.fadeState
|
return this.fadeState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,6 +653,31 @@ export class Minimap {
|
||||||
return this.lastRenderStats.viewport
|
return this.lastRenderStats.viewport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distance-based transparency lookup for fadeState 1 in Full Automap mode (`0x6fb103d5`),
|
||||||
|
* returning uniform alpha in other fade states or Corner Minimap mode.
|
||||||
|
*/
|
||||||
|
getCellFadeAlpha(
|
||||||
|
screenX: number,
|
||||||
|
screenY: number,
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
bMinimap: boolean,
|
||||||
|
): number {
|
||||||
|
if (this.fadeState === 0) return 1.0
|
||||||
|
if (this.fadeState === 2) return 0.5
|
||||||
|
if (this.fadeState === 3) return 0.25
|
||||||
|
if (bMinimap) return 0.75
|
||||||
|
const dx = Math.abs(screenX - centerX)
|
||||||
|
const dy = Math.abs(screenY - centerY)
|
||||||
|
if (dx > 140 || dy > 150) return 0.25
|
||||||
|
const dist = Math.hypot(dx, dy)
|
||||||
|
if (dist < 50) return 1.0
|
||||||
|
if (dist < 100) return 0.75
|
||||||
|
if (dist < 150) return 0.5
|
||||||
|
return 0.25
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles automap visibility (`Tab` key).
|
* Toggles automap visibility (`Tab` key).
|
||||||
*/
|
*/
|
||||||
|
|
@ -626,10 +721,11 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cycles automap fade state (`0 -> 1 -> 2 -> 0`, `F9` key).
|
* Cycles automap fade state (`0 -> 1 -> 2 -> 3 -> 0`, `F9` key).
|
||||||
*/
|
*/
|
||||||
cycleFade(): 0 | 1 | 2 {
|
cycleFade(): 0 | 1 | 2 | 3 {
|
||||||
this.fadeState = ((this.fadeState + 1) % 3) as 0 | 1 | 2
|
this.fadeState = ((this.fadeState + 1) % 4) as 0 | 1 | 2 | 3
|
||||||
|
this.persistFadeState()
|
||||||
return this.fadeState
|
return this.fadeState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -700,16 +796,18 @@ export class Minimap {
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
entries.map(async ([sheetName, meta]) => {
|
entries.map(async ([sheetName, meta]) => {
|
||||||
const candidateUrls = [
|
const url = baseUrl
|
||||||
meta.image,
|
? `${baseUrl.replace(/\/+$/, '')}/${meta.image.replace(/^\/+/, '')}`
|
||||||
`${baseUrl}/${meta.image.replace(/^\/+/, '')}`,
|
: meta.image
|
||||||
`/ui/automap-${sheetName.toLowerCase()}.png`,
|
|
||||||
`${baseUrl}/automap-${sheetName.toLowerCase()}.png`,
|
|
||||||
]
|
|
||||||
for (const url of candidateUrls) {
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url)
|
const res = await fetch(url)
|
||||||
if (!res.ok) continue
|
if (!res.ok) {
|
||||||
|
const err = new Error(
|
||||||
|
`Failed to load automap atlas ${sheetName} from ${url}: status ${res.status}`,
|
||||||
|
)
|
||||||
|
reportMissingArt('automap', sheetName, err.message)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
const blob = await res.blob()
|
const blob = await res.blob()
|
||||||
const bitmap = await createImageBitmap(blob, {
|
const bitmap = await createImageBitmap(blob, {
|
||||||
premultiplyAlpha: 'none',
|
premultiplyAlpha: 'none',
|
||||||
|
|
@ -719,10 +817,9 @@ export class Minimap {
|
||||||
bitmap.close()
|
bitmap.close()
|
||||||
this.atlases.set(sheetName, handle)
|
this.atlases.set(sheetName, handle)
|
||||||
loadedCount += 1
|
loadedCount += 1
|
||||||
return
|
} catch (err) {
|
||||||
} catch {
|
reportMissingArt('automap', sheetName, String(err))
|
||||||
// Try next candidate URL
|
throw err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -966,8 +1063,9 @@ export class Minimap {
|
||||||
gl.scissor(scissorX, scissorY, scissorW, scissorH)
|
gl.scissor(scissorX, scissorY, scissorW, scissorH)
|
||||||
}
|
}
|
||||||
|
|
||||||
const alpha = this.fadeAlpha
|
const isDistanceFade = this.fadeState === 1 && !this.bMinimap
|
||||||
const tint: readonly [number, number, number, number] = [1, 1, 1, alpha]
|
const uniformAlpha = AUTOMAP_FADE_ALPHAS[this.fadeState]
|
||||||
|
const uniformTint: readonly [number, number, number, number] = [1, 1, 1, uniformAlpha]
|
||||||
|
|
||||||
const drawLayerCells = (
|
const drawLayerCells = (
|
||||||
cells: readonly AutomapCell[],
|
cells: readonly AutomapCell[],
|
||||||
|
|
@ -995,6 +1093,21 @@ export class Minimap {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tint = isDistanceFade
|
||||||
|
? ([
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
this.getCellFadeAlpha(
|
||||||
|
rect.screenX,
|
||||||
|
rect.screenY,
|
||||||
|
vp.centerX,
|
||||||
|
vp.centerY,
|
||||||
|
this.bMinimap,
|
||||||
|
),
|
||||||
|
] as const)
|
||||||
|
: uniformTint
|
||||||
|
|
||||||
const atlasFrame: AtlasFrame = {
|
const atlasFrame: AtlasFrame = {
|
||||||
x: frame.x,
|
x: frame.x,
|
||||||
y: frame.y,
|
y: frame.y,
|
||||||
|
|
@ -1017,7 +1130,7 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strict 1.13c 4-layer draw order (`0x6fb103d0..0x6fb10450`):
|
// Strict 1.13c 4-layer draw order (`0x6fb10489`):
|
||||||
// 1. Layer 0 (`floors`)
|
// 1. Layer 0 (`floors`)
|
||||||
drawLayerCells(automapLayer.floors, 'floors', false)
|
drawLayerCells(automapLayer.floors, 'floors', false)
|
||||||
// 2. Layer 1 (`walls`)
|
// 2. Layer 1 (`walls`)
|
||||||
|
|
@ -1027,14 +1140,16 @@ export class Minimap {
|
||||||
// 4. Layer 3 (`extras` — pre-revealed town entrances/crosses + revealed warps/waypoints/shrines)
|
// 4. Layer 3 (`extras` — pre-revealed town entrances/crosses + revealed warps/waypoints/shrines)
|
||||||
drawLayerCells(automapLayer.extras, 'extras', true)
|
drawLayerCells(automapLayer.extras, 'extras', true)
|
||||||
|
|
||||||
// 5. Dynamic active Town Portals (`0x6fb10dc5`: `MaxiMap` / `MaxiMapS` frame 300 in Level 4, else 306/307)
|
// 5. Dynamic active Town Portals (`0x6fb11490`: `MaxiMap` / `MaxiMapS` frame 300 in Level 4, else 306/307)
|
||||||
const maxiSheetName = resolveAutomapSheetName(0, this.bMinimap)
|
const maxiSheetName = resolveAutomapSheetName(0, this.bMinimap)
|
||||||
const maxiMeta = AUTOMAP_UI_MANIFEST.atlases[maxiSheetName]
|
const maxiMeta = AUTOMAP_UI_MANIFEST.atlases[maxiSheetName]
|
||||||
const maxiAtlas = this.resolveAtlasHandle(renderer, maxiSheetName, maxiMeta)
|
const maxiAtlas = this.resolveAtlasHandle(renderer, maxiSheetName, maxiMeta)
|
||||||
const effectiveLevelId = options?.levelId ?? level.levelId ?? 1
|
const effectiveLevelId = options?.levelId ?? level.levelId ?? 0
|
||||||
|
const RED_PORTAL_EXCLUDED_LEVELS = new Set([111, 112, 117, 125, 126, 127])
|
||||||
if (options?.portals && maxiMeta && maxiAtlas) {
|
if (options?.portals && maxiMeta && maxiAtlas) {
|
||||||
for (const portal of options.portals) {
|
for (const portal of options.portals) {
|
||||||
if (portal.active === false) continue
|
if (portal.active === false) continue
|
||||||
|
if (portal.kind === 'red' && RED_PORTAL_EXCLUDED_LEVELS.has(effectiveLevelId)) continue
|
||||||
const pClientX = Math.round(portal.x - originX)
|
const pClientX = Math.round(portal.x - originX)
|
||||||
const pClientY = Math.round(portal.y - originY)
|
const pClientY = Math.round(portal.y - originY)
|
||||||
const xPixel = Math.trunc((pClientX * 2) / 10)
|
const xPixel = Math.trunc((pClientX * 2) / 10)
|
||||||
|
|
@ -1072,7 +1187,7 @@ export class Minimap {
|
||||||
atlas: maxiAtlas,
|
atlas: maxiAtlas,
|
||||||
width: rect.width / zoom,
|
width: rect.width / zoom,
|
||||||
height: rect.height / zoom,
|
height: rect.height / zoom,
|
||||||
tint,
|
tint: [1, 1, 1, this.fadeAlpha],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
this.lastRenderStats.spritesDrawn.portals += 1
|
this.lastRenderStats.spritesDrawn.portals += 1
|
||||||
|
|
@ -1089,10 +1204,12 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Dynamic Entity Crosshairs (`AUTOMAP_RenderEntitiesAndPortals` @ `0x6fb10dc5`)
|
// 6. Dynamic Entity Crosshairs (`AUTOMAP_DrawCrosshair` @ `0x6fb11490` & `0x6fabd390`)
|
||||||
if (options?.entities) {
|
if (options?.entities) {
|
||||||
for (const entity of options.entities) {
|
for (const entity of options.entities) {
|
||||||
if (entity.alive === false) continue
|
if (entity.kind !== 'corpse' && entity.alive === false) continue
|
||||||
|
if (entity.classId === 537 || entity.classId === 538 || entity.classId === 539) continue
|
||||||
|
|
||||||
const eClientX = Math.round(entity.x - originX)
|
const eClientX = Math.round(entity.x - originX)
|
||||||
const eClientY = Math.round(entity.y - originY)
|
const eClientY = Math.round(entity.y - originY)
|
||||||
const ex = Math.trunc(eClientX / vp.divisor) - offsetX
|
const ex = Math.trunc(eClientX / vp.divisor) - offsetX
|
||||||
|
|
@ -1102,17 +1219,30 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
let color: readonly [number, number, number, number] = AUTOMAP_CROSSHAIR_COLORS.npc
|
let color: readonly [number, number, number, number] = AUTOMAP_CROSSHAIR_COLORS.npc
|
||||||
if (entity.kind === 'party') {
|
if (entity.kind === 'player') {
|
||||||
|
color = AUTOMAP_CROSSHAIR_COLORS.player
|
||||||
|
this.lastRenderStats.crosshairsDrawn.player += 1
|
||||||
|
} else if (entity.kind === 'party') {
|
||||||
color = AUTOMAP_CROSSHAIR_COLORS.party
|
color = AUTOMAP_CROSSHAIR_COLORS.party
|
||||||
this.lastRenderStats.crosshairsDrawn.party += 1
|
this.lastRenderStats.crosshairsDrawn.party += 1
|
||||||
|
} else if (entity.kind === 'nonPartyPlayer') {
|
||||||
|
color = AUTOMAP_CROSSHAIR_COLORS.nonPartyPlayer
|
||||||
|
} else if (entity.kind === 'corpse') {
|
||||||
|
color = AUTOMAP_CROSSHAIR_COLORS.corpse
|
||||||
} else if (entity.kind === 'npc') {
|
} else if (entity.kind === 'npc') {
|
||||||
color = AUTOMAP_CROSSHAIR_COLORS.npc
|
color = AUTOMAP_CROSSHAIR_COLORS.npc
|
||||||
this.lastRenderStats.crosshairsDrawn.npcs += 1
|
this.lastRenderStats.crosshairsDrawn.npcs += 1
|
||||||
} else if (entity.kind === 'mercenary' || entity.kind === 'summon') {
|
} else if (entity.kind === 'ownPet' || entity.kind === 'mercenary') {
|
||||||
|
color = AUTOMAP_CROSSHAIR_COLORS.ownPet
|
||||||
|
this.lastRenderStats.crosshairsDrawn.mercenaries += 1
|
||||||
|
} else if (entity.kind === 'partyPet') {
|
||||||
|
color = AUTOMAP_CROSSHAIR_COLORS.partyPet
|
||||||
|
this.lastRenderStats.crosshairsDrawn.mercenaries += 1
|
||||||
|
} else if (entity.kind === 'summon') {
|
||||||
color =
|
color =
|
||||||
entity.classId === 357 || entity.classId === 291
|
entity.classId === 357 || entity.classId === 291
|
||||||
? AUTOMAP_CROSSHAIR_COLORS.valkyrieOrIronGolem
|
? AUTOMAP_CROSSHAIR_COLORS.valkyrieOrIronGolem
|
||||||
: AUTOMAP_CROSSHAIR_COLORS.mercenary
|
: AUTOMAP_CROSSHAIR_COLORS.ownPet
|
||||||
this.lastRenderStats.crosshairsDrawn.mercenaries += 1
|
this.lastRenderStats.crosshairsDrawn.mercenaries += 1
|
||||||
} else {
|
} else {
|
||||||
color = AUTOMAP_CROSSHAIR_COLORS.boss
|
color = AUTOMAP_CROSSHAIR_COLORS.boss
|
||||||
|
|
@ -1134,7 +1264,7 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Player 12-segment closed directional polygon crosshair (`AUTOMAP_DrawCrosshair` @ `0x6fb107f0`)
|
// 7. Player 12-segment closed directional polygon crosshair (`AUTOMAP_DrawCrosshair` @ `0x6fb11490`)
|
||||||
const playerScreenX = Math.trunc(playerClientX / vp.divisor) - offsetX
|
const playerScreenX = Math.trunc(playerClientX / vp.divisor) - offsetX
|
||||||
const playerScreenY = Math.trunc(playerClientY / vp.divisor) - offsetY
|
const playerScreenY = Math.trunc(playerClientY / vp.divisor) - offsetY
|
||||||
if (
|
if (
|
||||||
|
|
@ -1281,7 +1411,7 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the 12-segment closed directional polygon crosshair (`AUTOMAP_DrawCrosshair` @ `0x6fb107f0`).
|
* Draws the 12-segment closed directional polygon crosshair (`AUTOMAP_DrawCrosshair` @ `0x6fb11490`).
|
||||||
*/
|
*/
|
||||||
private drawCrosshairPolygon(
|
private drawCrosshairPolygon(
|
||||||
renderer: SpriteRenderer,
|
renderer: SpriteRenderer,
|
||||||
|
|
@ -1338,7 +1468,7 @@ export class Minimap {
|
||||||
seen: Uint8Array,
|
seen: Uint8Array,
|
||||||
cell: AutomapCell,
|
cell: AutomapCell,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (level.cellsX <= 0 || level.cellsY <= 0) return true
|
if (level.cellsX <= 0 || level.cellsY <= 0) return false
|
||||||
const cellX =
|
const cellX =
|
||||||
typeof cell.cellX === 'number' && Number.isFinite(cell.cellX)
|
typeof cell.cellX === 'number' && Number.isFinite(cell.cellX)
|
||||||
? cell.cellX
|
? cell.cellX
|
||||||
|
|
@ -1347,34 +1477,21 @@ export class Minimap {
|
||||||
typeof cell.cellY === 'number' && Number.isFinite(cell.cellY)
|
typeof cell.cellY === 'number' && Number.isFinite(cell.cellY)
|
||||||
? cell.cellY
|
? cell.cellY
|
||||||
: automapPixelToCellCoords(cell.xPixel, cell.yPixel).cellY
|
: automapPixelToCellCoords(cell.xPixel, cell.yPixel).cellY
|
||||||
const clampedX = Math.max(0, Math.min(level.cellsX - 1, cellX))
|
if (cellX < 0 || cellX >= level.cellsX || cellY < 0 || cellY >= level.cellsY) {
|
||||||
const clampedY = Math.max(0, Math.min(level.cellsY - 1, cellY))
|
return false
|
||||||
return seen[clampedY * level.cellsX + clampedX] !== 0
|
}
|
||||||
|
return seen[cellY * level.cellsX + cellX] !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the `AtlasHandle` for `sheetName`, synthesizing a deterministic handle
|
* Resolves the `AtlasHandle` for `sheetName`.
|
||||||
* when running under headless unit-test mocks without a live WebGL context.
|
|
||||||
*/
|
*/
|
||||||
private resolveAtlasHandle(
|
private resolveAtlasHandle(
|
||||||
renderer: SpriteRenderer,
|
_renderer: SpriteRenderer,
|
||||||
sheetName: AutomapSpriteKey,
|
sheetName: AutomapSpriteKey,
|
||||||
meta: AutomapSpriteAtlasMeta | undefined,
|
_meta: AutomapSpriteAtlasMeta | undefined,
|
||||||
): AtlasHandle | null {
|
): AtlasHandle | null {
|
||||||
const existing = this.atlases.get(sheetName)
|
return this.atlases.get(sheetName) ?? null
|
||||||
if (existing) return existing
|
|
||||||
if (!meta) return null
|
|
||||||
const gl = (renderer as unknown as { gl?: unknown }).gl
|
|
||||||
if (!gl) {
|
|
||||||
const synthetic: AtlasHandle = {
|
|
||||||
texture: sheetName as unknown as WebGLTexture,
|
|
||||||
width: meta.atlasWidth,
|
|
||||||
height: meta.atlasHeight,
|
|
||||||
}
|
|
||||||
this.atlases.set(sheetName, synthetic)
|
|
||||||
return synthetic
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1445,7 +1562,7 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer: AutomapLayerData = {
|
const layer: AutomapLayerData = {
|
||||||
layerId: level.levelId ?? 1,
|
layerId: level.levelId ?? 0,
|
||||||
levelTypeId: 1,
|
levelTypeId: 1,
|
||||||
floors,
|
floors,
|
||||||
walls,
|
walls,
|
||||||
|
|
|
||||||
|
|
@ -280,42 +280,42 @@ describe('R3 Authentic 1.13c Isometric Sprite Automap & Minimap UI', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Dual Viewports & Panel Shifts (computeAutomapViewport)', () => {
|
describe('Dual Viewports & Panel Shifts (computeAutomapViewport)', () => {
|
||||||
it('computes 279x225 clipped Corner Minimap viewport and shifts to (16, 16) when right panel is open', () => {
|
it('computes 279x225 clipped Corner Minimap viewport and shifts to (0, 75) when right panel is open', () => {
|
||||||
const vpRightClosed = computeAutomapViewport(800, 600, true, false, false, 0, 0)
|
const vpRightClosed = computeAutomapViewport(800, 600, true, false, false, 0, 0)
|
||||||
expect(vpRightClosed.bMinimap).toBe(true)
|
expect(vpRightClosed.bMinimap).toBe(true)
|
||||||
expect(vpRightClosed.divisor).toBe(20)
|
expect(vpRightClosed.divisor).toBe(20)
|
||||||
expect(vpRightClosed.x).toBe(505) // 800 - 295
|
expect(vpRightClosed.x).toBe(519) // 800 - 281
|
||||||
expect(vpRightClosed.y).toBe(16)
|
expect(vpRightClosed.y).toBe(57)
|
||||||
expect(vpRightClosed.width).toBe(279)
|
expect(vpRightClosed.width).toBe(279)
|
||||||
expect(vpRightClosed.height).toBe(225)
|
expect(vpRightClosed.height).toBe(225)
|
||||||
expect(vpRightClosed.centerX).toBe(505 + 139)
|
expect(vpRightClosed.centerX).toBe(658) // 800 - 142
|
||||||
expect(vpRightClosed.centerY).toBe(16 + 112)
|
expect(vpRightClosed.centerY).toBe(169) // 57 + 112
|
||||||
|
|
||||||
const vpRightOpen = computeAutomapViewport(800, 600, true, false, true, 0, 0)
|
const vpRightOpen = computeAutomapViewport(800, 600, true, false, true, 0, 0)
|
||||||
expect(vpRightOpen.x).toBe(16)
|
expect(vpRightOpen.x).toBe(0)
|
||||||
expect(vpRightOpen.y).toBe(16)
|
expect(vpRightOpen.y).toBe(75)
|
||||||
expect(vpRightOpen.width).toBe(279)
|
expect(vpRightOpen.width).toBe(279)
|
||||||
expect(vpRightOpen.height).toBe(225)
|
expect(vpRightOpen.height).toBe(225)
|
||||||
expect(vpRightOpen.centerX).toBe(16 + 139)
|
expect(vpRightOpen.centerX).toBe(139)
|
||||||
expect(vpRightOpen.centerY).toBe(16 + 112)
|
expect(vpRightOpen.centerY).toBe(187)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('computes Full-Screen Automap viewport [0, 0, W, H-48] and shifts center +-160px for open side panels', () => {
|
it('computes Full-Screen Automap viewport [0, 0, W, H] and shifts center +-W>>2 for open side panels', () => {
|
||||||
const vpNeutral = computeAutomapViewport(800, 600, false, false, false, 0, 0)
|
const vpNeutral = computeAutomapViewport(800, 600, false, false, false, 0, 0)
|
||||||
expect(vpNeutral.bMinimap).toBe(false)
|
expect(vpNeutral.bMinimap).toBe(false)
|
||||||
expect(vpNeutral.divisor).toBe(10)
|
expect(vpNeutral.divisor).toBe(10)
|
||||||
expect(vpNeutral.x).toBe(0)
|
expect(vpNeutral.x).toBe(0)
|
||||||
expect(vpNeutral.y).toBe(0)
|
expect(vpNeutral.y).toBe(0)
|
||||||
expect(vpNeutral.width).toBe(800)
|
expect(vpNeutral.width).toBe(800)
|
||||||
expect(vpNeutral.height).toBe(552) // 600 - 48
|
expect(vpNeutral.height).toBe(600)
|
||||||
expect(vpNeutral.centerX).toBe(400)
|
expect(vpNeutral.centerX).toBe(400)
|
||||||
expect(vpNeutral.centerY).toBe(276)
|
expect(vpNeutral.centerY).toBe(280) // (600 - 40) / 2
|
||||||
|
|
||||||
const vpLeftOpen = computeAutomapViewport(800, 600, false, true, false, 0, 0)
|
const vpLeftOpen = computeAutomapViewport(800, 600, false, true, false, 0, 0)
|
||||||
expect(vpLeftOpen.centerX).toBe(560) // 400 + 160
|
expect(vpLeftOpen.centerX).toBe(600) // 400 + 200
|
||||||
|
|
||||||
const vpRightOpen = computeAutomapViewport(800, 600, false, false, true, 0, 0)
|
const vpRightOpen = computeAutomapViewport(800, 600, false, false, true, 0, 0)
|
||||||
expect(vpRightOpen.centerX).toBe(240) // 400 - 160
|
expect(vpRightOpen.centerX).toBe(200) // 400 - 200
|
||||||
|
|
||||||
const vpBothOpen = computeAutomapViewport(800, 600, false, true, true, 0, 0)
|
const vpBothOpen = computeAutomapViewport(800, 600, false, true, true, 0, 0)
|
||||||
expect(vpBothOpen.centerX).toBe(400)
|
expect(vpBothOpen.centerX).toBe(400)
|
||||||
|
|
@ -342,9 +342,9 @@ describe('R3 Authentic 1.13c Isometric Sprite Automap & Minimap UI', () => {
|
||||||
{ playerWorldX: 0, playerWorldY: 320, levelId: 1 },
|
{ playerWorldX: 0, playerWorldY: 320, levelId: 1 },
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scissor was enabled with WebGL bottom-left coordinates (y = 600 - 552 = 48) and disabled afterwards
|
// Scissor was enabled with WebGL bottom-left coordinates (y = 600 - 600 = 0) and disabled afterwards
|
||||||
expect(scissors.length).toBe(2)
|
expect(scissors.length).toBe(2)
|
||||||
expect(scissors[0]).toEqual({ x: 0, y: 48, width: 800, height: 552 })
|
expect(scissors[0]).toEqual({ x: 0, y: 0, width: 800, height: 600 })
|
||||||
expect(scissors[1]).toBeNull()
|
expect(scissors[1]).toBeNull()
|
||||||
|
|
||||||
const spriteDraws = draws.filter((d): d is RecordedSpriteDraw => d.kind === 'sprite')
|
const spriteDraws = draws.filter((d): d is RecordedSpriteDraw => d.kind === 'sprite')
|
||||||
|
|
@ -435,13 +435,15 @@ describe('R3 Authentic 1.13c Isometric Sprite Automap & Minimap UI', () => {
|
||||||
expect(minimap.panX).toBe(0)
|
expect(minimap.panX).toBe(0)
|
||||||
expect(minimap.panY).toBe(0)
|
expect(minimap.panY).toBe(0)
|
||||||
|
|
||||||
// F9 fade mode cycles 0 -> 1 -> 2 -> 0
|
// F9 fade mode cycles 0 -> 1 -> 2 -> 3 -> 0
|
||||||
expect(minimap.fadeMode).toBe(0)
|
expect(minimap.fadeMode).toBe(0)
|
||||||
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[0])
|
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[0])
|
||||||
expect(minimap.cycleFade()).toBe(1)
|
expect(minimap.cycleFade()).toBe(1)
|
||||||
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[1])
|
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[1])
|
||||||
expect(minimap.cycleFade()).toBe(2)
|
expect(minimap.cycleFade()).toBe(2)
|
||||||
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
|
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
|
||||||
|
expect(minimap.cycleFade()).toBe(3)
|
||||||
|
expect(minimap.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[3])
|
||||||
expect(minimap.cycleFade()).toBe(0)
|
expect(minimap.cycleFade()).toBe(0)
|
||||||
|
|
||||||
// F10 name toggle
|
// F10 name toggle
|
||||||
|
|
|
||||||
|
|
@ -903,33 +903,33 @@ describe('Tier 1 — Feature Coverage (F1–F13, >= 5 tests per feature)', () =>
|
||||||
// F9: Corner Minimap (bMinimap=1) vs Full Automap (bMinimap=0), Viewport, Panel Shift & Hotkeys
|
// F9: Corner Minimap (bMinimap=1) vs Full Automap (bMinimap=0), Viewport, Panel Shift & Hotkeys
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
describe('F9: Corner Minimap (bMinimap=1) vs Full Automap (bMinimap=0), Viewport, Panel Shift & Hotkeys', () => {
|
describe('F9: Corner Minimap (bMinimap=1) vs Full Automap (bMinimap=0), Viewport, Panel Shift & Hotkeys', () => {
|
||||||
it('F9.1: computes 279x225 Corner Minimap viewport at (W-295, 16) and shifts to (16, 16) when right panel opens', () => {
|
it('F9.1: computes 279x225 Corner Minimap viewport at (W-281, 57) and shifts to (0, 75) when right panel opens', () => {
|
||||||
const vpTopRight = computeAutomapViewport(800, 600, true, false, false, 0, 0)
|
const vpTopRight = computeAutomapViewport(800, 600, true, false, false, 0, 0)
|
||||||
expect(vpTopRight).toMatchObject({
|
expect(vpTopRight).toMatchObject({
|
||||||
bMinimap: true,
|
bMinimap: true,
|
||||||
divisor: 20,
|
divisor: 20,
|
||||||
x: 505,
|
x: 519,
|
||||||
y: 16,
|
y: 57,
|
||||||
width: 279,
|
width: 279,
|
||||||
height: 225,
|
height: 225,
|
||||||
centerX: 644,
|
centerX: 658,
|
||||||
centerY: 128,
|
centerY: 169,
|
||||||
})
|
})
|
||||||
|
|
||||||
const vpTopLeft = computeAutomapViewport(800, 600, true, false, true, 0, 0)
|
const vpTopLeft = computeAutomapViewport(800, 600, true, false, true, 0, 0)
|
||||||
expect(vpTopLeft).toMatchObject({
|
expect(vpTopLeft).toMatchObject({
|
||||||
bMinimap: true,
|
bMinimap: true,
|
||||||
divisor: 20,
|
divisor: 20,
|
||||||
x: 16,
|
x: 0,
|
||||||
y: 16,
|
y: 75,
|
||||||
width: 279,
|
width: 279,
|
||||||
height: 225,
|
height: 225,
|
||||||
centerX: 155,
|
centerX: 139,
|
||||||
centerY: 128,
|
centerY: 187,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F9.2: computes Full Automap viewport [0, 0, W, H-48] and shifts centerX by +-160px when side panels open', () => {
|
it('F9.2: computes Full Automap viewport [0, 0, W, H] and shifts centerX by +-W>>2 when side panels open', () => {
|
||||||
const vpCenter = computeAutomapViewport(800, 600, false, false, false, 0, 0)
|
const vpCenter = computeAutomapViewport(800, 600, false, false, false, 0, 0)
|
||||||
expect(vpCenter).toMatchObject({
|
expect(vpCenter).toMatchObject({
|
||||||
bMinimap: false,
|
bMinimap: false,
|
||||||
|
|
@ -937,19 +937,19 @@ describe('Tier 1 — Feature Coverage (F1–F13, >= 5 tests per feature)', () =>
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 552,
|
height: 600,
|
||||||
centerX: 400,
|
centerX: 400,
|
||||||
centerY: 276,
|
centerY: 280,
|
||||||
centerShiftX: 0,
|
centerShiftX: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const vpLeft = computeAutomapViewport(800, 600, false, true, false, 0, 0)
|
const vpLeft = computeAutomapViewport(800, 600, false, true, false, 0, 0)
|
||||||
expect(vpLeft.centerX).toBe(560)
|
expect(vpLeft.centerX).toBe(600)
|
||||||
expect(vpLeft.centerShiftX).toBe(160)
|
expect(vpLeft.centerShiftX).toBe(200)
|
||||||
|
|
||||||
const vpRight = computeAutomapViewport(800, 600, false, false, true, 0, 0)
|
const vpRight = computeAutomapViewport(800, 600, false, false, true, 0, 0)
|
||||||
expect(vpRight.centerX).toBe(240)
|
expect(vpRight.centerX).toBe(200)
|
||||||
expect(vpRight.centerShiftX).toBe(-160)
|
expect(vpRight.centerShiftX).toBe(-200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F9.3: toggles visibility via Tab (toggle/toggleVisibility) and switches mode via V (toggleMode/toggleMinimapMode)', () => {
|
it('F9.3: toggles visibility via Tab (toggle/toggleVisibility) and switches mode via V (toggleMode/toggleMinimapMode)', () => {
|
||||||
|
|
@ -983,9 +983,9 @@ describe('Tier 1 — Feature Coverage (F1–F13, >= 5 tests per feature)', () =>
|
||||||
expect(minimap.panY).toBe(0)
|
expect(minimap.panY).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F9.5: cycles 3 fade states via F9 (1.0 -> 0.75 -> 0.5 -> 1.0) and toggles names via F10', () => {
|
it('F9.5: cycles 4 fade states via F9 (1.0 -> 0.75 -> 0.5 -> 0.25 -> 1.0) and toggles names via F10', () => {
|
||||||
const minimap = new Minimap()
|
const minimap = new Minimap()
|
||||||
expect(AUTOMAP_FADE_ALPHAS).toEqual([1.0, 0.75, 0.5])
|
expect(AUTOMAP_FADE_ALPHAS).toEqual([1.0, 0.75, 0.5, 0.25])
|
||||||
expect(minimap.fadeState).toBe(0)
|
expect(minimap.fadeState).toBe(0)
|
||||||
expect(minimap.fadeAlpha).toBe(1.0)
|
expect(minimap.fadeAlpha).toBe(1.0)
|
||||||
|
|
||||||
|
|
@ -993,6 +993,8 @@ describe('Tier 1 — Feature Coverage (F1–F13, >= 5 tests per feature)', () =>
|
||||||
expect(minimap.fadeAlpha).toBe(0.75)
|
expect(minimap.fadeAlpha).toBe(0.75)
|
||||||
expect(minimap.cycleFade()).toBe(2)
|
expect(minimap.cycleFade()).toBe(2)
|
||||||
expect(minimap.fadeAlpha).toBe(0.5)
|
expect(minimap.fadeAlpha).toBe(0.5)
|
||||||
|
expect(minimap.cycleFade()).toBe(3)
|
||||||
|
expect(minimap.fadeAlpha).toBe(0.25)
|
||||||
expect(minimap.cycleFade()).toBe(0)
|
expect(minimap.cycleFade()).toBe(0)
|
||||||
expect(minimap.fadeAlpha).toBe(1.0)
|
expect(minimap.fadeAlpha).toBe(1.0)
|
||||||
|
|
||||||
|
|
@ -1352,7 +1354,7 @@ describe('Tier 1 — Feature Coverage (F1–F13, >= 5 tests per feature)', () =>
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
expect(minimap.warpLabelsLastFrame).toContainEqual(
|
expect(minimap.warpLabelsLastFrame).toContainEqual(
|
||||||
expect.objectContaining({ text: 'Stash', kind: 'stash', color: 'gold' }),
|
expect.objectContaining({ text: expect.stringMatching(/^(?:Stash|储物箱)$/), kind: 'stash', color: 'gold' }),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -702,13 +702,13 @@ describe('Tier 2 — Boundary & Corner Cases (F1–F13, >= 5 tests per feature)'
|
||||||
expect(minimap.panY).toBe(0)
|
expect(minimap.panY).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F9.B5: wraps fadeState cleanly across multiple full cycles (0 -> 1 -> 2 -> 0 -> 1 -> 2 -> 0)', () => {
|
it('F9.B5: wraps fadeState cleanly across multiple full cycles (0 -> 1 -> 2 -> 3 -> 0 -> 1 -> 2)', () => {
|
||||||
const minimap = new Minimap()
|
const minimap = new Minimap()
|
||||||
const sequence: number[] = []
|
const sequence: number[] = []
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
sequence.push(minimap.cycleFade())
|
sequence.push(minimap.cycleFade())
|
||||||
}
|
}
|
||||||
expect(sequence).toEqual([1, 2, 0, 1, 2, 0])
|
expect(sequence).toEqual([1, 2, 3, 0, 1, 2])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,22 +261,22 @@ describe('Tier 3 — Cross-Feature Combinations (18 pairwise/multi-feature tests
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('C5 (F8 + F9 + F11): Full Automap HUD panel shift (+-160px) + Arrow-key panning (+-8px) + 12-segment player crosshair shift', () => {
|
it('C5 (F8 + F9 + F11): Full Automap HUD panel shift (+-200px) + Arrow-key panning (+-8px) + 12-segment player crosshair shift', () => {
|
||||||
const minimap = new Minimap()
|
const minimap = new Minimap()
|
||||||
registerAtlases(minimap)
|
registerAtlases(minimap)
|
||||||
minimap.mode = 'full'
|
minimap.mode = 'full'
|
||||||
minimap.panBySteps(2, -1) // panX = +16, panY = -8
|
minimap.panBySteps(2, -1) // panX = +16, panY = -8
|
||||||
|
|
||||||
const vpLeft = computeAutomapViewport(800, 600, false, true, false, minimap.panX, minimap.panY)
|
const vpLeft = computeAutomapViewport(800, 600, false, true, false, minimap.panX, minimap.panY)
|
||||||
expect(vpLeft.centerShiftX).toBe(160)
|
expect(vpLeft.centerShiftX).toBe(200)
|
||||||
expect(vpLeft.centerX).toBe(560)
|
expect(vpLeft.centerX).toBe(600)
|
||||||
expect(vpLeft.effectivePanX).toBe(16)
|
expect(vpLeft.effectivePanX).toBe(16)
|
||||||
expect(vpLeft.effectivePanY).toBe(-8)
|
expect(vpLeft.effectivePanY).toBe(-8)
|
||||||
|
|
||||||
// Player at (0, 0) -> offsetX = 0 - 560 + 16 = -544 -> playerScreenX = 0 - (-544) = 544
|
// Player at (0, 0) -> offsetX = 0 - 600 + 16 = -584 -> playerScreenX = 0 - (-584) = 584
|
||||||
// offsetY = 0 - 276 + (-8) = -284 -> playerScreenY = 0 - (-284) = 284
|
// offsetY = 0 - 280 + (-8) = -288 -> playerScreenY = 0 - (-288) = 288
|
||||||
const expectedSegs = getCrosshairSegments(544, 284, false)
|
const expectedSegs = getCrosshairSegments(584, 288, false)
|
||||||
expect(expectedSegs[0]).toEqual({ x0: 544, y0: 282, x1: 548, y1: 280 })
|
expect(expectedSegs[0]).toEqual({ x0: 584, y0: 286, x1: 588, y1: 284 })
|
||||||
|
|
||||||
const level: MinimapLevel = {
|
const level: MinimapLevel = {
|
||||||
key: 'c5-shift-pan',
|
key: 'c5-shift-pan',
|
||||||
|
|
@ -330,8 +330,8 @@ describe('Tier 3 — Cross-Feature Combinations (18 pairwise/multi-feature tests
|
||||||
{ width: 800, height: 600 },
|
{ width: 800, height: 600 },
|
||||||
{ playerWorldX: 0, playerWorldY: 0, rightPanelOpen: false },
|
{ playerWorldX: 0, playerWorldY: 0, rightPanelOpen: false },
|
||||||
)
|
)
|
||||||
expect(minimap.viewportLastFrame.x).toBe(505)
|
expect(minimap.viewportLastFrame.x).toBe(519)
|
||||||
expect(minimap.viewportLastFrame.centerX).toBe(644)
|
expect(minimap.viewportLastFrame.centerX).toBe(658)
|
||||||
|
|
||||||
minimap.draw(
|
minimap.draw(
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -342,8 +342,8 @@ describe('Tier 3 — Cross-Feature Combinations (18 pairwise/multi-feature tests
|
||||||
{ width: 800, height: 600 },
|
{ width: 800, height: 600 },
|
||||||
{ playerWorldX: 0, playerWorldY: 0, rightPanelOpen: true },
|
{ playerWorldX: 0, playerWorldY: 0, rightPanelOpen: true },
|
||||||
)
|
)
|
||||||
expect(minimap.viewportLastFrame.x).toBe(16)
|
expect(minimap.viewportLastFrame.x).toBe(0)
|
||||||
expect(minimap.viewportLastFrame.centerX).toBe(155)
|
expect(minimap.viewportLastFrame.centerX).toBe(139)
|
||||||
expect(minimap.crosshairsDrawnLastFrame.player).toBe(1)
|
expect(minimap.crosshairsDrawnLastFrame.player).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -426,10 +426,10 @@ describe('Tier 3 — Cross-Feature Combinations (18 pairwise/multi-feature tests
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('C9 (F8 + F9): F9 Fade Mode cycling (1.0 -> 0.75 -> 0.5) propagates exact tint alpha to all rendered sprite layers and portals', () => {
|
it('C9 (F8 + F9): F9 Fade Mode cycling (1.0 -> 0.75 -> 0.5 -> 0.25) propagates exact tint alpha to all rendered sprite layers and portals', () => {
|
||||||
const minimap = new Minimap()
|
const minimap = new Minimap()
|
||||||
registerAtlases(minimap)
|
registerAtlases(minimap)
|
||||||
minimap.mode = 'full'
|
minimap.mode = 'minimap'
|
||||||
const coords = computeAutomapTilePixelCoords(4, 4, 0)
|
const coords = computeAutomapTilePixelCoords(4, 4, 0)
|
||||||
const level: MinimapLevel = {
|
const level: MinimapLevel = {
|
||||||
key: 'c9-fade-tint',
|
key: 'c9-fade-tint',
|
||||||
|
|
@ -449,7 +449,7 @@ describe('Tier 3 — Cross-Feature Combinations (18 pairwise/multi-feature tests
|
||||||
}
|
}
|
||||||
minimap.reveal(level, 4, 4, 0, 320, true)
|
minimap.reveal(level, 4, 4, 0, 320, true)
|
||||||
|
|
||||||
for (const expectedAlpha of [1.0, 0.75, 0.5]) {
|
for (const expectedAlpha of [1.0, 0.75, 0.5, 0.25]) {
|
||||||
const { renderer, draws } = createRecordingRenderer()
|
const { renderer, draws } = createRecordingRenderer()
|
||||||
minimap.draw(
|
minimap.draw(
|
||||||
renderer,
|
renderer,
|
||||||
|
|
|
||||||
|
|
@ -741,7 +741,7 @@ describe('Tier 4 — Real-World E2E Scenarios (12 end-to-end scenarios across Ac
|
||||||
rightPanelOpen: false,
|
rightPanelOpen: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
expect(minimap.lastRenderStats.viewport.centerShiftX).toBe(160)
|
expect(minimap.lastRenderStats.viewport.centerShiftX).toBe(200)
|
||||||
expect(minimap.lastRenderStats.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
|
expect(minimap.lastRenderStats.fadeAlpha).toBe(AUTOMAP_FADE_ALPHAS[2])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -958,10 +958,10 @@ describe('Tier 5 — White-Box Adversarial & Differential Stress Coverage (1.13c
|
||||||
{ playerWorldX: 0, playerWorldY: 0 },
|
{ playerWorldX: 0, playerWorldY: 0 },
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hardware scissor must be enabled at (505, 600 - (16 + 225) = 359, 279, 225) and disabled after
|
// Hardware scissor must be enabled at (519, 600 - (57 + 225) = 318, 279, 225) and disabled after
|
||||||
expect(glCalls.enableCount).toBe(1)
|
expect(glCalls.enableCount).toBe(1)
|
||||||
expect(glCalls.disableCount).toBe(1)
|
expect(glCalls.disableCount).toBe(1)
|
||||||
expect(scissors[0]).toEqual({ x: 505, y: 359, width: 279, height: 225 })
|
expect(scissors[0]).toEqual({ x: 519, y: 318, width: 279, height: 225 })
|
||||||
expect(scissors[1]).toBeNull()
|
expect(scissors[1]).toBeNull()
|
||||||
|
|
||||||
// Exactly the 4 inside cells must be drawn, and the 4 outside cells culled!
|
// Exactly the 4 inside cells must be drawn, and the 4 outside cells culled!
|
||||||
|
|
@ -972,30 +972,30 @@ describe('Tier 5 — White-Box Adversarial & Differential Stress Coverage (1.13c
|
||||||
it('adv_f8_f9_simultaneous_panel_states_pan_clamp_and_bminimap_toggle', () => {
|
it('adv_f8_f9_simultaneous_panel_states_pan_clamp_and_bminimap_toggle', () => {
|
||||||
// All 4 (leftPanelOpen, rightPanelOpen) states in Full Automap (bMinimap = false)
|
// All 4 (leftPanelOpen, rightPanelOpen) states in Full Automap (bMinimap = false)
|
||||||
expect(computeAutomapViewport(800, 600, false, false, false)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, false, false, false)).toMatchObject({
|
||||||
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 400, centerY: 276, centerShiftX: 0, divisor: 10,
|
vpX: 0, vpY: 0, vpW: 800, vpH: 600, centerX: 400, centerY: 280, centerShiftX: 0, divisor: 10,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, false, true, false)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, false, true, false)).toMatchObject({
|
||||||
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 560, centerY: 276, centerShiftX: 160, divisor: 10,
|
vpX: 0, vpY: 0, vpW: 800, vpH: 600, centerX: 600, centerY: 280, centerShiftX: 200, divisor: 10,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, false, false, true)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, false, false, true)).toMatchObject({
|
||||||
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 240, centerY: 276, centerShiftX: -160, divisor: 10,
|
vpX: 0, vpY: 0, vpW: 800, vpH: 600, centerX: 200, centerY: 280, centerShiftX: -200, divisor: 10,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, false, true, true)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, false, true, true)).toMatchObject({
|
||||||
vpX: 0, vpY: 0, vpW: 800, vpH: 552, centerX: 400, centerY: 276, centerShiftX: 0, divisor: 10,
|
vpX: 0, vpY: 0, vpW: 800, vpH: 600, centerX: 400, centerY: 280, centerShiftX: 0, divisor: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
// All 4 (leftPanelOpen, rightPanelOpen) states in Corner Minimap (bMinimap = true)
|
// All 4 (leftPanelOpen, rightPanelOpen) states in Corner Minimap (bMinimap = true)
|
||||||
expect(computeAutomapViewport(800, 600, true, false, false)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, true, false, false)).toMatchObject({
|
||||||
vpX: 505, vpY: 16, vpW: 279, vpH: 225, centerX: 644, centerY: 128, divisor: 20,
|
vpX: 519, vpY: 57, vpW: 279, vpH: 225, centerX: 658, centerY: 169, divisor: 20,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, true, true, false)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, true, true, false)).toMatchObject({
|
||||||
vpX: 505, vpY: 16, vpW: 279, vpH: 225, centerX: 644, centerY: 128, divisor: 20,
|
vpX: 519, vpY: 57, vpW: 279, vpH: 225, centerX: 658, centerY: 169, divisor: 20,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, true, false, true)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, true, false, true)).toMatchObject({
|
||||||
vpX: 16, vpY: 16, vpW: 279, vpH: 225, centerX: 155, centerY: 128, divisor: 20,
|
vpX: 0, vpY: 75, vpW: 279, vpH: 225, centerX: 139, centerY: 187, divisor: 20,
|
||||||
})
|
})
|
||||||
expect(computeAutomapViewport(800, 600, true, true, true)).toMatchObject({
|
expect(computeAutomapViewport(800, 600, true, true, true)).toMatchObject({
|
||||||
vpX: 16, vpY: 16, vpW: 279, vpH: 225, centerX: 155, centerY: 128, divisor: 20,
|
vpX: 0, vpY: 75, vpW: 279, vpH: 225, centerX: 139, centerY: 187, divisor: 20,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pan behavior across mode switches
|
// Pan behavior across mode switches
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue