feat: 월드 스냅샷 및 시나리오 부트스트랩 관련 타입 및 로더 구현

This commit is contained in:
2025-12-28 15:26:25 +00:00
parent a4c74b860e
commit 54252dc6b4
8 changed files with 1008 additions and 0 deletions
+2
View File
@@ -33,6 +33,8 @@ Move items into the main docs once they are finalized.
- 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] 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). - [AI suggestion] Implement a DB-backed world loader for turn daemon startup (nation/city/general + scenario config).
- [AI suggestion] Add map/unit set loaders that normalize legacy `CityConstBase`/`GameUnitConstBase` into `MapDefinition`/`UnitSetDefinition` inputs.
- [AI suggestion] Add a DB seed writer that converts `WorldSeedPayload` into DB inserts with defaults and audit logs.
## Legacy Engine Docs ## Legacy Engine Docs
+2
View File
@@ -1,5 +1,7 @@
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 './ports/worldSnapshot.js';
export * from './scenario/index.js'; export * from './scenario/index.js';
export * from './triggers/index.js'; export * from './triggers/index.js';
export * from './world/index.js';
+19
View File
@@ -0,0 +1,19 @@
import type { City, General, Nation } from '../domain/entities.js';
import type { ScenarioConfig } from '../scenario/types.js';
import type { ScenarioMeta } from '../world/types.js';
// DB에서 월드 상태를 로드할 때 사용하는 조회 인터페이스.
export interface WorldStateSnapshotSource<
GeneralType extends General = General,
CityType extends City = City,
NationType extends Nation = Nation
> {
listGenerals(): Promise<GeneralType[]>;
listCities(): Promise<CityType[]>;
listNations(): Promise<NationType[]>;
}
export interface ScenarioConfigSource {
loadScenarioConfig(): Promise<ScenarioConfig>;
loadScenarioMeta?(): Promise<ScenarioMeta | undefined>;
}
+645
View File
@@ -0,0 +1,645 @@
import type {
City,
General,
GeneralTriggerState,
Nation,
TriggerValue,
} from '../domain/entities.js';
import type {
ScenarioDefinition,
ScenarioGeneral,
} from '../scenario/types.js';
import type {
CitySeed,
GeneralSeed,
MapDefaults,
MapDefinition,
NationSeed,
ScenarioMeta,
UnitSetDefinition,
WorldSeedPayload,
WorldSnapshot,
} from './types.js';
export interface ScenarioBootstrapOptions {
includeNeutralNation?: boolean;
includeNeutralNationInSeed?: boolean;
neutralNationName?: string;
neutralNationColor?: string;
defaultGeneralGold?: number;
defaultGeneralRice?: number;
nationTypePrefix?: string;
mapDefaults?: Partial<MapDefaults>;
}
export type ScenarioBootstrapWarningCode =
| 'map_name_mismatch'
| 'unit_set_mismatch'
| 'duplicate_city_name'
| 'unknown_city'
| 'duplicate_city_owner'
| 'unknown_general_city'
| 'unknown_general_nation';
export interface ScenarioBootstrapWarning {
code: ScenarioBootstrapWarningCode;
message: string;
}
export interface ScenarioBootstrapInput {
scenario: ScenarioDefinition;
map: MapDefinition;
unitSet?: UnitSetDefinition;
options?: ScenarioBootstrapOptions;
}
export interface ScenarioBootstrapResult {
snapshot: WorldSnapshot;
seed: WorldSeedPayload;
warnings: ScenarioBootstrapWarning[];
}
const DEFAULT_NEUTRAL_NATION_NAME = '재야';
const DEFAULT_NEUTRAL_NATION_COLOR = '#000000';
const DEFAULT_GENERAL_GOLD = 1000;
const DEFAULT_GENERAL_RICE = 1000;
const DEFAULT_CITY_TRUST = 50;
const DEFAULT_CITY_TRADE = 100;
const DEFAULT_CITY_SUPPLY_STATE = 1;
const DEFAULT_CITY_FRONT_STATE = 0;
const createScenarioMeta = (scenario: ScenarioDefinition): ScenarioMeta => ({
title: scenario.title,
startYear: scenario.startYear,
life: scenario.life,
fiction: scenario.fiction,
history: scenario.history,
ignoreDefaultEvents: scenario.ignoreDefaultEvents,
});
const createEmptyTriggerState = (): GeneralTriggerState => ({
flags: {},
counters: {},
modifiers: {},
meta: {},
});
const addTriggerMeta = (
meta: Record<string, TriggerValue>,
key: string,
value: TriggerValue | null | undefined
): void => {
if (value === null || value === undefined) {
return;
}
meta[key] = value;
};
const resolveMapDefaults = (
map: MapDefinition,
options?: ScenarioBootstrapOptions
): MapDefaults => {
const override = options?.mapDefaults ?? {};
return {
trust:
override.trust ??
map.defaults?.trust ??
DEFAULT_CITY_TRUST,
trade:
override.trade ??
map.defaults?.trade ??
DEFAULT_CITY_TRADE,
supplyState:
override.supplyState ??
map.defaults?.supplyState ??
DEFAULT_CITY_SUPPLY_STATE,
frontState:
override.frontState ??
map.defaults?.frontState ??
DEFAULT_CITY_FRONT_STATE,
};
};
const resolveNationType = (
rawType: string,
prefix: string
): string => {
const trimmed = rawType.trim();
if (!trimmed) {
return `${prefix}중립`;
}
if (trimmed.includes('_')) {
return trimmed;
}
return `${prefix}${trimmed}`;
};
const resolveBirthYear = (
birthYear: number,
startYear: number | null
): number => {
if (birthYear > 0) {
return birthYear;
}
if (startYear !== null) {
return startYear - 20;
}
return 0;
};
const resolveDeathYear = (
deathYear: number,
birthYear: number,
startYear: number | null
): number => {
if (deathYear > 0) {
return deathYear;
}
if (birthYear > 0) {
return birthYear + 60;
}
if (startYear !== null) {
return startYear + 60;
}
return 0;
};
const resolveAge = (startYear: number | null, birthYear: number): number => {
if (startYear === null || birthYear <= 0) {
return 20;
}
return Math.max(startYear - birthYear, 0);
};
const resolveOfficerLevel = (officerLevel: number, nationId: number): number => {
if (officerLevel > 0) {
return officerLevel;
}
if (nationId > 0) {
return 1;
}
return 0;
};
const resolveNationId = (
rawNation: number | string | null,
nationNameToId: Map<string, number>,
warnings: ScenarioBootstrapWarning[],
generalName: string
): number => {
if (rawNation === null) {
return 0;
}
if (typeof rawNation === 'number') {
return rawNation;
}
const nationId = nationNameToId.get(rawNation);
if (nationId === undefined) {
warnings.push({
code: 'unknown_general_nation',
message: `General ${generalName} references unknown nation ${rawNation}.`,
});
return 0;
}
return nationId;
};
const resolveCityId = (
rawCity: string | null,
cityByName: Map<string, { id: number }>,
warnings: ScenarioBootstrapWarning[],
generalName: string
): number => {
if (rawCity === null) {
return 0;
}
const city = cityByName.get(rawCity);
if (!city) {
warnings.push({
code: 'unknown_general_city',
message: `General ${generalName} references unknown city ${rawCity}.`,
});
return 0;
}
return city.id;
};
const buildGeneralSeeds = (
rows: ScenarioGeneral[],
npcType: number,
startId: number,
contextLabel: string,
scenario: ScenarioDefinition,
cityByName: Map<string, { id: number }>,
nationNameToId: Map<string, number>,
warnings: ScenarioBootstrapWarning[],
options?: ScenarioBootstrapOptions
): {
seeds: GeneralSeed[];
generals: General[];
nextId: number;
} => {
const seeds: GeneralSeed[] = [];
const generals: General[] = [];
let nextId = startId;
const defaultGold = options?.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD;
const defaultRice = options?.defaultGeneralRice ?? DEFAULT_GENERAL_RICE;
for (const row of rows) {
const id = nextId;
nextId += 1;
const nationId = resolveNationId(
row.nation,
nationNameToId,
warnings,
row.name
);
const cityId = resolveCityId(
row.city,
cityByName,
warnings,
row.name
);
const birthYear = resolveBirthYear(row.birthYear, scenario.startYear);
const deathYear = resolveDeathYear(
row.deathYear,
birthYear,
scenario.startYear
);
const officerLevel = resolveOfficerLevel(row.officerLevel, nationId);
const age = resolveAge(scenario.startYear, birthYear);
const stats = {
leadership: row.leadership,
strength: row.strength,
intelligence: row.intelligence,
};
const seedMeta: Record<string, unknown> = {
source: contextLabel,
};
if (row.affinity !== null) {
seedMeta.affinity = row.affinity;
}
if (row.personality !== null) {
seedMeta.personality = row.personality;
}
if (row.special !== null) {
seedMeta.special = row.special;
}
if (row.picture !== null) {
seedMeta.picture = row.picture;
}
if (row.text !== null) {
seedMeta.text = row.text;
}
const seed: GeneralSeed = {
id,
name: row.name,
nationId,
cityId,
stats,
officerLevel,
birthYear,
deathYear,
affinity: row.affinity,
personality: row.personality,
special: row.special,
picture: row.picture,
npcType,
text: row.text,
meta: seedMeta,
};
seeds.push(seed);
const generalMeta: Record<string, TriggerValue> = {
npcType,
};
addTriggerMeta(generalMeta, 'affinity', row.affinity);
addTriggerMeta(generalMeta, 'personality', row.personality ?? undefined);
addTriggerMeta(generalMeta, 'special', row.special ?? undefined);
addTriggerMeta(generalMeta, 'picture', row.picture ?? undefined);
addTriggerMeta(generalMeta, 'birthYear', birthYear);
addTriggerMeta(generalMeta, 'deathYear', deathYear);
addTriggerMeta(generalMeta, 'text', row.text ?? undefined);
generals.push({
id,
name: row.name,
nationId,
cityId,
troopId: 0,
stats,
experience: 0,
dedication: 0,
officerLevel,
injury: 0,
gold: defaultGold,
rice: defaultRice,
crew: 0,
train: 0,
age,
npcState: npcType,
triggerState: createEmptyTriggerState(),
meta: generalMeta,
});
}
return { seeds, generals, nextId };
};
// 시나리오, 맵, 유닛셋을 묶어 초기 월드 스냅샷과 시드 데이터를 만든다.
export const buildScenarioBootstrap = (
input: ScenarioBootstrapInput
): ScenarioBootstrapResult => {
const { scenario, map, unitSet, options } = input;
const warnings: ScenarioBootstrapWarning[] = [];
const environment = scenario.config.environment;
if (map.id !== environment.mapName && map.name !== environment.mapName) {
warnings.push({
code: 'map_name_mismatch',
message: `Scenario mapName ${environment.mapName} does not match map definition ${map.id}.`,
});
}
if (
unitSet &&
unitSet.id !== environment.unitSet &&
unitSet.name !== environment.unitSet
) {
warnings.push({
code: 'unit_set_mismatch',
message: `Scenario unitSet ${environment.unitSet} does not match unit set ${unitSet.id}.`,
});
}
const cityByName = new Map<string, { id: number }>();
for (const city of map.cities) {
if (cityByName.has(city.name)) {
warnings.push({
code: 'duplicate_city_name',
message: `Duplicate city name detected: ${city.name}.`,
});
continue;
}
cityByName.set(city.name, city);
}
const nationNameToId = new Map<string, number>();
for (const nation of scenario.nations) {
nationNameToId.set(nation.name, nation.id);
}
const cityOwnership = new Map<number, number>();
const nationCityIds = new Map<number, number[]>();
for (const nation of scenario.nations) {
const cityIds: number[] = [];
for (const cityName of nation.cities) {
const city = cityByName.get(cityName);
if (!city) {
warnings.push({
code: 'unknown_city',
message: `Nation ${nation.name} references unknown city ${cityName}.`,
});
continue;
}
const existing = cityOwnership.get(city.id);
if (existing !== undefined && existing !== nation.id) {
warnings.push({
code: 'duplicate_city_owner',
message: `City ${cityName} is already assigned to nation ${existing}.`,
});
}
cityOwnership.set(city.id, nation.id);
cityIds.push(city.id);
}
nationCityIds.set(nation.id, cityIds);
}
const scenarioMeta = createScenarioMeta(scenario);
const typePrefix =
options?.nationTypePrefix ?? `${environment.mapName}_`;
const seedNations: NationSeed[] = [];
const domainNations: Nation[] = [];
const includeNeutralNation = options?.includeNeutralNation ?? true;
const includeNeutralNationInSeed =
options?.includeNeutralNationInSeed ?? false;
if (includeNeutralNation) {
const neutralNation: Nation = {
id: 0,
name: options?.neutralNationName ?? DEFAULT_NEUTRAL_NATION_NAME,
color: options?.neutralNationColor ?? DEFAULT_NEUTRAL_NATION_COLOR,
capitalCityId: null,
chiefGeneralId: null,
gold: 0,
rice: 0,
power: 0,
level: 0,
typeCode: `${typePrefix}중립`,
meta: {},
};
domainNations.push(neutralNation);
if (includeNeutralNationInSeed) {
seedNations.push({
id: neutralNation.id,
name: neutralNation.name,
color: neutralNation.color,
gold: neutralNation.gold,
rice: neutralNation.rice,
infoText: null,
tech: 0,
typeCode: neutralNation.typeCode,
level: neutralNation.level,
cityIds: [],
capitalCityId: null,
meta: {},
});
}
}
for (const nation of scenario.nations) {
const cityIds = nationCityIds.get(nation.id) ?? [];
const capitalCityId = cityIds[0] ?? null;
const typeCode = resolveNationType(nation.type, typePrefix);
seedNations.push({
id: nation.id,
name: nation.name,
color: nation.color,
gold: nation.gold,
rice: nation.rice,
infoText: nation.infoText,
tech: nation.tech,
typeCode,
level: nation.level,
cityIds,
capitalCityId,
meta: {},
});
const nationMeta: Record<string, TriggerValue> = {
tech: nation.tech,
};
addTriggerMeta(nationMeta, 'infoText', nation.infoText ?? undefined);
domainNations.push({
id: nation.id,
name: nation.name,
color: nation.color,
capitalCityId,
chiefGeneralId: null,
gold: nation.gold,
rice: nation.rice,
power: 0,
level: nation.level,
typeCode,
meta: nationMeta,
});
}
const mapDefaults = resolveMapDefaults(map, options);
const seedCities: CitySeed[] = [];
const domainCities: City[] = [];
for (const city of map.cities) {
const nationId = cityOwnership.get(city.id) ?? 0;
const seed: CitySeed = {
id: city.id,
name: city.name,
nationId,
level: city.level,
population: city.initial.population,
populationMax: city.max.population,
agriculture: city.initial.agriculture,
agricultureMax: city.max.agriculture,
commerce: city.initial.commerce,
commerceMax: city.max.commerce,
security: city.initial.security,
securityMax: city.max.security,
defence: city.initial.defence,
defenceMax: city.max.defence,
wall: city.initial.wall,
wallMax: city.max.wall,
supplyState: mapDefaults.supplyState,
frontState: mapDefaults.frontState,
trust: mapDefaults.trust,
trade: mapDefaults.trade,
region: city.region,
position: city.position,
connections: city.connections,
meta: city.meta ?? {},
};
seedCities.push(seed);
const cityMeta: Record<string, TriggerValue> = {
region: city.region,
trust: seed.trust,
trade: seed.trade,
positionX: city.position.x,
positionY: city.position.y,
};
domainCities.push({
id: seed.id,
name: seed.name,
nationId: seed.nationId,
level: seed.level,
population: seed.population,
populationMax: seed.populationMax,
agriculture: seed.agriculture,
agricultureMax: seed.agricultureMax,
commerce: seed.commerce,
commerceMax: seed.commerceMax,
security: seed.security,
securityMax: seed.securityMax,
supplyState: seed.supplyState,
frontState: seed.frontState,
defence: seed.defence,
defenceMax: seed.defenceMax,
wall: seed.wall,
wallMax: seed.wallMax,
meta: cityMeta,
});
}
let nextGeneralId = 1;
const allGeneralSeeds: GeneralSeed[] = [];
const allGenerals: General[] = [];
const generalResult = buildGeneralSeeds(
scenario.generals,
2,
nextGeneralId,
'general',
scenario,
cityByName,
nationNameToId,
warnings,
options
);
allGeneralSeeds.push(...generalResult.seeds);
allGenerals.push(...generalResult.generals);
nextGeneralId = generalResult.nextId;
const generalExResult = buildGeneralSeeds(
scenario.generalsEx,
2,
nextGeneralId,
'general_ex',
scenario,
cityByName,
nationNameToId,
warnings,
options
);
allGeneralSeeds.push(...generalExResult.seeds);
allGenerals.push(...generalExResult.generals);
nextGeneralId = generalExResult.nextId;
const generalNeutralResult = buildGeneralSeeds(
scenario.generalsNeutral,
6,
nextGeneralId,
'general_neutral',
scenario,
cityByName,
nationNameToId,
warnings,
options
);
allGeneralSeeds.push(...generalNeutralResult.seeds);
allGenerals.push(...generalNeutralResult.generals);
const seed: WorldSeedPayload = {
scenarioConfig: scenario.config,
scenarioMeta,
map,
...(unitSet ? { unitSet } : {}),
nations: seedNations,
cities: seedCities,
generals: allGeneralSeeds,
diplomacy: scenario.diplomacy,
events: scenario.events,
initialEvents: scenario.initialEvents,
};
const snapshot: WorldSnapshot = {
scenarioConfig: scenario.config,
scenarioMeta,
map,
...(unitSet ? { unitSet } : {}),
nations: domainNations,
cities: domainCities,
generals: allGenerals,
diplomacy: scenario.diplomacy,
events: scenario.events,
initialEvents: scenario.initialEvents,
};
return { snapshot, seed, warnings };
};
+3
View File
@@ -0,0 +1,3 @@
export * from './types.js';
export * from './bootstrap.js';
export * from './loader.js';
+68
View File
@@ -0,0 +1,68 @@
import type { City, General, Nation } from '../domain/entities.js';
import type { ScenarioConfig, ScenarioDiplomacy } from '../scenario/types.js';
import type { ScenarioConfigSource, WorldStateSnapshotSource } from '../ports/worldSnapshot.js';
import type {
MapDefinition,
ScenarioMeta,
UnitSetDefinition,
WorldSnapshot,
} from './types.js';
export interface WorldSnapshotLoadInput<
GeneralType extends General = General,
CityType extends City = City,
NationType extends Nation = Nation
> {
worldSource: WorldStateSnapshotSource<GeneralType, CityType, NationType>;
scenarioConfig?: ScenarioConfig;
scenarioMeta?: ScenarioMeta;
scenarioSource?: ScenarioConfigSource;
map: MapDefinition;
unitSet?: UnitSetDefinition;
diplomacy?: ScenarioDiplomacy[];
events?: unknown[];
initialEvents?: unknown[];
}
// DB 기반 월드 로더: 세계 상태와 시나리오 설정을 합쳐 스냅샷을 만든다.
export const loadWorldSnapshot = async <
GeneralType extends General,
CityType extends City,
NationType extends Nation
>(
input: WorldSnapshotLoadInput<GeneralType, CityType, NationType>
): Promise<WorldSnapshot> => {
const { worldSource, scenarioSource } = input;
const scenarioConfig =
input.scenarioConfig ??
(scenarioSource ? await scenarioSource.loadScenarioConfig() : undefined);
if (!scenarioConfig) {
throw new Error('Scenario config is required to load world snapshot.');
}
const scenarioMeta =
input.scenarioMeta ??
(scenarioSource?.loadScenarioMeta
? await scenarioSource.loadScenarioMeta()
: undefined);
const [generals, cities, nations] = await Promise.all([
worldSource.listGenerals(),
worldSource.listCities(),
worldSource.listNations(),
]);
return {
scenarioConfig,
...(scenarioMeta ? { scenarioMeta } : {}),
map: input.map,
...(input.unitSet ? { unitSet: input.unitSet } : {}),
nations,
cities,
generals,
diplomacy: input.diplomacy ?? [],
events: input.events ?? [],
initialEvents: input.initialEvents ?? [],
};
};
+148
View File
@@ -0,0 +1,148 @@
import type { City, General, Nation, StatBlock } from '../domain/entities.js';
import type {
ScenarioConfig,
ScenarioDiplomacy,
} from '../scenario/types.js';
export interface ScenarioMeta {
title: string;
startYear: number | null;
life: number | null;
fiction: number | null;
history: string[];
ignoreDefaultEvents: boolean;
}
export interface MapCityStats {
population: number;
agriculture: number;
commerce: number;
security: number;
defence: number;
wall: number;
}
export interface MapCityDefinition {
id: number;
name: string;
level: number;
region: number;
position: {
x: number;
y: number;
};
connections: number[];
max: MapCityStats;
initial: MapCityStats;
meta?: Record<string, unknown>;
}
export interface MapDefaults {
trust: number;
trade: number;
supplyState: number;
frontState: number;
}
export interface MapDefinition {
id: string;
name: string;
cities: MapCityDefinition[];
defaults?: Partial<MapDefaults>;
meta?: Record<string, unknown>;
}
export interface UnitSetDefinition {
id: string;
name: string;
meta?: Record<string, unknown>;
}
export interface NationSeed {
id: number;
name: string;
color: string;
gold: number;
rice: number;
infoText: string | null;
tech: number;
typeCode: string;
level: number;
cityIds: number[];
capitalCityId: number | null;
meta: Record<string, unknown>;
}
export interface CitySeed {
id: number;
name: string;
nationId: number;
level: number;
population: number;
populationMax: number;
agriculture: number;
agricultureMax: number;
commerce: number;
commerceMax: number;
security: number;
securityMax: number;
defence: number;
defenceMax: number;
wall: number;
wallMax: number;
supplyState: number;
frontState: number;
trust: number;
trade: number;
region: number;
position: {
x: number;
y: number;
};
connections: number[];
meta: Record<string, unknown>;
}
export interface GeneralSeed {
id: number;
name: string;
nationId: number;
cityId: number;
stats: StatBlock;
officerLevel: number;
birthYear: number;
deathYear: number;
affinity: number | null;
personality: string | null;
special: string | null;
picture: number | string | null;
npcType: number;
text: string | null;
meta: Record<string, unknown>;
}
export interface WorldSeedPayload {
scenarioConfig: ScenarioConfig;
scenarioMeta: ScenarioMeta;
map: MapDefinition;
unitSet?: UnitSetDefinition;
nations: NationSeed[];
cities: CitySeed[];
generals: GeneralSeed[];
diplomacy: ScenarioDiplomacy[];
events: unknown[];
initialEvents: unknown[];
}
export interface WorldSnapshot {
scenarioConfig: ScenarioConfig;
scenarioMeta?: ScenarioMeta;
map: MapDefinition;
unitSet?: UnitSetDefinition;
nations: Nation[];
cities: City[];
generals: General[];
diplomacy: ScenarioDiplomacy[];
events: unknown[];
initialEvents: unknown[];
}
+121
View File
@@ -0,0 +1,121 @@
import { describe, expect, it } from 'vitest';
import type { ScenarioDefinition } from '../src/scenario/types.js';
import type { MapDefinition, UnitSetDefinition } from '../src/world/types.js';
import { buildScenarioBootstrap } from '../src/world/bootstrap.js';
describe('scenario bootstrap', () => {
it('builds snapshot and seed from scenario/map inputs', () => {
const scenario: ScenarioDefinition = {
title: 'Test Scenario',
startYear: 200,
life: null,
fiction: null,
history: [],
config: {
stat: {
total: 100,
min: 10,
max: 70,
npcTotal: 80,
npcMax: 60,
npcMin: 5,
chiefMin: 50,
},
iconPath: '.',
map: {},
const: {},
environment: {
mapName: 'test-map',
unitSet: 'test-unit',
},
},
nations: [
{
id: 1,
name: 'TestNation',
color: '#123456',
gold: 5000,
rice: 3000,
infoText: 'Test nation',
tech: 100,
type: 'Test',
level: 3,
cities: ['Alpha'],
},
],
diplomacy: [],
generals: [
{
affinity: 10,
name: 'TestGeneral',
picture: 101,
nation: 1,
city: 'Alpha',
leadership: 50,
strength: 60,
intelligence: 55,
officerLevel: 1,
birthYear: 180,
deathYear: 240,
personality: 'Calm',
special: 'Special',
text: 'Test line',
},
],
generalsEx: [],
generalsNeutral: [],
cities: [],
events: [],
initialEvents: [],
ignoreDefaultEvents: false,
};
const map: MapDefinition = {
id: 'test-map',
name: 'test-map',
cities: [
{
id: 1,
name: 'Alpha',
level: 3,
region: 1,
position: { x: 10, y: 20 },
connections: [],
max: {
population: 100000,
agriculture: 20000,
commerce: 20000,
security: 15000,
defence: 5000,
wall: 3000,
},
initial: {
population: 50000,
agriculture: 10000,
commerce: 10000,
security: 8000,
defence: 2500,
wall: 1500,
},
},
],
};
const unitSet: UnitSetDefinition = {
id: 'test-unit',
name: 'test-unit',
};
const result = buildScenarioBootstrap({ scenario, map, unitSet });
expect(result.warnings).toHaveLength(0);
expect(result.snapshot.nations).toHaveLength(2);
expect(result.seed.nations).toHaveLength(1);
expect(result.snapshot.cities[0]?.nationId).toBe(1);
expect(result.seed.cities[0]?.nationId).toBe(1);
expect(result.snapshot.generals[0]?.cityId).toBe(1);
expect(result.seed.generals[0]?.npcType).toBe(2);
expect(result.snapshot.scenarioMeta?.title).toBe('Test Scenario');
});
});