merge: 징병과 모병 추가 통솔 수정을 반영한다

This commit is contained in:
2026-08-22 15:48:30 +00:00
3 changed files with 202 additions and 3 deletions
@@ -597,7 +597,20 @@ export class ActionDefinition<
reqCityTrust(20),
reqGeneralGold(getCost, requirements),
reqGeneralRice(getRice, requirements),
reqGeneralCrewMargin((context) => resolveCrewTypeId(context.args), requirements),
reqGeneralCrewMargin(
(context) => resolveCrewTypeId(context.args),
requirements,
(context, view) => {
const calcContext = buildCalcContext<TriggerState>(context, view);
if (!calcContext) {
return null;
}
// Ref ReqGeneralCrewMargin reconstructs General and calls
// getLeadership(), so the precondition must use the same
// injured, trait, and item-adjusted capacity as getCost().
return this.command.resolveLeadership(calcContext) * 100;
}
),
];
if (ctx.mode === 'full') {
+6 -2
View File
@@ -432,7 +432,8 @@ export const allowJoinDestNation = (relYear: number): Constraint => ({
export const reqGeneralCrewMargin = (
getCrewTypeId: (ctx: ConstraintContext, view: StateView) => number | null,
requirements: RequirementKey[] = []
requirements: RequirementKey[] = [],
getMaxCrew?: (ctx: ConstraintContext, view: StateView) => number | null
): Constraint => ({
name: 'reqGeneralCrewMargin',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
@@ -453,7 +454,10 @@ export const reqGeneralCrewMargin = (
if (crewTypeId !== general.crewTypeId) {
return allow();
}
const maxCrew = general.stats.leadership * 100;
const maxCrew = getMaxCrew ? getMaxCrew(ctx, view) : general.stats.leadership * 100;
if (maxCrew === null) {
return unknownOrDeny(ctx, requirements, '장수 정보가 없습니다.');
}
if (maxCrew > general.crew) {
return allow();
}
@@ -0,0 +1,182 @@
import { describe, expect, it } from 'vitest';
import type { City, General, Nation } from '../../src/domain/entities.js';
import { loadActionModuleBundle } from '../../src/actionModules/bundle.js';
import { commandSpec as conscriptSpec } from '../../src/actions/turn/general/che_징병.js';
import { commandSpec as recruitSpec } from '../../src/actions/turn/general/che_모병.js';
import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js';
import type { WorldSnapshot } from '../../src/world/types.js';
import { MINIMAL_MAP } from '../fixtures/minimalMap.js';
import { InMemoryWorld, TestGameRunner } from '../testEnv.js';
const NATION_ID = 1;
const CITY_ID = 1;
const GENERAL_ID = 1;
const CREW_TYPE_ID = 1;
const UNIT_SET = {
id: 'default',
name: 'default',
crewTypes: [
{
id: CREW_TYPE_ID,
name: '보병',
armType: 1,
attack: 100,
defence: 100,
speed: 5,
avoid: 0,
magicCoef: 0,
cost: 10,
rice: 1,
requirements: [],
attackCoef: {},
defenceCoef: {},
info: [],
initSkillTrigger: null,
phaseSkillTrigger: null,
iActionList: null,
},
],
};
const makeWorld = (): InMemoryWorld => {
const nation: Nation = {
id: NATION_ID,
name: '추가 통솔 테스트국',
color: '#ffffff',
capitalCityId: CITY_ID,
chiefGeneralId: GENERAL_ID,
gold: 100_000,
rice: 100_000,
power: 0,
level: 5,
typeCode: 'test',
meta: { tech: 0 },
};
const cityDefinition = MINIMAL_MAP.cities[0];
if (!cityDefinition) {
throw new Error('minimal map city is missing');
}
const city: City = {
id: CITY_ID,
name: cityDefinition.name,
nationId: NATION_ID,
level: 1,
state: 0,
population: 100_000,
populationMax: 100_000,
agriculture: 1_000,
agricultureMax: 1_000,
commerce: 1_000,
commerceMax: 1_000,
security: 1_000,
securityMax: 1_000,
defence: 1_000,
defenceMax: 1_000,
wall: 1_000,
wallMax: 1_000,
supplyState: 1,
frontState: 0,
meta: { trust: 100 },
};
const general: General = {
id: GENERAL_ID,
name: '추가 통솔 장수',
nationId: NATION_ID,
cityId: CITY_ID,
troopId: 0,
npcState: 0,
experience: 0,
dedication: 0,
officerLevel: 5,
gold: 100_000,
rice: 100_000,
crew: 10_000,
crewTypeId: CREW_TYPE_ID,
train: 100,
atmos: 100,
injury: 0,
age: 30,
stats: { leadership: 100, strength: 70, intelligence: 70 },
role: {
personality: null,
specialDomestic: null,
specialWar: null,
items: { horse: 'che_명마_10_옥추마', weapon: null, book: null, item: null },
},
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
meta: { killturn: 24 },
};
const snapshot: WorldSnapshot = {
scenarioConfig: { environment: { mapName: 'minimal_map', unitSet: 'default' }, options: {} } as never,
scenarioMeta: {
title: '추가 통솔 테스트',
startYear: 200,
life: 0,
fiction: 0,
history: [],
ignoreDefaultEvents: false,
},
map: MINIMAL_MAP,
unitSet: UNIT_SET,
nations: [nation],
cities: [city],
generals: [general],
troops: [],
diplomacy: [],
events: [],
initialEvents: [],
};
return new InMemoryWorld(snapshot);
};
const makeEnv = async (): Promise<TurnCommandEnv> => {
const actionModules = await loadActionModuleBundle();
return {
unitSet: UNIT_SET,
develCost: 50,
trainDelta: 35,
atmosDelta: 35,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
sabotageDefaultProb: 0.5,
sabotageProbCoefByStat: 0.1,
sabotageDefenceCoefByGeneralCount: 0.1,
sabotageDamageMin: 10,
sabotageDamageMax: 20,
openingPartYear: 200,
maxGeneral: 10,
defaultNpcGold: 1_000,
defaultNpcRice: 1_000,
defaultCrewTypeId: CREW_TYPE_ID,
defaultSpecialDomestic: null,
defaultSpecialWar: null,
initialNationGenLimit: 10,
maxTechLevel: 10,
baseGold: 1_000,
baseRice: 1_000,
maxResourceActionAmount: 1_000,
generalActionModules: actionModules.general,
};
};
describe.each([
['징병', 'che_징병', conscriptSpec],
['모병', 'che_모병', recruitSpec],
] as const)('%s 추가 통솔 병력 여유', (_name, commandKey, commandSpec) => {
it('원시 통솔 한도에 도달해도 보정 통솔만큼 같은 병종을 추가 모집한다', async () => {
const world = makeWorld();
const runner = new TestGameRunner(world, 200, 1);
const definition = commandSpec.createDefinition(await makeEnv());
await runner.runTurn([
{
generalId: GENERAL_ID,
commandKey,
resolver: definition,
args: { crewType: CREW_TYPE_ID, amount: 100 },
},
]);
expect(world.getGeneral(GENERAL_ID)?.crew).toBe(10_100);
});
});