멸망 후 등용장에 현재 임관 모드 적용

This commit is contained in:
2026-09-22 23:24:15 +00:00
parent e34c24c74d
commit 908ae72cba
5 changed files with 87 additions and 3 deletions
@@ -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,
@@ -76,6 +76,8 @@ export interface ActionContextWorldRef {
export interface ActionContextOptions<TArgs extends Record<string, unknown> = Record<string, unknown>> {
world: ActionContextWorldState;
/** Current game configuration, distinct from the legacy world meta. */
worldConfig?: Record<string, unknown>;
/** 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. */
@@ -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,
+40 -2
View File
@@ -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', () => {
+39
View File
@@ -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];