664 lines
25 KiB
TypeScript
664 lines
25 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../src/context.js';
|
||
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
||
import type { GeneralActionModule, MapDefinition, UnitSetDefinition } from '@sammo-ts/logic';
|
||
import { buildRecruitmentCommandInfo, buildTurnCommandTable } from '../src/turns/commandTable.js';
|
||
|
||
const buildWorldState = (joinMode = 'full', constOverrides: Record<string, unknown> = {}): WorldStateRow =>
|
||
({
|
||
id: 1,
|
||
scenarioCode: 'default',
|
||
currentYear: 3,
|
||
currentMonth: 1,
|
||
tickSeconds: 600,
|
||
config: {
|
||
joinMode,
|
||
const: {
|
||
baseGold: 1000,
|
||
baseRice: 1000,
|
||
develCost: 100,
|
||
...constOverrides,
|
||
},
|
||
},
|
||
meta: {
|
||
scenarioMeta: {
|
||
startYear: 1,
|
||
},
|
||
},
|
||
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
||
}) as unknown as WorldStateRow;
|
||
|
||
const buildGeneral = (): GeneralRow =>
|
||
({
|
||
id: 1,
|
||
name: 'TestGeneral',
|
||
nationId: 1,
|
||
cityId: 1,
|
||
troopId: 0,
|
||
leadership: 70,
|
||
strength: 60,
|
||
intel: 60,
|
||
experience: 0,
|
||
dedication: 0,
|
||
officerLevel: 6,
|
||
personalCode: null,
|
||
specialCode: null,
|
||
special2Code: null,
|
||
horseCode: null,
|
||
weaponCode: null,
|
||
bookCode: null,
|
||
itemCode: null,
|
||
injury: 0,
|
||
gold: 0,
|
||
rice: 0,
|
||
crew: 100,
|
||
crewTypeId: 1100,
|
||
train: 0,
|
||
atmos: 0,
|
||
age: 25,
|
||
npcState: 0,
|
||
meta: { killturn: 24 },
|
||
}) as unknown as GeneralRow;
|
||
|
||
const buildCity = (): CityRow =>
|
||
({
|
||
id: 1,
|
||
name: 'TestCity',
|
||
nationId: 1,
|
||
level: 5,
|
||
population: 100000,
|
||
populationMax: 200000,
|
||
agriculture: 1000,
|
||
agricultureMax: 2000,
|
||
commerce: 1000,
|
||
commerceMax: 2000,
|
||
security: 1000,
|
||
securityMax: 2000,
|
||
supplyState: 1,
|
||
frontState: 0,
|
||
defence: 0,
|
||
defenceMax: 0,
|
||
wall: 0,
|
||
wallMax: 0,
|
||
meta: {},
|
||
trust: 50,
|
||
trade: 0,
|
||
region: 0,
|
||
}) as unknown as CityRow;
|
||
|
||
const buildNation = (): NationRow =>
|
||
({
|
||
id: 1,
|
||
name: 'TestNation',
|
||
color: '#000000',
|
||
capitalCityId: 1,
|
||
gold: 0,
|
||
rice: 0,
|
||
level: 1,
|
||
typeCode: 'default',
|
||
tech: 0,
|
||
meta: {},
|
||
}) as unknown as NationRow;
|
||
|
||
describe('buildTurnCommandTable', () => {
|
||
it('projects the general and chief reserved-turn categories and command order from Ref', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
|
||
expect(table.general.map(({ category }) => category)).toEqual(['개인', '내정', '군사', '인사', '계략', '국가']);
|
||
expect(
|
||
Object.fromEntries(table.general.map(({ category, values }) => [category, values.map(({ key }) => key)]))
|
||
).toEqual({
|
||
개인: [
|
||
'휴식',
|
||
'che_요양',
|
||
'che_단련',
|
||
'che_숙련전환',
|
||
'che_견문',
|
||
'che_은퇴',
|
||
'che_장비매매',
|
||
'che_군량매매',
|
||
'che_내정특기초기화',
|
||
'che_전투특기초기화',
|
||
],
|
||
내정: [
|
||
'che_농지개간',
|
||
'che_상업투자',
|
||
'che_기술연구',
|
||
'che_수비강화',
|
||
'che_성벽보수',
|
||
'che_치안강화',
|
||
'che_정착장려',
|
||
'che_주민선정',
|
||
],
|
||
군사: [
|
||
'che_징병',
|
||
'che_모병',
|
||
'che_훈련',
|
||
'che_사기진작',
|
||
'che_출병',
|
||
'che_집합',
|
||
'che_소집해제',
|
||
'che_첩보',
|
||
],
|
||
인사: [
|
||
'che_이동',
|
||
'che_강행',
|
||
'che_인재탐색',
|
||
'che_등용',
|
||
'che_귀환',
|
||
'che_임관',
|
||
'che_랜덤임관',
|
||
'che_장수대상임관',
|
||
],
|
||
계략: ['che_선동', 'che_탈취', 'che_파괴', 'che_화계'],
|
||
국가: ['che_증여', 'che_헌납', 'che_물자조달', 'che_하야', 'che_거병', 'che_건국', 'che_선양', 'che_해산'],
|
||
});
|
||
expect(table.nation.map(({ category }) => category)).toEqual(['휴식', '인사', '외교', '특수', '전략', '기타']);
|
||
expect(
|
||
Object.fromEntries(table.nation.map(({ category, values }) => [category, values.map(({ key }) => key)]))
|
||
).toEqual({
|
||
휴식: ['휴식'],
|
||
인사: ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'],
|
||
외교: ['che_물자원조', 'che_불가침제의', 'che_선전포고', 'che_종전제의', 'che_불가침파기제의'],
|
||
특수: ['che_초토화', 'che_천도', 'che_증축', 'che_감축'],
|
||
전략: [
|
||
'che_필사즉생',
|
||
'che_백성동원',
|
||
'che_수몰',
|
||
'che_허보',
|
||
'che_의병모집',
|
||
'che_이호경식',
|
||
'che_급습',
|
||
'che_피장파장',
|
||
],
|
||
기타: ['che_국기변경', 'che_국호변경'],
|
||
});
|
||
expect(
|
||
Object.fromEntries(table.nation.map(({ category, values }) => [category, values.map(({ name }) => name)]))
|
||
).toEqual({
|
||
휴식: ['휴식'],
|
||
인사: ['발령', '포상', '몰수', '부대 탈퇴 지시'],
|
||
외교: ['원조', '불가침 제의', '선전포고', '종전 제의', '불가침 파기 제의'],
|
||
특수: ['초토화', '천도', '증축', '감축'],
|
||
전략: ['필사즉생', '백성동원', '수몰', '허보', '의병모집', '이호경식', '급습', '피장파장'],
|
||
기타: ['국기변경', '국호변경'],
|
||
});
|
||
expect(
|
||
Object.fromEntries(
|
||
table.nation
|
||
.flatMap(({ values }) => values)
|
||
.filter(({ turnDurationText }) => turnDurationText)
|
||
.map(({ key, turnDurationText }) => [key, turnDurationText])
|
||
)
|
||
).toEqual({
|
||
che_초토화: '3턴',
|
||
che_천도: '1+거리×2턴',
|
||
che_증축: '6턴',
|
||
che_감축: '6턴',
|
||
che_필사즉생: '3턴',
|
||
che_수몰: '3턴',
|
||
che_허보: '2턴',
|
||
che_의병모집: '3턴',
|
||
che_피장파장: '2턴',
|
||
});
|
||
expect(table.general.flatMap(({ values }) => values)).not.toContainEqual(
|
||
expect.objectContaining({ turnDurationText: expect.any(String) })
|
||
);
|
||
const generalCosts = Object.fromEntries(
|
||
table.general.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText]))
|
||
);
|
||
expect(generalCosts).toMatchObject({
|
||
che_농지개간: '금 100',
|
||
che_기술연구: '금 100',
|
||
che_정착장려: '쌀 200',
|
||
che_주민선정: '쌀 200',
|
||
che_화계: '금 500 · 쌀 500',
|
||
che_첩보: '금 300 · 쌀 300',
|
||
che_사기진작: '금 1',
|
||
che_출병: '쌀 1',
|
||
che_이동: '금 100',
|
||
che_강행: '금 500',
|
||
});
|
||
const nationCosts = Object.fromEntries(
|
||
table.nation.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText]))
|
||
);
|
||
expect(nationCosts).toMatchObject({
|
||
che_천도: '금·쌀 500 × 2^거리',
|
||
che_증축: '금 110,000 · 쌀 110,000',
|
||
che_감축: '금 80,000 · 쌀 80,000 회수',
|
||
});
|
||
});
|
||
|
||
it('projects scenario-specific command categories instead of the default profile', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState('full', {
|
||
availableGeneralCommand: {
|
||
개인: ['휴식'],
|
||
내정: ['che_물자조달'],
|
||
군사: ['cr_맹훈련'],
|
||
},
|
||
availableChiefCommand: {
|
||
휴식: ['휴식'],
|
||
특수: ['cr_인구이동'],
|
||
연구: ['event_대검병연구', 'event_화륜차연구'],
|
||
},
|
||
}),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
|
||
expect(table.general.map(({ category }) => category)).toEqual(['개인', '내정', '군사']);
|
||
expect(table.general.flatMap(({ values }) => values.map(({ key }) => key))).toEqual([
|
||
'휴식',
|
||
'che_물자조달',
|
||
'cr_맹훈련',
|
||
]);
|
||
expect(
|
||
Object.fromEntries(table.nation.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText])))
|
||
).toMatchObject({
|
||
cr_인구이동: '금·쌀 100 × 인구[만]',
|
||
event_대검병연구: '금 50,000 · 쌀 50,000',
|
||
event_화륜차연구: '금 100,000 · 쌀 100,000',
|
||
});
|
||
expect(table.nation.map(({ category }) => category)).toEqual(['휴식', '특수', '연구']);
|
||
expect(table.nation.flatMap(({ values }) => values.map(({ key }) => key))).toEqual([
|
||
'휴식',
|
||
'cr_인구이동',
|
||
'event_대검병연구',
|
||
'event_화륜차연구',
|
||
]);
|
||
expect(
|
||
Object.fromEntries(
|
||
table.nation.flatMap(({ values }) => values.map(({ key, turnDurationText }) => [key, turnDurationText]))
|
||
)
|
||
).toMatchObject({
|
||
event_대검병연구: '12턴',
|
||
event_화륜차연구: '24턴',
|
||
});
|
||
});
|
||
|
||
it('projects costs from the current world develcost used by command execution', async () => {
|
||
const worldState = buildWorldState();
|
||
(worldState as unknown as { meta: Record<string, unknown> }).meta.develcost = 120;
|
||
const table = await buildTurnCommandTable({
|
||
worldState,
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
const costs = Object.fromEntries(
|
||
[...table.general, ...table.nation].flatMap(({ values }) =>
|
||
values.map(({ key, costText }) => [key, costText])
|
||
)
|
||
);
|
||
expect(costs).toMatchObject({
|
||
che_농지개간: '금 120',
|
||
che_화계: '금 600 · 쌀 600',
|
||
che_천도: '금·쌀 600 × 2^거리',
|
||
che_증축: '금 120,000 · 쌀 120,000',
|
||
});
|
||
});
|
||
|
||
it('projects the real 904/905/910/912 world command profiles into the API table', async () => {
|
||
const buildScenarioTable = async (scenarioId: number) => {
|
||
const scenario = await loadScenarioDefinitionById(scenarioId);
|
||
return buildTurnCommandTable({
|
||
worldState: buildWorldState('full', scenario.config.const),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
};
|
||
const commandCategory = (groups: Awaited<ReturnType<typeof buildScenarioTable>>['general'], key: string) =>
|
||
groups.find((group) => group.values.some((command) => command.key === key))?.category;
|
||
const nationCommandCategory = (groups: Awaited<ReturnType<typeof buildScenarioTable>>['nation'], key: string) =>
|
||
groups.find((group) => group.values.some((command) => command.key === key))?.category;
|
||
const [scenario904, scenario905, scenario910, scenario912] = await Promise.all(
|
||
[904, 905, 910, 912].map(buildScenarioTable)
|
||
);
|
||
|
||
expect(commandCategory(scenario904.general, 'che_물자조달')).toBe('내정');
|
||
expect(commandCategory(scenario904.general, 'che_거병')).toBeUndefined();
|
||
expect(nationCommandCategory(scenario904.nation, 'che_피장파장')).toBe('기타');
|
||
expect(nationCommandCategory(scenario904.nation, 'che_선전포고')).toBeUndefined();
|
||
expect(nationCommandCategory(scenario904.nation, 'che_부대탈퇴지시')).toBeUndefined();
|
||
|
||
expect(commandCategory(scenario905.general, 'che_무작위건국')).toBe('국가');
|
||
expect(nationCommandCategory(scenario905.nation, 'che_무작위수도이전')).toBe('기타');
|
||
|
||
expect(commandCategory(scenario910.general, 'cr_맹훈련')).toBe('군사');
|
||
expect(commandCategory(scenario910.general, 'cr_건국')).toBe('국가');
|
||
expect(nationCommandCategory(scenario910.nation, 'cr_인구이동')).toBe('특수');
|
||
|
||
expect(nationCommandCategory(scenario912.nation, 'event_대검병연구')).toBe('연구');
|
||
expect(
|
||
scenario912.nation.find((group) => group.category === '연구')?.values.map((command) => command.key)
|
||
).toEqual([
|
||
'event_대검병연구',
|
||
'event_극병연구',
|
||
'event_화시병연구',
|
||
'event_원융노병연구',
|
||
'event_산저병연구',
|
||
'event_음귀병연구',
|
||
'event_무희연구',
|
||
'event_상병연구',
|
||
'event_화륜차연구',
|
||
]);
|
||
});
|
||
|
||
it('keeps every default general and chief argument command inside the shared frontend field contract', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
const supportedKinds = new Set(['text', 'number', 'boolean', 'select', 'numberTuple', 'hidden']);
|
||
|
||
for (const [scope, groups] of [
|
||
['general', table.general],
|
||
['nation', table.nation],
|
||
] as const) {
|
||
for (const command of groups.flatMap((group) => group.values)) {
|
||
if (!command.reqArg) continue;
|
||
expect(command.inputFields.length, `${scope}:${command.key}`).toBeGreaterThan(0);
|
||
expect(new Set(command.inputFields.map((field) => field.key)).size, `${scope}:${command.key}`).toBe(
|
||
command.inputFields.length
|
||
);
|
||
for (const field of command.inputFields) {
|
||
expect(supportedKinds.has(field.kind), `${scope}:${command.key}:${field.key}`).toBe(true);
|
||
if (field.kind === 'select') {
|
||
expect(
|
||
Boolean(field.options?.length || field.optionSource),
|
||
`${scope}:${command.key}:${field.key}`
|
||
).toBe(true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
it('projects the Ref twenty-year non-aggression end-year window', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
const proposal = table.nation
|
||
.flatMap((group) => group.values)
|
||
.find((command) => command.key === 'che_불가침제의');
|
||
|
||
expect(proposal?.inputFields.find((field) => field.key === 'year')).toMatchObject({
|
||
kind: 'number',
|
||
min: 4,
|
||
max: 23,
|
||
defaultValue: 4,
|
||
});
|
||
expect(proposal?.inputFields.find((field) => field.key === 'month')).toMatchObject({ defaultValue: 1 });
|
||
});
|
||
|
||
it('projects the Ref availability boundaries for force move, retirement, and resignation', async () => {
|
||
const buildTable = (general: GeneralRow, nation: NationRow | null = buildNation()) =>
|
||
buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general,
|
||
city: buildCity(),
|
||
nation,
|
||
nationGenerals: null,
|
||
});
|
||
const findCommand = (table: Awaited<ReturnType<typeof buildTable>>, key: string) =>
|
||
table.general.flatMap((group) => group.values).find((command) => command.key === key);
|
||
|
||
const ordinary = await buildTable(buildGeneral());
|
||
expect(findCommand(ordinary, 'che_강행')).toMatchObject({
|
||
name: '강행',
|
||
reqArg: true,
|
||
possible: true,
|
||
inputFields: [{ key: 'destCityId', optionSource: 'cities' }],
|
||
});
|
||
expect(findCommand(ordinary, 'che_은퇴')).toMatchObject({
|
||
name: '은퇴',
|
||
possible: false,
|
||
status: 'blocked',
|
||
reason: '나이가 60세 이상이어야 합니다.',
|
||
});
|
||
expect(findCommand(ordinary, 'che_하야')).toMatchObject({
|
||
name: '하야',
|
||
possible: true,
|
||
status: 'available',
|
||
});
|
||
|
||
const oldEnough = await buildTable({ ...buildGeneral(), age: 60 } as GeneralRow);
|
||
expect(findCommand(oldEnough, 'che_은퇴')).toMatchObject({ possible: true, status: 'available' });
|
||
|
||
const ruler = await buildTable({ ...buildGeneral(), officerLevel: 12 } as GeneralRow);
|
||
expect(findCommand(ruler, 'che_하야')).toMatchObject({
|
||
possible: false,
|
||
status: 'blocked',
|
||
reason: expect.stringContaining('군주'),
|
||
});
|
||
|
||
const neutral = await buildTable({ ...buildGeneral(), nationId: 0, officerLevel: 0 } as GeneralRow, null);
|
||
expect(findCommand(neutral, 'che_하야')).toMatchObject({
|
||
possible: false,
|
||
status: 'blocked',
|
||
reason: '재야입니다.',
|
||
});
|
||
});
|
||
|
||
it('exposes the user-only spy command with a city target when the actor can pay the Ref cost', async () => {
|
||
const general = { ...buildGeneral(), gold: 300, rice: 300 } as GeneralRow;
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general,
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
|
||
const spy = table.general
|
||
.find(({ category }) => category === '군사')
|
||
?.values.find(({ key }) => key === 'che_첩보');
|
||
|
||
expect(spy).toMatchObject({
|
||
name: '첩보',
|
||
reqArg: true,
|
||
possible: true,
|
||
status: 'available',
|
||
inputFields: [
|
||
{
|
||
key: 'destCityId',
|
||
label: '대상 도시',
|
||
kind: 'select',
|
||
required: true,
|
||
optionSource: 'cities',
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('uses min-condition constraints for availability', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState(),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
|
||
const nationCommand = table.nation
|
||
.flatMap((group) => group.values)
|
||
.find((command) => command.key === 'che_포상');
|
||
|
||
expect(nationCommand).toBeDefined();
|
||
expect(nationCommand?.possible).toBe(true);
|
||
expect(nationCommand?.status).not.toBe('blocked');
|
||
});
|
||
|
||
it('provides join_mode to reservation availability constraints', async () => {
|
||
const table = await buildTurnCommandTable({
|
||
worldState: buildWorldState('onlyRandom'),
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
});
|
||
|
||
const appointment = table.general
|
||
.flatMap((group) => group.values)
|
||
.find((command) => command.key === 'che_임관');
|
||
|
||
expect(appointment).toMatchObject({
|
||
possible: false,
|
||
status: 'blocked',
|
||
reason: '랜덤 임관만 가능합니다',
|
||
});
|
||
});
|
||
|
||
it('hides founding at the Ref maxnation boundary without counting Core id=0', async () => {
|
||
const worldState = buildWorldState();
|
||
worldState.meta = {
|
||
...((worldState.meta ?? {}) as Record<string, unknown>),
|
||
refGameEnv: { maxnation: '2' },
|
||
};
|
||
const buildAtCount = (realNationCount: number) =>
|
||
buildTurnCommandTable({
|
||
worldState,
|
||
general: buildGeneral(),
|
||
city: buildCity(),
|
||
nation: buildNation(),
|
||
nationGenerals: null,
|
||
realNationCount,
|
||
});
|
||
const findFounding = (table: Awaited<ReturnType<typeof buildAtCount>>) =>
|
||
table.general.flatMap((group) => group.values).find((command) => command.key === 'che_건국');
|
||
|
||
expect(findFounding(await buildAtCount(1))).not.toMatchObject({
|
||
reason: '더 이상 건국은 불가능합니다.',
|
||
});
|
||
expect(findFounding(await buildAtCount(2))).toMatchObject({
|
||
possible: false,
|
||
status: 'blocked',
|
||
reason: '더 이상 건국은 불가능합니다.',
|
||
});
|
||
});
|
||
|
||
it('projects Ref recruitment availability, combat values, descriptions, and adjusted costs', () => {
|
||
const general = buildGeneral();
|
||
general.injury = 3;
|
||
general.gold = 12_345;
|
||
const nation = buildNation();
|
||
nation.tech = 1000;
|
||
const unitSet = {
|
||
id: 'test',
|
||
name: 'test',
|
||
defaultCrewTypeId: 1100,
|
||
armTypes: { 1: '보병' },
|
||
crewTypes: [
|
||
{
|
||
id: 1100,
|
||
armType: 1,
|
||
name: '보병',
|
||
attack: 100,
|
||
defence: 150,
|
||
speed: 7,
|
||
avoid: 10,
|
||
magicCoef: 0,
|
||
cost: 9,
|
||
rice: 9,
|
||
requirements: [],
|
||
attackCoef: {},
|
||
defenceCoef: {},
|
||
info: ['표준적인 보병입니다.'],
|
||
initSkillTrigger: null,
|
||
phaseSkillTrigger: null,
|
||
iActionList: null,
|
||
},
|
||
{
|
||
id: 1101,
|
||
armType: 1,
|
||
name: '정예병',
|
||
attack: 150,
|
||
defence: 200,
|
||
speed: 8,
|
||
avoid: 20,
|
||
magicCoef: 0,
|
||
cost: 12,
|
||
rice: 10,
|
||
requirements: [{ type: 'ReqTech', tech: 2000 }],
|
||
attackCoef: {},
|
||
defenceCoef: {},
|
||
info: ['강력하지만 기술이 필요합니다.'],
|
||
initSkillTrigger: null,
|
||
phaseSkillTrigger: null,
|
||
iActionList: null,
|
||
},
|
||
],
|
||
} satisfies UnitSetDefinition;
|
||
const map = {
|
||
id: 'test',
|
||
name: 'test',
|
||
cities: [{ id: 1, name: 'TestCity', region: 1 }],
|
||
} as unknown as MapDefinition;
|
||
const costDiscount: GeneralActionModule = {
|
||
onCalcDomestic: (_context, _turnType, varType, value) => (varType === 'cost' ? value * 0.9 : value),
|
||
};
|
||
|
||
const info = buildRecruitmentCommandInfo({
|
||
worldState: buildWorldState(),
|
||
general,
|
||
city: buildCity(),
|
||
nation,
|
||
cities: [buildCity()],
|
||
map,
|
||
unitSet,
|
||
generalActionModules: [costDiscount],
|
||
});
|
||
|
||
expect(info).toMatchObject({
|
||
techLevel: 1,
|
||
fullLeadership: 70,
|
||
currentCrewTypeId: 1100,
|
||
currentCrewTypeName: '보병',
|
||
crew: 100,
|
||
gold: 12_345,
|
||
});
|
||
expect(info.leadership).toBeLessThan(info.fullLeadership);
|
||
expect(info.groups).toHaveLength(1);
|
||
expect(info.groups[0]?.values[0]).toMatchObject({
|
||
name: '보병',
|
||
available: true,
|
||
special: false,
|
||
attack: 125,
|
||
defence: 175,
|
||
speed: 7,
|
||
avoid: 10,
|
||
info: ['표준적인 보병입니다.'],
|
||
});
|
||
expect(info.groups[0]?.values[0]?.baseCost).toBeCloseTo(9 * 1.15 * 0.9, 10);
|
||
expect(info.groups[0]?.values[0]?.baseRice).toBeCloseTo(9 * 1.15, 10);
|
||
expect(info.groups[0]?.values[1]).toMatchObject({
|
||
name: '정예병',
|
||
available: false,
|
||
special: true,
|
||
attack: 175,
|
||
defence: 225,
|
||
info: ['강력하지만 기술이 필요합니다.'],
|
||
});
|
||
});
|
||
});
|