diablo2-web/tests/act-scene-monster-plan.test.ts

80 lines
3.7 KiB
TypeScript

/**
* The act scene must not invent monsters (issue #58 item 5).
*
* `DEMO_MONSTERS` was three hand-written monsters the scene spawned 12 of
* whenever a level's plan came back empty — including in a town whose name did
* not happen to contain the English substring "town". They also rendered as red
* boxes, because `demo-*` ids are in no art table, so a data failure looked like
* content twice over.
*/
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { monsterPlanStatusText } from '../src/scene/act-scene.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')
/**
* Comments are stripped before scanning: the docstrings deliberately still name
* the deleted fallback to explain why it is gone, and that is not a code path.
*/
const ACT_SCENE_CODE = ACT_SCENE_SOURCE.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
describe('no synthetic monster fallback', () => {
it('demo-data no longer exports a monster roster', () => {
expect(Object.keys(demoData)).not.toContain('DEMO_MONSTERS')
// The other demo tables are still used by the scene's engine options.
expect(Object.keys(demoData)).toContain('DEMO_SKILLS')
})
it('the act scene has no placeholder spawn path left', () => {
expect(ACT_SCENE_CODE).not.toContain('DEMO_MONSTERS')
expect(ACT_SCENE_CODE).not.toContain('PLACEHOLDER_MONSTER_COUNT')
// The spawn gate was an English substring match on the *display* name, so a
// renamed or translated town spawned 12 fake monsters in the square. (The
// pack index's `slug.includes('town')` level lookup is a different thing
// and is still there.)
expect(ACT_SCENE_CODE).not.toContain("toLowerCase().includes('town')")
expect(ACT_SCENE_CODE).not.toContain('spawnMonsters(')
})
it('reports an empty plan rather than hiding it', () => {
expect(monsterPlanStatusText(0)).toContain('本关未规划到怪物')
expect(monsterPlanStatusText(0)).toContain('怪物 0')
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 minion1 is his minion id, neither in m1..m3 (siegebeast1, demonimp1, enflayer1)
expect(plan.types).toContain('overseer2')
expect(plan.types).toContain('minion1')
expect(plan.types).toContain('imp3')
})
})