From 908ae72cba09563a384d5a240d22d978465aca3b Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 22 Sep 2026 23:24:15 +0000 Subject: [PATCH] =?UTF-8?q?=EB=A9=B8=EB=A7=9D=20=ED=9B=84=20=EB=93=B1?= =?UTF-8?q?=EC=9A=A9=EC=9E=A5=EC=97=90=20=ED=98=84=EC=9E=AC=20=EC=9E=84?= =?UTF-8?q?=EA=B4=80=20=EB=AA=A8=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/turn/reservedTurnHandler.ts | 1 + .../logic/src/actions/turn/actionContext.ts | 2 + .../src/actions/turn/general/che_출병.ts | 6 ++- packages/logic/test/dispatchWarAction.test.ts | 42 ++++++++++++++++++- packages/logic/test/warAftermath.test.ts | 39 +++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index e7648e83..ef8ab6c6 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -2825,6 +2825,7 @@ export const createImmediateGeneralActionExecutor = async (options: { baseContext, { world: state, + worldConfig: options.world.getWorldConfig(), scenarioConfig: options.world.getScenarioConfig(), scenarioMeta: options.scenarioMeta, map: options.map, diff --git a/packages/logic/src/actions/turn/actionContext.ts b/packages/logic/src/actions/turn/actionContext.ts index 268b8c7d..51ffdbcc 100644 --- a/packages/logic/src/actions/turn/actionContext.ts +++ b/packages/logic/src/actions/turn/actionContext.ts @@ -76,6 +76,8 @@ export interface ActionContextWorldRef { export interface ActionContextOptions = Record> { world: ActionContextWorldState; + /** Current game configuration, distinct from the legacy world meta. */ + worldConfig?: Record; /** Ref Message::sendRaw stamps the current logical game tick, not the actor turn time. */ gameNow?: Date; /** Differential fixtures may point shared message icons at the Ref asset origin. */ diff --git a/packages/logic/src/actions/turn/general/che_출병.ts b/packages/logic/src/actions/turn/general/che_출병.ts index 10784117..9ceaecb8 100644 --- a/packages/logic/src/actions/turn/general/che_출병.ts +++ b/packages/logic/src/actions/turn/general/che_출병.ts @@ -824,7 +824,11 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => { const diplomacy = options.worldRef.listDiplomacy(); const warConfig = buildWarConfig(options.scenarioConfig, options.unitSet); const aftermathConfig = buildWarAftermathConfig(options.scenarioConfig, warConfig.castleCrewTypeId); - const joinModeRaw = options.world.meta?.join_mode ?? options.world.meta?.joinMode; + const joinModeRaw = + options.worldConfig?.join_mode ?? + options.worldConfig?.joinMode ?? + options.world.meta?.join_mode ?? + options.world.meta?.joinMode; aftermathConfig.joinMode = joinModeRaw === 'onlyRandom' ? 'onlyRandom' : 'full'; return { ...base, diff --git a/packages/logic/test/dispatchWarAction.test.ts b/packages/logic/test/dispatchWarAction.test.ts index 9f314a43..e46d4a34 100644 --- a/packages/logic/test/dispatchWarAction.test.ts +++ b/packages/logic/test/dispatchWarAction.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { City, General, Nation } from '../src/domain/entities.js'; import { resolveGeneralAction } from '../src/actions/engine.js'; -import { ActionDefinition, orderDefenderGenerals } from '../src/actions/turn/general/che_출병.js'; +import { ActionDefinition, actionContextBuilder, orderDefenderGenerals } from '../src/actions/turn/general/che_출병.js'; import type { DispatchResolveContext } from '../src/actions/turn/general/che_출병.js'; import type { TurnSchedule } from '../src/turn/calendar.js'; import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js'; @@ -192,6 +192,44 @@ const uniqueItem: ItemModule = { }; describe('che_출병', () => { + it.each(['full', 'onlyRandom'] as const)( + 'reads %s join mode from current world config for collapse aftermath', + (joinMode) => { + const general = { ...buildGeneral(1, 1, 1), turnTime: new Date('0200-01-01T00:00:00.000Z') }; + const destination = buildCity(2, 2); + const context = actionContextBuilder( + { general, rng }, + { + world: { currentYear: 200, currentMonth: 1, tickSeconds: 3600, meta: { joinMode: 'full' } }, + worldConfig: { joinMode }, + scenarioConfig: { + stat: { total: 0, min: 0, max: 0, npcTotal: 0, npcMin: 0, npcMax: 0, chiefMin: 0 }, + iconPath: '', + map: {}, + const: {}, + environment: { mapName: 'test', unitSet: 'test' }, + }, + map: { id: 'test', name: 'test', cities: [] }, + unitSet, + worldRef: { + getCityById: () => destination, + getNationById: () => buildNation(2), + listCities: () => [destination], + listNations: () => [buildNation(2)], + listGenerals: () => [general], + listDiplomacy: () => [], + } as never, + actionArgs: { destCityId: destination.id }, + createGeneralId: () => 3, + createNationId: () => 3, + seedBase: 'test', + } + ); + + expect(context?.aftermathConfig).toMatchObject({ joinMode }); + } + ); + it('runs war battle and emits patches/logs', () => { const attackerNation = buildNation(1); const defenderNation = buildNation(2); @@ -494,7 +532,7 @@ describe('che_출병', () => { // assignment, so the aftermath alone would not patch it). const defenderPatch = resolution.patches?.generals.find((patch) => patch.id === defender.id); expect(defenderPatch).toBeDefined(); - expect((defenderPatch?.patch.crew ?? defender.crew)).toBeLessThan(1500); + expect(defenderPatch?.patch.crew ?? defender.crew).toBeLessThan(1500); }); it('orders equal-priority defender inputs by general number before the stable battle sort', () => { diff --git a/packages/logic/test/warAftermath.test.ts b/packages/logic/test/warAftermath.test.ts index ce3b87a1..e55884ed 100644 --- a/packages/logic/test/warAftermath.test.ts +++ b/packages/logic/test/warAftermath.test.ts @@ -726,6 +726,45 @@ describe('war aftermath', () => { ]); }); + it('does not draw or send collapse recruitment in random-only mode', () => { + const attackerNation = buildNation(1); + const defenderNation = buildNation(2); + defenderNation.chiefGeneralId = 2; + const attackerCity = buildCity(1, 1); + const defenderCity = buildCity(2, 2); + const attacker = buildGeneral(1, 1, 1); + const lord = buildGeneral(2, 2, 2); + lord.officerLevel = 12; + lord.npcState = 2; + const rng = { + nextRange: vi.fn(() => 0.2), + nextBool: vi.fn(() => true), + nextRangeInt: vi.fn(() => 1), + } as unknown as RandUtil; + + const outcome = resolveWarAftermath({ + battle: { attacker, defenders: [], defenderCity, logs: [], conquered: true, reports: [] }, + attackerNation, + defenderNation, + attackerCity, + defenderCity, + nations: [attackerNation, defenderNation], + cities: [attackerCity, defenderCity], + generals: [attacker, lord], + unitSet: buildUnitSet(), + map: DEFAULT_MAP, + config: { ...buildConfig(), joinMode: 'onlyRandom' }, + time: { year: 186, month: 1, startYear: 179 }, + messageTime: MESSAGE_TIME, + rng, + }); + + expect(outcome.conquest?.nationCollapsed).toBe(true); + expect(outcome.conquest?.messages).toEqual([]); + expect(outcome.conquest?.ruinedNpcJoinPlans).toEqual([]); + expect(rng.nextBool).not.toHaveBeenCalled(); + }); + it('dispatches city conquest to every stationed defender before collapse RNG', () => { const rng = new RandUtil(new ConstantRNG(0)); const draws = [0.01, 0.02, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7];