fix(logic): 징병과 모병에 추가 통솔 상한을 반영한다
병력 여유 제약이 실제 통솔 계산기를 사용하도록 연결하고, 명마 보정이 적용되는 징병·모병 회귀 테스트를 추가한다.
This commit is contained in:
@@ -597,7 +597,20 @@ export class ActionDefinition<
|
|||||||
reqCityTrust(20),
|
reqCityTrust(20),
|
||||||
reqGeneralGold(getCost, requirements),
|
reqGeneralGold(getCost, requirements),
|
||||||
reqGeneralRice(getRice, 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') {
|
if (ctx.mode === 'full') {
|
||||||
|
|||||||
@@ -432,7 +432,8 @@ export const allowJoinDestNation = (relYear: number): Constraint => ({
|
|||||||
|
|
||||||
export const reqGeneralCrewMargin = (
|
export const reqGeneralCrewMargin = (
|
||||||
getCrewTypeId: (ctx: ConstraintContext, view: StateView) => number | null,
|
getCrewTypeId: (ctx: ConstraintContext, view: StateView) => number | null,
|
||||||
requirements: RequirementKey[] = []
|
requirements: RequirementKey[] = [],
|
||||||
|
getMaxCrew?: (ctx: ConstraintContext, view: StateView) => number | null
|
||||||
): Constraint => ({
|
): Constraint => ({
|
||||||
name: 'reqGeneralCrewMargin',
|
name: 'reqGeneralCrewMargin',
|
||||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||||
@@ -453,7 +454,10 @@ export const reqGeneralCrewMargin = (
|
|||||||
if (crewTypeId !== general.crewTypeId) {
|
if (crewTypeId !== general.crewTypeId) {
|
||||||
return allow();
|
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) {
|
if (maxCrew > general.crew) {
|
||||||
return allow();
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user