merge: scenario 2601 GUI progression support
This commit is contained in:
@@ -205,6 +205,9 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
|||||||
const generalPoolEntries = targetGeneralPool
|
const generalPoolEntries = targetGeneralPool
|
||||||
? await loadGeneralPoolEntries(targetGeneralPool, options.generalPoolOptions)
|
? await loadGeneralPoolEntries(targetGeneralPool, options.generalPoolOptions)
|
||||||
: [];
|
: [];
|
||||||
|
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
||||||
|
const hiddenSeed =
|
||||||
|
integrationSeed && integrationSeed.length > 0 ? integrationSeed : randomBytes(16).toString('hex');
|
||||||
|
|
||||||
const { seed, warnings } = buildScenarioBootstrap({
|
const { seed, warnings } = buildScenarioBootstrap({
|
||||||
scenario: scenarioDefinition,
|
scenario: scenarioDefinition,
|
||||||
@@ -212,6 +215,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
|||||||
unitSet,
|
unitSet,
|
||||||
options: {
|
options: {
|
||||||
includeNeutralNationInSeed: options.includeNeutralNationInSeed ?? true,
|
includeNeutralNationInSeed: options.includeNeutralNationInSeed ?? true,
|
||||||
|
hiddenSeed,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
seed.cities = applyInitialChangeCityEvents(seed.cities, seed.initialEvents);
|
seed.cities = applyInitialChangeCityEvents(seed.cities, seed.initialEvents);
|
||||||
@@ -273,9 +277,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
|||||||
worldMeta.installCommitSha = install.installCommitSha.trim();
|
worldMeta.installCommitSha = install.installCommitSha.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
worldMeta.hiddenSeed = hiddenSeed;
|
||||||
worldMeta.hiddenSeed =
|
|
||||||
integrationSeed && integrationSeed.length > 0 ? integrationSeed : randomBytes(16).toString('hex');
|
|
||||||
|
|
||||||
if (install?.preopenAt) {
|
if (install?.preopenAt) {
|
||||||
worldMeta.preopenAt = formatDateTime(install.preopenAt);
|
worldMeta.preopenAt = formatDateTime(install.preopenAt);
|
||||||
|
|||||||
@@ -553,6 +553,7 @@ export const createDatabaseTurnHooks = async (
|
|||||||
profileName?: string;
|
profileName?: string;
|
||||||
reservedTurns?: InMemoryReservedTurnStore;
|
reservedTurns?: InMemoryReservedTurnStore;
|
||||||
turnDaemonLease?: DatabaseTurnDaemonLease;
|
turnDaemonLease?: DatabaseTurnDaemonLease;
|
||||||
|
transactionTimeoutMs?: number;
|
||||||
}
|
}
|
||||||
): Promise<DatabaseTurnHooks> => {
|
): Promise<DatabaseTurnHooks> => {
|
||||||
// 턴 처리 결과를 DB에 반영하는 훅을 만든다.
|
// 턴 처리 결과를 DB에 반영하는 훅을 만든다.
|
||||||
@@ -999,7 +1000,10 @@ export const createDatabaseTurnHooks = async (
|
|||||||
if (transaction) {
|
if (transaction) {
|
||||||
await persist(transaction);
|
await persist(transaction);
|
||||||
} else {
|
} else {
|
||||||
await prisma.$transaction(persist);
|
await prisma.$transaction(
|
||||||
|
persist,
|
||||||
|
options?.transactionTimeoutMs ? { timeout: options.transactionTimeoutMs } : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -1020,11 +1024,14 @@ export const createDatabaseTurnHooks = async (
|
|||||||
acknowledge();
|
acknowledge();
|
||||||
},
|
},
|
||||||
executeCommand: async (requestId, execute) => {
|
executeCommand: async (requestId, execute) => {
|
||||||
const committed = await prisma.$transaction(async (transaction) => {
|
const committed = await prisma.$transaction(
|
||||||
const result = await execute({ db: transaction });
|
async (transaction) => {
|
||||||
const acknowledge = await persistChanges(transaction, { requestId, result });
|
const result = await execute({ db: transaction });
|
||||||
return { result, acknowledge };
|
const acknowledge = await persistChanges(transaction, { requestId, result });
|
||||||
});
|
return { result, acknowledge };
|
||||||
|
},
|
||||||
|
options?.transactionTimeoutMs ? { timeout: options.transactionTimeoutMs } : undefined
|
||||||
|
);
|
||||||
committed.acknowledge();
|
committed.acknowledge();
|
||||||
return committed.result;
|
return committed.result;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ export interface TurnDaemonRuntimeOptions {
|
|||||||
leaseDurationMs?: number;
|
leaseDurationMs?: number;
|
||||||
leaseOwnerId?: string;
|
leaseOwnerId?: string;
|
||||||
enableLeaseHeartbeat?: boolean;
|
enableLeaseHeartbeat?: boolean;
|
||||||
|
/**
|
||||||
|
* Isolated, single-process fixture acceleration only. Reserved turns are
|
||||||
|
* loaded once and no concurrent API writer may touch this database.
|
||||||
|
*/
|
||||||
|
exclusiveFastForward?: boolean;
|
||||||
|
databaseTransactionTimeoutMs?: number;
|
||||||
|
onActionResolved?: NonNullable<Parameters<typeof createReservedTurnHandler>[0]['onActionResolved']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TurnDaemonRuntime {
|
export interface TurnDaemonRuntime {
|
||||||
@@ -179,6 +186,9 @@ const createTurnDaemonRuntimeWithLease = async (
|
|||||||
databaseFlushEnabled: boolean,
|
databaseFlushEnabled: boolean,
|
||||||
turnDaemonLease: DatabaseTurnDaemonLease | null
|
turnDaemonLease: DatabaseTurnDaemonLease | null
|
||||||
): Promise<TurnDaemonRuntime> => {
|
): Promise<TurnDaemonRuntime> => {
|
||||||
|
if (options.exclusiveFastForward && options.profileName) {
|
||||||
|
throw new Error('exclusiveFastForward cannot be used with a gateway-managed profile.');
|
||||||
|
}
|
||||||
// DB에서 월드를 읽고 턴 데몬을 구동할 런타임을 만든다.
|
// DB에서 월드를 읽고 턴 데몬을 구동할 런타임을 만든다.
|
||||||
const { state, snapshot } = await loadTurnWorldFromDatabase({
|
const { state, snapshot } = await loadTurnWorldFromDatabase({
|
||||||
databaseUrl: options.databaseUrl,
|
databaseUrl: options.databaseUrl,
|
||||||
@@ -499,6 +509,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
|||||||
commandProfile,
|
commandProfile,
|
||||||
commandEnv: monthlyCommandEnv,
|
commandEnv: monthlyCommandEnv,
|
||||||
getAdditionalOccupiedUniqueItemKeys: () => occupiedAuctionUniqueItemKeys,
|
getAdditionalOccupiedUniqueItemKeys: () => occupiedAuctionUniqueItemKeys,
|
||||||
|
onActionResolved: options.onActionResolved,
|
||||||
})),
|
})),
|
||||||
calendarHandler: calendarHandler ?? undefined,
|
calendarHandler: calendarHandler ?? undefined,
|
||||||
autoAdvanceDiplomacyMonth: false,
|
autoAdvanceDiplomacyMonth: false,
|
||||||
@@ -538,10 +549,20 @@ const createTurnDaemonRuntimeWithLease = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stateStore = new InMemoryTurnStateStore(world);
|
const stateStore = new InMemoryTurnStateStore(world);
|
||||||
|
let fastForwardPreparedMonth = '';
|
||||||
const processor = new InMemoryTurnProcessor(world, {
|
const processor = new InMemoryTurnProcessor(world, {
|
||||||
tickMinutes,
|
tickMinutes,
|
||||||
beforeExecuteGeneral: reservedTurnStoreHandle
|
beforeExecuteGeneral: reservedTurnStoreHandle
|
||||||
? async (general) => {
|
? async (general) => {
|
||||||
|
if (options.exclusiveFastForward) {
|
||||||
|
const state = world.getState();
|
||||||
|
const monthKey = `${state.currentYear}-${state.currentMonth}`;
|
||||||
|
if (fastForwardPreparedMonth !== monthKey) {
|
||||||
|
await refreshOccupiedAuctionUniqueItemKeys();
|
||||||
|
fastForwardPreparedMonth = monthKey;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const promises: Promise<unknown>[] = [];
|
const promises: Promise<unknown>[] = [];
|
||||||
promises.push(
|
promises.push(
|
||||||
reservedTurnStoreHandle.store.prepareTurnsForExecution(
|
reservedTurnStoreHandle.store.prepareTurnsForExecution(
|
||||||
@@ -597,6 +618,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
|||||||
profileName: options.profileName ?? options.profile,
|
profileName: options.profileName ?? options.profile,
|
||||||
reservedTurns: reservedTurnStoreHandle?.store,
|
reservedTurns: reservedTurnStoreHandle?.store,
|
||||||
turnDaemonLease: turnDaemonLease ?? undefined,
|
turnDaemonLease: turnDaemonLease ?? undefined,
|
||||||
|
transactionTimeoutMs: options.databaseTransactionTimeoutMs,
|
||||||
});
|
});
|
||||||
auctionBidder = await createAuctionBidder({
|
auctionBidder = await createAuctionBidder({
|
||||||
databaseUrl: options.databaseUrl,
|
databaseUrl: options.databaseUrl,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { loadTurnCommandProfile } from '../src/turn/turnCommandProfile.js';
|
||||||
|
|
||||||
|
const GENERAL_AI_ACTIONS = [
|
||||||
|
'che_군량매매',
|
||||||
|
'che_귀환',
|
||||||
|
'che_랜덤임관',
|
||||||
|
'che_모병',
|
||||||
|
'che_물자조달',
|
||||||
|
'che_선양',
|
||||||
|
'che_소집해제',
|
||||||
|
'che_이동',
|
||||||
|
'che_정착장려',
|
||||||
|
'che_해산',
|
||||||
|
'che_헌납',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const NATION_AI_ACTIONS = ['che_몰수', 'che_발령', 'che_선전포고', 'che_천도', 'che_포상'] as const;
|
||||||
|
|
||||||
|
describe('default turn command profile AI coverage', () => {
|
||||||
|
it('loads every action selected directly by the general and nation AI', async () => {
|
||||||
|
const profile = await loadTurnCommandProfile();
|
||||||
|
|
||||||
|
expect(profile.general).toEqual(expect.arrayContaining([...GENERAL_AI_ACTIONS]));
|
||||||
|
expect(profile.nation).toEqual(expect.arrayContaining([...NATION_AI_ACTIONS]));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,6 +31,12 @@ export interface ScenarioBootstrapOptions {
|
|||||||
defaultCrewTypeId?: number;
|
defaultCrewTypeId?: number;
|
||||||
nationTypePrefix?: string;
|
nationTypePrefix?: string;
|
||||||
mapDefaults?: Partial<MapDefaults>;
|
mapDefaults?: Partial<MapDefaults>;
|
||||||
|
/**
|
||||||
|
* Legacy scenario installation uses the world's hidden seed while resolving
|
||||||
|
* generals whose city is omitted. Keep this input explicit so bootstrap
|
||||||
|
* remains deterministic in tests and reset operations.
|
||||||
|
*/
|
||||||
|
hiddenSeed?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScenarioBootstrapWarningCode =
|
export type ScenarioBootstrapWarningCode =
|
||||||
@@ -208,11 +214,7 @@ const resolveGeneralBootstrapDisposition = (
|
|||||||
return 'active';
|
return 'active';
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDelayedGeneralAction = (
|
const buildDelayedGeneralAction = (general: ScenarioGeneral, nationId: number, npcType: 2 | 6): unknown[] => {
|
||||||
general: ScenarioGeneral,
|
|
||||||
nationId: number,
|
|
||||||
npcType: 2 | 6
|
|
||||||
): unknown[] => {
|
|
||||||
const common = [
|
const common = [
|
||||||
general.affinity ?? 0,
|
general.affinity ?? 0,
|
||||||
general.name,
|
general.name,
|
||||||
@@ -309,6 +311,9 @@ const buildGeneralSeeds = (
|
|||||||
nationNameToId: Map<string, number>,
|
nationNameToId: Map<string, number>,
|
||||||
warnings: ScenarioBootstrapWarning[],
|
warnings: ScenarioBootstrapWarning[],
|
||||||
defaultCrewTypeId: number,
|
defaultCrewTypeId: number,
|
||||||
|
mapCities: MapDefinition['cities'],
|
||||||
|
nationCityIds: Map<number, number[]>,
|
||||||
|
placementRng: RandUtil,
|
||||||
options?: ScenarioBootstrapOptions
|
options?: ScenarioBootstrapOptions
|
||||||
): {
|
): {
|
||||||
seeds: GeneralSeed[];
|
seeds: GeneralSeed[];
|
||||||
@@ -330,7 +335,14 @@ const buildGeneralSeeds = (
|
|||||||
nextId += 1;
|
nextId += 1;
|
||||||
|
|
||||||
const nationId = resolveNationId(row.nation, nationNameToId, warnings, row.name);
|
const nationId = resolveNationId(row.nation, nationNameToId, warnings, row.name);
|
||||||
const cityId = resolveCityId(row.city, cityByName, warnings, row.name);
|
let cityId = resolveCityId(row.city, cityByName, warnings, row.name);
|
||||||
|
if (row.city === null) {
|
||||||
|
const ownedCityIds = nationId > 0 ? (nationCityIds.get(nationId) ?? []) : [];
|
||||||
|
const candidateCityIds = ownedCityIds.length > 0 ? ownedCityIds : mapCities.map((city) => city.id);
|
||||||
|
if (candidateCityIds.length > 0) {
|
||||||
|
cityId = placementRng.choice(candidateCityIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
const birthYear = resolveBirthYear(row.birthYear, scenario.startYear);
|
const birthYear = resolveBirthYear(row.birthYear, scenario.startYear);
|
||||||
const deathYear = resolveDeathYear(row.deathYear, birthYear, scenario.startYear);
|
const deathYear = resolveDeathYear(row.deathYear, birthYear, scenario.startYear);
|
||||||
const deathMonth = resolveScenarioGeneralDeathMonth({
|
const deathMonth = resolveScenarioGeneralDeathMonth({
|
||||||
@@ -620,6 +632,9 @@ export const buildScenarioBootstrap = (input: ScenarioBootstrapInput): ScenarioB
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDefaults = resolveMapDefaults(map, options);
|
const mapDefaults = resolveMapDefaults(map, options);
|
||||||
|
const placementRng = new RandUtil(
|
||||||
|
new LiteHashDRBG(simpleSerialize(options?.hiddenSeed ?? scenario.title, 'InitScenarioGeneralCities'))
|
||||||
|
);
|
||||||
const defaultCrewTypeId = unitSet?.defaultCrewTypeId ?? options?.defaultCrewTypeId ?? DEFAULT_CREWTYPE_ID;
|
const defaultCrewTypeId = unitSet?.defaultCrewTypeId ?? options?.defaultCrewTypeId ?? DEFAULT_CREWTYPE_ID;
|
||||||
const seedCities: CitySeed[] = [];
|
const seedCities: CitySeed[] = [];
|
||||||
const domainCities: City[] = [];
|
const domainCities: City[] = [];
|
||||||
@@ -733,6 +748,9 @@ export const buildScenarioBootstrap = (input: ScenarioBootstrapInput): ScenarioB
|
|||||||
nationNameToId,
|
nationNameToId,
|
||||||
warnings,
|
warnings,
|
||||||
defaultCrewTypeId,
|
defaultCrewTypeId,
|
||||||
|
map.cities,
|
||||||
|
nationCityIds,
|
||||||
|
placementRng,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
allGeneralSeeds.push(...generalResult.seeds);
|
allGeneralSeeds.push(...generalResult.seeds);
|
||||||
@@ -749,6 +767,9 @@ export const buildScenarioBootstrap = (input: ScenarioBootstrapInput): ScenarioB
|
|||||||
nationNameToId,
|
nationNameToId,
|
||||||
warnings,
|
warnings,
|
||||||
defaultCrewTypeId,
|
defaultCrewTypeId,
|
||||||
|
map.cities,
|
||||||
|
nationCityIds,
|
||||||
|
placementRng,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
allGeneralSeeds.push(...generalExResult.seeds);
|
allGeneralSeeds.push(...generalExResult.seeds);
|
||||||
@@ -765,20 +786,21 @@ export const buildScenarioBootstrap = (input: ScenarioBootstrapInput): ScenarioB
|
|||||||
nationNameToId,
|
nationNameToId,
|
||||||
warnings,
|
warnings,
|
||||||
defaultCrewTypeId,
|
defaultCrewTypeId,
|
||||||
|
map.cities,
|
||||||
|
nationCityIds,
|
||||||
|
placementRng,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
allGeneralSeeds.push(...generalNeutralResult.seeds);
|
allGeneralSeeds.push(...generalNeutralResult.seeds);
|
||||||
allGenerals.push(...generalNeutralResult.generals);
|
allGenerals.push(...generalNeutralResult.generals);
|
||||||
|
|
||||||
const delayedGeneralEvents = Array.from(delayedActionsByBirthYear.entries()).map(
|
const delayedGeneralEvents = Array.from(delayedActionsByBirthYear.entries()).map(([birthYear, actions]) => [
|
||||||
([birthYear, actions]) => [
|
'Month',
|
||||||
'Month',
|
1_000,
|
||||||
1_000,
|
['Date', '>=', birthYear + ADULT_GENERAL_AGE, 1],
|
||||||
['Date', '>=', birthYear + ADULT_GENERAL_AGE, 1],
|
...actions,
|
||||||
...actions,
|
['DeleteEvent'],
|
||||||
['DeleteEvent'],
|
]);
|
||||||
]
|
|
||||||
);
|
|
||||||
const events = [...scenario.events, ...delayedGeneralEvents];
|
const events = [...scenario.events, ...delayedGeneralEvents];
|
||||||
|
|
||||||
const seed: WorldSeedPayload = {
|
const seed: WorldSeedPayload = {
|
||||||
|
|||||||
@@ -142,6 +142,114 @@ describe('scenario bootstrap', () => {
|
|||||||
expect(result.snapshot.scenarioMeta?.title).toBe('Test Scenario');
|
expect(result.snapshot.scenarioMeta?.title).toBe('Test Scenario');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('places generals without an explicit city in a deterministic valid city', () => {
|
||||||
|
const scenario: ScenarioDefinition = {
|
||||||
|
title: 'Random placement',
|
||||||
|
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: null,
|
||||||
|
tech: 100,
|
||||||
|
type: 'Test',
|
||||||
|
level: 3,
|
||||||
|
cities: ['Alpha'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
diplomacy: [],
|
||||||
|
generals: [
|
||||||
|
{
|
||||||
|
affinity: 10,
|
||||||
|
name: 'NationGeneral',
|
||||||
|
picture: null,
|
||||||
|
nation: 1,
|
||||||
|
city: null,
|
||||||
|
leadership: 50,
|
||||||
|
strength: 50,
|
||||||
|
intelligence: 50,
|
||||||
|
officerLevel: 1,
|
||||||
|
birthYear: 180,
|
||||||
|
deathYear: 240,
|
||||||
|
personality: null,
|
||||||
|
special: '',
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
affinity: 20,
|
||||||
|
name: 'NeutralGeneral',
|
||||||
|
picture: null,
|
||||||
|
nation: null,
|
||||||
|
city: null,
|
||||||
|
leadership: 50,
|
||||||
|
strength: 50,
|
||||||
|
intelligence: 50,
|
||||||
|
officerLevel: 0,
|
||||||
|
birthYear: 180,
|
||||||
|
deathYear: 240,
|
||||||
|
personality: null,
|
||||||
|
special: '',
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
generalsEx: [],
|
||||||
|
generalsNeutral: [],
|
||||||
|
cities: [],
|
||||||
|
events: [],
|
||||||
|
initialEvents: [],
|
||||||
|
ignoreDefaultEvents: false,
|
||||||
|
};
|
||||||
|
const map: MapDefinition = {
|
||||||
|
id: 'test-map',
|
||||||
|
name: 'test-map',
|
||||||
|
cities: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Alpha',
|
||||||
|
level: 5,
|
||||||
|
region: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
connections: [2],
|
||||||
|
max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||||
|
initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Beta',
|
||||||
|
level: 5,
|
||||||
|
region: 1,
|
||||||
|
position: { x: 1, y: 0 },
|
||||||
|
connections: [1],
|
||||||
|
max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||||
|
initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = buildScenarioBootstrap({ scenario, map, options: { hiddenSeed: 'placement-seed' } });
|
||||||
|
const second = buildScenarioBootstrap({ scenario, map, options: { hiddenSeed: 'placement-seed' } });
|
||||||
|
|
||||||
|
expect(first.seed.generals.map((general) => general.cityId)).toEqual(
|
||||||
|
second.seed.generals.map((general) => general.cityId)
|
||||||
|
);
|
||||||
|
expect(first.seed.generals[0]?.cityId).toBe(1);
|
||||||
|
expect([1, 2]).toContain(first.seed.generals[1]?.cityId);
|
||||||
|
expect(first.seed.generals.every((general) => general.cityId > 0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('defers future generals into birth-year registration events and omits expired rows', () => {
|
it('defers future generals into birth-year registration events and omits expired rows', () => {
|
||||||
const general = (
|
const general = (
|
||||||
name: string,
|
name: string,
|
||||||
@@ -192,11 +300,7 @@ describe('scenario bootstrap', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
diplomacy: [],
|
diplomacy: [],
|
||||||
generals: [
|
generals: [general('현재', 180, 240), general('미래1', 190, 250, 'TestNation'), general('만료', 170, 200)],
|
||||||
general('현재', 180, 240),
|
|
||||||
general('미래1', 190, 250, 'TestNation'),
|
|
||||||
general('만료', 170, 200),
|
|
||||||
],
|
|
||||||
generalsEx: [general('미래확장', 190, 260)],
|
generalsEx: [general('미래확장', 190, 260)],
|
||||||
generalsNeutral: [general('미래재야', 191, 260, 0)],
|
generalsNeutral: [general('미래재야', 191, 260, 0)],
|
||||||
cities: [],
|
cities: [],
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
"general": [
|
"general": [
|
||||||
"che_거병",
|
"che_거병",
|
||||||
"che_임관",
|
"che_임관",
|
||||||
|
"che_랜덤임관",
|
||||||
|
"che_귀환",
|
||||||
"che_건국",
|
"che_건국",
|
||||||
"che_훈련",
|
"che_훈련",
|
||||||
"che_단련",
|
"che_단련",
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
"che_전투특기초기화",
|
"che_전투특기초기화",
|
||||||
"che_출병",
|
"che_출병",
|
||||||
"che_주민선정",
|
"che_주민선정",
|
||||||
|
"che_정착장려",
|
||||||
"che_농지개간",
|
"che_농지개간",
|
||||||
"che_상업투자",
|
"che_상업투자",
|
||||||
"che_기술연구",
|
"che_기술연구",
|
||||||
@@ -23,13 +26,23 @@
|
|||||||
"che_집합",
|
"che_집합",
|
||||||
"che_인재탐색",
|
"che_인재탐색",
|
||||||
"che_징병",
|
"che_징병",
|
||||||
|
"che_모병",
|
||||||
|
"che_소집해제",
|
||||||
|
"che_군량매매",
|
||||||
|
"che_물자조달",
|
||||||
|
"che_헌납",
|
||||||
|
"che_이동",
|
||||||
|
"che_선양",
|
||||||
|
"che_해산",
|
||||||
"휴식"
|
"휴식"
|
||||||
],
|
],
|
||||||
"nation": [
|
"nation": [
|
||||||
"휴식",
|
"휴식",
|
||||||
"che_포상",
|
"che_포상",
|
||||||
|
"che_몰수",
|
||||||
"che_부대탈퇴지시",
|
"che_부대탈퇴지시",
|
||||||
"che_발령",
|
"che_발령",
|
||||||
|
"che_천도",
|
||||||
"che_선전포고",
|
"che_선전포고",
|
||||||
"che_불가침제의",
|
"che_불가침제의",
|
||||||
"che_불가침파기제의",
|
"che_불가침파기제의",
|
||||||
|
|||||||
Reference in New Issue
Block a user