80 lines
3.7 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
|