feat: refactor database connection handling and add legacy map loading functionality
- Updated check-connections.mts to use Prisma with PostgreSQL adapter. - Introduced databaseUrl.ts for resolving database URLs from environment variables. - Added legacyMapLoader.ts to load and parse legacy map data. - Implemented scenarioSeeder.ts to seed scenario data into the database. - Created tests for scenario seeding in scenarioSeeder.test.ts. - Configured Prisma datasource in prisma.config.ts to support dynamic database URLs.
This commit is contained in:
@@ -4,3 +4,6 @@ export * from './lifecycle/inMemoryControlQueue.js';
|
||||
export * from './lifecycle/turnDaemonLifecycle.js';
|
||||
export * from './lifecycle/getNextTickTime.js';
|
||||
export * from './scenario/scenarioLoader.js';
|
||||
export * from './scenario/databaseUrl.js';
|
||||
export * from './scenario/legacyMapLoader.js';
|
||||
export * from './scenario/scenarioSeeder.js';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const DEFAULT_ENV_FILE = path.resolve(__dirname, '..', '..', '..', '..', '.env.ci');
|
||||
|
||||
type EnvMap = Record<string, string | undefined>;
|
||||
|
||||
export interface DatabaseUrlOptions {
|
||||
envFile?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
const parseEnvFile = (rawText: string): EnvMap => {
|
||||
const env: EnvMap = {};
|
||||
const lines = rawText.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const index = trimmed.indexOf('=');
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
const key = trimmed.slice(0, index).trim();
|
||||
let value = trimmed.slice(index + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
};
|
||||
|
||||
const loadEnvFile = async (envFile: string): Promise<EnvMap> => {
|
||||
try {
|
||||
const text = await fs.readFile(envFile, 'utf8');
|
||||
return parseEnvFile(text);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveDatabaseUrl = async (
|
||||
options?: DatabaseUrlOptions
|
||||
): Promise<string> => {
|
||||
const env = options?.env ?? process.env;
|
||||
if (env.DATABASE_URL) {
|
||||
return env.DATABASE_URL;
|
||||
}
|
||||
|
||||
const envFile = options?.envFile ?? DEFAULT_ENV_FILE;
|
||||
const fileEnv = await loadEnvFile(envFile);
|
||||
if (fileEnv.DATABASE_URL) {
|
||||
return fileEnv.DATABASE_URL;
|
||||
}
|
||||
|
||||
const host = env.POSTGRES_HOST ?? fileEnv.POSTGRES_HOST ?? '127.0.0.1';
|
||||
const port = env.POSTGRES_PORT ?? fileEnv.POSTGRES_PORT ?? '15432';
|
||||
const user = env.POSTGRES_USER ?? fileEnv.POSTGRES_USER ?? 'sammo';
|
||||
const password = env.POSTGRES_PASSWORD ?? fileEnv.POSTGRES_PASSWORD ?? '';
|
||||
const dbName = env.POSTGRES_DB ?? fileEnv.POSTGRES_DB ?? 'sammo';
|
||||
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
|
||||
};
|
||||
@@ -0,0 +1,427 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type {
|
||||
MapCityDefinition,
|
||||
MapCityStats,
|
||||
MapDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const DEFAULT_LEGACY_MAP_ROOT = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'legacy',
|
||||
'hwe',
|
||||
'scenario',
|
||||
'map'
|
||||
);
|
||||
const DEFAULT_LEGACY_BASE_FILE = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'legacy',
|
||||
'hwe',
|
||||
'sammo',
|
||||
'CityConstBase.php'
|
||||
);
|
||||
|
||||
const LEVEL_MAP: Record<string, number> = {
|
||||
'수': 1,
|
||||
'진': 2,
|
||||
'관': 3,
|
||||
'이': 4,
|
||||
'소': 5,
|
||||
'중': 6,
|
||||
'대': 7,
|
||||
'특': 8,
|
||||
};
|
||||
|
||||
const LEVEL_LABELS: Record<number, string> = Object.entries(LEVEL_MAP)
|
||||
.reduce<Record<number, string>>((acc, [label, value]) => {
|
||||
acc[value] = label;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const REGION_MAP: Record<string, number> = {
|
||||
'하북': 1,
|
||||
'중원': 2,
|
||||
'서북': 3,
|
||||
'서촉': 4,
|
||||
'남중': 5,
|
||||
'초': 6,
|
||||
'오월': 7,
|
||||
'동이': 8,
|
||||
};
|
||||
|
||||
const BUILD_INIT_COMMON = {
|
||||
trust: 50,
|
||||
trade: 100,
|
||||
};
|
||||
|
||||
const BUILD_INIT: Record<string, MapCityStats> = {
|
||||
'수': {
|
||||
population: 5000,
|
||||
agriculture: 100,
|
||||
commerce: 100,
|
||||
security: 100,
|
||||
defence: 500,
|
||||
wall: 500,
|
||||
},
|
||||
'진': {
|
||||
population: 5000,
|
||||
agriculture: 100,
|
||||
commerce: 100,
|
||||
security: 100,
|
||||
defence: 500,
|
||||
wall: 500,
|
||||
},
|
||||
'관': {
|
||||
population: 10000,
|
||||
agriculture: 100,
|
||||
commerce: 100,
|
||||
security: 100,
|
||||
defence: 1000,
|
||||
wall: 1000,
|
||||
},
|
||||
'이': {
|
||||
population: 50000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
defence: 1000,
|
||||
wall: 1000,
|
||||
},
|
||||
'소': {
|
||||
population: 100000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
defence: 2000,
|
||||
wall: 2000,
|
||||
},
|
||||
'중': {
|
||||
population: 100000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
defence: 3000,
|
||||
wall: 3000,
|
||||
},
|
||||
'대': {
|
||||
population: 150000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
defence: 4000,
|
||||
wall: 4000,
|
||||
},
|
||||
'특': {
|
||||
population: 150000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
defence: 5000,
|
||||
wall: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_SUPPLY_STATE = 1;
|
||||
const DEFAULT_FRONT_STATE = 0;
|
||||
|
||||
interface LegacyCityRow {
|
||||
id: number;
|
||||
name: string;
|
||||
level: string | number;
|
||||
population: number;
|
||||
agriculture: number;
|
||||
commerce: number;
|
||||
security: number;
|
||||
defence: number;
|
||||
wall: number;
|
||||
region: string | number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
connectionNames: string[];
|
||||
}
|
||||
|
||||
export interface LegacyMapLoaderOptions {
|
||||
mapRoot?: string;
|
||||
baseFilePath?: string;
|
||||
}
|
||||
|
||||
const readFileOrNull = async (filePath: string): Promise<string | null> => {
|
||||
try {
|
||||
return await fs.readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const extractPhpArray = (source: string, marker: string): string | null => {
|
||||
const markerIndex = source.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
const start = source.indexOf('[', markerIndex);
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let inString: '"' | "'" | null = null;
|
||||
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const char = source[i];
|
||||
if (inString) {
|
||||
if (char === '\\') {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === inString) {
|
||||
inString = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
inString = char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '[') {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === ']') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const stripPhpComments = (source: string): string =>
|
||||
source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/#.*$/gm, '');
|
||||
|
||||
const normalizePhpArray = (source: string): string =>
|
||||
stripPhpComments(source)
|
||||
.replace(/\bNULL\b/gi, 'null')
|
||||
.replace(/'/g, '"')
|
||||
.replace(/,(\s*[\]\}])/g, '$1');
|
||||
|
||||
const parseLegacyCityRows = (value: unknown): LegacyCityRow[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Legacy map data is not an array.');
|
||||
}
|
||||
|
||||
return value.map((row, index) => {
|
||||
if (!Array.isArray(row)) {
|
||||
throw new Error(`Legacy map row ${index} is not an array.`);
|
||||
}
|
||||
const [
|
||||
id,
|
||||
name,
|
||||
level,
|
||||
population,
|
||||
agriculture,
|
||||
commerce,
|
||||
security,
|
||||
defence,
|
||||
wall,
|
||||
region,
|
||||
positionX,
|
||||
positionY,
|
||||
connections,
|
||||
] = row;
|
||||
|
||||
if (typeof id !== 'number' || typeof name !== 'string') {
|
||||
throw new Error(`Legacy map row ${index} has invalid id/name.`);
|
||||
}
|
||||
if (
|
||||
typeof level !== 'string' &&
|
||||
typeof level !== 'number'
|
||||
) {
|
||||
throw new Error(`Legacy map row ${index} has invalid level.`);
|
||||
}
|
||||
const stats = [
|
||||
population,
|
||||
agriculture,
|
||||
commerce,
|
||||
security,
|
||||
defence,
|
||||
wall,
|
||||
];
|
||||
if (stats.some((value) => typeof value !== 'number')) {
|
||||
throw new Error(`Legacy map row ${index} has invalid stats.`);
|
||||
}
|
||||
if (
|
||||
typeof region !== 'string' &&
|
||||
typeof region !== 'number'
|
||||
) {
|
||||
throw new Error(`Legacy map row ${index} has invalid region.`);
|
||||
}
|
||||
if (
|
||||
typeof positionX !== 'number' ||
|
||||
typeof positionY !== 'number'
|
||||
) {
|
||||
throw new Error(`Legacy map row ${index} has invalid position.`);
|
||||
}
|
||||
|
||||
const connectionNames = Array.isArray(connections)
|
||||
? connections.filter(
|
||||
(value): value is string => typeof value === 'string'
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
level,
|
||||
population,
|
||||
agriculture,
|
||||
commerce,
|
||||
security,
|
||||
defence,
|
||||
wall,
|
||||
region,
|
||||
positionX,
|
||||
positionY,
|
||||
connectionNames,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const resolveLevelLabel = (level: string | number): string => {
|
||||
if (typeof level === 'string') {
|
||||
return level;
|
||||
}
|
||||
const label = LEVEL_LABELS[level];
|
||||
if (!label) {
|
||||
throw new Error(`Unknown level value: ${level}`);
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
const resolveLevelValue = (level: string | number): number => {
|
||||
if (typeof level === 'number') {
|
||||
return level;
|
||||
}
|
||||
const value = LEVEL_MAP[level];
|
||||
if (!value) {
|
||||
throw new Error(`Unknown level label: ${level}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const resolveRegionValue = (region: string | number): number => {
|
||||
if (typeof region === 'number') {
|
||||
return region;
|
||||
}
|
||||
const value = REGION_MAP[region];
|
||||
if (!value) {
|
||||
throw new Error(`Unknown region label: ${region}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const buildCityDefinition = (
|
||||
row: LegacyCityRow,
|
||||
nameToId: Map<string, number>
|
||||
): MapCityDefinition => {
|
||||
const levelLabel = resolveLevelLabel(row.level);
|
||||
const initial = BUILD_INIT[levelLabel];
|
||||
if (!initial) {
|
||||
throw new Error(`Missing build init for level ${levelLabel}.`);
|
||||
}
|
||||
|
||||
const connections = row.connectionNames
|
||||
.map((name) => nameToId.get(name))
|
||||
.filter((value): value is number => typeof value === 'number');
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
level: resolveLevelValue(row.level),
|
||||
region: resolveRegionValue(row.region),
|
||||
position: {
|
||||
x: row.positionX,
|
||||
y: row.positionY,
|
||||
},
|
||||
connections,
|
||||
max: {
|
||||
population: row.population * 100,
|
||||
agriculture: row.agriculture * 100,
|
||||
commerce: row.commerce * 100,
|
||||
security: row.security * 100,
|
||||
defence: row.defence * 100,
|
||||
wall: row.wall * 100,
|
||||
},
|
||||
initial,
|
||||
meta: {
|
||||
source: 'legacy',
|
||||
connectionNames: row.connectionNames,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const loadLegacyMapDefinition = async (
|
||||
mapName: string,
|
||||
options?: LegacyMapLoaderOptions
|
||||
): Promise<MapDefinition> => {
|
||||
const mapRoot = options?.mapRoot ?? DEFAULT_LEGACY_MAP_ROOT;
|
||||
const baseFilePath = options?.baseFilePath ?? DEFAULT_LEGACY_BASE_FILE;
|
||||
const mapFilePath = path.resolve(mapRoot, `${mapName}.php`);
|
||||
|
||||
const [mapSource, baseSource] = await Promise.all([
|
||||
readFileOrNull(mapFilePath),
|
||||
readFileOrNull(baseFilePath),
|
||||
]);
|
||||
|
||||
if (!baseSource) {
|
||||
throw new Error(`Legacy base map file is missing: ${baseFilePath}`);
|
||||
}
|
||||
|
||||
const mapInitCity =
|
||||
(mapSource
|
||||
? extractPhpArray(mapSource, 'protected static $initCity')
|
||||
: null) ??
|
||||
extractPhpArray(baseSource, 'protected static $initCity');
|
||||
|
||||
if (!mapInitCity) {
|
||||
throw new Error(`Legacy map data not found for ${mapName}.`);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(normalizePhpArray(mapInitCity)) as unknown;
|
||||
const rows = parseLegacyCityRows(parsed);
|
||||
const nameToId = new Map(rows.map((row) => [row.name, row.id]));
|
||||
|
||||
return {
|
||||
id: mapName,
|
||||
name: mapName,
|
||||
cities: rows.map((row) => buildCityDefinition(row, nameToId)),
|
||||
defaults: {
|
||||
trust: BUILD_INIT_COMMON.trust,
|
||||
trade: BUILD_INIT_COMMON.trade,
|
||||
supplyState: DEFAULT_SUPPLY_STATE,
|
||||
frontState: DEFAULT_FRONT_STATE,
|
||||
},
|
||||
meta: {
|
||||
source: 'legacy',
|
||||
mapName,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import { createPostgresConnector } from '@sammo-ts/infra';
|
||||
import {
|
||||
buildScenarioBootstrap,
|
||||
type ScenarioBootstrapWarning,
|
||||
type WorldSeedPayload,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import type { LegacyMapLoaderOptions } from './legacyMapLoader.js';
|
||||
import { loadLegacyMapDefinition } from './legacyMapLoader.js';
|
||||
import type { ScenarioLoaderOptions } from './scenarioLoader.js';
|
||||
import { loadScenarioDefinitionById } from './scenarioLoader.js';
|
||||
|
||||
const DEFAULT_TICK_SECONDS = 120 * 60;
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
const DEFAULT_GENERAL_RICE = 1000;
|
||||
|
||||
export interface ScenarioSeedOptions {
|
||||
scenarioId: number;
|
||||
databaseUrl: string;
|
||||
scenarioOptions?: ScenarioLoaderOptions;
|
||||
mapOptions?: LegacyMapLoaderOptions;
|
||||
resetTables?: boolean;
|
||||
now?: Date;
|
||||
tickSeconds?: number;
|
||||
includeNeutralNationInSeed?: boolean;
|
||||
defaultGeneralGold?: number;
|
||||
defaultGeneralRice?: number;
|
||||
}
|
||||
|
||||
export interface ScenarioSeedResult {
|
||||
seed: WorldSeedPayload;
|
||||
warnings: ScenarioBootstrapWarning[];
|
||||
}
|
||||
|
||||
const asJson = (value: unknown): Prisma.InputJsonValue =>
|
||||
value as Prisma.InputJsonValue;
|
||||
|
||||
const resolveGeneralAge = (
|
||||
startYear: number | null,
|
||||
birthYear: number
|
||||
): number => {
|
||||
if (startYear === null || birthYear <= 0) {
|
||||
return 20;
|
||||
}
|
||||
return Math.max(startYear - birthYear, 0);
|
||||
};
|
||||
|
||||
const buildEventRows = (
|
||||
rows: unknown[],
|
||||
targetOverride?: string
|
||||
): Prisma.EventCreateManyInput[] => {
|
||||
const result: Prisma.EventCreateManyInput[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (!Array.isArray(row)) {
|
||||
continue;
|
||||
}
|
||||
if (targetOverride) {
|
||||
const [condition, ...actions] = row;
|
||||
result.push({
|
||||
targetCode: targetOverride,
|
||||
priority: 0,
|
||||
condition: asJson(condition ?? null),
|
||||
action: asJson(actions),
|
||||
meta: asJson({ source: targetOverride }),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const [target, priority, condition, ...actions] = row;
|
||||
if (typeof target !== 'string' || typeof priority !== 'number') {
|
||||
continue;
|
||||
}
|
||||
result.push({
|
||||
targetCode: target,
|
||||
priority,
|
||||
condition: asJson(condition ?? null),
|
||||
action: asJson(actions),
|
||||
meta: asJson({ source: 'scenario' }),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 시나리오 초기 데이터를 로드해 DB에 저장한다.
|
||||
export const seedScenarioToDatabase = async (
|
||||
options: ScenarioSeedOptions
|
||||
): Promise<ScenarioSeedResult> => {
|
||||
const scenario = await loadScenarioDefinitionById(
|
||||
options.scenarioId,
|
||||
options.scenarioOptions
|
||||
);
|
||||
const map = await loadLegacyMapDefinition(
|
||||
scenario.config.environment.mapName,
|
||||
options.mapOptions
|
||||
);
|
||||
|
||||
const { seed, warnings } = buildScenarioBootstrap({
|
||||
scenario,
|
||||
map,
|
||||
options: {
|
||||
includeNeutralNationInSeed:
|
||||
options.includeNeutralNationInSeed ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
const connector = createPostgresConnector({ url: options.databaseUrl });
|
||||
const now = options.now ?? new Date();
|
||||
const tickSeconds = options.tickSeconds ?? DEFAULT_TICK_SECONDS;
|
||||
const generalGold = options.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD;
|
||||
const generalRice = options.defaultGeneralRice ?? DEFAULT_GENERAL_RICE;
|
||||
|
||||
await connector.connect();
|
||||
try {
|
||||
const prisma = connector.prisma;
|
||||
|
||||
if (options.resetTables ?? true) {
|
||||
await prisma.event.deleteMany();
|
||||
await prisma.diplomacy.deleteMany();
|
||||
await prisma.general.deleteMany();
|
||||
await prisma.city.deleteMany();
|
||||
await prisma.nation.deleteMany();
|
||||
await prisma.worldState.deleteMany();
|
||||
}
|
||||
|
||||
await prisma.worldState.create({
|
||||
data: {
|
||||
scenarioCode: String(options.scenarioId),
|
||||
currentYear: scenario.startYear ?? 0,
|
||||
currentMonth: 1,
|
||||
tickSeconds,
|
||||
config: asJson(seed.scenarioConfig),
|
||||
meta: asJson({
|
||||
scenarioId: options.scenarioId,
|
||||
scenarioMeta: seed.scenarioMeta,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (seed.nations.length > 0) {
|
||||
await prisma.nation.createMany({
|
||||
data: seed.nations.map((nation) => ({
|
||||
id: nation.id,
|
||||
name: nation.name,
|
||||
color: nation.color,
|
||||
capitalCityId: nation.capitalCityId ?? null,
|
||||
gold: nation.gold,
|
||||
rice: nation.rice,
|
||||
tech: nation.tech,
|
||||
level: nation.level,
|
||||
typeCode: nation.typeCode,
|
||||
meta: asJson({
|
||||
infoText: nation.infoText,
|
||||
cityIds: nation.cityIds,
|
||||
}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.cities.length > 0) {
|
||||
await prisma.city.createMany({
|
||||
data: seed.cities.map((city) => ({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
level: city.level,
|
||||
nationId: city.nationId,
|
||||
supplyState: city.supplyState,
|
||||
frontState: city.frontState,
|
||||
population: city.population,
|
||||
populationMax: city.populationMax,
|
||||
agriculture: city.agriculture,
|
||||
agricultureMax: city.agricultureMax,
|
||||
commerce: city.commerce,
|
||||
commerceMax: city.commerceMax,
|
||||
security: city.security,
|
||||
securityMax: city.securityMax,
|
||||
trust: city.trust,
|
||||
trade: city.trade,
|
||||
defence: city.defence,
|
||||
defenceMax: city.defenceMax,
|
||||
wall: city.wall,
|
||||
wallMax: city.wallMax,
|
||||
region: city.region,
|
||||
conflict: asJson({}),
|
||||
meta: asJson({
|
||||
position: city.position,
|
||||
connections: city.connections,
|
||||
...city.meta,
|
||||
}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.generals.length > 0) {
|
||||
await prisma.general.createMany({
|
||||
data: seed.generals.map((general) => ({
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
nationId: general.nationId,
|
||||
cityId: general.cityId,
|
||||
npcState: general.npcType,
|
||||
affinity: general.affinity,
|
||||
bornYear: general.birthYear,
|
||||
deadYear: general.deathYear,
|
||||
picture:
|
||||
general.picture === null
|
||||
? null
|
||||
: String(general.picture),
|
||||
leadership: general.stats.leadership,
|
||||
strength: general.stats.strength,
|
||||
intel: general.stats.intelligence,
|
||||
officerLevel: general.officerLevel,
|
||||
gold: generalGold,
|
||||
rice: generalRice,
|
||||
crewTypeId: general.crewTypeId,
|
||||
horseCode: general.horse ?? 'None',
|
||||
weaponCode: general.weapon ?? 'None',
|
||||
bookCode: general.book ?? 'None',
|
||||
itemCode: general.item ?? 'None',
|
||||
turnTime: now,
|
||||
age: resolveGeneralAge(
|
||||
scenario.startYear ?? null,
|
||||
general.birthYear
|
||||
),
|
||||
personalCode: general.personality ?? 'None',
|
||||
specialCode: general.special ?? 'None',
|
||||
special2Code: general.specialWar ?? 'None',
|
||||
lastTurn: asJson({}),
|
||||
meta: asJson({
|
||||
npcType: general.npcType,
|
||||
crewTypeId: general.crewTypeId,
|
||||
...general.meta,
|
||||
}),
|
||||
penalty: asJson({}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.diplomacy.length > 0) {
|
||||
await prisma.diplomacy.createMany({
|
||||
data: seed.diplomacy.map((row) => ({
|
||||
srcNationId: row.fromNationId,
|
||||
destNationId: row.toNationId,
|
||||
stateCode: row.state,
|
||||
term: row.durationMonths,
|
||||
meta: asJson({}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const eventRows = [
|
||||
...buildEventRows(seed.events),
|
||||
...buildEventRows(seed.initialEvents, 'initial'),
|
||||
];
|
||||
if (eventRows.length > 0) {
|
||||
await prisma.event.createMany({
|
||||
data: eventRows,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await connector.disconnect();
|
||||
}
|
||||
|
||||
return { seed, warnings };
|
||||
};
|
||||
Reference in New Issue
Block a user