fix: 커맨드 차등 생명주기와 로그 그래프를 보강

장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다.

실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
2026-08-23 21:48:29 +00:00
parent 85591c68ad
commit c63a49bd07
128 changed files with 8615 additions and 848 deletions
+10 -1
View File
@@ -28,12 +28,15 @@ export interface GeneralActionResolveContext<
rng: RandomGenerator;
city?: City;
nation?: Nation | null;
/** Ref General이 한 장수 lifecycle 동안 유지하는 정적 국가 조회값. */
legacyStaticNationName?: string;
addLog(message: string, options?: Partial<Omit<LogEntryDraft, 'text'>>): void;
addPostProgressionLog?(message: string, options?: Partial<Omit<LogEntryDraft, 'text'>>): void;
}
export type GeneralActionResolveInputContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Omit<
GeneralActionResolveContext<TriggerState>,
'addLog'
'addLog' | 'addPostProgressionLog'
>;
export interface TurnScheduleContext {
@@ -125,6 +128,7 @@ export interface GeneralActionResolution {
completed: boolean;
nextTurnAt: Date;
logs: LogEntryDraft[];
postProgressionLogs: LogEntryDraft[];
effects: GeneralActionEffect[];
destroyedNationIds?: NationId[];
created?: {
@@ -216,6 +220,7 @@ export const createLogEffect = (message: string, options: Partial<Omit<LogEntryD
...(options.userId !== undefined ? { userId: options.userId } : {}),
...(options.subType !== undefined ? { subType: options.subType } : {}),
...(options.meta !== undefined ? { meta: options.meta } : {}),
...(options.legacyFlushGroup !== undefined ? { legacyFlushGroup: options.legacyFlushGroup } : {}),
format: options.format ?? LogFormat.MONTH,
},
});
@@ -357,6 +362,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
args: Args
): GeneralActionResolution => {
const logs: LogEntryDraft[] = [];
const postProgressionLogs: LogEntryDraft[] = [];
const accumulator: ActionResolutionAccumulator = {
createdGenerals: [],
createdNations: [],
@@ -372,6 +378,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
} as WorldState<TriggerState>,
(draft) => {
const addLog = createActionLogSink(context, logs);
const addPostProgressionLog = createActionLogSink(context, postProgressionLogs);
outcome = resolver.resolve(
{
@@ -381,6 +388,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
city: castDraft(draft.city),
nation: castDraft(draft.nation),
addLog,
addPostProgressionLog,
} as GeneralActionResolveContext<TriggerState>,
args
);
@@ -405,6 +413,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
completed: outcome?.completed !== false,
nextTurnAt,
logs,
postProgressionLogs,
effects: accumulator.pendingEffects,
...(outcome?.alternative ? { alternative: outcome.alternative } : {}),
...(outcome?.deletedTroopIds?.length ? { deletedTroopIds: outcome.deletedTroopIds } : {}),
@@ -13,6 +13,8 @@ export interface ActionRandomSource {
}
export interface ActionContextGeneral extends General {
picture?: string | null;
imageServer?: number;
turnTime: Date;
}
@@ -23,6 +25,8 @@ export type ActionContextBase = {
worldView?: GeneralWorldView;
rng: ActionRandomSource;
uniqueLottery?: UniqueLotteryRunner;
/** Ref General#getStaticNation()이 수뇌턴과 장수턴 사이에 유지하는 캐시값. */
legacyStaticNationName?: string;
time?: {
year: number;
month: number;
@@ -72,6 +76,10 @@ export interface ActionContextWorldRef {
export interface ActionContextOptions<TArgs extends Record<string, unknown> = Record<string, unknown>> {
world: ActionContextWorldState;
/** 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. */
messageSharedIconBaseUrl?: string;
scenarioConfig: ScenarioConfig;
scenarioMeta?: ScenarioMeta;
map?: MapDefinition;
+106 -20
View File
@@ -1,6 +1,6 @@
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
import { asStringArray } from '@sammo-ts/common';
import { asStringArray, isRecord } from '@sammo-ts/common';
import { TurnCommandProfileInputSchema } from '../../resources/turnCommandSchema.js';
export interface TurnCommandProfile {
@@ -8,31 +8,43 @@ export interface TurnCommandProfile {
nation: NationTurnCommandKey[];
}
const asStringArrayOrNull = (value: unknown): string[] | null => {
if (!Array.isArray(value)) {
return null;
}
const list = asStringArray(value);
return list.length > 0 ? list : null;
};
export interface TurnCommandGroup<Key extends string> {
category: string;
commands: Key[];
}
export interface ScenarioTurnCommandProfileResolution {
profile: TurnCommandProfile;
generalGroups: Array<TurnCommandGroup<GeneralTurnCommandKey>> | null;
nationGroups: Array<TurnCommandGroup<NationTurnCommandKey>> | null;
}
const parseKeyList = <T extends string>(options: {
raw: unknown;
defaults: T[];
isKey: (value: string) => value is T;
label: string;
}): T[] => {
const rawList = asStringArrayOrNull(options.raw);
if (!rawList) {
return options.defaults;
if (!Array.isArray(options.raw) || options.raw.length === 0) {
throw new Error(`${options.label} command profile must be a non-empty array.`);
}
const parsed: T[] = [];
for (const value of rawList) {
const seen = new Set<T>();
for (const value of asStringArray(options.raw)) {
if (!options.isKey(value)) {
throw new Error(`Unknown ${options.label} command key: ${value}`);
}
if (seen.has(value)) {
throw new Error(`Duplicate ${options.label} command key: ${value}`);
}
seen.add(value);
parsed.push(value);
}
if (parsed.length !== options.raw.length) {
throw new Error(`${options.label} command profile contains a non-string key.`);
}
if (!parsed.includes('휴식' as T)) {
throw new Error(`${options.label} command profile must include 휴식.`);
}
return parsed;
};
@@ -41,27 +53,101 @@ export const DEFAULT_TURN_COMMAND_PROFILE: TurnCommandProfile = {
nation: [...NATION_TURN_COMMAND_KEYS],
};
export const parseTurnCommandProfile = (
raw: unknown,
fallback: TurnCommandProfile = DEFAULT_TURN_COMMAND_PROFILE
): TurnCommandProfile => {
export const parseTurnCommandProfile = (raw: unknown): TurnCommandProfile => {
const parsed = TurnCommandProfileInputSchema.safeParse(raw);
if (!parsed.success) {
return fallback;
throw new Error(`Invalid turn command profile: ${parsed.error.message}`);
}
const data = parsed.data;
return {
general: parseKeyList({
raw: data.general,
defaults: fallback.general,
isKey: isGeneralTurnCommandKey,
label: 'general',
}),
nation: parseKeyList({
raw: data.nation,
defaults: fallback.nation,
isKey: isNationTurnCommandKey,
label: 'nation',
}),
};
};
const parseScenarioCommandGroups = <Key extends string>(options: {
raw: unknown;
isKey: (value: string) => value is Key;
label: string;
}): Array<TurnCommandGroup<Key>> | null => {
if (options.raw === undefined || options.raw === null) {
return null;
}
if (!isRecord(options.raw)) {
throw new Error(`Scenario ${options.label} command groups must be an object.`);
}
const groups: Array<TurnCommandGroup<Key>> = [];
const seen = new Set<Key>();
for (const [category, rawCommands] of Object.entries(options.raw)) {
if (!category.trim()) {
throw new Error(`Scenario ${options.label} command category must be non-empty.`);
}
if (!Array.isArray(rawCommands)) {
throw new Error(`Scenario ${options.label} command category ${category} must be an array.`);
}
const commands: Key[] = [];
for (const value of asStringArray(rawCommands)) {
if (!options.isKey(value)) {
throw new Error(`Unknown scenario ${options.label} command key: ${value}`);
}
if (seen.has(value)) {
throw new Error(`Duplicate scenario ${options.label} command key: ${value}`);
}
seen.add(value);
commands.push(value);
}
if (commands.length !== rawCommands.length) {
throw new Error(`Scenario ${options.label} command category ${category} contains a non-string key.`);
}
groups.push({ category, commands });
}
const flattened = groups.flatMap((group) => group.commands);
if (!flattened.includes('휴식' as Key)) {
throw new Error(`Scenario ${options.label} command groups must include 휴식.`);
}
return groups;
};
/**
* Ref replaces GameConst::$availableGeneralCommand/$availableChiefCommand with
* the scenario const when present. Resolve that same public/executable profile
* here so the API and daemon cannot silently use the default profile instead.
*/
export const resolveScenarioTurnCommandProfile = (
scenarioConst: unknown,
fallback: TurnCommandProfile
): ScenarioTurnCommandProfileResolution => {
if (scenarioConst !== undefined && scenarioConst !== null && !isRecord(scenarioConst)) {
throw new Error('Scenario const must be an object.');
}
const config = isRecord(scenarioConst) ? scenarioConst : {};
const generalGroups = parseScenarioCommandGroups({
raw: config.availableGeneralCommand,
isKey: isGeneralTurnCommandKey,
label: 'general',
});
const nationGroups = parseScenarioCommandGroups({
raw: config.availableChiefCommand,
isKey: isNationTurnCommandKey,
label: 'nation',
});
return {
profile: {
general: generalGroups ? generalGroups.flatMap((group) => group.commands) : [...fallback.general],
nation: nationGroups ? nationGroups.flatMap((group) => group.commands) : [...fallback.nation],
},
generalGroups,
nationGroups,
};
};
@@ -24,6 +24,7 @@ export const processGeneralActionWithFallback = async <TriggerState extends Gene
let currentArgs = initialArgs;
let loopLimit = 5; // Prevent infinite loops
const accumulatedLogs: LogEntryDraft[] = [];
const accumulatedPostProgressionLogs: LogEntryDraft[] = [];
while (loopLimit > 0) {
loopLimit--;
@@ -32,6 +33,7 @@ export const processGeneralActionWithFallback = async <TriggerState extends Gene
if (resolution.alternative) {
accumulatedLogs.push(...resolution.logs);
accumulatedPostProgressionLogs.push(...resolution.postProgressionLogs);
const { commandKey, args } = resolution.alternative;
const nextResolver = await commandLoader.load(commandKey);
@@ -46,6 +48,9 @@ export const processGeneralActionWithFallback = async <TriggerState extends Gene
if (accumulatedLogs.length > 0) {
resolution.logs.unshift(...accumulatedLogs);
}
if (accumulatedPostProgressionLogs.length > 0) {
resolution.postProgressionLogs.unshift(...accumulatedPostProgressionLogs);
}
return resolution;
}
@@ -131,6 +131,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: target.id,
legacyFlushGroup: -1,
})
);
}
@@ -140,13 +140,8 @@ export class ActionDefinition<
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
context.addLog(`<Y>${general.name}</>${josaYi} <G><b>${cityName}</b></>에서 거병`, {
scope: LogScope.NATION,
nationId: newNationId,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
// Ref queues the national history entry on the actor logger created
// while nationID is still 0, so ActionLogger::flush discards it.
tryApplyUniqueLottery(context, {
acquireType: '아이템',
reason: ACTION_NAME,
@@ -105,7 +105,7 @@ export class ActionDefinition<
`군량 <C>${Math.round(buyAmount).toLocaleString()}</>을 사서 자금 <C>${Math.round(
sellAmount
).toLocaleString()}</>을 썼습니다.`,
{ format: LogFormat.PLAIN }
{ format: LogFormat.MONTH }
);
} else {
const sellAmount = Math.min(args.amount, general.rice);
@@ -118,7 +118,7 @@ export class ActionDefinition<
`군량 <C>${Math.round(sellAmount).toLocaleString()}</>을 팔아 자금 <C>${Math.round(
buyAmount
).toLocaleString()}</>을 얻었습니다.`,
{ format: LogFormat.PLAIN }
{ format: LogFormat.MONTH }
);
}
@@ -17,13 +17,13 @@ import type {
GeneralActionResolver,
GeneralActionEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogScope } from '@sammo-ts/logic/logging/types.js';
import { createGeneralPatchEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory } from '@sammo-ts/logic/logging/types.js';
import { z } from 'zod';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/actions/turn/actionContext.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import { buildScoutMessageDraft } from '@sammo-ts/logic/messages/scoutMessage.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { parseArgsWithSchema } from '../parseArgs.js';
@@ -33,6 +33,7 @@ export interface EmployResolveContext<
destGeneral?: General;
env?: TurnCommandEnv;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '등용';
@@ -88,10 +89,9 @@ export class ActionResolver<
> implements GeneralActionResolver<TriggerState, EmployArgs> {
readonly key = ACTION_KEY;
resolve(context: GeneralActionResolveContext<TriggerState>, args: EmployArgs): GeneralActionOutcome<TriggerState> {
resolve(context: GeneralActionResolveContext<TriggerState>, _args: EmployArgs): GeneralActionOutcome<TriggerState> {
const ctx = context as EmployResolveContext<TriggerState>;
const general = ctx.general;
const { destGeneralId } = args;
const destGeneral = ctx.destGeneral;
if (!destGeneral) {
@@ -137,45 +137,19 @@ export class ActionResolver<
const destNation = ctx.worldView
?.listNations?.()
.find((candidate) => candidate.id === destGeneral.nationId);
const josaRo = JosaUtil.pick(ctx.nation.name, '로');
effects.push(
createMessageEffect({
msgType: 'private',
src: {
generalId: general.id,
generalName: general.name,
nationId: ctx.nation.id,
nationName: ctx.nation.name,
color: ctx.nation.color,
icon: '',
},
dest: {
generalId: destGeneral.id,
generalName: destGeneral.name,
nationId: destGeneral.nationId,
nationName: destNation?.name ?? '재야',
color: destNation?.color ?? '#000000',
icon: '',
},
text: `${ctx.nation.name}${josaRo} 망명 권유 서신`,
time: ctx.messageTime,
validUntil: new Date('9999-12-31T12:59:59.000Z'),
option: { action: 'scout' },
})
);
const message = buildScoutMessageDraft({
srcGeneral: general,
destGeneral,
srcNation: ctx.nation,
destNation: destNation ?? null,
time: ctx.messageTime,
...(ctx.messageSharedIconBaseUrl ? { sharedIconBaseUrl: ctx.messageSharedIconBaseUrl } : {}),
});
if (message) {
effects.push(createMessageEffect(message));
}
}
effects.push(
createLogEffect(
`<Y>${general.name}</>(${ctx.nation?.name ?? '재야'})로 부터 등용 권유 서신이 도착했습니다.`,
{
scope: LogScope.GENERAL,
generalId: destGeneralId,
category: LogCategory.ACTION,
}
)
);
return { effects };
}
}
@@ -249,10 +223,12 @@ export const actionContextBuilder = (base: ActionContextBase, options: ActionCon
...base,
destGeneral,
env: options.scenarioConfig.const as unknown as TurnCommandEnv,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime:
(base.general as General & { turnTime?: Date }).turnTime instanceof Date
options.gameNow ??
((base.general as General & { turnTime?: Date }).turnTime instanceof Date
? (base.general as General & { turnTime: Date }).turnTime
: options.world.lastTurnTime,
: options.world.lastTurnTime),
};
};
@@ -86,14 +86,14 @@ export class ActionResolver<
// Self Log
context.addLog(`<D>${destNationName}</>${josaRo} 망명하여 수도로 이동합니다.`, {
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
});
// Global Log
context.addLog(`<Y>${generalName}</>${josaYi} <D><b>${destNationName}</b></>${josaRo} <S>망명</>하였습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
});
// 2. Recruiter Rewards
@@ -143,6 +143,7 @@ export class ActionResolver<
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
legacyFlushGroup: 1,
}
)
);
@@ -164,6 +165,7 @@ export class ActionResolver<
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
legacyFlushGroup: 1,
}
)
);
@@ -283,18 +285,22 @@ export class ActionResolver<
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
context.addLog(`<Y>${generalName}</> 등용에 성공했습니다.`, {
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
});
context.addLog(`<Y>${generalName}</> 등용에 성공`, {
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
effects.push(
createLogEffect(`<Y>${generalName}</> 등용에 성공했습니다.`, {
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
legacyFlushGroup: 1,
}),
createLogEffect(`<Y>${generalName}</> 등용에 성공`, {
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: 1,
})
);
const deletedTroopIds: number[] = [];
if (general.troopId === general.id) {
@@ -14,11 +14,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralPatchEffect,
createLogEffect,
createNationPatchEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -76,11 +72,13 @@ export class ActionDefinition<
{
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<Y>${general.name}</>${josaYi} <Y>${lord.name}</>에게서 군주자리를 찬탈`, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
context.addLog('모반에 성공했습니다.', {
scope: LogScope.GENERAL,
@@ -90,6 +88,7 @@ export class ActionDefinition<
context.addLog(`모반으로 <D><b>${nation.name}</b></>의 군주자리를 찬탈`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
effects.push(
@@ -97,7 +96,8 @@ export class ActionDefinition<
scope: LogScope.GENERAL,
generalId: lord.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
legacyFlushGroup: 1,
}),
createLogEffect(
`<D><b>${general.name}</b></>의 모반으로 인해 <D><b>${nation.name}</b></>의 군주자리를 박탈당함`,
@@ -105,7 +105,8 @@ export class ActionDefinition<
scope: LogScope.GENERAL,
generalId: lord.id,
category: LogCategory.HISTORY,
format: LogFormat.PLAIN,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: 1,
}
),
createGeneralPatchEffect(
@@ -7,11 +7,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralPatchEffect,
createLogEffect,
createNationPatchEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
import { z } from 'zod';
@@ -107,11 +103,13 @@ export class ActionDefinition<
{
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<Y>${general.name}</>${josaYi} <Y>${destGeneral.name}</>에게 선양`, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
context.addLog(`<Y>${destGeneral.name}</>에게 군주의 자리를 물려줍니다.`, {
scope: LogScope.GENERAL,
@@ -121,6 +119,7 @@ export class ActionDefinition<
context.addLog(`<D><b>${nation.name}</b></>의 군주자리를 <Y>${destGeneral.name}</>에게 선양`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
effects.push(
@@ -128,13 +127,15 @@ export class ActionDefinition<
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
legacyFlushGroup: 1,
}),
createLogEffect(`<D><b>${nation.name}</b></>의 군주자리를 물려 받음`, {
scope: LogScope.GENERAL,
generalId: destGeneral.id,
category: LogCategory.HISTORY,
format: LogFormat.PLAIN,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: 1,
}),
createGeneralPatchEffect(
{
@@ -60,7 +60,7 @@ export class ActionResolver<
context.addLog(`<Y>${general.name}</>${josaYi} <R>은퇴</>하고 그 자손이 유지를 이어받았습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.RAWTEXT,
format: LogFormat.MONTH,
});
context.addLog('나이가 들어 <R>은퇴</>하고 자손에게 자리를 물려줍니다.', {
category: LogCategory.ACTION,
@@ -13,8 +13,8 @@ import type {
GeneralActionResolver,
GeneralActionEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
import { createGeneralPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
import { z } from 'zod';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -113,6 +113,17 @@ export class ActionResolver<
target.id
)
);
if (!isSelf) {
effects.push(
createLogEffect(`방랑군 세력이 <G><b>${destCityName}</b></>${josaRo} 이동했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
}
return { effects };
@@ -493,6 +493,7 @@ export class ActionResolver<
const meta: GeneralMeta = {
killturn,
npcType: NPC_TYPE,
npc_org: NPC_TYPE,
explevel: 0,
dedlevel: 1,
crewTypeId: this.env.defaultCrewTypeId,
@@ -179,6 +179,7 @@ export class ActionDefinition<
context.addLog(`<D><b>${destNation.name}</b></>에 임관`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
context.addLog(`<Y>${general.name}</>${josaYi} <D><b>${destNation.name}</b></>에 <S>임관</>했습니다.`, {
scope: LogScope.SYSTEM,
@@ -128,6 +128,7 @@ export class ActionDefinition<
category: LogCategory.ACTION,
generalId: destGeneral.id,
format: LogFormat.PLAIN,
legacyFlushGroup: 1,
}),
createLogEffect(`<Y>${destGeneral.name}</>에게 ${resName} <C>${amountText}</>을 증여했습니다.`, {
scope: LogScope.GENERAL,
@@ -72,6 +72,7 @@ export class ActionDefinition<
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: member.id,
legacyFlushGroup: -1,
})
);
}
@@ -215,7 +215,7 @@ export class ActionResolver<
: '<C>↓</>미미';
ctx.addLog(`【<span class='ev_notice'>${destNation.name}</span>】아국대비기술:${techText}`, {
category: LogCategory.ACTION,
format: LogFormat.RAWTEXT,
format: LogFormat.MONTH,
});
}
} else if (distance === 2) {
@@ -317,12 +317,7 @@ export class ActionDefinition<
return [notOccupiedDestCity(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)];
}
formatConstraintFailure(
reason: string,
_ctx: ConstraintContext,
args: SpyArgs,
view: StateView
): string | null {
formatConstraintFailure(reason: string, _ctx: ConstraintContext, args: SpyArgs, view: StateView): string | null {
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'location');
}
@@ -21,6 +21,7 @@ import {
createCityPatchEffect,
createDiplomacyPatchEffect,
createGeneralPatchEffect,
createMessageEffect,
createNationPatchEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { JosaUtil, LiteHashDRBG } from '@sammo-ts/common';
@@ -30,6 +31,7 @@ import type { GeneralTurnCommandSpec } from './index.js';
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
import { resolveWarAftermath } from '@sammo-ts/logic/war/aftermath.js';
import { resolveWarBattle } from '@sammo-ts/logic/war/engine.js';
import { LegacyWarLogFlushSequence } from '@sammo-ts/logic/war/legacyFlushSequence.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { NationTraitModule } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
@@ -63,6 +65,8 @@ export interface DispatchResolveContext<
seedBase: string;
warConfig: WarEngineConfig;
aftermathConfig: WarAftermathConfig;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
export const orderDefenderGenerals = <TriggerState extends GeneralTriggerState>(
@@ -70,6 +74,7 @@ export const orderDefenderGenerals = <TriggerState extends GeneralTriggerState>(
): General<TriggerState>[] => [...generals].sort((left, right) => left.id - right.id);
const ACTION_NAME = '출병';
const LEGACY_SORTIE_FLUSH_GROUP_START = Number.MIN_SAFE_INTEGER;
const ARGS_SCHEMA = z.object({
destCityId: z.number(),
});
@@ -512,16 +517,24 @@ export class ActionDefinition<
};
}
// Ref che_출병은 명령 내부에서도 General::applyDB()/ActionLogger::flush()를
// 여러 번 수행한다. 외부 TurnExecutionHelper progression(group 0)과
// 섞이지 않도록 명령 내부 epoch은 작은 음수부터 단조 증가시킨다.
const legacyFlushSequence = new LegacyWarLogFlushSequence(LEGACY_SORTIE_FLUSH_GROUP_START);
const preWarFlushGroup = legacyFlushSequence.claimGroup()!;
if (finalTargetCity.id !== destCity.id) {
const josaRo = JosaUtil.pick(finalTargetCity.name, '로');
const josaUl = JosaUtil.pick(destCity.name, '을');
if (minDist === 0) {
context.addLog(
`<G><b>${finalTargetCity.name}</b></>${josaRo} 가기 위해 <G><b>${destCity.name}</b></>${josaUl} 거쳐야 합니다.`
`<G><b>${finalTargetCity.name}</b></>${josaRo} 가기 위해 <G><b>${destCity.name}</b></>${josaUl} 거쳐야 합니다.`,
{ legacyFlushGroup: preWarFlushGroup }
);
} else {
context.addLog(
`<G><b>${finalTargetCity.name}</b></>${josaRo} 가는 도중 <G><b>${destCity.name}</b></>${josaUl} 거치기로 합니다.`
`<G><b>${finalTargetCity.name}</b></>${josaRo} 가는 도중 <G><b>${destCity.name}</b></>${josaUl} 거치기로 합니다.`,
{ legacyFlushGroup: preWarFlushGroup }
);
}
}
@@ -614,6 +627,7 @@ export class ActionDefinition<
})),
defenderCity,
defenderNation,
legacyFlushSequence,
...(shouldTraceWar
? {
trace: (event) => {
@@ -635,7 +649,9 @@ export class ActionDefinition<
unitSet,
config: context.aftermathConfig,
time,
messageTime: context.messageTime,
hiddenSeed: context.seedBase,
legacyFlushSequence,
generalActionModules: this.generalModules,
calcNationTechGain: ({ nation, baseGain }) => {
const module = this.nationTraitModules.get(nation.typeCode);
@@ -644,6 +660,7 @@ export class ActionDefinition<
baseGain
);
},
...(context.messageSharedIconBaseUrl ? { messageSharedIconBaseUrl: context.messageSharedIconBaseUrl } : {}),
...(this.trace ? { trace: this.trace } : {}),
});
@@ -676,12 +693,22 @@ export class ActionDefinition<
}
const effects: Array<GeneralActionEffect<TriggerState>> = [];
// processWar() 반환 후 StaticEvent/unique 로직이 끝나면 Ref line 259의
// actor applyDB가 실행된다. 이 epoch은 command 반환 후 outer progression과 별개다.
const finalActorFlushGroup = legacyFlushSequence.claimGroup()!;
for (const entry of battle.logs) {
effects.push({ type: 'log', entry });
}
for (const entry of aftermath.logs) {
effects.push({ type: 'log', entry });
effects.push({
type: 'log',
entry:
entry.legacyFlushGroup === undefined ? { ...entry, legacyFlushGroup: finalActorFlushGroup } : entry,
});
}
for (const message of aftermath.conquest?.messages ?? []) {
effects.push(createMessageEffect(message));
}
const generalPatches = new Map<number, General<TriggerState>>();
@@ -745,7 +772,20 @@ export class ActionDefinition<
);
}
tryApplyUniqueLottery(context, { acquireType: '아이템', reason: ACTION_NAME });
const addFinalActorLog: NonNullable<typeof context.addPostProgressionLog> = (message, options = {}) => {
const sink = context.addPostProgressionLog ?? context.addLog;
sink(message, {
...options,
legacyFlushGroup: finalActorFlushGroup,
});
};
tryApplyUniqueLottery(
{
...context,
addPostProgressionLog: addFinalActorLog,
},
{ acquireType: '아이템', reason: ACTION_NAME }
);
return {
effects,
@@ -792,6 +832,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
seedBase: options.seedBase,
warConfig,
aftermathConfig,
messageTime: options.gameNow ?? base.general.turnTime,
...(options.messageSharedIconBaseUrl ? { messageSharedIconBaseUrl: options.messageSharedIconBaseUrl } : {}),
};
};
@@ -65,7 +65,6 @@ export class ActionResolver<
// Penalty
const betrayal = typeof general.meta.betray === 'number' ? general.meta.betray : 0;
const belong = typeof general.meta.belong === 'number' ? general.meta.belong : 0;
const maxBelong = typeof general.meta.max_belong === 'number' ? general.meta.max_belong : 0;
const penaltyRatio = betrayal * 0.1;
const nextExp = Math.round(general.experience * (1 - penaltyRatio));
@@ -83,7 +82,7 @@ export class ActionResolver<
context.addLog(`<Y>${general.name}</>${josaYi} <D><b>${nation.name}</b></>에서 <R>하야</>했습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.RAWTEXT,
format: LogFormat.MONTH,
});
effects.push(
@@ -101,7 +100,9 @@ export class ActionResolver<
...general.meta,
betray: Math.min(9, betrayal + 1),
belong: 0,
...(general.npcState < 2 ? { max_belong: Math.max(belong, maxBelong) } : {}),
// Ref resets belong before max_belong is refreshed here.
// Preserve that ordering, including its legacy behavior.
...(general.npcState < 2 ? { max_belong: Math.max(0, maxBelong) } : {}),
makelimit: 12,
officer_city: 0,
permission: 'normal',
@@ -23,8 +23,7 @@ import type { GeneralTurnCommandSpec } from './index.js';
const ACTION_NAME = '해산';
const ACTION_KEY = 'che_해산';
const readMetaNumber = (value: unknown): number =>
typeof value === 'number' && Number.isFinite(value) ? value : 0;
const readMetaNumber = (value: unknown): number => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
export interface DisbandFactionArgs {}
@@ -77,7 +76,13 @@ export class ActionDefinition<
const defaultGold = this.env.defaultNpcGold > 0 ? this.env.defaultNpcGold : 1000;
const defaultRice = this.env.defaultNpcRice > 0 ? this.env.defaultNpcRice : 1000;
const nationGenerals = context.nationGenerals ?? [];
const nationGenerals = [...(context.nationGenerals ?? [])].sort((left, right) => {
const leftIsActor = left.id === general.id;
const rightIsActor = right.id === general.id;
if (leftIsActor !== rightIsActor) return leftIsActor ? 1 : -1;
return left.id - right.id;
});
const nonActorCount = nationGenerals.filter((targetGeneral) => targetGeneral.id !== general.id).length;
for (const targetGeneral of nationGenerals) {
const isActor = targetGeneral.id === general.id;
const belong = readMetaNumber(targetGeneral.meta.belong);
@@ -148,6 +153,7 @@ export class ActionDefinition<
context.addLog(`<D><b>${nation.name}</b></>${josaUl} 해산`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const josaUn = JosaUtil.pick(nation.name, '은');
@@ -159,19 +165,22 @@ export class ActionDefinition<
format: LogFormat.YEAR_MONTH,
})
);
for (const targetGeneral of nationGenerals) {
for (const [targetIndex, targetGeneral] of nationGenerals.entries()) {
const legacyFlushGroup = targetGeneral.id === general.id ? undefined : targetIndex - nonActorCount;
effects.push(
createLogEffect(`<D><b>${nation.name}</b></>${josaNationYi} <R>멸망</>했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: targetGeneral.id,
...(legacyFlushGroup === undefined ? {} : { legacyFlushGroup }),
}),
createLogEffect(`<D><b>${nation.name}</b></>${josaNationYi} <R>멸망</>`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
generalId: targetGeneral.id,
...(legacyFlushGroup === undefined ? {} : { legacyFlushGroup }),
})
);
}
@@ -185,8 +185,8 @@ export class ActionDefinition<
`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
// Global History Log
@@ -115,28 +115,29 @@ export class ActionResolver<
}),
];
for (const target of context.friendlyGenerals) {
if (target.id === general.id) {
continue;
}
const friendlyTargets = context.friendlyGenerals.filter((target) => target.id !== general.id);
const firstLegacyFlushGroup = -(friendlyTargets.length + context.destNationGenerals.length + 1);
for (const [index, target] of friendlyTargets.entries()) {
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + index,
})
);
}
const destBroadcast = `아국에 <M>${ACTION_NAME}</>${JosaUtil.pick(ACTION_NAME, '이')} 발동되었습니다.`;
for (const target of context.destNationGenerals) {
for (const [index, target] of context.destNationGenerals.entries()) {
effects.push(
createLogEffect(destBroadcast, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + friendlyTargets.length + index,
})
);
}
@@ -148,12 +149,15 @@ export class ActionResolver<
strategic_cmd_limit: globalDelay,
};
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
})
createLogEffect(
`<Y>${generalName}</>${generalJosa} <D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
{
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
}
)
);
}
effects.push(
@@ -163,7 +167,8 @@ export class ActionResolver<
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: context.destNation.id,
format: LogFormat.PLAIN,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: -1,
}
)
);
@@ -30,6 +30,7 @@ import { clamp } from 'es-toolkit';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { normalizeResourceActionAmount } from '../resourceAmount.js';
import { resolveMessageTargetIcon } from '@sammo-ts/logic/messages/message.js';
const ARGS_SCHEMA = z.object({
isGold: z.boolean(),
@@ -43,6 +44,7 @@ export interface SeizureResolveContext<
> extends GeneralActionResolveContext<TriggerState> {
destGeneral: General<TriggerState>;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '몰수';
@@ -67,16 +69,6 @@ const pickLegacyNpcMessage = (rng: GeneralActionResolveContext['rng']): string =
return NPC_SEIZURE_MESSAGES[index]!;
};
const resolveGeneralIcon = (general: General): string => {
const runtimePicture = (general as General & { picture?: unknown }).picture;
const rawPicture = runtimePicture ?? general.meta.picture;
const picture =
(typeof rawPicture === 'string' && rawPicture !== '') || typeof rawPicture === 'number'
? String(rawPicture)
: 'default.jpg';
return `https://sam-image.hided.net/icons/${picture}`;
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, SeizureArgs, SeizureResolveContext<TriggerState>> {
@@ -170,6 +162,7 @@ export class ActionDefinition<
generalId: destGeneral.id,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
legacyFlushGroup: 1,
}),
];
@@ -183,7 +176,7 @@ export class ActionDefinition<
nationId: nation.id,
nationName: nation.name,
color: nation.color,
icon: resolveGeneralIcon(destGeneral),
icon: resolveMessageTargetIcon(destGeneral, context.messageSharedIconBaseUrl),
};
effects.push(
createMessageEffect({
@@ -214,7 +207,8 @@ export const actionContextBuilder: ActionContextBuilder<SeizureArgs> = (base, op
return {
...base,
destGeneral,
messageTime: base.general.turnTime,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime: options.gameNow ?? base.general.turnTime,
};
};
@@ -148,8 +148,8 @@ export class ActionDefinition<
`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>수도 이전</>하였습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
// Global History Log
@@ -202,6 +202,7 @@ export class ActionDefinition<
category: LogCategory.ACTION,
generalId: targetGeneral.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -199,6 +199,7 @@ export class ActionDefinition<
nationId: destNation.id,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: 1,
}
),
// General Action Log
@@ -222,6 +223,7 @@ export class ActionDefinition<
category: LogCategory.ACTION,
generalId: chief.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -234,6 +236,7 @@ export class ActionDefinition<
category: LogCategory.ACTION,
generalId: chief.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -128,6 +128,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: destGeneral.id,
format: LogFormat.MONTH,
legacyFlushGroup: 1,
})
);
effects.push(
@@ -113,6 +113,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -136,12 +137,15 @@ export class ActionResolver<
strategic_cmd_limit: globalDelay,
};
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
})
createLogEffect(
`<Y>${generalName}</>${generalJosa} <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
{
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
}
)
);
}
@@ -90,8 +90,9 @@ export class ActionDefinition<
createLogEffect(`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
generalId: destGeneral.id,
legacyFlushGroup: 1,
})
);
@@ -19,6 +19,7 @@ import type { NationTurnCommandSpec } from './index.js';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { resolveDiplomacyMessageValidMinutes } from '../../../diplomacy/messageValidity.js';
import { resolveMessageTargetIcon } from '@sammo-ts/logic/messages/message.js';
const ARGS_SCHEMA = z.object({
destNationId: z.number().int().positive(),
@@ -33,6 +34,7 @@ interface NonAggressionProposalContext<
destNation: Nation;
messageValidMinutes: number;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '불가침 제의';
@@ -161,7 +163,7 @@ export class ActionDefinition<
nationId: nation.id,
nationName: nation.name,
color: nation.color,
icon: '',
icon: resolveMessageTargetIcon(general, context.messageSharedIconBaseUrl),
},
dest: {
generalId: 0,
@@ -169,7 +171,7 @@ export class ActionDefinition<
nationId: destNation.id,
nationName: destNation.name,
color: destNation.color,
icon: '',
icon: resolveMessageTargetIcon(null, context.messageSharedIconBaseUrl),
},
text: `${nation.name}${josaWa} ${args.year}${args.month}월까지 불가침 제의 서신`,
time: context.messageTime,
@@ -195,7 +197,8 @@ export const actionContextBuilder: ActionContextBuilder<NonAggressionProposalArg
return {
...base,
destNation,
messageTime: base.general.turnTime,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime: options.gameNow ?? base.general.turnTime,
messageValidMinutes: resolveDiplomacyMessageValidMinutes(options.world.tickSeconds),
};
};
@@ -18,6 +18,7 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
import type { NationTurnCommandSpec } from './index.js';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { resolveMessageTargetIcon } from '@sammo-ts/logic/messages/message.js';
const ARGS_SCHEMA = z.object({
destNationId: z.number().int().positive(),
@@ -30,6 +31,7 @@ interface NonAggressionCancelProposalContext<
destNation: Nation;
messageValidMinutes: number;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '불가침 파기 제의';
@@ -86,7 +88,7 @@ export class ActionDefinition<
nationId: nation.id,
nationName: nation.name,
color: nation.color,
icon: '',
icon: resolveMessageTargetIcon(general, context.messageSharedIconBaseUrl),
},
dest: {
generalId: 0,
@@ -94,7 +96,7 @@ export class ActionDefinition<
nationId: destNation.id,
nationName: destNation.name,
color: destNation.color,
icon: '',
icon: resolveMessageTargetIcon(null, context.messageSharedIconBaseUrl),
},
text: `${nation.name}의 불가침 파기 제의 서신`,
time: context.messageTime,
@@ -120,7 +122,8 @@ export const actionContextBuilder: ActionContextBuilder<NonAggressionCancelPropo
return {
...base,
destNation,
messageTime: base.general.turnTime,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime: options.gameNow ?? base.general.turnTime,
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
};
};
@@ -23,6 +23,7 @@ import { JosaUtil } from '@sammo-ts/common';
import type { NationTurnCommandSpec } from './index.js';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { resolveMessageTargetIcon } from '@sammo-ts/logic/messages/message.js';
const ARGS_SCHEMA = z.object({
destNationId: z.number().int().positive(),
@@ -36,6 +37,7 @@ interface DeclareWarResolveContext<
currentYear: number;
currentMonth: number;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '선전포고';
@@ -152,6 +154,8 @@ export class ActionDefinition<
nationId: args.destNationId,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
// Ref는 actor logger를 applyDB로 flush한 뒤 상대국 logger를 따로 flush한다.
legacyFlushGroup: 1,
}),
// Global Action Log
createLogEffect(
@@ -179,7 +183,7 @@ export class ActionDefinition<
nationId,
nationName,
color: context.nation?.color ?? '',
icon: '',
icon: resolveMessageTargetIcon(context.general, context.messageSharedIconBaseUrl),
},
dest: {
generalId: 0,
@@ -187,7 +191,7 @@ export class ActionDefinition<
nationId: args.destNationId,
nationName: destNationName,
color: context.destNation.color,
icon: '',
icon: resolveMessageTargetIcon(null, context.messageSharedIconBaseUrl),
},
text: `【외교】${context.currentYear}${context.currentMonth}월:${nationName}에서 ${destNationName}에 선전포고`,
time: context.messageTime,
@@ -219,7 +223,8 @@ export const actionContextBuilder: ActionContextBuilder<DeclareWarArgs> = (base,
destNation,
currentYear: options.world.currentYear,
currentMonth: options.world.currentMonth,
messageTime: base.general.turnTime,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime: options.gameNow ?? base.general.turnTime,
};
};
@@ -127,6 +127,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -138,6 +139,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -180,6 +182,7 @@ export class ActionResolver<
category: LogCategory.HISTORY,
nationId: context.destNation.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
}
)
);
@@ -376,7 +376,7 @@ export class ActionResolver<
const actionName = ACTION_NAME;
context.addLog(`${actionName} 발동!`);
context.addLog(`${actionName} 발동`, {
context.addLog(`<M>${actionName}</>${actionJosa} 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
@@ -416,6 +416,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
})
);
}
@@ -518,6 +519,9 @@ export class ActionResolver<
const meta: GeneralMeta = {
killturn,
npcType: NPC_TYPE,
npc_org: NPC_TYPE,
explevel: 0,
dedlevel: 1,
crewTypeId: this.env.defaultCrewTypeId,
dex1: dex[0],
dex2: dex[1],
@@ -567,6 +571,7 @@ export class ActionResolver<
}),
turnTime,
...(turnTick === undefined ? {} : { turnTick }),
affinity,
bornYear: birthYear,
deadYear: deathYear,
imageServer: candidate.imageServer ?? 0,
@@ -122,28 +122,29 @@ export class ActionResolver<
}),
];
for (const target of context.friendlyGenerals) {
if (target.id === general.id) {
continue;
}
const friendlyTargets = context.friendlyGenerals.filter((target) => target.id !== general.id);
const firstLegacyFlushGroup = -(friendlyTargets.length + context.destNationGenerals.length + 1);
for (const [index, target] of friendlyTargets.entries()) {
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + index,
})
);
}
const destBroadcast = `<D><b>${nationName}</b></>${nationJosa} 아국에 <M>${ACTION_NAME}</>${actionJosa} 발동하였습니다.`;
for (const target of context.destNationGenerals) {
for (const [index, target] of context.destNationGenerals.entries()) {
effects.push(
createLogEffect(destBroadcast, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + friendlyTargets.length + index,
})
);
}
@@ -155,12 +156,15 @@ export class ActionResolver<
strategic_cmd_limit: globalDelay,
};
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
})
createLogEffect(
`<Y>${generalName}</>${generalJosa} <D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
{
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
}
)
);
}
effects.push(
@@ -170,7 +174,8 @@ export class ActionResolver<
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: context.destNation.id,
format: LogFormat.PLAIN,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: -1,
}
)
);
@@ -18,6 +18,7 @@ import type { NationTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { resolveMessageTargetIcon } from '@sammo-ts/logic/messages/message.js';
const ARGS_SCHEMA = z.object({
destNationId: z.number().int().positive(),
@@ -30,6 +31,7 @@ interface StopWarProposalContext<
destNation: Nation;
messageValidMinutes: number;
messageTime: Date;
messageSharedIconBaseUrl?: string;
}
const ACTION_NAME = '종전 제의';
@@ -81,7 +83,7 @@ export class ActionDefinition<
nationId: nation.id,
nationName: nation.name,
color: nation.color,
icon: '',
icon: resolveMessageTargetIcon(general, context.messageSharedIconBaseUrl),
},
dest: {
generalId: 0,
@@ -89,7 +91,7 @@ export class ActionDefinition<
nationId: destNation.id,
nationName: destNation.name,
color: destNation.color,
icon: '',
icon: resolveMessageTargetIcon(null, context.messageSharedIconBaseUrl),
},
text: `${nation.name}의 종전 제의 서신`,
time: context.messageTime,
@@ -115,7 +117,8 @@ export const actionContextBuilder: ActionContextBuilder<StopWarProposalArgs> = (
return {
...base,
destNation,
messageTime: base.general.turnTime,
messageSharedIconBaseUrl: options.messageSharedIconBaseUrl,
messageTime: options.gameNow ?? base.general.turnTime,
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
};
};
@@ -175,8 +175,8 @@ export class ActionDefinition<
`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
// Global History Log
@@ -224,8 +224,8 @@ export class ActionDefinition<
`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>를 명령하였습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
// Global History Log
@@ -204,8 +204,8 @@ export class ActionDefinition<
`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
createLogEffect(
@@ -138,6 +138,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: context.destGeneral.id,
format: LogFormat.PLAIN,
legacyFlushGroup: 1,
})
);
effects.push(
@@ -177,27 +177,28 @@ export class ActionResolver<
'이'
)} 발동되었습니다.`;
for (const target of context.friendlyGenerals) {
if (target.id === general.id) {
continue;
}
const friendlyTargets = context.friendlyGenerals.filter((target) => target.id !== general.id);
const firstLegacyFlushGroup = -(friendlyTargets.length + context.destNationGenerals.length + 1);
for (const [index, target] of friendlyTargets.entries()) {
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + index,
})
);
}
for (const target of context.destNationGenerals) {
for (const [index, target] of context.destNationGenerals.entries()) {
effects.push(
createLogEffect(destBroadcastMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + friendlyTargets.length + index,
})
);
}
@@ -232,7 +233,8 @@ export class ActionResolver<
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: destNation.id,
format: LogFormat.PLAIN,
format: LogFormat.YEAR_MONTH,
legacyFlushGroup: -1,
}
)
);
@@ -112,10 +112,9 @@ export class ActionResolver<
general.atmos = selfPatch.atmos;
}
for (const target of context.nationGenerals) {
if (target.id === general.id) {
continue;
}
const nationTargets = context.nationGenerals.filter((target) => target.id !== general.id);
const firstLegacyFlushGroup = -nationTargets.length;
for (const [index, target] of nationTargets.entries()) {
const patch = updateTrainAtmos(target);
if (patch) {
effects.push(createGeneralPatchEffect(patch, target.id));
@@ -126,6 +125,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + index,
})
);
}
@@ -137,7 +137,7 @@ export class ActionResolver<
strategic_cmd_limit: globalDelay,
};
effects.push(
createLogEffect(broadcastMessage, {
createLogEffect(`<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>을 발동`, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
@@ -127,21 +127,25 @@ export class ActionResolver<
const effects: Array<GeneralActionEffect<TriggerState>> = [];
for (const target of context.friendlyGenerals) {
if (target.id === general.id) {
continue;
}
const friendlyTargets = context.friendlyGenerals.filter((target) => target.id !== general.id);
const firstLegacyFlushGroup = -(
friendlyTargets.length +
context.destCityGenerals.length +
(context.destNation ? 1 : 0)
);
for (const [index, target] of friendlyTargets.entries()) {
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + index,
})
);
}
for (const target of context.destCityGenerals) {
for (const [index, target] of context.destCityGenerals.entries()) {
const moveCityId = pickMoveCityId(context.rng, context.destCity.id, context.destNationSupplyCities);
effects.push(
createLogEffect(destBroadcastMessage, {
@@ -149,6 +153,7 @@ export class ActionResolver<
category: LogCategory.ACTION,
generalId: target.id,
format: LogFormat.PLAIN,
legacyFlushGroup: firstLegacyFlushGroup + friendlyTargets.length + index,
})
);
if (moveCityId !== target.cityId) {
@@ -163,12 +168,15 @@ export class ActionResolver<
strategic_cmd_limit: globalDelay,
};
effects.push(
createLogEffect(broadcastMessage, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
})
createLogEffect(
`<Y>${generalName}</>${generalJosa} <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`,
{
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
}
)
);
}
@@ -181,6 +189,7 @@ export class ActionResolver<
category: LogCategory.HISTORY,
nationId: context.destNation.id,
format: LogFormat.PLAIN,
legacyFlushGroup: -1,
}
)
);
@@ -101,7 +101,7 @@ export class ActionDefinition<
const amount = clamp(args.amount, 0, available);
const cost = calcCost(this.env.develCost, args.amount);
const amountText = amount.toLocaleString();
const amountText = String(amount);
const destCityName = destCity.name;
const josaRo = JosaUtil.pick(destCityName, '로');