diff --git a/app/game-engine/src/turn/worldCommandHandler.ts b/app/game-engine/src/turn/worldCommandHandler.ts index 9ccd880b..8c59ffe7 100644 --- a/app/game-engine/src/turn/worldCommandHandler.ts +++ b/app/game-engine/src/turn/worldCommandHandler.ts @@ -2004,8 +2004,10 @@ async function handleSetMySetting( const scenarioEffect = world.getScenarioConfig().environment.scenarioEffect; const ignoresPenalty = isDefenceTrainPenaltyWaivedByScenarioEffect(scenarioEffect); const constValues = asRecord(world.getScenarioConfig().const); - const maxTrain = readMetaNumber(constValues, 'maxTrainByWar', 100); - const maxAtmos = readMetaNumber(constValues, 'maxAtmosByWar', 100); + // Ref GameConstBase의 전투 상한을 사용한다. 명령 상한(100)으로 제한하면 + // 전투에서 오른 훈련·사기가 수비끔 전환 한 번에 소실된다. + const maxTrain = readMetaNumber(constValues, 'maxTrainByWar', 110); + const maxAtmos = readMetaNumber(constValues, 'maxAtmosByWar', 150); const trainDelta = ignoresPenalty ? 0 : -3; const atmosDelta = ignoresPenalty ? 0 : -6; nextTrain = Math.max(20, Math.min(maxTrain, general.train + trainDelta)); diff --git a/app/game-engine/test/myInformationCommands.test.ts b/app/game-engine/test/myInformationCommands.test.ts index c7c25ffb..d483b7b7 100644 --- a/app/game-engine/test/myInformationCommands.test.ts +++ b/app/game-engine/test/myInformationCommands.test.ts @@ -87,7 +87,11 @@ const buildGeneral = (overrides: Partial = {}): TurnGeneral => ({ const buildWorld = ( general = buildGeneral(), - options: { autorunLimit?: boolean; scenarioEffect?: ScenarioEffectKey | null } = {} + options: { + autorunLimit?: boolean; + scenarioEffect?: ScenarioEffectKey | null; + scenarioConst?: Record; + } = {} ) => { const state: TurnWorldState = { id: 1, @@ -126,7 +130,7 @@ const buildWorld = ( stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 65 }, iconPath: '', map: {}, - const: { maxTrainByWar: 100, maxAtmosByWar: 100 }, + const: options.scenarioConst ?? {}, environment: { mapName: '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([ 'event_UnlimitedDefenceThresholdChange', 'event_StrongAttacker', @@ -284,14 +335,18 @@ describe('my information world commands', () => { ] satisfies ScenarioEffectKey[])( 'preserves the %s scenario that waives the no-defence penalty', async (scenarioEffect) => { - const fixture = buildWorld(buildGeneral(), { scenarioEffect }); + const fixture = buildWorld(buildGeneral({ train: 110, atmos: 150 }), { scenarioEffect }); await fixture.handler.handle({ type: 'setMySetting', userId: 'user-7', generalId: 7, 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 }, + }); } );