feat(wip): 시나리오 로더 및 파서 구현, 관련 타입 추가
This commit is contained in:
@@ -10,7 +10,8 @@
|
|||||||
"test": "vitest run --config vitest.config.ts"
|
"test": "vitest run --config vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sammo-ts/common": "workspace:*"
|
"@sammo-ts/common": "workspace:*",
|
||||||
|
"@sammo-ts/logic": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "^0.18.3",
|
"tsdown": "^0.18.3",
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './lifecycle/clock.js';
|
|||||||
export * from './lifecycle/inMemoryControlQueue.js';
|
export * from './lifecycle/inMemoryControlQueue.js';
|
||||||
export * from './lifecycle/turnSchedule.js';
|
export * from './lifecycle/turnSchedule.js';
|
||||||
export * from './lifecycle/turnDaemonLifecycle.js';
|
export * from './lifecycle/turnDaemonLifecycle.js';
|
||||||
|
export * from './scenario/scenarioLoader.js';
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseScenarioDefaults,
|
||||||
|
parseScenarioDefinition,
|
||||||
|
type ScenarioDefaults,
|
||||||
|
type ScenarioDefinition,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
export interface ScenarioLoaderOptions {
|
||||||
|
scenarioRoot: string;
|
||||||
|
defaultsFileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readJsonFile = async (filePath: string): Promise<unknown> => {
|
||||||
|
const raw = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveScenarioDefaultsPath = (
|
||||||
|
options: ScenarioLoaderOptions
|
||||||
|
): string =>
|
||||||
|
path.resolve(
|
||||||
|
options.scenarioRoot,
|
||||||
|
options.defaultsFileName ?? 'default.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resolveScenarioPath = (
|
||||||
|
options: ScenarioLoaderOptions,
|
||||||
|
scenarioId: number
|
||||||
|
): string => path.resolve(options.scenarioRoot, `scenario_${scenarioId}.json`);
|
||||||
|
|
||||||
|
export const loadScenarioDefaults = async (
|
||||||
|
defaultsPath: string
|
||||||
|
): Promise<ScenarioDefaults> => {
|
||||||
|
// 기본 시나리오 파일을 읽고 정규화한다.
|
||||||
|
const raw = await readJsonFile(defaultsPath);
|
||||||
|
return parseScenarioDefaults(raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadScenarioDefinition = async (
|
||||||
|
scenarioPath: string,
|
||||||
|
defaults: ScenarioDefaults
|
||||||
|
): Promise<ScenarioDefinition> => {
|
||||||
|
// 시나리오 본문을 읽고 기본값과 합쳐서 파싱한다.
|
||||||
|
const raw = await readJsonFile(scenarioPath);
|
||||||
|
return parseScenarioDefinition(raw, defaults);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadScenarioDefinitionById = async (
|
||||||
|
scenarioId: number,
|
||||||
|
options: ScenarioLoaderOptions
|
||||||
|
): Promise<ScenarioDefinition> => {
|
||||||
|
// 시나리오 번호로 파일을 찾고 파싱한다.
|
||||||
|
const defaultsPath = resolveScenarioDefaultsPath(options);
|
||||||
|
const scenarioPath = resolveScenarioPath(options, scenarioId);
|
||||||
|
const defaults = await loadScenarioDefaults(defaultsPath);
|
||||||
|
return loadScenarioDefinition(scenarioPath, defaults);
|
||||||
|
};
|
||||||
@@ -31,6 +31,8 @@ Move items into the main docs once they are finalized.
|
|||||||
|
|
||||||
- "Next-turn intent" (예턴) data schema and lifecycle
|
- "Next-turn intent" (예턴) data schema and lifecycle
|
||||||
- Profile selection workflow and deployment mapping
|
- Profile selection workflow and deployment mapping
|
||||||
|
- [AI suggestion] Define a scenario bootstrap pipeline that turns scenario/map/unit set inputs into a world snapshot or DB seed payload.
|
||||||
|
- [AI suggestion] Implement a DB-backed world loader for turn daemon startup (nation/city/general + scenario config).
|
||||||
|
|
||||||
## Legacy Engine Docs
|
## Legacy Engine Docs
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"test": "vitest run --config vitest.config.ts"
|
"test": "vitest run --config vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sammo-ts/common": "workspace:*"
|
"@sammo-ts/common": "workspace:*",
|
||||||
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "^0.18.3",
|
"tsdown": "^0.18.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './domain/entities.js';
|
export * from './domain/entities.js';
|
||||||
export type { RandomGenerator } from '@sammo-ts/common';
|
export type { RandomGenerator } from '@sammo-ts/common';
|
||||||
export * from './ports/world.js';
|
export * from './ports/world.js';
|
||||||
|
export * from './scenario/index.js';
|
||||||
export * from './triggers/index.js';
|
export * from './triggers/index.js';
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types.js';
|
||||||
|
export * from './parseScenario.js';
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import type {
|
||||||
|
ScenarioConfig,
|
||||||
|
ScenarioDefaults,
|
||||||
|
ScenarioDefinition,
|
||||||
|
ScenarioDiplomacy,
|
||||||
|
ScenarioEnvironment,
|
||||||
|
ScenarioGeneral,
|
||||||
|
ScenarioNation,
|
||||||
|
ScenarioStatBlock,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
type UnknownRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
const FALLBACK_STAT: ScenarioStatBlock = {
|
||||||
|
total: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
npcTotal: 0,
|
||||||
|
npcMax: 0,
|
||||||
|
npcMin: 0,
|
||||||
|
chiefMin: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRecord = (value: unknown): value is UnknownRecord =>
|
||||||
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
|
||||||
|
const asArray = (value: unknown): unknown[] =>
|
||||||
|
Array.isArray(value) ? value : [];
|
||||||
|
|
||||||
|
const asRecord = (value: unknown): UnknownRecord =>
|
||||||
|
isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
const asNumber = (value: unknown, fallback: number): number =>
|
||||||
|
typeof value === 'number' ? value : fallback;
|
||||||
|
|
||||||
|
const asString = (value: unknown, fallback: string): string =>
|
||||||
|
typeof value === 'string' ? value : fallback;
|
||||||
|
|
||||||
|
const asNullableNumber = (value: unknown): number | null =>
|
||||||
|
typeof value === 'number' ? value : null;
|
||||||
|
|
||||||
|
const asNullableString = (value: unknown): string | null =>
|
||||||
|
typeof value === 'string' ? value : null;
|
||||||
|
|
||||||
|
const asStringArray = (value: unknown): string[] =>
|
||||||
|
asArray(value).filter((item): item is string => typeof item === 'string');
|
||||||
|
|
||||||
|
const parseScenarioStatBlock = (
|
||||||
|
value: unknown,
|
||||||
|
fallback: ScenarioStatBlock
|
||||||
|
): ScenarioStatBlock => {
|
||||||
|
const data = asRecord(value);
|
||||||
|
return {
|
||||||
|
total: asNumber(data.total, fallback.total),
|
||||||
|
min: asNumber(data.min, fallback.min),
|
||||||
|
max: asNumber(data.max, fallback.max),
|
||||||
|
npcTotal: asNumber(data.npcTotal, fallback.npcTotal),
|
||||||
|
npcMax: asNumber(data.npcMax, fallback.npcMax),
|
||||||
|
npcMin: asNumber(data.npcMin, fallback.npcMin),
|
||||||
|
chiefMin: asNumber(data.chiefMin, fallback.chiefMin),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseScenarioEnvironment = (
|
||||||
|
mapConfig: UnknownRecord,
|
||||||
|
constConfig: UnknownRecord
|
||||||
|
): ScenarioEnvironment => {
|
||||||
|
const merged = { ...mapConfig, ...constConfig };
|
||||||
|
const mapName = asString(merged.mapName, 'che');
|
||||||
|
const unitSet = asString(merged.unitSet, 'che');
|
||||||
|
const scenarioEffect =
|
||||||
|
typeof merged.scenarioEffect === 'string' || merged.scenarioEffect === null
|
||||||
|
? merged.scenarioEffect
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return { mapName, unitSet, scenarioEffect };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureTitle = (value: unknown): string => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error('Scenario title must be a string.');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseNationRow = (row: unknown, index: number): ScenarioNation => {
|
||||||
|
if (!Array.isArray(row)) {
|
||||||
|
throw new Error(`Scenario nation row ${index} is not an array.`);
|
||||||
|
}
|
||||||
|
const [
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
gold,
|
||||||
|
rice,
|
||||||
|
infoText,
|
||||||
|
tech,
|
||||||
|
type,
|
||||||
|
level,
|
||||||
|
cities,
|
||||||
|
] = row;
|
||||||
|
|
||||||
|
const nationName = asString(name, '');
|
||||||
|
if (!nationName) {
|
||||||
|
throw new Error(`Scenario nation row ${index} has no name.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
name: nationName,
|
||||||
|
color: asString(color, '#000000'),
|
||||||
|
gold: asNumber(gold, 0),
|
||||||
|
rice: asNumber(rice, 0),
|
||||||
|
infoText: asNullableString(infoText),
|
||||||
|
tech: asNumber(tech, 0),
|
||||||
|
type: asString(type, ''),
|
||||||
|
level: asNumber(level, 0),
|
||||||
|
cities: asStringArray(cities),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDiplomacyRow = (row: unknown, index: number): ScenarioDiplomacy => {
|
||||||
|
if (!Array.isArray(row)) {
|
||||||
|
throw new Error(`Scenario diplomacy row ${index} is not an array.`);
|
||||||
|
}
|
||||||
|
const [fromNationId, toNationId, state, durationMonths] = row;
|
||||||
|
return {
|
||||||
|
fromNationId: asNumber(fromNationId, 0),
|
||||||
|
toNationId: asNumber(toNationId, 0),
|
||||||
|
state: asNumber(state, 0),
|
||||||
|
durationMonths: asNumber(durationMonths, 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseGeneralRow = (
|
||||||
|
row: unknown,
|
||||||
|
index: number,
|
||||||
|
label: string
|
||||||
|
): ScenarioGeneral => {
|
||||||
|
if (!Array.isArray(row)) {
|
||||||
|
throw new Error(`Scenario ${label} row ${index} is not an array.`);
|
||||||
|
}
|
||||||
|
const values = [...row];
|
||||||
|
while (values.length < 14) {
|
||||||
|
values.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
affinity,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
nation,
|
||||||
|
city,
|
||||||
|
leadership,
|
||||||
|
strength,
|
||||||
|
intelligence,
|
||||||
|
officerLevel,
|
||||||
|
birthYear,
|
||||||
|
deathYear,
|
||||||
|
personality,
|
||||||
|
special,
|
||||||
|
text,
|
||||||
|
] = values;
|
||||||
|
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
throw new Error(`Scenario ${label} row ${index} has no name.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
affinity: asNullableNumber(affinity),
|
||||||
|
name,
|
||||||
|
picture:
|
||||||
|
typeof picture === 'number' || typeof picture === 'string'
|
||||||
|
? picture
|
||||||
|
: null,
|
||||||
|
nation:
|
||||||
|
typeof nation === 'number' || typeof nation === 'string'
|
||||||
|
? nation
|
||||||
|
: null,
|
||||||
|
city: asNullableString(city),
|
||||||
|
leadership: asNumber(leadership, 0),
|
||||||
|
strength: asNumber(strength, 0),
|
||||||
|
intelligence: asNumber(intelligence, 0),
|
||||||
|
officerLevel: asNumber(officerLevel, 0),
|
||||||
|
birthYear: asNumber(birthYear, 0),
|
||||||
|
deathYear: asNumber(deathYear, 0),
|
||||||
|
personality: asNullableString(personality),
|
||||||
|
special: asNullableString(special),
|
||||||
|
text: asNullableString(text),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseGeneralRows = (rows: unknown[], label: string): ScenarioGeneral[] =>
|
||||||
|
rows.map((row, index) => parseGeneralRow(row, index, label));
|
||||||
|
|
||||||
|
const parseNationRows = (rows: unknown[]): ScenarioNation[] =>
|
||||||
|
rows.map((row, index) => parseNationRow(row, index));
|
||||||
|
|
||||||
|
const parseDiplomacyRows = (rows: unknown[]): ScenarioDiplomacy[] =>
|
||||||
|
rows.map((row, index) => parseDiplomacyRow(row, index));
|
||||||
|
|
||||||
|
export const parseScenarioDefaults = (raw: unknown): ScenarioDefaults => {
|
||||||
|
// 기본 시나리오 설정값을 안전하게 읽는다.
|
||||||
|
const data = asRecord(raw);
|
||||||
|
const stat = parseScenarioStatBlock(data.stat, FALLBACK_STAT);
|
||||||
|
const iconPath = asString(data.iconPath, '.');
|
||||||
|
return { stat, iconPath };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseScenarioDefinition = (
|
||||||
|
raw: unknown,
|
||||||
|
defaults: ScenarioDefaults
|
||||||
|
): ScenarioDefinition => {
|
||||||
|
// 시나리오 JSON을 런타임에서 쓰는 구조로 정규화한다.
|
||||||
|
const data = asRecord(raw);
|
||||||
|
const stat = parseScenarioStatBlock(data.stat, defaults.stat);
|
||||||
|
const mapConfig = asRecord(data.map);
|
||||||
|
const constConfig = asRecord(data.const);
|
||||||
|
const config: ScenarioConfig = {
|
||||||
|
stat,
|
||||||
|
iconPath: asString(data.iconPath, defaults.iconPath),
|
||||||
|
map: mapConfig,
|
||||||
|
const: constConfig,
|
||||||
|
environment: parseScenarioEnvironment(mapConfig, constConfig),
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = ensureTitle(data.title);
|
||||||
|
const startYear =
|
||||||
|
typeof data.startYear === 'number' ? data.startYear : null;
|
||||||
|
const life = typeof data.life === 'number' ? data.life : null;
|
||||||
|
const fiction = typeof data.fiction === 'number' ? data.fiction : null;
|
||||||
|
const history = asStringArray(data.history);
|
||||||
|
const ignoreDefaultEvents = Boolean(data.ignoreDefaultEvents);
|
||||||
|
const nations = parseNationRows(asArray(data.nation));
|
||||||
|
const diplomacy = parseDiplomacyRows(asArray(data.diplomacy));
|
||||||
|
const generals = parseGeneralRows(asArray(data.general), 'general');
|
||||||
|
const generalsEx = parseGeneralRows(asArray(data.general_ex), 'general_ex');
|
||||||
|
const generalsNeutral = parseGeneralRows(
|
||||||
|
asArray(data.general_neutral),
|
||||||
|
'general_neutral'
|
||||||
|
);
|
||||||
|
const events = asArray(data.events);
|
||||||
|
const initialEvents = asArray(
|
||||||
|
data.initialEvents ?? data.initialActions ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
startYear,
|
||||||
|
life,
|
||||||
|
fiction,
|
||||||
|
history,
|
||||||
|
config,
|
||||||
|
nations,
|
||||||
|
diplomacy,
|
||||||
|
generals,
|
||||||
|
generalsEx,
|
||||||
|
generalsNeutral,
|
||||||
|
cities: asArray(data.cities),
|
||||||
|
events,
|
||||||
|
initialEvents,
|
||||||
|
ignoreDefaultEvents,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
export interface ScenarioStatBlock {
|
||||||
|
total: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
npcTotal: number;
|
||||||
|
npcMax: number;
|
||||||
|
npcMin: number;
|
||||||
|
chiefMin: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioDefaults {
|
||||||
|
stat: ScenarioStatBlock;
|
||||||
|
iconPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioEnvironment {
|
||||||
|
mapName: string;
|
||||||
|
unitSet: string;
|
||||||
|
scenarioEffect?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioConfig {
|
||||||
|
stat: ScenarioStatBlock;
|
||||||
|
iconPath: string;
|
||||||
|
map: Record<string, unknown>;
|
||||||
|
const: Record<string, unknown>;
|
||||||
|
environment: ScenarioEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioNation {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
gold: number;
|
||||||
|
rice: number;
|
||||||
|
infoText: string | null;
|
||||||
|
tech: number;
|
||||||
|
type: string;
|
||||||
|
level: number;
|
||||||
|
cities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioDiplomacy {
|
||||||
|
fromNationId: number;
|
||||||
|
toNationId: number;
|
||||||
|
state: number;
|
||||||
|
durationMonths: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioGeneral {
|
||||||
|
affinity: number | null;
|
||||||
|
name: string;
|
||||||
|
picture: number | string | null;
|
||||||
|
nation: number | string | null;
|
||||||
|
city: string | null;
|
||||||
|
leadership: number;
|
||||||
|
strength: number;
|
||||||
|
intelligence: number;
|
||||||
|
officerLevel: number;
|
||||||
|
birthYear: number;
|
||||||
|
deathYear: number;
|
||||||
|
personality: string | null;
|
||||||
|
special: string | null;
|
||||||
|
text: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioDefinition {
|
||||||
|
title: string;
|
||||||
|
startYear: number | null;
|
||||||
|
life: number | null;
|
||||||
|
fiction: number | null;
|
||||||
|
history: string[];
|
||||||
|
config: ScenarioConfig;
|
||||||
|
nations: ScenarioNation[];
|
||||||
|
diplomacy: ScenarioDiplomacy[];
|
||||||
|
generals: ScenarioGeneral[];
|
||||||
|
generalsEx: ScenarioGeneral[];
|
||||||
|
generalsNeutral: ScenarioGeneral[];
|
||||||
|
cities: unknown[];
|
||||||
|
events: unknown[];
|
||||||
|
initialEvents: unknown[];
|
||||||
|
ignoreDefaultEvents: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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, 'legacy', 'hwe', 'scenario');
|
||||||
|
|
||||||
|
const readJson = async (filePath: string): Promise<unknown> => {
|
||||||
|
const raw = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('scenario parser', () => {
|
||||||
|
it('reads defaults from legacy 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(1001);
|
||||||
|
expect(firstGeneral.nation).toBe(1);
|
||||||
|
expect(firstGeneral.city).toBe(null);
|
||||||
|
expect(firstGeneral.leadership).toBe(20);
|
||||||
|
expect(firstGeneral.personality).toBe('유지');
|
||||||
|
expect(firstGeneral.special).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+11
@@ -29,6 +29,9 @@ importers:
|
|||||||
'@sammo-ts/common':
|
'@sammo-ts/common':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/common
|
version: link:../../packages/common
|
||||||
|
'@sammo-ts/logic':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/logic
|
||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.3
|
specifier: ^0.18.3
|
||||||
@@ -81,6 +84,9 @@ importers:
|
|||||||
'@sammo-ts/common':
|
'@sammo-ts/common':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../common
|
version: link:../common
|
||||||
|
zod:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.3
|
specifier: ^0.18.3
|
||||||
@@ -957,6 +963,9 @@ packages:
|
|||||||
yallist@4.0.0:
|
yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
|
zod@4.2.1:
|
||||||
|
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/generator@7.28.5':
|
'@babel/generator@7.28.5':
|
||||||
@@ -1647,3 +1656,5 @@ snapshots:
|
|||||||
stackback: 0.0.2
|
stackback: 0.0.2
|
||||||
|
|
||||||
yallist@4.0.0: {}
|
yallist@4.0.0: {}
|
||||||
|
|
||||||
|
zod@4.2.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user