Fix flippy DC6 offsets, missile data-driven sprites, automap cel extraction, and art loader fail-fast (Closes #609, #612, #619, #622, #623)
This commit is contained in:
parent
56f4f7f4fe
commit
6af87acc13
|
|
@ -505,13 +505,7 @@ export class AutomapView {
|
||||||
const originX = view.isoOriginX ?? (minCellX - minCellY) * 80
|
const originX = view.isoOriginX ?? (minCellX - minCellY) * 80
|
||||||
const originY = view.isoOriginY ?? (minCellX + minCellY) * 40
|
const originY = view.isoOriginY ?? (minCellX + minCellY) * 40
|
||||||
|
|
||||||
const markers: MinimapMarker[] = snap.warpMarkers.map(wm => ({
|
const markers: MinimapMarker[] = []
|
||||||
cellX: wm.cellX - minCellX,
|
|
||||||
cellY: wm.cellY - minCellY,
|
|
||||||
kind: wm.toAreaId === this.currentAreaId ? 'waypoint' : 'exit',
|
|
||||||
toLevelId: wm.toAreaId,
|
|
||||||
targetName: wm.label,
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (Array.isArray(view.entrances)) {
|
if (Array.isArray(view.entrances)) {
|
||||||
for (const ent of view.entrances) {
|
for (const ent of view.entrances) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { ADDITIVE_MONSTER_TOKENS, type LoadedMonsterArt } from '../render/monste
|
||||||
import { resolveMonsterArtSpec } from '../../common/units/monster-mapping.ts'
|
import { resolveMonsterArtSpec } from '../../common/units/monster-mapping.ts'
|
||||||
import type { AtlasFrame } from '../render/atlas.ts'
|
import type { AtlasFrame } from '../render/atlas.ts'
|
||||||
import type { AtlasHandle, SpriteRenderer } from '../render/renderer.ts'
|
import type { AtlasHandle, SpriteRenderer } from '../render/renderer.ts'
|
||||||
|
import { reportMissingArt } from './world-renderer.ts'
|
||||||
import { normalizeHeroToken } from '../../common/world/scene-population.ts'
|
import { normalizeHeroToken } from '../../common/world/scene-population.ts'
|
||||||
|
|
||||||
export { ADDITIVE_MONSTER_TOKENS }
|
export { ADDITIVE_MONSTER_TOKENS }
|
||||||
|
|
@ -305,8 +306,13 @@ export async function loadCharacterArt(
|
||||||
renderer: SpriteRenderer,
|
renderer: SpriteRenderer,
|
||||||
packBase?: string,
|
packBase?: string,
|
||||||
act?: number,
|
act?: number,
|
||||||
token = 'so',
|
token?: string,
|
||||||
): Promise<LoadedCharacter | null> {
|
): Promise<LoadedCharacter | null> {
|
||||||
|
if (!token || !token.trim()) {
|
||||||
|
throw new Error(
|
||||||
|
'[entity-art-loader] Missing character token in loadCharacterArt (fail-fast, no silent "so" fallback)',
|
||||||
|
)
|
||||||
|
}
|
||||||
const heroToken = normalizeHeroToken(token)
|
const heroToken = normalizeHeroToken(token)
|
||||||
if (packBase !== undefined) {
|
if (packBase !== undefined) {
|
||||||
const r8CandidatePrefixes = [
|
const r8CandidatePrefixes = [
|
||||||
|
|
@ -391,6 +397,7 @@ export async function loadCharacterArt(
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`character art unavailable for ${heroToken} in pre-baked packs`)
|
console.warn(`character art unavailable for ${heroToken} in pre-baked packs`)
|
||||||
|
reportMissingArt('player', heroToken, `character art unavailable for ${heroToken} in pre-baked packs`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,7 +452,13 @@ export async function loadObjectArt(
|
||||||
handle,
|
handle,
|
||||||
groups,
|
groups,
|
||||||
...(glowGroups !== undefined ? { glowGroups } : {}),
|
...(glowGroups !== undefined ? { glowGroups } : {}),
|
||||||
blendMode: meta.blendMode ?? indexEntry?.blendMode ?? 'normal',
|
blendMode:
|
||||||
|
meta.blendMode === 'additive' ||
|
||||||
|
indexEntry?.blendMode === 'additive' ||
|
||||||
|
indexEntry?.trans === 1 ||
|
||||||
|
indexEntry?.trans === 2
|
||||||
|
? 'additive'
|
||||||
|
: 'normal',
|
||||||
directions: Math.max(1, meta.directions || 1),
|
directions: Math.max(1, meta.directions || 1),
|
||||||
standOffset: meta.standOffset ?? 0,
|
standOffset: meta.standOffset ?? 0,
|
||||||
standFrames: Math.max(1, meta.standFrames || 1),
|
standFrames: Math.max(1, meta.standFrames || 1),
|
||||||
|
|
@ -489,7 +502,13 @@ export async function loadObjectArt(
|
||||||
handle,
|
handle,
|
||||||
groups,
|
groups,
|
||||||
...(glowGroups !== undefined ? { glowGroups } : {}),
|
...(glowGroups !== undefined ? { glowGroups } : {}),
|
||||||
blendMode: meta.blendMode ?? indexEntry?.blendMode ?? 'normal',
|
blendMode:
|
||||||
|
meta.blendMode === 'additive' ||
|
||||||
|
indexEntry?.blendMode === 'additive' ||
|
||||||
|
indexEntry?.trans === 1 ||
|
||||||
|
indexEntry?.trans === 2
|
||||||
|
? 'additive'
|
||||||
|
: 'normal',
|
||||||
directions: Math.max(1, meta.directions || 1),
|
directions: Math.max(1, meta.directions || 1),
|
||||||
standOffset: meta.standOffset ?? 0,
|
standOffset: meta.standOffset ?? 0,
|
||||||
standFrames: Math.max(1, meta.standFrames || 1),
|
standFrames: Math.max(1, meta.standFrames || 1),
|
||||||
|
|
@ -506,6 +525,7 @@ export async function loadObjectArt(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportMissingArt('object', cleanSlug, `Failed to load object art for slug=${cleanSlug}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -660,6 +680,7 @@ export class EntityArtCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.indexState = 'failed'
|
this.indexState = 'failed'
|
||||||
|
reportMissingArt('entity-index', 'entity-index.json', 'Failed to load entity catalog index')
|
||||||
this.onArtLoaded?.()
|
this.onArtLoaded?.()
|
||||||
return null
|
return null
|
||||||
})()
|
})()
|
||||||
|
|
@ -740,12 +761,14 @@ export class EntityArtCache {
|
||||||
this.monsterByClassIdCache.set(classId, withScale)
|
this.monsterByClassIdCache.set(classId, withScale)
|
||||||
} else {
|
} else {
|
||||||
this.failedMonsters.add(classId)
|
this.failedMonsters.add(classId)
|
||||||
|
reportMissingArt('monster', specKey, `Failed to load monster art for specKey=${specKey}`)
|
||||||
}
|
}
|
||||||
this.onArtLoaded?.()
|
this.onArtLoaded?.()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(err => {
|
||||||
this.inflightMonsters.delete(classId)
|
this.inflightMonsters.delete(classId)
|
||||||
this.failedMonsters.add(classId)
|
this.failedMonsters.add(classId)
|
||||||
|
reportMissingArt('monster', specKey, String(err))
|
||||||
this.onArtLoaded?.()
|
this.onArtLoaded?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -812,12 +835,14 @@ export class EntityArtCache {
|
||||||
this.objectByClassIdCache.set(cacheKey, merged)
|
this.objectByClassIdCache.set(cacheKey, merged)
|
||||||
} else {
|
} else {
|
||||||
this.failedObjects.add(cacheKey)
|
this.failedObjects.add(cacheKey)
|
||||||
|
reportMissingArt('object', slug, `Failed to load object art for slug=${slug}`)
|
||||||
}
|
}
|
||||||
this.onArtLoaded?.()
|
this.onArtLoaded?.()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(err => {
|
||||||
this.inflightObjects.delete(cacheKey)
|
this.inflightObjects.delete(cacheKey)
|
||||||
this.failedObjects.add(cacheKey)
|
this.failedObjects.add(cacheKey)
|
||||||
|
reportMissingArt('object', slug, String(err))
|
||||||
this.onArtLoaded?.()
|
this.onArtLoaded?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -828,8 +853,14 @@ export class EntityArtCache {
|
||||||
art: LoadedObjectArt,
|
art: LoadedObjectArt,
|
||||||
entry: ObjectIndexEntry,
|
entry: ObjectIndexEntry,
|
||||||
): LoadedObjectArt {
|
): LoadedObjectArt {
|
||||||
|
const isAdditive =
|
||||||
|
entry.blendMode === 'additive' ||
|
||||||
|
entry.trans === 1 ||
|
||||||
|
entry.trans === 2 ||
|
||||||
|
art.blendMode === 'additive'
|
||||||
return {
|
return {
|
||||||
...art,
|
...art,
|
||||||
|
blendMode: isAdditive ? 'additive' : art.blendMode ?? 'normal',
|
||||||
...(entry.frameCnt !== undefined ? { frameCnt: entry.frameCnt } : {}),
|
...(entry.frameCnt !== undefined ? { frameCnt: entry.frameCnt } : {}),
|
||||||
...(entry.frameDelta !== undefined ? { frameDelta: entry.frameDelta } : {}),
|
...(entry.frameDelta !== undefined ? { frameDelta: entry.frameDelta } : {}),
|
||||||
...(entry.cycleAnim !== undefined ? { cycleAnim: entry.cycleAnim } : {}),
|
...(entry.cycleAnim !== undefined ? { cycleAnim: entry.cycleAnim } : {}),
|
||||||
|
|
|
||||||
|
|
@ -532,9 +532,6 @@ export class Minimap {
|
||||||
/** Loaded WebGL atlas handles keyed by sheet name (`MaxiMap`, `MaxiMapS`, etc.). */
|
/** Loaded WebGL atlas handles keyed by sheet name (`MaxiMap`, `MaxiMapS`, etc.). */
|
||||||
private readonly atlases = new Map<AutomapSpriteKey, AtlasHandle>()
|
private readonly atlases = new Map<AutomapSpriteKey, AtlasHandle>()
|
||||||
|
|
||||||
/** Cached synthesized `AutomapLayerData` for minimal test `MinimapLevel` objects lacking `level.automap`. */
|
|
||||||
private readonly synthesizedLayers = new Map<string, AutomapLayerData>()
|
|
||||||
|
|
||||||
/** level key -> one byte per cell, non-zero meaning explored. */
|
/** level key -> one byte per cell, non-zero meaning explored. */
|
||||||
private readonly explored = new Map<string, Uint8Array>()
|
private readonly explored = new Map<string, Uint8Array>()
|
||||||
|
|
||||||
|
|
@ -988,7 +985,14 @@ export class Minimap {
|
||||||
this.panY,
|
this.panY,
|
||||||
)
|
)
|
||||||
|
|
||||||
const automapLayer = level.automap ?? this.getOrSynthesizeAutomapLayer(level)
|
const automapLayer = level.automap
|
||||||
|
if (!automapLayer) {
|
||||||
|
reportMissingArt(
|
||||||
|
'automap',
|
||||||
|
String(level.levelId ?? level.key),
|
||||||
|
'Missing authentic automap layer data (no synthetic fallback allowed)',
|
||||||
|
)
|
||||||
|
}
|
||||||
this.lastRenderStats.mode = this.mode
|
this.lastRenderStats.mode = this.mode
|
||||||
this.lastRenderStats.bMinimap = this.bMinimap
|
this.lastRenderStats.bMinimap = this.bMinimap
|
||||||
this.lastRenderStats.divisor = vp.divisor
|
this.lastRenderStats.divisor = vp.divisor
|
||||||
|
|
@ -999,15 +1003,16 @@ export class Minimap {
|
||||||
this.lastRenderStats.panY = this.panY
|
this.lastRenderStats.panY = this.panY
|
||||||
this.lastRenderStats.viewport = vp
|
this.lastRenderStats.viewport = vp
|
||||||
this.lastRenderStats.cellCounts = {
|
this.lastRenderStats.cellCounts = {
|
||||||
floors: automapLayer.floors.length,
|
floors: automapLayer?.floors.length ?? 0,
|
||||||
walls: automapLayer.walls.length,
|
walls: automapLayer?.walls.length ?? 0,
|
||||||
objects: automapLayer.objects.length,
|
objects: automapLayer?.objects.length ?? 0,
|
||||||
extras: automapLayer.extras.length,
|
extras: automapLayer?.extras.length ?? 0,
|
||||||
total:
|
total: automapLayer
|
||||||
automapLayer.floors.length +
|
? automapLayer.floors.length +
|
||||||
automapLayer.walls.length +
|
automapLayer.walls.length +
|
||||||
automapLayer.objects.length +
|
automapLayer.objects.length +
|
||||||
automapLayer.extras.length,
|
automapLayer.extras.length
|
||||||
|
: 0,
|
||||||
}
|
}
|
||||||
this.lastRenderStats.spritesDrawn = {
|
this.lastRenderStats.spritesDrawn = {
|
||||||
floors: 0,
|
floors: 0,
|
||||||
|
|
@ -1133,14 +1138,16 @@ export class Minimap {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strict 1.13c 4-layer draw order (`0x6fb10489`):
|
// Strict 1.13c 4-layer draw order (`0x6fb10489`):
|
||||||
// 1. Layer 0 (`floors`)
|
if (automapLayer) {
|
||||||
drawLayerCells(automapLayer.floors, 'floors', false)
|
// 1. Layer 0 (`floors`)
|
||||||
// 2. Layer 1 (`walls`)
|
drawLayerCells(automapLayer.floors, 'floors', false)
|
||||||
drawLayerCells(automapLayer.walls, 'walls', false)
|
// 2. Layer 1 (`walls`)
|
||||||
// 3. Layer 2 (`objects`)
|
drawLayerCells(automapLayer.walls, 'walls', false)
|
||||||
drawLayerCells(automapLayer.objects, 'objects', false)
|
// 3. Layer 2 (`objects`)
|
||||||
// 4. Layer 3 (`extras` — pre-revealed town entrances/crosses + revealed warps/waypoints/shrines)
|
drawLayerCells(automapLayer.objects, 'objects', false)
|
||||||
drawLayerCells(automapLayer.extras, 'extras', true)
|
// 4. Layer 3 (`extras` — pre-revealed town entrances/crosses + revealed warps/waypoints/shrines)
|
||||||
|
drawLayerCells(automapLayer.extras, 'extras', true)
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Dynamic active Town Portals (`0x6fb11490`: `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)
|
||||||
|
|
@ -1496,95 +1503,6 @@ export class Minimap {
|
||||||
return this.atlases.get(sheetName) ?? null
|
return this.atlases.get(sheetName) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthesizes a 4-layer `AutomapLayerData` from a minimal `MinimapLevel` (when `level.automap`
|
|
||||||
* is not explicitly passed, e.g. in standalone unit tests).
|
|
||||||
*/
|
|
||||||
private getOrSynthesizeAutomapLayer(level: MinimapLevel): AutomapLayerData {
|
|
||||||
const cached = this.synthesizedLayers.get(level.key)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const floors: AutomapCell[] = []
|
|
||||||
const walls: AutomapCell[] = []
|
|
||||||
const objects: AutomapCell[] = []
|
|
||||||
const extras: AutomapCell[] = []
|
|
||||||
|
|
||||||
for (let y = 0; y < level.cellsY; y += 1) {
|
|
||||||
for (let x = 0; x < level.cellsX; x += 1) {
|
|
||||||
const coords = computeAutomapTilePixelCoords(x, y, 0)
|
|
||||||
if (this.cellBlocked(level, x, y)) {
|
|
||||||
walls.push({
|
|
||||||
cellNo: 9,
|
|
||||||
xPixel: coords.xPixel,
|
|
||||||
yPixel: coords.yPixel,
|
|
||||||
cellX: x,
|
|
||||||
cellY: y,
|
|
||||||
fSaved: 0,
|
|
||||||
weight: 0,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
floors.push({
|
|
||||||
cellNo: 0,
|
|
||||||
xPixel: coords.xPixel,
|
|
||||||
yPixel: coords.yPixel,
|
|
||||||
cellX: x,
|
|
||||||
cellY: y,
|
|
||||||
fSaved: 0,
|
|
||||||
weight: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const marker of level.markers) {
|
|
||||||
const subTileX = marker.cellX * 5 + 2
|
|
||||||
const subTileY = marker.cellY * 5 + 2
|
|
||||||
const coords = computeAutomapObjectPixelCoords(subTileX, subTileY)
|
|
||||||
if (marker.kind === 'waypoint') {
|
|
||||||
extras.push({
|
|
||||||
cellNo: 309,
|
|
||||||
xPixel: coords.xPixel,
|
|
||||||
yPixel: coords.yPixel,
|
|
||||||
cellX: marker.cellX,
|
|
||||||
cellY: marker.cellY,
|
|
||||||
fSaved: 0,
|
|
||||||
weight: 0,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
objects.push({
|
|
||||||
cellNo: 306,
|
|
||||||
xPixel: coords.xPixel,
|
|
||||||
yPixel: coords.yPixel,
|
|
||||||
cellX: marker.cellX,
|
|
||||||
cellY: marker.cellY,
|
|
||||||
fSaved: 0,
|
|
||||||
weight: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const layer: AutomapLayerData = {
|
|
||||||
layerId: level.levelId ?? 0,
|
|
||||||
levelTypeId: 1,
|
|
||||||
floors,
|
|
||||||
walls,
|
|
||||||
objects,
|
|
||||||
extras,
|
|
||||||
}
|
|
||||||
this.synthesizedLayers.set(level.key, layer)
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a cell is blocked in the sub-tile walkability grid.
|
|
||||||
*/
|
|
||||||
private cellBlocked(level: MinimapLevel, cellX: number, cellY: number): boolean {
|
|
||||||
const sx = cellX * 5 + 2
|
|
||||||
const sy = cellY * 5 + 2
|
|
||||||
const idx = sy * level.gridWidth + sx
|
|
||||||
return idx >= 0 && idx < level.blocked.length && level.blocked[idx] !== 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The explored bytes for a level, created on first use.
|
* The explored bytes for a level, created on first use.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -941,7 +941,56 @@ export function itemToUiInventoryItem(
|
||||||
: (rawItem as any).id ?? `item-${code}-${++itemBridgeSeqCounter}`,
|
: (rawItem as any).id ?? `item-${code}-${++itemBridgeSeqCounter}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const flippyFile = (rawItem as any).flippyFile ?? (base as any).flippyfile
|
let flippyFile: string | undefined = (rawItem as any).flippyFile ?? (rawItem as any).flippyfile
|
||||||
|
if (!flippyFile && quality === 'unique') {
|
||||||
|
flippyFile = rawItem.uniqueItemDef?.flippyfile
|
||||||
|
if (!flippyFile && dropTables?.uniques) {
|
||||||
|
const targetName = (rawItem.name ?? '').trim()
|
||||||
|
let match =
|
||||||
|
(targetName ? dropTables.uniques.get?.(targetName) : undefined) ??
|
||||||
|
(targetName
|
||||||
|
? dropTables.uniques.all?.find(
|
||||||
|
u => u.enabled && u.index.trim().toLowerCase() === targetName.toLowerCase(),
|
||||||
|
)
|
||||||
|
: undefined)
|
||||||
|
if (!match && code) {
|
||||||
|
const byCode = dropTables.uniques.getEnabledByCode?.(code.trim())
|
||||||
|
if (byCode && byCode.length === 1) {
|
||||||
|
match = byCode[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match?.flippyfile) {
|
||||||
|
flippyFile = match.flippyfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!flippyFile && quality === 'set') {
|
||||||
|
flippyFile = rawItem.setItemDef?.flippyfile
|
||||||
|
if (!flippyFile && dropTables?.sets?.items) {
|
||||||
|
const targetName = (rawItem.name ?? '').trim()
|
||||||
|
let match =
|
||||||
|
(targetName ? dropTables.sets.items.getByIndex?.(targetName) : undefined) ??
|
||||||
|
(targetName
|
||||||
|
? dropTables.sets.items.entries?.find(
|
||||||
|
s => s.index.trim().toLowerCase() === targetName.toLowerCase(),
|
||||||
|
)
|
||||||
|
: undefined)
|
||||||
|
if (!match && code) {
|
||||||
|
const lowerCode = code.trim().toLowerCase()
|
||||||
|
const byCode = dropTables.sets.items.entries?.filter(
|
||||||
|
s => s.item?.trim().toLowerCase() === lowerCode,
|
||||||
|
)
|
||||||
|
if (byCode && byCode.length === 1) {
|
||||||
|
match = byCode[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match?.flippyfile) {
|
||||||
|
flippyFile = match.flippyfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flippyFile) {
|
||||||
|
flippyFile = (base as any).flippyfile
|
||||||
|
}
|
||||||
const dropSound = (rawItem as any).dropSound ?? (rawItem as any).dropsound ?? (base as any).dropsound
|
const dropSound = (rawItem as any).dropSound ?? (rawItem as any).dropsound ?? (base as any).dropsound
|
||||||
const dropSfxFrame = (rawItem as any).dropSfxFrame ?? (rawItem as any).dropsfxframe ?? (base as any).dropsfxframe
|
const dropSfxFrame = (rawItem as any).dropSfxFrame ?? (rawItem as any).dropsfxframe ?? (base as any).dropsfxframe
|
||||||
const permStore = Boolean(rawItem.permStore ?? base.permStoreItem)
|
const permStore = Boolean(rawItem.permStore ?? base.permStoreItem)
|
||||||
|
|
@ -1451,7 +1500,10 @@ export function onlineItemDataToUiInventoryItem(
|
||||||
readonly twoHandDamage?: { readonly min: number; readonly max: number } | undefined
|
readonly twoHandDamage?: { readonly min: number; readonly max: number } | undefined
|
||||||
} {
|
} {
|
||||||
const tables = dropTables ?? getEmbeddedDropTables()
|
const tables = dropTables ?? getEmbeddedDropTables()
|
||||||
const code = String(item.code ?? '').trim().toLowerCase() || 'rin'
|
const code = String(item.code ?? '').trim().toLowerCase()
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('[item-bridge] Missing item code in onlineItemDataToUiInventoryItem (fail-fast, no silent fallback)')
|
||||||
|
}
|
||||||
|
|
||||||
const weaponRow = tables.weapons?.get(code)
|
const weaponRow = tables.weapons?.get(code)
|
||||||
const armorRow = !weaponRow ? tables.armor?.get(code) : undefined
|
const armorRow = !weaponRow ? tables.armor?.get(code) : undefined
|
||||||
|
|
@ -1759,7 +1811,7 @@ export function onlineItemDataToUiInventoryItem(
|
||||||
invfile: baseRow?.invfile ?? baseRow?.invFile,
|
invfile: baseRow?.invfile ?? baseRow?.invFile,
|
||||||
uniqueinvfile: baseRow?.uniqueinvfile,
|
uniqueinvfile: baseRow?.uniqueinvfile,
|
||||||
setinvfile: baseRow?.setinvfile,
|
setinvfile: baseRow?.setinvfile,
|
||||||
flippyfile: baseRow?.flippyfile,
|
flippyfile: uniqueItemDef?.flippyfile ?? setItemDef?.flippyfile ?? baseRow?.flippyfile,
|
||||||
dropsound: baseRow?.dropsound,
|
dropsound: baseRow?.dropsound,
|
||||||
dropsfxframe: baseRow?.dropsfxframe,
|
dropsfxframe: baseRow?.dropsfxframe,
|
||||||
twoHanded: Boolean(baseRow?.twoHanded || baseRow?.twohanded === '1' || baseRow?.['2handed'] === 1),
|
twoHanded: Boolean(baseRow?.twoHanded || baseRow?.twohanded === '1' || baseRow?.['2handed'] === 1),
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ describe('Tier 2 — Boundary & Corner Cases (F1–F13, >= 5 tests per feature)'
|
||||||
expect(minimap.spritesDrawnLastFrame.extras).toBe(1)
|
expect(minimap.spritesDrawnLastFrame.extras).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F8.B3: synthesizes a fallback 4-layer AutomapLayerData when level.automap is omitted', () => {
|
it('F8.B3: does not synthesize fake fallback layers when level.automap is omitted', () => {
|
||||||
const minimap = new Minimap()
|
const minimap = new Minimap()
|
||||||
registerAtlases(minimap)
|
registerAtlases(minimap)
|
||||||
minimap.mode = 'full'
|
minimap.mode = 'full'
|
||||||
|
|
@ -613,8 +613,8 @@ describe('Tier 2 — Boundary & Corner Cases (F1–F13, >= 5 tests per feature)'
|
||||||
}
|
}
|
||||||
minimap.reveal(level, 2, 2, 0, 160, true)
|
minimap.reveal(level, 2, 2, 0, 160, true)
|
||||||
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
|
minimap.draw(createNoopRenderer(), level, 2, 2, { x: 0, y: 160, zoom: 1 }, { width: 800, height: 600 })
|
||||||
expect(minimap.lastRenderStats.cellCounts.total).toBe(100 + 2)
|
expect(minimap.lastRenderStats.cellCounts.total).toBe(0)
|
||||||
expect(minimap.spritesDrawnLastFrame.total).toBeGreaterThan(0)
|
expect(minimap.spritesDrawnLastFrame.total).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('F8.B4: skips all rendering when minimap.visible === false while resetting frame draw stats to 0', () => {
|
it('F8.B4: skips all rendering when minimap.visible === false while resetting frame draw stats to 0', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue