fix(monsters): include superunique and minion monster IDs in PlannedLevel types
Ensure monster art is loaded for all superunique bosses and their minions to prevent red placeholder boxes when they spawn. TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
9d568ad0c3
commit
04d562a99c
|
|
@ -1567,10 +1567,27 @@ export function planLevelMonsters(
|
||||||
packs.unshift(...suPacks)
|
packs.unshift(...suPacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allMonsterTypes = [...types.map(kind => kind.id)]
|
||||||
|
for (const pack of packs) {
|
||||||
|
for (const member of pack.members) {
|
||||||
|
if (!allMonsterTypes.includes(member.id)) {
|
||||||
|
allMonsterTypes.push(member.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const suSpec of suSpecs) {
|
||||||
|
if (!allMonsterTypes.includes(suSpec.monsterId)) {
|
||||||
|
allMonsterTypes.push(suSpec.monsterId)
|
||||||
|
}
|
||||||
|
if (!allMonsterTypes.includes(suSpec.minionMonsterId)) {
|
||||||
|
allMonsterTypes.push(suSpec.minionMonsterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
levelId,
|
levelId,
|
||||||
levelName: plan.levelName,
|
levelName: plan.levelName,
|
||||||
types: types.map(kind => kind.id),
|
types: allMonsterTypes,
|
||||||
budget,
|
budget,
|
||||||
packs,
|
packs,
|
||||||
elitePacks: groups.filter(group => group.rank !== 'normal').length + superUniques.length,
|
elitePacks: groups.filter(group => group.rank !== 'normal').length + superUniques.length,
|
||||||
|
|
|
||||||
|
|
@ -760,13 +760,16 @@ async function buildPackRuntime(
|
||||||
spawn,
|
spawn,
|
||||||
npcs: scene.npcs ?? [],
|
npcs: scene.npcs ?? [],
|
||||||
monsterPacks: scene.monsters?.packs ?? [],
|
monsterPacks: scene.monsters?.packs ?? [],
|
||||||
monsterTypes: scene.monsters?.types ?? [],
|
monsterTypes: Array.from(new Set([
|
||||||
|
...(scene.monsters?.types ?? []),
|
||||||
|
...(scene.monsters?.packs ?? []).flatMap(p => p.members.map(m => m.id)),
|
||||||
|
])),
|
||||||
objects: scene.objects.length,
|
objects: scene.objects.length,
|
||||||
frames: scene.frames.length,
|
frames: scene.frames.length,
|
||||||
notes: [
|
notes: [
|
||||||
`首屏页 ${priority.join(',')},共 ${String(pages.length)} 页`,
|
`首屏页 ${priority.join(',')},共 ${String(pages.length)} 页`,
|
||||||
...(scene.monsters?.packs && scene.monsters.packs.length > 0
|
...(scene.monsters?.packs && scene.monsters.packs.length > 0
|
||||||
? [`怪物:${String(scene.monsters.budget)} 只 / ${String(scene.monsters.packs.length)} 群(${scene.monsters.types.join('、')})`]
|
? [`怪物:${String(scene.monsters.budget)} 只 / ${String(scene.monsters.packs.length)} 群(${Array.from(new Set([...(scene.monsters.types ?? []), ...(scene.monsters.packs ?? []).flatMap(p => p.members.map(m => m.id))])).join('、')})`]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
pages,
|
pages,
|
||||||
|
|
@ -977,7 +980,10 @@ async function loadLiveRuntime(
|
||||||
spawn: findIsoSpawn(scene) ?? { x: scene.widthPx / 2, y: scene.heightPx / 2 },
|
spawn: findIsoSpawn(scene) ?? { x: scene.widthPx / 2, y: scene.heightPx / 2 },
|
||||||
npcs: [],
|
npcs: [],
|
||||||
monsterPacks: population.packs,
|
monsterPacks: population.packs,
|
||||||
monsterTypes: population.types,
|
monsterTypes: Array.from(new Set([
|
||||||
|
...population.types,
|
||||||
|
...population.packs.flatMap(p => p.members.map(m => m.id)),
|
||||||
|
])),
|
||||||
objects: level.objects.length,
|
objects: level.objects.length,
|
||||||
frames: scene.frames.length,
|
frames: scene.frames.length,
|
||||||
notes,
|
notes,
|
||||||
|
|
@ -1899,7 +1905,8 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
||||||
const paletteForEntities = next.palette
|
const paletteForEntities = next.palette
|
||||||
const packEntityBase = next.source === 'pack' ? next.base : undefined
|
const packEntityBase = next.source === 'pack' ? next.base : undefined
|
||||||
const packEntityAct = next.source === 'pack' ? next.act : undefined
|
const packEntityAct = next.source === 'pack' ? next.act : undefined
|
||||||
const activeMonsterIds = next.monsterTypes
|
const packMonsterIds = next.monsterPacks.flatMap(p => p.members.map(m => m.id))
|
||||||
|
const activeMonsterIds = Array.from(new Set([...next.monsterTypes, ...packMonsterIds]))
|
||||||
if (activeMonsterIds.length > 0) {
|
if (activeMonsterIds.length > 0) {
|
||||||
void loadMonsterArtMap(
|
void loadMonsterArtMap(
|
||||||
DEFAULT_BASES,
|
DEFAULT_BASES,
|
||||||
|
|
@ -2440,7 +2447,8 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
||||||
const packEntityBase = runtime.source === 'pack' ? runtime.base : undefined
|
const packEntityBase = runtime.source === 'pack' ? runtime.base : undefined
|
||||||
const packEntityAct = runtime.source === 'pack' ? runtime.act : undefined
|
const packEntityAct = runtime.source === 'pack' ? runtime.act : undefined
|
||||||
// Collect monster types to load
|
// Collect monster types to load
|
||||||
const activeMonsterIds = runtime.monsterTypes
|
const packMonsterIds = runtime.monsterPacks.flatMap(p => p.members.map(m => m.id))
|
||||||
|
const activeMonsterIds = Array.from(new Set([...runtime.monsterTypes, ...packMonsterIds]))
|
||||||
|
|
||||||
if (activeMonsterIds.length > 0) {
|
if (activeMonsterIds.length > 0) {
|
||||||
void loadMonsterArtMap(
|
void loadMonsterArtMap(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ import { describe, expect, it } from 'vitest'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { monsterPlanStatusText } from '../src/scene/act-scene.ts'
|
import { monsterPlanStatusText } from '../src/scene/act-scene.ts'
|
||||||
import * as demoData from '../src/game/demo-data.ts'
|
import * as demoData from '../src/game/demo-data.ts'
|
||||||
|
import { planLevelMonsters } from '../src/game/monsters.ts'
|
||||||
|
import { parseTable } from '../src/game/acts.ts'
|
||||||
|
import type { D2Table } from '../src/game/acts.ts'
|
||||||
|
|
||||||
|
function mockTable(tsv: string): D2Table {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
return parseTable(encoder.encode(tsv.trim()))
|
||||||
|
}
|
||||||
|
|
||||||
const ACT_SCENE_SOURCE = readFileSync(new URL('../src/scene/act-scene.ts', import.meta.url), 'utf8')
|
const ACT_SCENE_SOURCE = readFileSync(new URL('../src/scene/act-scene.ts', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
|
@ -43,4 +51,29 @@ describe('no synthetic monster fallback', () => {
|
||||||
expect(monsterPlanStatusText(0)).toContain('怪物 0')
|
expect(monsterPlanStatusText(0)).toContain('怪物 0')
|
||||||
expect(monsterPlanStatusText(37)).toBe(' · 怪物 37')
|
expect(monsterPlanStatusText(37)).toBe(' · 怪物 37')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('includes superunique boss and minion monster IDs in PlannedLevel.types', () => {
|
||||||
|
const levelsTxt = mockTable([
|
||||||
|
'Id\tName\tMonDen\tMonUMin\tMonUMax\tMonLvl1Ex\tm1\tm2\tm3',
|
||||||
|
'110\tBloody Foothills\t600\t2\t4\t35\tsiegebeast1\tdemonimp1\tenflayer1',
|
||||||
|
].join('\n'))
|
||||||
|
const monstatsTxt = mockTable([
|
||||||
|
'Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee',
|
||||||
|
'demonimp1\tdemonimp1\tDemon Imp\t35\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1',
|
||||||
|
'overseer1\toverseer1\tOverseer\t35\t1\t2\t7\t10\t10\t35\t16\t26\t3\t6\t30\t1\t1\t1\t1',
|
||||||
|
'siegebeast1\tsiegebeast1\tSiege Beast\t35\t1\t2\t6\t8\t15\t50\t20\t35\t4\t8\t30\t1\t1\t1\t1',
|
||||||
|
].join('\n'))
|
||||||
|
const monlvlTxt = mockTable([
|
||||||
|
'Level\tAC\tTH\tHP\tDM\tXP',
|
||||||
|
'35\t100\t100\t100\t100\t100',
|
||||||
|
].join('\n'))
|
||||||
|
const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt }
|
||||||
|
|
||||||
|
const plan = planLevelMonsters(tables, 110, 240 * 48, 11042, 3.5, 'normal')
|
||||||
|
// overseer2 is Shenk's boss id, and enslaved1 is his minion id, neither in m1..m3 (siegebeast1, demonimp1, enflayer1)
|
||||||
|
expect(plan.types).toContain('overseer2')
|
||||||
|
expect(plan.types).toContain('enslaved1')
|
||||||
|
expect(plan.types).toContain('demonimp1')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue