diff --git a/src/game/monster-mapping.ts b/src/game/monster-mapping.ts index 4c9661e..236f66d 100644 --- a/src/game/monster-mapping.ts +++ b/src/game/monster-mapping.ts @@ -109,11 +109,6 @@ export const MONSTER_ART_MAP: Record = { "bloodlord5": { token: "L3", weapon: "hth" }, "bloodlord6": { token: "L3", weapon: "hth" }, "bloodlord7": { token: "L3", weapon: "hth" }, - "bloodmage1": { token: "XX", weapon: "hth" }, - "bloodmage2": { token: "XX", weapon: "hth" }, - "bloodmage3": { token: "XX", weapon: "hth" }, - "bloodmage4": { token: "XX", weapon: "hth" }, - "bloodmage5": { token: "XX", weapon: "hth" }, "bloodraven": { token: "CR", weapon: "bow" }, "blunderbore1": { token: "PN", weapon: "hth" }, "blunderbore2": { token: "PN", weapon: "hth" }, @@ -212,11 +207,6 @@ export const MONSTER_ART_MAP: Record = { "crownest3": { token: "BN", weapon: "hth" }, "crownest4": { token: "BN", weapon: "hth" }, "cycleoflife": { token: "K9", weapon: "hth" }, - "darkguard1": { token: "XX", weapon: "hth" }, - "darkguard2": { token: "XX", weapon: "hth" }, - "darkguard3": { token: "XX", weapon: "hth" }, - "darkguard4": { token: "XX", weapon: "hth" }, - "darkguard5": { token: "XX", weapon: "hth" }, "darkwanderer": { token: "1Z", weapon: "hth" }, "deathmauler1": { token: "M5", weapon: "hth" }, "deathmauler2": { token: "M5", weapon: "hth" }, @@ -345,7 +335,7 @@ export const MONSTER_ART_MAP: Record = { "griswold": { token: "GZ", weapon: "hth" }, "halbu": { token: "20", weapon: "hth" }, "heartofwolverine": { token: "X3", weapon: "hth" }, - "hellbovine": { token: "CW", weapon: "hth" }, + "hellbovine": { token: "EC", weapon: "hth" }, "hellmeteor": { token: "K9", weapon: "hth" }, "hephasto": { token: "5P", weapon: "hth" }, "hratli": { token: "HR", weapon: "hth" }, @@ -375,7 +365,6 @@ export const MONSTER_ART_MAP: Record = { "kashya": { token: "RC", weapon: "hth" }, "larva": { token: "LV", weapon: "hth" }, "larzuk": { token: "XR", weapon: "hth" }, - "lightningbeast": { token: "XX", weapon: "hth" }, "lightningsentry": { token: "LG", weapon: "hth" }, "lightningspire": { token: "AE", weapon: "hth" }, "lysander": { token: "LY", weapon: "hth" }, @@ -409,17 +398,6 @@ export const MONSTER_ART_MAP: Record = { "mephistospirit": { token: "M6", weapon: "hth" }, "meshif1": { token: "MS", weapon: "hth" }, "meshif2": { token: "M3", weapon: "hth" }, - "minion1": { token: "XX", weapon: "hth" }, - "minion10": { token: "XX", weapon: "hth" }, - "minion11": { token: "XX", weapon: "hth" }, - "minion2": { token: "XX", weapon: "hth" }, - "minion3": { token: "XX", weapon: "hth" }, - "minion4": { token: "XX", weapon: "hth" }, - "minion5": { token: "XX", weapon: "hth" }, - "minion6": { token: "XX", weapon: "hth" }, - "minion7": { token: "XX", weapon: "hth" }, - "minion8": { token: "XX", weapon: "hth" }, - "minion9": { token: "XX", weapon: "hth" }, "minionspawner1": { token: "XA", weapon: "hth" }, "minionspawner2": { token: "XA", weapon: "hth" }, "minionspawner3": { token: "XA", weapon: "hth" }, @@ -609,7 +587,6 @@ export const MONSTER_ART_MAP: Record = { "snowyeti2": { token: "IO", weapon: "hth" }, "snowyeti3": { token: "IO", weapon: "hth" }, "snowyeti4": { token: "IO", weapon: "hth" }, - "spiritmummy": { token: "XX", weapon: "hth" }, "spiritofbarbs": { token: "X4", weapon: "hth" }, "spiritwolf": { token: "WF", weapon: "hth" }, "succubus1": { token: "0B", weapon: "hth" }, @@ -765,7 +742,7 @@ export const ACT_MONSTER_SPECS: Record = { 2: [{ token: "FA", weapon: "hth" }, { token: "ZM", weapon: "hth" }, { token: "SI", weapon: "hth" }, { token: "YE", weapon: "hth" }, { token: "SK", weapon: "hth" }, { token: "CW", weapon: "hth" }, { token: "SL", weapon: "hth" }, { token: "PW", weapon: "hth" }, { token: "SC", weapon: "hth" }, { token: "VD", weapon: "hth" }, { token: "PW", weapon: "1ht" }, { token: "BN", weapon: "hth" }, { token: "SM", weapon: "hth" }, { token: "SW", weapon: "hth" }, { token: "SR", weapon: "hth" }, { token: "SD", weapon: "hth" }, { token: "MM", weapon: "hth" }, { token: "GY", weapon: "hth" }, { token: "SK", weapon: "1hs" }, { token: "SK", weapon: "bow" }, { token: "BB", weapon: "hth" }, { token: "PN", weapon: "hth" }, { token: "BT", weapon: "hth" }, { token: "WR", weapon: "hth" }, { token: "VA", weapon: "hth" }, { token: "GM", weapon: "2hs" }], 3: [{ token: "FA", weapon: "hth" }, { token: "ZM", weapon: "hth" }, { token: "SI", weapon: "hth" }, { token: "YE", weapon: "hth" }, { token: "SK", weapon: "hth" }, { token: "CW", weapon: "hth" }, { token: "BB", weapon: "hth" }, { token: "MO", weapon: "hth" }, { token: "TH", weapon: "hth" }, { token: "FE", weapon: "1hs" }, { token: "FC", weapon: "hth" }, { token: "BK", weapon: "hth" }, { token: "FD", weapon: "hth" }, { token: "FW", weapon: "hth" }, { token: "WW", weapon: "hth" }, { token: "VD", weapon: "hth" }, { token: "SL", weapon: "hth" }, { token: "ZZ", weapon: "hth" }, { token: "SW", weapon: "hth" }, { token: "ZP", weapon: "hth" }, { token: "SR", weapon: "hth" }, { token: "VA", weapon: "hth" }, { token: "SP", weapon: "hth" }, { token: "BT", weapon: "hth" }, { token: "SM", weapon: "hth" }, { token: "MM", weapon: "hth" }, { token: "FK", weapon: "1hs" }, { token: "WR", weapon: "hth" }, { token: "GY", weapon: "hth" }, { token: "CR", weapon: "1hs" }, { token: "SD", weapon: "hth" }, { token: "SC", weapon: "hth" }, { token: "CR", weapon: "bow" }, { token: "PN", weapon: "hth" }], 4: [{ token: "FA", weapon: "hth" }, { token: "ZM", weapon: "hth" }, { token: "SI", weapon: "hth" }, { token: "YE", weapon: "hth" }, { token: "SK", weapon: "hth" }, { token: "CW", weapon: "hth" }, { token: "SL", weapon: "hth" }, { token: "VM", weapon: "hth" }, { token: "FR", weapon: "hth" }, { token: "CS", weapon: "hth" }, { token: "UM", weapon: "fgt" }, { token: "DM", weapon: "hth" }, { token: "WW", weapon: "hth" }, { token: "BH", weapon: "hth" }, { token: "BT", weapon: "hth" }, { token: "UM", weapon: "hth" }, { token: "SM", weapon: "hth" }, { token: "PN", weapon: "hth" }, { token: "UM", weapon: "mgk" }], - 5: [{ token: "FA", weapon: "hth" }, { token: "ZM", weapon: "hth" }, { token: "SI", weapon: "hth" }, { token: "YE", weapon: "hth" }, { token: "SK", weapon: "hth" }, { token: "CW", weapon: "hth" }, { token: "XX", weapon: "hth" }, { token: "M5", weapon: "hth" }, { token: "OS", weapon: "hth" }, { token: "IP", weapon: "hth" }, { token: "SK", weapon: "bow" }, { token: "BH", weapon: "hth" }, { token: "OX", weapon: "hth" }, { token: "F0", weapon: "hth" }, { token: "0B", weapon: "hth" }, { token: "L3", weapon: "hth" }, { token: "IO", weapon: "hth" }, { token: "RE", weapon: "hth" }, { token: "0D", weapon: "hth" }, { token: "0F", weapon: "hth" }, { token: "0E", weapon: "hth" }, { token: "45", weapon: "hth" }, { token: "0C", weapon: "hth" }, { token: "XY", weapon: "hth" }, { token: "CR", weapon: "1hs" }, { token: "CR", weapon: "bow" }, { token: "CR", weapon: "2ht" }, { token: "SW", weapon: "hth" }, { token: "SM", weapon: "hth" }, { token: "SP", weapon: "hth" }, { token: "SC", weapon: "hth" }, { token: "UM", weapon: "mgk" }, { token: "DM", weapon: "hth" }], + 5: [{ token: "FA", weapon: "hth" }, { token: "ZM", weapon: "hth" }, { token: "SI", weapon: "hth" }, { token: "YE", weapon: "hth" }, { token: "SK", weapon: "hth" }, { token: "CW", weapon: "hth" }, { token: "M5", weapon: "hth" }, { token: "OS", weapon: "hth" }, { token: "IP", weapon: "hth" }, { token: "SK", weapon: "bow" }, { token: "BH", weapon: "hth" }, { token: "OX", weapon: "hth" }, { token: "F0", weapon: "hth" }, { token: "0B", weapon: "hth" }, { token: "L3", weapon: "hth" }, { token: "IO", weapon: "hth" }, { token: "RE", weapon: "hth" }, { token: "0D", weapon: "hth" }, { token: "0F", weapon: "hth" }, { token: "0E", weapon: "hth" }, { token: "45", weapon: "hth" }, { token: "0C", weapon: "hth" }, { token: "XY", weapon: "hth" }, { token: "CR", weapon: "1hs" }, { token: "CR", weapon: "bow" }, { token: "CR", weapon: "2ht" }, { token: "SW", weapon: "hth" }, { token: "SM", weapon: "hth" }, { token: "SP", weapon: "hth" }, { token: "SC", weapon: "hth" }, { token: "UM", weapon: "mgk" }, { token: "DM", weapon: "hth" }], } /** @@ -773,13 +750,15 @@ export const ACT_MONSTER_SPECS: Record = { */ export function resolveMonsterArtSpec(id: string): MonsterArtSpec | null { const lower = id.toLowerCase() - if (MONSTER_ART_MAP[lower]) { - return MONSTER_ART_MAP[lower] + const direct = MONSTER_ART_MAP[lower] + if (direct && direct.token !== 'XX' && direct.token.trim() !== '') { + return direct } const base = lower.replace(/\d+$/, '') - for (const [key, spec] of Object.entries(MONSTER_ART_MAP)) { - if (key.startsWith(base)) { - return spec + if (base !== lower) { + const baseSpec = MONSTER_ART_MAP[base] + if (baseSpec && baseSpec.token !== 'XX' && baseSpec.token.trim() !== '') { + return baseSpec } } return null diff --git a/tests/monster-mapping.test.ts b/tests/monster-mapping.test.ts index 5c38e11..a72546a 100644 --- a/tests/monster-mapping.test.ts +++ b/tests/monster-mapping.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { resolveMonsterArtSpec, MONSTER_ART_MAP } from '../src/game/monster-mapping.ts' +import { resolveMonsterArtSpec, MONSTER_ART_MAP, ACT_MONSTER_SPECS } from '../src/game/monster-mapping.ts' describe('monster-mapping', () => { it('resolves standard Act 1 monsters', () => { @@ -10,13 +10,36 @@ describe('monster-mapping', () => { expect(resolveMonsterArtSpec('cr_archer1')).toEqual({ token: 'CR', weapon: 'bow' }) }) - it('resolves numbered variants through prefix fallback', () => { + it('resolves numbered variants through exact or base lookup without prefix iteration order', () => { expect(resolveMonsterArtSpec('fallen3')?.token).toBe('FA') expect(resolveMonsterArtSpec('zombie5')?.token).toBe('ZM') expect(resolveMonsterArtSpec('quillrat6')?.token).toBe('SI') + // Suffix with arbitrary letters must not prefix-match + expect(resolveMonsterArtSpec('fallen_fake')).toBeNull() + }) + + it('resolves hellbovine to EC token instead of CW', () => { + expect(resolveMonsterArtSpec('hellbovine')).toEqual({ token: 'EC', weapon: 'hth' }) + expect(MONSTER_ART_MAP.hellbovine).toEqual({ token: 'EC', weapon: 'hth' }) + }) + + it('does not leak XX tokens or crash on cut monsters', () => { + const cutMonsters = ['bloodmage1', 'darkguard1', 'lightningbeast', 'minion1', 'spiritmummy'] + for (const id of cutMonsters) { + expect(resolveMonsterArtSpec(id)).toBeNull() + } + }) + + it('ACT_MONSTER_SPECS does not contain any XX tokens in any act', () => { + for (const [act, specs] of Object.entries(ACT_MONSTER_SPECS)) { + for (const spec of specs) { + expect(spec.token, `Act ${act} contains invalid token ${spec.token}`).not.toBe('XX') + } + } }) it('returns null for unknown monsters', () => { expect(resolveMonsterArtSpec('non_existent_monster_xyz')).toBeNull() }) }) + diff --git a/tests/packed-entity-art.test.ts b/tests/packed-entity-art.test.ts index fd52739..52b8e78 100644 --- a/tests/packed-entity-art.test.ts +++ b/tests/packed-entity-art.test.ts @@ -11,7 +11,7 @@ describe('offline pre-baked entity atlases & full Act 1..5 pack index (Issues #3 it('maps 700+ monster IDs across Acts 1..5 in MONSTER_ART_MAP', () => { expect(Object.keys(MONSTER_ART_MAP).length).toBeGreaterThanOrEqual(700) expect(MONSTER_ART_MAP.fallen).toEqual({ token: 'FA', weapon: 'hth' }) - expect(MONSTER_ART_MAP.hellbovine).toEqual({ token: 'CW', weapon: 'hth' }) + expect(MONSTER_ART_MAP.hellbovine).toEqual({ token: 'EC', weapon: 'hth' }) expect(MONSTER_ART_MAP.skeleton).toEqual({ token: 'SK', weapon: 'hth' }) }) @@ -103,7 +103,7 @@ describe('offline pre-baked entity atlases & full Act 1..5 pack index (Issues #3 uniqueSpecs.add(`${spec.token.toLowerCase()}-${spec.weapon.toLowerCase()}`) } } - expect(uniqueSpecs.size).toBe(66) + expect(uniqueSpecs.size).toBe(65) for (const key of uniqueSpecs) { const slug = `monster-${key}`