feat: port scenario action effects

This commit is contained in:
2026-07-30 19:00:04 +00:00
parent 7f31459385
commit c68d992046
39 changed files with 1131 additions and 95 deletions
+30 -3
View File
@@ -108,6 +108,33 @@ const unitSet: UnitSetDefinition = {
};
describe('reserved turn crew type wiring', () => {
it('derives scenario modules from the stored scenario config and keeps them before items', async () => {
const env = buildCommandEnv(
{
...scenarioConfig,
environment: {
...scenarioConfig.environment,
scenarioEffect: 'event_MoreEffect',
},
},
unitSet
);
await buildReservedTurnDefinitions({
env,
commandProfile: { general: ['휴식'], nation: ['휴식'] },
defaultActionKey: '휴식',
});
expect(env.scenarioEffect).toBe('event_MoreEffect');
const scenarioGeneral = env.generalActionModules?.at(-2);
const scenarioWar = env.warActionModules?.at(-2);
const attacker = { isAttacker: () => true } as unknown as WarUnitGeneral;
const defender = { isAttacker: () => false } as unknown as WarUnitGeneral;
expect(scenarioGeneral?.onCalcDomestic?.({ general }, '상업', 'score', 10)).toBe(20);
expect(scenarioWar?.getWarPowerMultiplier?.({ general }, attacker, defender)).toEqual([1.4, 0.7143]);
});
it('installs the crew action router before inherit and item handlers', async () => {
const env = buildCommandEnv(scenarioConfig, unitSet);
@@ -221,9 +248,9 @@ describe('reserved turn crew type wiring', () => {
100,
100
);
expect(
warPipeline.getWarPowerMultiplier(attacker.getActionContext(), attacker, defender)
).toEqual([1.284, 0.93]);
expect(warPipeline.getWarPowerMultiplier(attacker.getActionContext(), attacker, defender)).toEqual([
1.284, 0.93,
]);
expect(warPipeline.onCalcStat(attacker.getActionContext(), 'bonusTrain', 100)).toBe(105);
});
});
@@ -198,7 +198,7 @@ describe('core monthly event actions at the real month boundary', () => {
]);
});
it('uses the ProcessIncome resource argument instead of inferring it from the month', async () => {
it('uses the ProcessIncome resource argument and keeps MoreEffect income dormant like ref', async () => {
const world = buildWorld(
[
{
@@ -226,7 +226,11 @@ describe('core monthly event actions at the real month boundary', () => {
iconPath: '',
map: {},
const: { baseGold: 0, baseRice: 0 },
environment: { mapName: map.id, unitSet: 'default' },
environment: {
mapName: map.id,
unitSet: 'default',
scenarioEffect: 'event_MoreEffect',
},
},
nationTraits: new Map(),
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { TurnSchedule } from '@sammo-ts/logic';
import type { ScenarioEffectKey, TurnSchedule } from '@sammo-ts/logic';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
@@ -51,7 +51,7 @@ const buildGeneral = (overrides: Partial<TurnGeneral> = {}): TurnGeneral => ({
const buildWorld = (
general = buildGeneral(),
options: { autorunLimit?: boolean; scenarioEffect?: string | null } = {}
options: { autorunLimit?: boolean; scenarioEffect?: ScenarioEffectKey | null } = {}
) => {
const state: TurnWorldState = {
id: 1,
@@ -143,15 +143,22 @@ describe('my information world commands', () => {
});
});
it('preserves the event scenarios that waive the no-defence penalty', async () => {
const fixture = buildWorld(buildGeneral(), { scenarioEffect: 'event_StrongAttacker' });
await fixture.handler.handle({
type: 'setMySetting',
generalId: 7,
settings: { defence_train: 999 },
});
expect(fixture.world.getGeneralById(7)).toMatchObject({ train: 90, atmos: 90 });
});
it.each([
'event_UnlimitedDefenceThresholdChange',
'event_StrongAttacker',
'event_MoreEffect',
] satisfies ScenarioEffectKey[])(
'preserves the %s scenario that waives the no-defence penalty',
async (scenarioEffect) => {
const fixture = buildWorld(buildGeneral(), { scenarioEffect });
await fixture.handler.handle({
type: 'setMySetting',
generalId: 7,
settings: { defence_train: 999 },
});
expect(fixture.world.getGeneralById(7)).toMatchObject({ train: 90, atmos: 90 });
}
);
it('applies vacation killturn and rejects it in automatic-turn mode', async () => {
const allowed = buildWorld();
+20 -9
View File
@@ -50,15 +50,7 @@ type ScenarioSeederPrismaClient = {
};
};
const requiredTables = [
'world_state',
'nation',
'city',
'general',
'diplomacy',
'troop',
'event',
];
const requiredTables = ['world_state', 'nation', 'city', 'general', 'diplomacy', 'troop', 'event'];
const hasRequiredTables = async (prisma: ScenarioSeederPrismaClient, schemaName: string): Promise<boolean> => {
for (const table of requiredTables) {
@@ -243,6 +235,25 @@ describeDb('scenario database seed', () => {
await connector.disconnect();
}
});
test('persists a tracked scenario effect in the world configuration', async () => {
await seedScenarioToDatabase({
scenarioId: 906,
databaseUrl,
});
const connector = createGamePostgresConnector({ url: databaseUrl });
await connector.connect();
try {
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
const worldState = await prisma.worldState.findFirst();
const config = (worldState?.config ?? {}) as Record<string, unknown>;
const environment = (config.environment ?? {}) as Record<string, unknown>;
expect(environment.scenarioEffect).toBe('event_StrongAttacker');
} finally {
await connector.disconnect();
}
});
});
describe('tracked scenario composition', () => {