/** * 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') }) })