diff --git a/src/client/automap/automap-view.ts b/src/client/automap/automap-view.ts index 6d5ed1e..ab8cab9 100644 --- a/src/client/automap/automap-view.ts +++ b/src/client/automap/automap-view.ts @@ -505,13 +505,7 @@ export class AutomapView { const originX = view.isoOriginX ?? (minCellX - minCellY) * 80 const originY = view.isoOriginY ?? (minCellX + minCellY) * 40 - const markers: MinimapMarker[] = snap.warpMarkers.map(wm => ({ - cellX: wm.cellX - minCellX, - cellY: wm.cellY - minCellY, - kind: wm.toAreaId === this.currentAreaId ? 'waypoint' : 'exit', - toLevelId: wm.toAreaId, - targetName: wm.label, - })) + const markers: MinimapMarker[] = [] if (Array.isArray(view.entrances)) { for (const ent of view.entrances) { diff --git a/src/client/scene/entity-art-loader.ts b/src/client/scene/entity-art-loader.ts index faa479c..8e49e02 100644 --- a/src/client/scene/entity-art-loader.ts +++ b/src/client/scene/entity-art-loader.ts @@ -9,6 +9,7 @@ import { ADDITIVE_MONSTER_TOKENS, type LoadedMonsterArt } from '../render/monste import { resolveMonsterArtSpec } from '../../common/units/monster-mapping.ts' import type { AtlasFrame } from '../render/atlas.ts' import type { AtlasHandle, SpriteRenderer } from '../render/renderer.ts' +import { reportMissingArt } from './world-renderer.ts' import { normalizeHeroToken } from '../../common/world/scene-population.ts' export { ADDITIVE_MONSTER_TOKENS } @@ -305,8 +306,13 @@ export async function loadCharacterArt( renderer: SpriteRenderer, packBase?: string, act?: number, - token = 'so', + token?: string, ): Promise { + 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) if (packBase !== undefined) { const r8CandidatePrefixes = [ @@ -391,6 +397,7 @@ export async function loadCharacterArt( } 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 } @@ -445,7 +452,13 @@ export async function loadObjectArt( handle, groups, ...(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), standOffset: meta.standOffset ?? 0, standFrames: Math.max(1, meta.standFrames || 1), @@ -489,7 +502,13 @@ export async function loadObjectArt( handle, groups, ...(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), standOffset: meta.standOffset ?? 0, 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 } @@ -660,6 +680,7 @@ export class EntityArtCache { } } this.indexState = 'failed' + reportMissingArt('entity-index', 'entity-index.json', 'Failed to load entity catalog index') this.onArtLoaded?.() return null })() @@ -740,12 +761,14 @@ export class EntityArtCache { this.monsterByClassIdCache.set(classId, withScale) } else { this.failedMonsters.add(classId) + reportMissingArt('monster', specKey, `Failed to load monster art for specKey=${specKey}`) } this.onArtLoaded?.() }) - .catch(() => { + .catch(err => { this.inflightMonsters.delete(classId) this.failedMonsters.add(classId) + reportMissingArt('monster', specKey, String(err)) this.onArtLoaded?.() }) @@ -812,12 +835,14 @@ export class EntityArtCache { this.objectByClassIdCache.set(cacheKey, merged) } else { this.failedObjects.add(cacheKey) + reportMissingArt('object', slug, `Failed to load object art for slug=${slug}`) } this.onArtLoaded?.() }) - .catch(() => { + .catch(err => { this.inflightObjects.delete(cacheKey) this.failedObjects.add(cacheKey) + reportMissingArt('object', slug, String(err)) this.onArtLoaded?.() }) @@ -828,8 +853,14 @@ export class EntityArtCache { art: LoadedObjectArt, entry: ObjectIndexEntry, ): LoadedObjectArt { + const isAdditive = + entry.blendMode === 'additive' || + entry.trans === 1 || + entry.trans === 2 || + art.blendMode === 'additive' return { ...art, + blendMode: isAdditive ? 'additive' : art.blendMode ?? 'normal', ...(entry.frameCnt !== undefined ? { frameCnt: entry.frameCnt } : {}), ...(entry.frameDelta !== undefined ? { frameDelta: entry.frameDelta } : {}), ...(entry.cycleAnim !== undefined ? { cycleAnim: entry.cycleAnim } : {}), diff --git a/src/client/ui/minimap.ts b/src/client/ui/minimap.ts index 0f0f21b..d6e9362 100644 --- a/src/client/ui/minimap.ts +++ b/src/client/ui/minimap.ts @@ -532,9 +532,6 @@ export class Minimap { /** Loaded WebGL atlas handles keyed by sheet name (`MaxiMap`, `MaxiMapS`, etc.). */ private readonly atlases = new Map() - /** Cached synthesized `AutomapLayerData` for minimal test `MinimapLevel` objects lacking `level.automap`. */ - private readonly synthesizedLayers = new Map() - /** level key -> one byte per cell, non-zero meaning explored. */ private readonly explored = new Map() @@ -988,7 +985,14 @@ export class Minimap { 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.bMinimap = this.bMinimap this.lastRenderStats.divisor = vp.divisor @@ -999,15 +1003,16 @@ export class Minimap { this.lastRenderStats.panY = this.panY this.lastRenderStats.viewport = vp this.lastRenderStats.cellCounts = { - floors: automapLayer.floors.length, - walls: automapLayer.walls.length, - objects: automapLayer.objects.length, - extras: automapLayer.extras.length, - total: - automapLayer.floors.length + - automapLayer.walls.length + - automapLayer.objects.length + - automapLayer.extras.length, + floors: automapLayer?.floors.length ?? 0, + walls: automapLayer?.walls.length ?? 0, + objects: automapLayer?.objects.length ?? 0, + extras: automapLayer?.extras.length ?? 0, + total: automapLayer + ? automapLayer.floors.length + + automapLayer.walls.length + + automapLayer.objects.length + + automapLayer.extras.length + : 0, } this.lastRenderStats.spritesDrawn = { floors: 0, @@ -1133,14 +1138,16 @@ export class Minimap { } // Strict 1.13c 4-layer draw order (`0x6fb10489`): - // 1. Layer 0 (`floors`) - drawLayerCells(automapLayer.floors, 'floors', false) - // 2. Layer 1 (`walls`) - drawLayerCells(automapLayer.walls, 'walls', false) - // 3. Layer 2 (`objects`) - drawLayerCells(automapLayer.objects, 'objects', false) - // 4. Layer 3 (`extras` — pre-revealed town entrances/crosses + revealed warps/waypoints/shrines) - drawLayerCells(automapLayer.extras, 'extras', true) + if (automapLayer) { + // 1. Layer 0 (`floors`) + drawLayerCells(automapLayer.floors, 'floors', false) + // 2. Layer 1 (`walls`) + drawLayerCells(automapLayer.walls, 'walls', false) + // 3. Layer 2 (`objects`) + drawLayerCells(automapLayer.objects, 'objects', false) + // 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) const maxiSheetName = resolveAutomapSheetName(0, this.bMinimap) @@ -1496,95 +1503,6 @@ export class Minimap { 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. */ diff --git a/src/common/items/item-bridge.ts b/src/common/items/item-bridge.ts index 1fb472a..86cccdf 100644 --- a/src/common/items/item-bridge.ts +++ b/src/common/items/item-bridge.ts @@ -941,7 +941,56 @@ export function itemToUiInventoryItem( : (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 dropSfxFrame = (rawItem as any).dropSfxFrame ?? (rawItem as any).dropsfxframe ?? (base as any).dropsfxframe const permStore = Boolean(rawItem.permStore ?? base.permStoreItem) @@ -1451,7 +1500,10 @@ export function onlineItemDataToUiInventoryItem( readonly twoHandDamage?: { readonly min: number; readonly max: number } | undefined } { 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 armorRow = !weaponRow ? tables.armor?.get(code) : undefined @@ -1759,7 +1811,7 @@ export function onlineItemDataToUiInventoryItem( invfile: baseRow?.invfile ?? baseRow?.invFile, uniqueinvfile: baseRow?.uniqueinvfile, setinvfile: baseRow?.setinvfile, - flippyfile: baseRow?.flippyfile, + flippyfile: uniqueItemDef?.flippyfile ?? setItemDef?.flippyfile ?? baseRow?.flippyfile, dropsound: baseRow?.dropsound, dropsfxframe: baseRow?.dropsfxframe, twoHanded: Boolean(baseRow?.twoHanded || baseRow?.twohanded === '1' || baseRow?.['2handed'] === 1), diff --git a/tests/e2e-automap-113c/tier2-boundary-corner.test.ts b/tests/e2e-automap-113c/tier2-boundary-corner.test.ts index 486995d..70d682f 100644 --- a/tests/e2e-automap-113c/tier2-boundary-corner.test.ts +++ b/tests/e2e-automap-113c/tier2-boundary-corner.test.ts @@ -594,7 +594,7 @@ describe('Tier 2 — Boundary & Corner Cases (F1–F13, >= 5 tests per feature)' 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() registerAtlases(minimap) 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.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.spritesDrawnLastFrame.total).toBeGreaterThan(0) + expect(minimap.lastRenderStats.cellCounts.total).toBe(0) + expect(minimap.spritesDrawnLastFrame.total).toBe(0) }) it('F8.B4: skips all rendering when minimap.visible === false while resetting frame draw stats to 0', () => {