최신 main의 수비 설정 수정을 NPC 정책 작업에 통합한다

This commit is contained in:
2026-10-22 23:50:35 +09:00
2 changed files with 63 additions and 6 deletions
@@ -2004,8 +2004,10 @@ async function handleSetMySetting(
const scenarioEffect = world.getScenarioConfig().environment.scenarioEffect; const scenarioEffect = world.getScenarioConfig().environment.scenarioEffect;
const ignoresPenalty = isDefenceTrainPenaltyWaivedByScenarioEffect(scenarioEffect); const ignoresPenalty = isDefenceTrainPenaltyWaivedByScenarioEffect(scenarioEffect);
const constValues = asRecord(world.getScenarioConfig().const); const constValues = asRecord(world.getScenarioConfig().const);
const maxTrain = readMetaNumber(constValues, 'maxTrainByWar', 100); // Ref GameConstBase의 전투 상한을 사용한다. 명령 상한(100)으로 제한하면
const maxAtmos = readMetaNumber(constValues, 'maxAtmosByWar', 100); // 전투에서 오른 훈련·사기가 수비끔 전환 한 번에 소실된다.
const maxTrain = readMetaNumber(constValues, 'maxTrainByWar', 110);
const maxAtmos = readMetaNumber(constValues, 'maxAtmosByWar', 150);
const trainDelta = ignoresPenalty ? 0 : -3; const trainDelta = ignoresPenalty ? 0 : -3;
const atmosDelta = ignoresPenalty ? 0 : -6; const atmosDelta = ignoresPenalty ? 0 : -6;
nextTrain = Math.max(20, Math.min(maxTrain, general.train + trainDelta)); nextTrain = Math.max(20, Math.min(maxTrain, general.train + trainDelta));
@@ -87,7 +87,11 @@ const buildGeneral = (overrides: Partial<TurnGeneral> = {}): TurnGeneral => ({
const buildWorld = ( const buildWorld = (
general = buildGeneral(), general = buildGeneral(),
options: { autorunLimit?: boolean; scenarioEffect?: ScenarioEffectKey | null } = {} options: {
autorunLimit?: boolean;
scenarioEffect?: ScenarioEffectKey | null;
scenarioConst?: Record<string, unknown>;
} = {}
) => { ) => {
const state: TurnWorldState = { const state: TurnWorldState = {
id: 1, id: 1,
@@ -126,7 +130,7 @@ const buildWorld = (
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 65 }, stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 65 },
iconPath: '', iconPath: '',
map: {}, map: {},
const: { maxTrainByWar: 100, maxAtmosByWar: 100 }, const: options.scenarioConst ?? {},
environment: { environment: {
mapName: 'test', mapName: 'test',
unitSet: 'test', unitSet: 'test',
@@ -277,6 +281,53 @@ describe('my information world commands', () => {
}); });
}); });
it.each([
{ train: 110, atmos: 150, expectedTrain: 107, expectedAtmos: 144 },
{ train: 104, atmos: 107, expectedTrain: 101, expectedAtmos: 101 },
{ train: 90, atmos: 90, expectedTrain: 87, expectedAtmos: 84 },
{ train: 21, atmos: 25, expectedTrain: 20, expectedAtmos: 20 },
{
train: 140,
atmos: 180,
scenarioConst: { maxTrainByWar: 120, maxAtmosByWar: 160 },
expectedTrain: 120,
expectedAtmos: 160,
},
{
train: 110,
atmos: 150,
scenarioConst: { maxTrainByWar: 100, maxAtmosByWar: 100 },
expectedTrain: 100,
expectedAtmos: 100,
},
])('applies war caps to no-defence penalties: $train/$atmos', async (testCase) => {
const fixture = buildWorld(buildGeneral({ train: testCase.train, atmos: testCase.atmos }), {
scenarioConst: testCase.scenarioConst,
});
const command = {
type: 'setMySetting' as const,
userId: 'user-7',
generalId: 7,
settings: { defence_train: 999 },
};
await expect(fixture.handler.handle(command)).resolves.toMatchObject({ ok: true });
const expected = {
train: testCase.expectedTrain,
atmos: testCase.expectedAtmos,
meta: { defence_train: 999, myset: 2 },
};
expect(fixture.world.getGeneralById(7)).toMatchObject(expected);
// 같은 설정의 재저장과 수비 재개는 훈련·사기를 다시 차감하지 않는다.
await fixture.handler.handle(command);
expect(fixture.world.getGeneralById(7)).toMatchObject(expected);
await fixture.handler.handle({ ...command, settings: { defence_train: 90 } });
expect(fixture.world.getGeneralById(7)).toMatchObject({
...expected,
meta: { defence_train: 90, myset: 1 },
});
});
it.each([ it.each([
'event_UnlimitedDefenceThresholdChange', 'event_UnlimitedDefenceThresholdChange',
'event_StrongAttacker', 'event_StrongAttacker',
@@ -284,14 +335,18 @@ describe('my information world commands', () => {
] satisfies ScenarioEffectKey[])( ] satisfies ScenarioEffectKey[])(
'preserves the %s scenario that waives the no-defence penalty', 'preserves the %s scenario that waives the no-defence penalty',
async (scenarioEffect) => { async (scenarioEffect) => {
const fixture = buildWorld(buildGeneral(), { scenarioEffect }); const fixture = buildWorld(buildGeneral({ train: 110, atmos: 150 }), { scenarioEffect });
await fixture.handler.handle({ await fixture.handler.handle({
type: 'setMySetting', type: 'setMySetting',
userId: 'user-7', userId: 'user-7',
generalId: 7, generalId: 7,
settings: { defence_train: 999 }, settings: { defence_train: 999 },
}); });
expect(fixture.world.getGeneralById(7)).toMatchObject({ train: 90, atmos: 90 }); expect(fixture.world.getGeneralById(7)).toMatchObject({
train: 110,
atmos: 150,
meta: { defence_train: 999, myset: 2 },
});
} }
); );