From 856187b315c2f676a1ea9865340751c58c429a20 Mon Sep 17 00:00:00 2001 From: hide_d Date: Thu, 22 Oct 2026 23:11:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EB=B9=84=EB=81=94=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=EC=97=90=EC=84=9C=20=EC=A0=84=ED=88=AC=20=ED=9B=88?= =?UTF-8?q?=EB=A0=A8=C2=B7=EC=82=AC=EA=B8=B0=20=EC=83=81=ED=95=9C=EC=9D=84?= =?UTF-8?q?=20=EB=B3=B4=EC=A1=B4=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/turn/worldCommandHandler.ts | 6 +- .../test/myInformationCommands.test.ts | 63 +++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) 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 }, + }); } );