Files
core2026/packages/logic/test/specialActions.test.ts
T
Hide_D 5d5ed9ee2a feat: add killturn to general meta and update related tests
- Introduced a new meta property `killturn` to the GeneralMeta interface.
- Implemented a function `readMetaNumber` to handle the retrieval of numeric values from meta.
- Updated the `mapGeneralRow` function to ensure `killturn` is required and properly handled.
- Modified various tests to include the `killturn` property in the general's meta data.
- Adjusted related action files to utilize the new GeneralMeta type for better type safety.
- Ensured backward compatibility by updating existing test cases across multiple scenarios.
2026-01-25 05:48:56 +00:00

314 lines
9.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { ConstantRNG, RandUtil } from '@sammo-ts/common';
import type { City, General, Nation } from '../src/domain/entities.js';
import type { RandomGenerator } from '../src/index.js';
import type { UnitSetDefinition } from '../src/world/types.js';
import { GeneralActionPipeline } from '../src/triggers/general-action.js';
import { createGeneralTriggerContext } from '../src/triggers/general.js';
import {
createTraitModuleRegistry,
createTraitModules,
loadDomesticTraitModules,
loadWarTraitModules,
} from '../src/triggers/special/index.js';
import { ActionLogger } from '../src/logging/actionLogger.js';
import { WarActionPipeline } from '../src/war/actions.js';
import { WarCrewType } from '../src/war/crewType.js';
import { createWarTriggerEnv } from '../src/war/triggers.js';
import type { WarEngineConfig } from '../src/war/types.js';
import { WarUnitCity, WarUnitGeneral } from '../src/war/units.js';
const buildGeneral = (overrides: Partial<General> = {}): General => ({
id: 1,
name: 'Tester',
nationId: 1,
cityId: 1,
troopId: 0,
stats: {
leadership: 80,
strength: 70,
intelligence: 60,
},
experience: 0,
dedication: 0,
officerLevel: 3,
role: {
personality: null,
specialDomestic: null,
specialWar: null,
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
injury: 0,
gold: 1000,
rice: 1000,
crew: 1000,
crewTypeId: 100,
train: 80,
atmos: 80,
age: 20,
npcState: 0,
triggerState: {
flags: {},
counters: {},
modifiers: {},
meta: {},
},
meta: { killturn: 24 },
...overrides,
});
const buildCity = (): City => ({
id: 1,
name: 'TestCity',
nationId: 1,
level: 2,
state: 0,
population: 10000,
populationMax: 10000,
agriculture: 500,
agricultureMax: 1000,
commerce: 500,
commerceMax: 1000,
security: 500,
securityMax: 1000,
defence: 100,
defenceMax: 200,
supplyState: 1,
frontState: 0,
wall: 100,
wallMax: 200,
meta: {},
});
const buildNation = (): Nation => ({
id: 1,
name: 'TestNation',
color: '#000000',
capitalCityId: 1,
chiefGeneralId: null,
gold: 1000,
rice: 1000,
power: 0,
level: 1,
typeCode: 'test',
meta: {},
});
const buildConfig = (): WarEngineConfig => ({
armPerPhase: 500,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
maxTrainByWar: 110,
maxAtmosByWar: 150,
castleCrewTypeId: 999,
armTypes: {
footman: 1,
wizard: 4,
siege: 5,
misc: 6,
castle: 9,
},
});
const buildUnitSet = (): UnitSetDefinition => ({
id: 'test',
name: 'test',
crewTypes: [
{
id: 100,
armType: 1,
name: '보병',
attack: 100,
defence: 100,
speed: 7,
avoid: 10,
magicCoef: 0,
cost: 9,
rice: 9,
requirements: [],
attackCoef: {},
defenceCoef: {},
info: [],
initSkillTrigger: null,
phaseSkillTrigger: null,
iActionList: null,
},
{
id: 999,
armType: 9,
name: '성벽',
attack: 0,
defence: 0,
speed: 1,
avoid: 0,
magicCoef: 0,
cost: 0,
rice: 0,
requirements: [],
attackCoef: {},
defenceCoef: {},
info: [],
initSkillTrigger: null,
phaseSkillTrigger: null,
iActionList: null,
},
],
});
describe('trait modules', () => {
it('loads trait modules by key', async () => {
const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']);
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
expect(domestic.map((module) => module.key)).toEqual(['che_인덕', 'che_발명']);
expect(war.map((module) => module.key)).toEqual(['che_의술', 'che_징병']);
});
it('applies domestic and war modifiers in general pipeline', async () => {
const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']);
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
const registry = createTraitModuleRegistry({ domestic, war });
const traitModules = createTraitModules(registry);
const pipeline = new GeneralActionPipeline(traitModules.general);
const general = buildGeneral({
role: {
personality: null,
specialDomestic: 'che_인덕',
specialWar: 'che_징병',
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
});
const context = { general };
expect(pipeline.onCalcDomestic(context, '민심', 'score', 100)).toBeCloseTo(110);
expect(pipeline.onCalcDomestic(context, '징병', 'train', 40)).toBe(70);
expect(pipeline.onCalcStat(context, 'leadership', 80)).toBe(100);
});
it('heals city generals with 의술 pre-turn trigger', async () => {
const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']);
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
const registry = createTraitModuleRegistry({ domestic, war });
const traitModules = createTraitModules(registry);
const pipeline = new GeneralActionPipeline(traitModules.general);
const general = buildGeneral({
injury: 20,
role: {
personality: null,
specialDomestic: null,
specialWar: 'che_의술',
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
});
const patient = buildGeneral({
id: 2,
name: 'Patient',
injury: 15,
});
const worldView = {
listGeneralsByCity: (_cityId: number) => [general, patient],
listGenerals: () => [general, patient],
};
const rng: RandomGenerator = {
nextFloat1: () => 0,
nextBool: () => true,
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
};
const caller = pipeline.getPreTurnExecuteTriggerList({
general,
worldView,
});
const triggerContext = createGeneralTriggerContext({
general,
rng,
worldView,
log: { push: () => {} },
});
caller.fire(triggerContext, {});
expect(general.injury).toBe(0);
expect(patient.injury).toBe(0);
expect(general.triggerState.flags['pre.치료']).toBe(true);
});
it('activates 의술 battle trigger and reduces damage', async () => {
const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']);
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
const registry = createTraitModuleRegistry({ domestic, war });
const traitModules = createTraitModules(registry);
const rng = new RandUtil(new ConstantRNG(0));
const config = buildConfig();
const unitSet = buildUnitSet();
const crewType = new WarCrewType(unitSet.crewTypes![0]!);
const city = buildCity();
const nation = buildNation();
const general = buildGeneral({
injury: 10,
role: {
personality: null,
specialDomestic: null,
specialWar: 'che_의술',
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
});
const attacker = new WarUnitGeneral(
rng,
config,
general,
city,
nation,
true,
crewType,
new ActionLogger({ generalId: 1, nationId: 1 }),
new WarActionPipeline(traitModules.war)
);
const defender = new WarUnitCity(
rng,
config,
city,
nation,
new WarCrewType(unitSet.crewTypes![1]!),
new ActionLogger({}),
200,
180
);
attacker.setOppose(defender);
defender.setOppose(attacker);
attacker.beginPhase();
const caller = attacker.getActionPipeline().getBattlePhaseTriggerList(attacker.getActionContext());
caller.fire({ rng, attacker, defender }, createWarTriggerEnv());
expect(defender.getWarPowerMultiply()).toBeCloseTo(0.7);
expect(general.injury).toBe(0);
});
});