merge: preserve pre-month notice log date
This commit is contained in:
@@ -32,7 +32,7 @@ export const createProcessIncomeActionHandler = (incomeHandler: IncomeHandler):
|
|||||||
export const createNoticeToHistoryLogHandler = (options: {
|
export const createNoticeToHistoryLogHandler = (options: {
|
||||||
getWorld: () => InMemoryTurnWorld | null;
|
getWorld: () => InMemoryTurnWorld | null;
|
||||||
}): MonthlyEventActionHandler => {
|
}): MonthlyEventActionHandler => {
|
||||||
return (args) => {
|
return (args, environment) => {
|
||||||
const text = args[0];
|
const text = args[0];
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
throw new Error('NoticeToHistoryLog message must be a string.');
|
throw new Error('NoticeToHistoryLog message must be a string.');
|
||||||
@@ -46,6 +46,8 @@ export const createNoticeToHistoryLogHandler = (options: {
|
|||||||
category: LogCategory.HISTORY,
|
category: LogCategory.HISTORY,
|
||||||
text,
|
text,
|
||||||
format: parseLogFormat(args[1]),
|
format: parseLogFormat(args[1]),
|
||||||
|
year: environment.year,
|
||||||
|
month: environment.month,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
|
||||||
|
|
||||||
|
import { createTurnDaemonRuntime } from '../src/turn/turnDaemon.js';
|
||||||
|
|
||||||
|
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
||||||
|
const integration = describe.skipIf(!databaseUrl);
|
||||||
|
const profile = 'monthly-catalog-boundary';
|
||||||
|
const eventIds = [991_001, 991_002] as const;
|
||||||
|
|
||||||
|
const scenarioConfig = {
|
||||||
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||||
|
iconPath: '.',
|
||||||
|
map: {},
|
||||||
|
const: {},
|
||||||
|
environment: { mapName: 'che', unitSet: 'che' },
|
||||||
|
};
|
||||||
|
|
||||||
|
integration('monthly catalog boundary persistence', () => {
|
||||||
|
let db: GamePrismaClient;
|
||||||
|
let closeDb: (() => Promise<void>) | undefined;
|
||||||
|
|
||||||
|
const clean = async () => {
|
||||||
|
await db.logEntry.deleteMany({});
|
||||||
|
await db.yearbookHistory.deleteMany({});
|
||||||
|
await db.generalTurn.deleteMany({});
|
||||||
|
await db.nationTurn.deleteMany({});
|
||||||
|
await db.rankData.deleteMany({});
|
||||||
|
await db.generalAccessLog.deleteMany({});
|
||||||
|
await db.diplomacy.deleteMany({});
|
||||||
|
await db.event.deleteMany({});
|
||||||
|
await db.general.deleteMany({});
|
||||||
|
await db.nation.deleteMany({});
|
||||||
|
await db.city.deleteMany({});
|
||||||
|
await db.worldState.deleteMany({});
|
||||||
|
await db.turnDaemonLease.deleteMany({ where: { profile } });
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
||||||
|
await connector.connect();
|
||||||
|
db = connector.prisma;
|
||||||
|
closeDb = () => connector.disconnect();
|
||||||
|
await clean();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clean();
|
||||||
|
await closeDb?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches the ref PRE_MONTH, month actions, and post-month projection in one daemon boundary', async () => {
|
||||||
|
await db.city.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '경계도시',
|
||||||
|
nationId: 1,
|
||||||
|
level: 1,
|
||||||
|
supplyState: 1,
|
||||||
|
frontState: 2,
|
||||||
|
population: 10_000,
|
||||||
|
populationMax: 20_000,
|
||||||
|
agriculture: 1_000,
|
||||||
|
agricultureMax: 2_000,
|
||||||
|
commerce: 1_000,
|
||||||
|
commerceMax: 2_000,
|
||||||
|
security: 1_000,
|
||||||
|
securityMax: 2_000,
|
||||||
|
trust: 50,
|
||||||
|
trade: 100,
|
||||||
|
defence: 1_000,
|
||||||
|
defenceMax: 2_000,
|
||||||
|
wall: 1_000,
|
||||||
|
wallMax: 2_000,
|
||||||
|
region: 1,
|
||||||
|
conflict: {},
|
||||||
|
meta: { state: 31, term: 1, officer_set: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '중립도시',
|
||||||
|
nationId: 0,
|
||||||
|
level: 8,
|
||||||
|
supplyState: 0,
|
||||||
|
frontState: 0,
|
||||||
|
population: 12_000,
|
||||||
|
populationMax: 22_000,
|
||||||
|
agriculture: 1_200,
|
||||||
|
agricultureMax: 2_200,
|
||||||
|
commerce: 1_200,
|
||||||
|
commerceMax: 2_200,
|
||||||
|
security: 1_200,
|
||||||
|
securityMax: 2_200,
|
||||||
|
trust: 55,
|
||||||
|
trade: 100,
|
||||||
|
defence: 1_200,
|
||||||
|
defenceMax: 2_200,
|
||||||
|
wall: 1_200,
|
||||||
|
wallMax: 2_200,
|
||||||
|
region: 1,
|
||||||
|
conflict: {},
|
||||||
|
meta: { state: 0, term: 0, officer_set: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.nation.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: '경계국',
|
||||||
|
color: '#123456',
|
||||||
|
capitalCityId: 1,
|
||||||
|
chiefGeneralId: 1,
|
||||||
|
gold: 10_000,
|
||||||
|
rice: 20_000,
|
||||||
|
tech: 100,
|
||||||
|
level: 2,
|
||||||
|
typeCode: 'che_유가',
|
||||||
|
meta: {
|
||||||
|
power: 10,
|
||||||
|
gennum: 1,
|
||||||
|
bill: 100,
|
||||||
|
rate: 20,
|
||||||
|
rate_tmp: 10,
|
||||||
|
scout: 1,
|
||||||
|
chief_set: 1,
|
||||||
|
strategic_cmd_limit: 2,
|
||||||
|
surlimit: 1,
|
||||||
|
spy: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await db.general.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '경계군주',
|
||||||
|
nationId: 1,
|
||||||
|
cityId: 1,
|
||||||
|
officerLevel: 12,
|
||||||
|
npcState: 0,
|
||||||
|
leadership: 50,
|
||||||
|
strength: 50,
|
||||||
|
intel: 50,
|
||||||
|
age: 30,
|
||||||
|
bornYear: 160,
|
||||||
|
deadYear: 250,
|
||||||
|
gold: 1_000,
|
||||||
|
rice: 1_000,
|
||||||
|
crew: 100,
|
||||||
|
crewTypeId: 1100,
|
||||||
|
train: 80,
|
||||||
|
atmos: 70,
|
||||||
|
turnTime: new Date('2026-07-25T01:50:00.000Z'),
|
||||||
|
meta: { belong: 3, makelimit: 2, killturn: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '경계재야',
|
||||||
|
nationId: 0,
|
||||||
|
cityId: 2,
|
||||||
|
officerLevel: 0,
|
||||||
|
npcState: 0,
|
||||||
|
leadership: 40,
|
||||||
|
strength: 40,
|
||||||
|
intel: 40,
|
||||||
|
age: 25,
|
||||||
|
bornYear: 165,
|
||||||
|
deadYear: 250,
|
||||||
|
gold: 500,
|
||||||
|
rice: 500,
|
||||||
|
crew: 50,
|
||||||
|
crewTypeId: 1100,
|
||||||
|
train: 60,
|
||||||
|
atmos: 50,
|
||||||
|
turnTime: new Date('2026-07-25T01:50:00.000Z'),
|
||||||
|
meta: { belong: 4, makelimit: 0, killturn: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.generalTurn.createMany({
|
||||||
|
data: [1, 2].flatMap((generalId) =>
|
||||||
|
Array.from({ length: 30 }, (_, turnIdx) => ({
|
||||||
|
generalId,
|
||||||
|
turnIdx,
|
||||||
|
actionCode: '휴식',
|
||||||
|
arg: {},
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
});
|
||||||
|
await db.event.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: eventIds[0],
|
||||||
|
targetCode: 'PRE_MONTH',
|
||||||
|
priority: 1_000,
|
||||||
|
condition: true,
|
||||||
|
action: [['NoticeToHistoryLog', 'aggregate-pre', 6]],
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: eventIds[1],
|
||||||
|
targetCode: 'MONTH',
|
||||||
|
priority: 1_000,
|
||||||
|
condition: true,
|
||||||
|
action: [
|
||||||
|
['ProcessIncome', 'gold'],
|
||||||
|
['NewYear'],
|
||||||
|
['ResetOfficerLock'],
|
||||||
|
['RandomizeCityTradeRate'],
|
||||||
|
['ChangeCity', 'all', { pop: '+10', trust: 60 }],
|
||||||
|
['NoticeToHistoryLog', 'aggregate-month', 6],
|
||||||
|
['DeleteEvent'],
|
||||||
|
],
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.worldState.create({
|
||||||
|
data: {
|
||||||
|
scenarioCode: profile,
|
||||||
|
currentYear: 190,
|
||||||
|
currentMonth: 12,
|
||||||
|
tickSeconds: 600,
|
||||||
|
config: scenarioConfig,
|
||||||
|
meta: {
|
||||||
|
hiddenSeed: 'monthly-boundary-core-catalog',
|
||||||
|
lastTurnTime: '2026-07-25T01:50:00.000Z',
|
||||||
|
isunited: 1,
|
||||||
|
refreshLimit: 3,
|
||||||
|
block_change_scout: false,
|
||||||
|
develcost: 18,
|
||||||
|
scenarioMeta: {
|
||||||
|
title: 'monthly catalog boundary',
|
||||||
|
startYear: 190,
|
||||||
|
life: null,
|
||||||
|
fiction: null,
|
||||||
|
history: [],
|
||||||
|
ignoreDefaultEvents: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = await createTurnDaemonRuntime({
|
||||||
|
profile,
|
||||||
|
databaseUrl: databaseUrl!,
|
||||||
|
enableLeaseHeartbeat: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await runtime.world.advanceMonth(new Date('2026-07-25T02:00:00.000Z'));
|
||||||
|
await runtime.hooks?.flushChanges?.({
|
||||||
|
lastTurnTime: runtime.world.getState().lastTurnTime.toISOString(),
|
||||||
|
processedGenerals: 0,
|
||||||
|
processedTurns: 1,
|
||||||
|
durationMs: 0,
|
||||||
|
partial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await db.worldState.findFirstOrThrow()).toMatchObject({
|
||||||
|
currentYear: 191,
|
||||||
|
currentMonth: 1,
|
||||||
|
meta: expect.objectContaining({ develcost: 20 }),
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await db.general.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
select: { id: true, age: true, gold: true, meta: true },
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{ id: 1, age: 31, gold: 1_400, meta: expect.objectContaining({ belong: 4, makelimit: 1 }) },
|
||||||
|
{ id: 2, age: 26, gold: 500, meta: expect.objectContaining({ belong: 4, makelimit: 0 }) },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
await db.city.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
population: true,
|
||||||
|
trust: true,
|
||||||
|
trade: true,
|
||||||
|
frontState: true,
|
||||||
|
meta: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
population: 10_010,
|
||||||
|
trust: 60,
|
||||||
|
trade: null,
|
||||||
|
frontState: 0,
|
||||||
|
meta: expect.objectContaining({ state: 0, term: 0, officer_set: 0 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
population: 12_010,
|
||||||
|
trust: 60,
|
||||||
|
trade: 99,
|
||||||
|
frontState: 0,
|
||||||
|
meta: expect.objectContaining({ state: 0, term: 0, officer_set: 0 }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(await db.nation.findUniqueOrThrow({ where: { id: 1 } })).toMatchObject({
|
||||||
|
gold: 9_753,
|
||||||
|
rice: 20_000,
|
||||||
|
meta: expect.objectContaining({
|
||||||
|
power: 68,
|
||||||
|
gennum: 1,
|
||||||
|
prev_income_gold: 153,
|
||||||
|
rate_tmp: 20,
|
||||||
|
strategic_cmd_limit: 1,
|
||||||
|
surlimit: 0,
|
||||||
|
chief_set: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await db.logEntry.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
select: { year: true, month: true, text: true },
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{ year: 190, month: 12, text: '<S>◆</>190년 12월:aggregate-pre' },
|
||||||
|
{ year: 191, month: 1, text: '<C>●</>이번 수입은 금 <C>153</>입니다.' },
|
||||||
|
{ year: 191, month: 1, text: '<C>●</>봉급으로 금 <C>400</>을 받았습니다.' },
|
||||||
|
{
|
||||||
|
year: 191,
|
||||||
|
month: 1,
|
||||||
|
text: '<C>●</>191년 1월:<W><b>【지급】</b></>봄이 되어 봉록에 따라 자금이 지급됩니다.',
|
||||||
|
},
|
||||||
|
{ year: 191, month: 1, text: '<C>●</>1월:<C>191</>년이 되었습니다.' },
|
||||||
|
{ year: 191, month: 1, text: '<S>◆</>191년 1월:aggregate-month' },
|
||||||
|
]);
|
||||||
|
expect(await db.event.findMany({ select: { id: true }, orderBy: { id: 'asc' } })).toEqual([
|
||||||
|
{ id: eventIds[0] },
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
await runtime.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -186,6 +186,8 @@ describe('core monthly event actions at the real month boundary', () => {
|
|||||||
category: LogCategory.HISTORY,
|
category: LogCategory.HISTORY,
|
||||||
text: '<S>새해 알림</>',
|
text: '<S>새해 알림</>',
|
||||||
format: LogFormat.EVENT_YEAR_MONTH,
|
format: LogFormat.EVENT_YEAR_MONTH,
|
||||||
|
year: 191,
|
||||||
|
month: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
|
|||||||
Reference in New Issue
Block a user