fix: 내부 전용 예약 명령 실행 경계를 분리한다
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
INTERNAL_GENERAL_TURN_COMMAND_KEYS,
|
||||
LEGACY_RANDOM_GENERAL_FIRST_NAMES,
|
||||
LEGACY_RANDOM_GENERAL_LAST_NAMES,
|
||||
LEGACY_DEFAULT_MAX_LEVEL,
|
||||
@@ -173,6 +174,7 @@ export const buildReservedTurnDefinitions = async (options: {
|
||||
env: TurnCommandEnv;
|
||||
commandProfile: TurnCommandProfile;
|
||||
defaultActionKey: GeneralTurnCommandKey & NationTurnCommandKey;
|
||||
internalGeneralCommandKeys?: readonly GeneralTurnCommandKey[];
|
||||
}): Promise<{
|
||||
general: Map<string, GeneralActionDefinition>;
|
||||
nation: Map<string, GeneralActionDefinition>;
|
||||
@@ -198,7 +200,10 @@ export const buildReservedTurnDefinitions = async (options: {
|
||||
options.env.warActionModules ??= moduleBundle.war;
|
||||
options.env.nationTraitModules = moduleBundle.nationTraitModules;
|
||||
|
||||
const generalSpecs = await loadGeneralTurnCommandSpecs(options.commandProfile.general);
|
||||
const generalSpecs = await loadGeneralTurnCommandSpecs([
|
||||
...options.commandProfile.general,
|
||||
...(options.internalGeneralCommandKeys ?? INTERNAL_GENERAL_TURN_COMMAND_KEYS),
|
||||
]);
|
||||
const nationSpecs = await loadNationTurnCommandSpecs(options.commandProfile.nation);
|
||||
|
||||
const general = new Map(generalSpecs.map((spec) => [spec.key, spec.createDefinition(options.env)]));
|
||||
|
||||
@@ -10,13 +10,13 @@ import type {
|
||||
ScenarioConfig,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
GeneralTurnCommandKey,
|
||||
TurnCommandProfile,
|
||||
TurnCommandEnv,
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
DEFAULT_TURN_COMMAND_PROFILE,
|
||||
INTERNAL_GENERAL_TURN_COMMAND_KEYS,
|
||||
GeneralTurnCommandLoader,
|
||||
GeneralActionPipeline,
|
||||
NationTurnCommandLoader,
|
||||
@@ -71,8 +71,6 @@ import {
|
||||
} from './scenarioStaticEvents.js';
|
||||
|
||||
const DEFAULT_ACTION = '휴식';
|
||||
const AI_INTERNAL_GENERAL_ACTION_KEYS = ['che_NPC능동'] as const satisfies readonly GeneralTurnCommandKey[];
|
||||
|
||||
const LEGACY_STAT_CHANGE_GENERAL_ACTIONS = new Set([
|
||||
'che_소집해제',
|
||||
'che_랜덤임관',
|
||||
@@ -964,10 +962,9 @@ export const createReservedTurnHandler = async (options: {
|
||||
};
|
||||
const generalModuleLoader = new GeneralTurnCommandLoader();
|
||||
const nationModuleLoader = new NationTurnCommandLoader();
|
||||
// NPC AI emits a few engine-internal commands that are intentionally not
|
||||
// exposed by the scenario's player command profile. Keep their definitions
|
||||
// available to AI resolution without adding them to the public profile.
|
||||
for (const key of AI_INTERNAL_GENERAL_ACTION_KEYS) {
|
||||
// AI·월간 이벤트·서신이 생성하는 내부 명령은 사용자 선택 profile에는
|
||||
// 노출하지 않지만, 내부 실행 경로에서는 항상 정의와 context를 찾을 수 있어야 한다.
|
||||
for (const key of INTERNAL_GENERAL_TURN_COMMAND_KEYS) {
|
||||
const module = await generalModuleLoader.load(key);
|
||||
if (!generalDefinitions.has(key)) {
|
||||
generalDefinitions.set(key, module.commandSpec.createDefinition(env));
|
||||
@@ -2460,18 +2457,13 @@ export const createImmediateGeneralActionExecutor = async (options: {
|
||||
const env = buildCommandEnv(options.world.getScenarioConfig(), options.world.getUnitSet());
|
||||
const commandProfile = options.commandProfile ?? DEFAULT_TURN_COMMAND_PROFILE;
|
||||
// 등용수락은 예약 화면에 노출되는 명령이 아니라 등용 서신의 응답이
|
||||
// 직접 실행하는 내부 명령이다. 선택 가능 명령 프로필에 없더라도 등용
|
||||
// 서신을 수락할 수 있도록 즉시 행동 정의에는 항상 포함한다.
|
||||
const immediateCommandProfile: TurnCommandProfile = commandProfile.general.includes('che_등용수락')
|
||||
? commandProfile
|
||||
: {
|
||||
...commandProfile,
|
||||
general: [...commandProfile.general, 'che_등용수락'],
|
||||
};
|
||||
// 직접 실행하는 내부 명령이다. 공통 내부 집합 전체 대신 이 실행기에 필요한
|
||||
// 정의만 명시해 loader 경계를 재사용한다.
|
||||
const { general: definitions } = await buildReservedTurnDefinitions({
|
||||
env,
|
||||
commandProfile: immediateCommandProfile,
|
||||
commandProfile,
|
||||
defaultActionKey: DEFAULT_ACTION,
|
||||
internalGeneralCommandKeys: ['che_등용수락'],
|
||||
});
|
||||
const generalModuleLoader = new GeneralTurnCommandLoader();
|
||||
const contextBuilders = new Map<string, ActionContextBuilder>();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DEFAULT_TURN_COMMAND_PROFILE, type ScenarioConfig } from '@sammo-ts/logic';
|
||||
import {
|
||||
DEFAULT_TURN_COMMAND_PROFILE,
|
||||
INTERNAL_GENERAL_TURN_COMMAND_KEYS,
|
||||
type ScenarioConfig,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import { buildCommandEnv, buildReservedTurnDefinitions } from '../src/turn/reservedTurnCommands.js';
|
||||
|
||||
@@ -54,6 +58,7 @@ describe('Ref active-action inheritance inventory', () => {
|
||||
env: buildCommandEnv(scenarioConfig),
|
||||
commandProfile: DEFAULT_TURN_COMMAND_PROFILE,
|
||||
defaultActionKey: '휴식',
|
||||
internalGeneralCommandKeys: INTERNAL_GENERAL_TURN_COMMAND_KEYS,
|
||||
});
|
||||
|
||||
const generalWithFixedOrContextAmount = [...general.entries()]
|
||||
@@ -71,4 +76,14 @@ describe('Ref active-action inheritance inventory', () => {
|
||||
.sort();
|
||||
expect(nationWithPoint).toEqual([...nationCommands].sort());
|
||||
});
|
||||
|
||||
it('loads every internal command without adding it to the selectable profile', async () => {
|
||||
const { general } = await buildReservedTurnDefinitions({
|
||||
env: buildCommandEnv(scenarioConfig),
|
||||
commandProfile: { general: ['휴식'], nation: ['휴식'] },
|
||||
defaultActionKey: '휴식',
|
||||
});
|
||||
|
||||
expect([...general.keys()].sort()).toEqual(['che_NPC능동', 'che_등용수락', 'che_방랑', '휴식'].sort());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js';
|
||||
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
||||
import { buildCommandEnv } from '../src/turn/reservedTurnCommands.js';
|
||||
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
||||
import type { TurnEvent, TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
|
||||
const buildCity = (id: number, nationId: number, level: number): City => ({
|
||||
@@ -313,8 +314,24 @@ describe('invader monthly actions', () => {
|
||||
getWorld: () => world,
|
||||
reservedTurns: harness.reservedTurns,
|
||||
});
|
||||
const reservedTurnHandler = await createReservedTurnHandler({
|
||||
reservedTurns: harness.reservedTurns,
|
||||
scenarioConfig,
|
||||
scenarioMeta: {
|
||||
title: '내부 방랑 실행 fixture',
|
||||
startYear: 190,
|
||||
life: null,
|
||||
fiction: null,
|
||||
history: [],
|
||||
ignoreDefaultEvents: false,
|
||||
},
|
||||
map: harness.snapshot.map,
|
||||
getWorld: () => world,
|
||||
commandProfile: { general: ['휴식'], nation: ['휴식'] },
|
||||
});
|
||||
world = new InMemoryTurnWorld(state, harness.snapshot, {
|
||||
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||
generalTurnHandler: reservedTurnHandler,
|
||||
calendarHandler: createMonthlyEventHandler({
|
||||
getWorld: () => world,
|
||||
startYear: 190,
|
||||
@@ -342,6 +359,17 @@ describe('invader monthly actions', () => {
|
||||
Array.from({ length: 30 }, () => ({ action: 'che_방랑', args: {} }))
|
||||
);
|
||||
expect(harness.reservedTurns.peekDirtyState().generalIds).toEqual([2]);
|
||||
|
||||
const ruler = world.getGeneralById(2);
|
||||
expect(ruler).not.toBeNull();
|
||||
expect(() => world.executeGeneralTurn(ruler!)).not.toThrow();
|
||||
expect(world.getNationById(2)).toMatchObject({
|
||||
name: 'ⓞ도시2대왕',
|
||||
level: 0,
|
||||
capitalCityId: 0,
|
||||
typeCode: 'None',
|
||||
});
|
||||
expect(world.getCityById(2)?.nationId).toBe(0);
|
||||
});
|
||||
|
||||
it('finishes with the legacy user-win logs and refresh multiplier', async () => {
|
||||
|
||||
Reference in New Issue
Block a user