import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { parseScenarioDefaults, parseScenarioDefinition } from '../src/scenario/parseScenario.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..', '..', '..'); const scenarioRoot = path.join(repoRoot, 'resources', 'scenario'); const readJson = async (filePath: string): Promise => { const raw = await fs.readFile(filePath, 'utf8'); return JSON.parse(raw) as unknown; }; describe('scenario parser', () => { it('reads defaults from the tracked legacy-format default.json', async () => { const defaultsRaw = await readJson(path.join(scenarioRoot, 'default.json')); const defaults = parseScenarioDefaults(defaultsRaw); expect(defaults.stat.total).toBe(165); expect(defaults.stat.min).toBe(15); expect(defaults.stat.max).toBe(80); expect(defaults.stat.npcTotal).toBe(150); expect(defaults.stat.npcMax).toBe(75); expect(defaults.stat.npcMin).toBe(10); expect(defaults.stat.chiefMin).toBe(65); expect(defaults.iconPath).toBe('.'); }); it('defaults map/unit set when scenario omits map config', async () => { const defaultsRaw = await readJson(path.join(scenarioRoot, 'default.json')); const scenarioRaw = await readJson(path.join(scenarioRoot, 'scenario_0.json')); const defaults = parseScenarioDefaults(defaultsRaw); const scenario = parseScenarioDefinition(scenarioRaw, defaults); expect(scenario.config.environment.mapName).toBe('che'); expect(scenario.config.environment.unitSet).toBe('che'); }); it('parses nation/general rows from a legacy scenario', async () => { const defaultsRaw = await readJson(path.join(scenarioRoot, 'default.json')); const scenarioRaw = await readJson(path.join(scenarioRoot, 'scenario_1010.json')); const defaults = parseScenarioDefaults(defaultsRaw); const scenario = parseScenarioDefinition(scenarioRaw, defaults); expect(scenario.nations.length).toBeGreaterThan(0); expect(scenario.generals.length).toBeGreaterThan(0); const firstNation = scenario.nations[0]!; expect(firstNation.name).toBe('후한'); expect(firstNation.color).toBe('#800000'); const firstGeneral = scenario.generals[0]!; expect(firstGeneral.name).toBe('소제1'); expect(firstGeneral.affinity).toBe(1); expect(firstGeneral.picture).toBe('장수/소제1.jpg'); expect(firstGeneral.nation).toBe(1); expect(firstGeneral.city).toBe(null); expect(firstGeneral.leadership).toBe(20); expect(firstGeneral.personality).toBe('유지'); expect(firstGeneral.special).toBe(null); }); it('reuses the canonical name-based icon in a scenario that omitted numeric pictures', async () => { const defaultsRaw = await readJson(path.join(scenarioRoot, 'default.json')); const scenarioRaw = await readJson(path.join(scenarioRoot, 'scenario_2220.json')); const defaults = parseScenarioDefaults(defaultsRaw); const scenario = parseScenarioDefinition(scenarioRaw, defaults); expect(scenario.generals[0]).toMatchObject({ name: '아회남', picture: '장수/아회남.jpg', }); }); it('keeps the complete seven-scenario effect inventory typed and executable', async () => { const defaults = parseScenarioDefaults(await readJson(path.join(scenarioRoot, 'default.json'))); const expected = new Map([ [906, 'event_StrongAttacker'], [911, 'event_UnlimitedDefenceThresholdChange'], [913, 'event_MoreEffect'], [2703, 'event_StrongAttacker'], [2704, 'event_StrongAttacker'], [2903, 'event_StrongAttacker'], [2904, 'event_StrongAttacker'], ]); for (const [scenarioId, scenarioEffect] of expected) { const raw = await readJson(path.join(scenarioRoot, `scenario_${scenarioId}.json`)); expect(parseScenarioDefinition(raw, defaults).config.environment.scenarioEffect).toBe(scenarioEffect); } }); it('fails before seeding when a scenario references an unknown effect', async () => { const defaults = parseScenarioDefaults(await readJson(path.join(scenarioRoot, 'default.json'))); const raw = (await readJson(path.join(scenarioRoot, 'scenario_0.json'))) as Record; raw.const = { ...((raw.const as Record | undefined) ?? {}), scenarioEffect: 'event_Missing', }; expect(() => parseScenarioDefinition(raw, defaults)).toThrow(); }); });