feat: preserve legacy nation turn execution
This commit is contained in:
@@ -11,18 +11,16 @@ export const do불가침제의 = (ai: GeneralAI) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const meta = asRecord(ai.nation.meta);
|
const meta = asRecord(ai.nation.meta);
|
||||||
const recvAssist = Array.isArray(meta.recv_assist) ? meta.recv_assist : [];
|
const recvAssist = Array.isArray(meta.recv_assist) ? meta.recv_assist : Object.values(asRecord(meta.recv_assist));
|
||||||
const respAssist = asRecord(meta.resp_assist);
|
const respAssist = asRecord(meta.resp_assist);
|
||||||
const respAssistTry = asRecord(meta.resp_assist_try);
|
const respAssistTry = asRecord(meta.resp_assist_try);
|
||||||
const yearMonth = joinYearMonth(ai.world.currentYear, ai.world.currentMonth);
|
const yearMonth = joinYearMonth(ai.world.currentYear, ai.world.currentMonth);
|
||||||
|
|
||||||
const candidateList: Record<number, number> = {};
|
const candidateList: Record<number, number> = {};
|
||||||
for (const entry of recvAssist) {
|
for (const entry of recvAssist) {
|
||||||
if (!Array.isArray(entry) || entry.length < 2) {
|
const entryRecord = asRecord(entry);
|
||||||
continue;
|
const destNationId = Number(Array.isArray(entry) ? entry[0] : entryRecord['0']);
|
||||||
}
|
const amount = Number(Array.isArray(entry) ? entry[1] : entryRecord['1']);
|
||||||
const destNationId = Number(entry[0]);
|
|
||||||
const amount = Number(entry[1]);
|
|
||||||
if (!Number.isFinite(destNationId) || !Number.isFinite(amount)) {
|
if (!Number.isFinite(destNationId) || !Number.isFinite(amount)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ import {
|
|||||||
type TurnEngineTroopUpdateInput,
|
type TurnEngineTroopUpdateInput,
|
||||||
type TurnEngineWorldStateUpdateInput,
|
type TurnEngineWorldStateUpdateInput,
|
||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
import { finalizeLogEntry, LogCategory, LogScope, type LogEntryDraft } from '@sammo-ts/logic';
|
import {
|
||||||
|
finalizeLogEntry,
|
||||||
|
LogCategory,
|
||||||
|
LogScope,
|
||||||
|
sendMessage,
|
||||||
|
type LogEntryDraft,
|
||||||
|
type MessageRecordDraft,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
import { asRecord, type RankDataType } from '@sammo-ts/common';
|
import { asRecord, type RankDataType } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||||
@@ -323,6 +330,7 @@ export const createDatabaseTurnHooks = async (
|
|||||||
deletedNationSnapshots,
|
deletedNationSnapshots,
|
||||||
diplomacy,
|
diplomacy,
|
||||||
logs,
|
logs,
|
||||||
|
messages,
|
||||||
createdGenerals,
|
createdGenerals,
|
||||||
createdNations,
|
createdNations,
|
||||||
createdTroops,
|
createdTroops,
|
||||||
@@ -566,6 +574,33 @@ export const createDatabaseTurnHooks = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const message of messages) {
|
||||||
|
await sendMessage(
|
||||||
|
{
|
||||||
|
insertMessage: async (draft: MessageRecordDraft) => {
|
||||||
|
const rows = await prisma.$queryRaw<Array<{ id: number }>>`
|
||||||
|
INSERT INTO message (mailbox, type, src, dest, time, valid_until, message)
|
||||||
|
VALUES (
|
||||||
|
${draft.mailbox},
|
||||||
|
${draft.msgType},
|
||||||
|
${draft.srcId},
|
||||||
|
${draft.destId},
|
||||||
|
${draft.time},
|
||||||
|
${draft.validUntil},
|
||||||
|
CAST(${JSON.stringify(draft.payload)} AS jsonb)
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
const id = rows[0]?.id;
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Failed to persist turn message.');
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
if (options?.reservedTurns && reservedTurnChanges) {
|
if (options?.reservedTurns && reservedTurnChanges) {
|
||||||
await options.reservedTurns.persistChanges(prisma, reservedTurnChanges);
|
await options.reservedTurns.persistChanges(prisma, reservedTurnChanges);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { City, LogEntryDraft, Nation, ScenarioConfig, Troop, TurnSchedule } from '@sammo-ts/logic';
|
import type { City, LogEntryDraft, MessageDraft, Nation, ScenarioConfig, Troop, TurnSchedule } from '@sammo-ts/logic';
|
||||||
import { getNextTurnAt } from '@sammo-ts/logic';
|
import { getNextTurnAt } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
||||||
@@ -25,6 +25,7 @@ export interface GeneralTurnResult {
|
|||||||
nation?: Nation | null;
|
nation?: Nation | null;
|
||||||
nextTurnAt?: Date;
|
nextTurnAt?: Date;
|
||||||
logs?: LogEntryDraft[];
|
logs?: LogEntryDraft[];
|
||||||
|
messages?: MessageDraft[];
|
||||||
patches?: {
|
patches?: {
|
||||||
generals: Array<{ id: number; patch: Partial<TurnGeneral> }>;
|
generals: Array<{ id: number; patch: Partial<TurnGeneral> }>;
|
||||||
cities: Array<{ id: number; patch: Partial<City> }>;
|
cities: Array<{ id: number; patch: Partial<City> }>;
|
||||||
@@ -79,6 +80,7 @@ export interface TurnWorldChanges {
|
|||||||
deletedNationSnapshots: Array<{ nation: Nation; generalIds: number[]; removedAt: Date }>;
|
deletedNationSnapshots: Array<{ nation: Nation; generalIds: number[]; removedAt: Date }>;
|
||||||
diplomacy: TurnDiplomacy[];
|
diplomacy: TurnDiplomacy[];
|
||||||
logs: LogEntryDraft[];
|
logs: LogEntryDraft[];
|
||||||
|
messages: MessageDraft[];
|
||||||
createdGenerals: TurnGeneral[];
|
createdGenerals: TurnGeneral[];
|
||||||
createdNations: Nation[];
|
createdNations: Nation[];
|
||||||
createdTroops: Troop[];
|
createdTroops: Troop[];
|
||||||
@@ -247,6 +249,7 @@ export class InMemoryTurnWorld {
|
|||||||
removedAt: Date;
|
removedAt: Date;
|
||||||
}> = [];
|
}> = [];
|
||||||
private readonly logs: LogEntryDraft[] = [];
|
private readonly logs: LogEntryDraft[] = [];
|
||||||
|
private readonly messages: MessageDraft[] = [];
|
||||||
private readonly scenarioConfig: ScenarioConfig;
|
private readonly scenarioConfig: ScenarioConfig;
|
||||||
private checkpoint?: TurnCheckpoint;
|
private checkpoint?: TurnCheckpoint;
|
||||||
private state: TurnWorldState;
|
private state: TurnWorldState;
|
||||||
@@ -587,6 +590,9 @@ export class InMemoryTurnWorld {
|
|||||||
if (result.logs && result.logs.length > 0) {
|
if (result.logs && result.logs.length > 0) {
|
||||||
this.logs.push(...result.logs);
|
this.logs.push(...result.logs);
|
||||||
}
|
}
|
||||||
|
if (result.messages && result.messages.length > 0) {
|
||||||
|
this.messages.push(...result.messages);
|
||||||
|
}
|
||||||
if (result.patches) {
|
if (result.patches) {
|
||||||
for (const patch of result.patches.generals) {
|
for (const patch of result.patches.generals) {
|
||||||
const target = this.generals.get(patch.id);
|
const target = this.generals.get(patch.id);
|
||||||
@@ -744,6 +750,7 @@ export class InMemoryTurnWorld {
|
|||||||
const deletedNations = Array.from(this.deletedNationIds);
|
const deletedNations = Array.from(this.deletedNationIds);
|
||||||
const deletedNationSnapshots = this.deletedNationSnapshots.slice();
|
const deletedNationSnapshots = this.deletedNationSnapshots.slice();
|
||||||
const logs = this.logs.slice();
|
const logs = this.logs.slice();
|
||||||
|
const messages = this.messages.slice();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generals,
|
generals,
|
||||||
@@ -756,6 +763,7 @@ export class InMemoryTurnWorld {
|
|||||||
deletedNationSnapshots,
|
deletedNationSnapshots,
|
||||||
diplomacy,
|
diplomacy,
|
||||||
logs,
|
logs,
|
||||||
|
messages,
|
||||||
createdGenerals,
|
createdGenerals,
|
||||||
createdNations,
|
createdNations,
|
||||||
createdTroops,
|
createdTroops,
|
||||||
@@ -782,6 +790,7 @@ export class InMemoryTurnWorld {
|
|||||||
for (const id of changes.deletedNations) this.deletedNationIds.delete(id);
|
for (const id of changes.deletedNations) this.deletedNationIds.delete(id);
|
||||||
this.deletedNationSnapshots.splice(0, changes.deletedNationSnapshots.length);
|
this.deletedNationSnapshots.splice(0, changes.deletedNationSnapshots.length);
|
||||||
this.logs.splice(0, changes.logs.length);
|
this.logs.splice(0, changes.logs.length);
|
||||||
|
this.messages.splice(0, changes.messages.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeDirtyState(): TurnWorldChanges {
|
consumeDirtyState(): TurnWorldChanges {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
|
import type { InMemoryTurnWorld, TurnCalendarHandler } from './inMemoryWorld.js';
|
||||||
|
|
||||||
|
const decrementLimit = (value: unknown): number => {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return Math.max(0, Math.floor(value) - 1);
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return Math.max(0, Math.floor(parsed) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ref preUpdateMonthly(): 전략 제한과 외교 제한은 매 월턴마다 1씩 감소한다.
|
||||||
|
export const createNationTurnMonthlyHandler = (options: {
|
||||||
|
getWorld: () => InMemoryTurnWorld | null;
|
||||||
|
}): TurnCalendarHandler => ({
|
||||||
|
onMonthChanged: () => {
|
||||||
|
const world = options.getWorld();
|
||||||
|
if (!world) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const nation of world.listNations()) {
|
||||||
|
const meta = asRecord(nation.meta);
|
||||||
|
world.updateNation(nation.id, {
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
strategic_cmd_limit: decrementLimit(meta.strategic_cmd_limit),
|
||||||
|
surlimit: decrementLimit(meta.surlimit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
GeneralActionDefinition,
|
GeneralActionDefinition,
|
||||||
LogEntryDraft,
|
LogEntryDraft,
|
||||||
MapDefinition,
|
MapDefinition,
|
||||||
|
MessageDraft,
|
||||||
Nation,
|
Nation,
|
||||||
ScenarioConfig,
|
ScenarioConfig,
|
||||||
ScenarioMeta,
|
ScenarioMeta,
|
||||||
@@ -100,6 +101,40 @@ const serializeSeed = (...values: Array<string | number>): string =>
|
|||||||
|
|
||||||
const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;
|
const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;
|
||||||
|
|
||||||
|
type NationLastTurn = {
|
||||||
|
command: string;
|
||||||
|
arg?: Record<string, unknown>;
|
||||||
|
term?: number;
|
||||||
|
seq?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nationLastTurnKey = (officerLevel: number): string => `turn_last_${officerLevel}`;
|
||||||
|
|
||||||
|
const normalizeLastTurn = (value: unknown): NationLastTurn => {
|
||||||
|
const raw = asRecord(value);
|
||||||
|
return {
|
||||||
|
command: typeof raw.command === 'string' ? raw.command : '휴식',
|
||||||
|
...(asRecord(raw.arg) && Object.keys(asRecord(raw.arg)).length > 0 ? { arg: asRecord(raw.arg) } : undefined),
|
||||||
|
...(typeof raw.term === 'number' && Number.isFinite(raw.term) ? { term: Math.floor(raw.term) } : undefined),
|
||||||
|
...(typeof raw.seq === 'number' && Number.isFinite(raw.seq) ? { seq: Math.floor(raw.seq) } : undefined),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sameArgs = (left: Record<string, unknown> | undefined, right: Record<string, unknown>): boolean =>
|
||||||
|
JSON.stringify(left ?? {}) === JSON.stringify(right);
|
||||||
|
|
||||||
|
const readNextAvailableTurn = (nation: Nation, actionName: string): number | null => {
|
||||||
|
const raw = asRecord(nation.meta)[`next_execute_${actionName}`];
|
||||||
|
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
||||||
|
return Math.floor(raw);
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const parsed = Number(raw);
|
||||||
|
return Number.isFinite(parsed) ? Math.floor(parsed) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback: number): number => {
|
const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback: number): number => {
|
||||||
const value = meta[key];
|
const value = meta[key];
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
@@ -214,7 +249,6 @@ const buildUniqueLotteryRunner = (options: {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
type WorldView = {
|
type WorldView = {
|
||||||
getGeneralById(id: number): TurnGeneral | null;
|
getGeneralById(id: number): TurnGeneral | null;
|
||||||
getCityById(id: number): City | null;
|
getCityById(id: number): City | null;
|
||||||
@@ -573,6 +607,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
...(options.unitSet ? { unitSet: options.unitSet } : {}),
|
...(options.unitSet ? { unitSet: options.unitSet } : {}),
|
||||||
};
|
};
|
||||||
const logs: LogEntryDraft[] = [];
|
const logs: LogEntryDraft[] = [];
|
||||||
|
const messages: MessageDraft[] = [];
|
||||||
const patches = {
|
const patches = {
|
||||||
generals: [] as Array<{ id: number; patch: Partial<TurnGeneral> }>,
|
generals: [] as Array<{ id: number; patch: Partial<TurnGeneral> }>,
|
||||||
cities: [] as Array<{ id: number; patch: Partial<City> }>,
|
cities: [] as Array<{ id: number; patch: Partial<City> }>,
|
||||||
@@ -592,6 +627,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
let currentNation = context.nation ?? null;
|
let currentNation = context.nation ?? null;
|
||||||
|
|
||||||
const runAction = (
|
const runAction = (
|
||||||
|
kind: 'nation' | 'general',
|
||||||
definitionMap: Map<string, GeneralActionDefinition>,
|
definitionMap: Map<string, GeneralActionDefinition>,
|
||||||
fallbackDefinition: GeneralActionDefinition,
|
fallbackDefinition: GeneralActionDefinition,
|
||||||
command: ReservedTurnEntry,
|
command: ReservedTurnEntry,
|
||||||
@@ -644,6 +680,19 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
const meta = result.kind === 'deny' ? { constraintName: result.constraintName } : undefined;
|
const meta = result.kind === 'deny' ? { constraintName: result.constraintName } : undefined;
|
||||||
logs.push(createActionLog(reason, meta));
|
logs.push(createActionLog(reason, meta));
|
||||||
}
|
}
|
||||||
|
if (kind === 'nation' && !usedFallback && currentNation) {
|
||||||
|
const currentYearMonth = joinYearMonth(context.world.currentYear, context.world.currentMonth);
|
||||||
|
const nextAvailableTurn = readNextAvailableTurn(currentNation, definition.name);
|
||||||
|
if (nextAvailableTurn !== null && currentYearMonth < nextAvailableTurn) {
|
||||||
|
const remainTurn = nextAvailableTurn - currentYearMonth;
|
||||||
|
definition = fallbackDefinition;
|
||||||
|
actionArgs = definition.parseArgs({}) ?? {};
|
||||||
|
actionKey = definition.key;
|
||||||
|
usedFallback = true;
|
||||||
|
blockedReason = `${remainTurn}턴 더 기다려야 합니다`;
|
||||||
|
logs.push(createActionLog(blockedReason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const seedBase = buildSeedBase(context.world);
|
const seedBase = buildSeedBase(context.world);
|
||||||
const buildRng = (key: string) => {
|
const buildRng = (key: string) => {
|
||||||
@@ -694,6 +743,8 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
definition = fallbackDefinition;
|
definition = fallbackDefinition;
|
||||||
actionArgs = definition.parseArgs({}) ?? {};
|
actionArgs = definition.parseArgs({}) ?? {};
|
||||||
actionKey = definition.key;
|
actionKey = definition.key;
|
||||||
|
usedFallback = true;
|
||||||
|
blockedReason = '예약된 명령을 실행하지 못했습니다.';
|
||||||
logs.push(createActionLog('예약된 명령을 실행하지 못했습니다.'));
|
logs.push(createActionLog('예약된 명령을 실행하지 못했습니다.'));
|
||||||
baseContext = {
|
baseContext = {
|
||||||
general: currentGeneral,
|
general: currentGeneral,
|
||||||
@@ -704,6 +755,52 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
specificContext = baseContext;
|
specificContext = baseContext;
|
||||||
}
|
}
|
||||||
const actionContext = specificContext ?? baseContext;
|
const actionContext = specificContext ?? baseContext;
|
||||||
|
const executionDefinition = definition as unknown as {
|
||||||
|
getPreReqTurn?: (context: ActionContextBase, args: unknown) => number;
|
||||||
|
getPostReqTurn?: (context: ActionContextBase, args: unknown) => number;
|
||||||
|
getStackSequence?: (context: ActionContextBase, args: unknown) => number | null;
|
||||||
|
};
|
||||||
|
const preReqTurn =
|
||||||
|
kind === 'nation' && !usedFallback
|
||||||
|
? Math.max(0, Math.floor(executionDefinition.getPreReqTurn?.(actionContext, actionArgs) ?? 0))
|
||||||
|
: 0;
|
||||||
|
const postReqTurn =
|
||||||
|
kind === 'nation' && !usedFallback
|
||||||
|
? Math.max(0, Math.floor(executionDefinition.getPostReqTurn?.(actionContext, actionArgs) ?? 0))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (kind === 'nation' && !usedFallback && currentNation && preReqTurn > 0) {
|
||||||
|
const metaKey = nationLastTurnKey(currentGeneral.officerLevel);
|
||||||
|
const lastTurn = normalizeLastTurn(asRecord(currentNation.meta)[metaKey]);
|
||||||
|
const stackSequence = executionDefinition.getStackSequence?.(actionContext, actionArgs) ?? null;
|
||||||
|
const sequenceChanged =
|
||||||
|
stackSequence !== null && (lastTurn.seq === undefined || lastTurn.seq < stackSequence);
|
||||||
|
const continuing =
|
||||||
|
lastTurn.command === definition.name &&
|
||||||
|
sameArgs(lastTurn.arg, actionArgsRecord) &&
|
||||||
|
!sequenceChanged;
|
||||||
|
const nextTerm = continuing ? (lastTurn.term ?? 0) + 1 : 1;
|
||||||
|
|
||||||
|
if (!continuing || (lastTurn.term ?? 0) < preReqTurn) {
|
||||||
|
const nextLastTurn: NationLastTurn = {
|
||||||
|
command: definition.name,
|
||||||
|
...(Object.keys(actionArgsRecord).length > 0 ? { arg: actionArgsRecord } : undefined),
|
||||||
|
term: nextTerm,
|
||||||
|
...(stackSequence !== null ? { seq: stackSequence } : undefined),
|
||||||
|
};
|
||||||
|
const nextNation: Nation = {
|
||||||
|
...currentNation,
|
||||||
|
meta: {
|
||||||
|
...currentNation.meta,
|
||||||
|
[metaKey]: nextLastTurn,
|
||||||
|
} as Nation['meta'],
|
||||||
|
};
|
||||||
|
currentNation = nextNation;
|
||||||
|
worldOverlay?.syncNation(nextNation);
|
||||||
|
logs.push(createActionLog(`${definition.name} 수행중... (${nextTerm}/${preReqTurn + 1})`));
|
||||||
|
return { actionKey, usedFallback, blockedReason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolution = resolveGeneralAction(
|
const resolution = resolveGeneralAction(
|
||||||
definition,
|
definition,
|
||||||
@@ -718,12 +815,39 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
currentGeneral = resolution.general as TurnGeneral;
|
currentGeneral = resolution.general as TurnGeneral;
|
||||||
currentCity = resolution.city ?? currentCity;
|
currentCity = resolution.city ?? currentCity;
|
||||||
currentNation = resolution.nation ?? currentNation;
|
currentNation = resolution.nation ?? currentNation;
|
||||||
|
if (kind === 'nation' && !usedFallback && definition.countsAsInheritanceActiveAction) {
|
||||||
|
const meta = { ...currentGeneral.meta };
|
||||||
|
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
||||||
|
meta.inherit_active_action = active + 1;
|
||||||
|
currentGeneral = { ...currentGeneral, meta };
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentNation && resolution.created?.nations) {
|
if (!currentNation && resolution.created?.nations) {
|
||||||
currentNation =
|
currentNation =
|
||||||
(resolution.created.nations as Nation[]).find((n) => n.id === currentGeneral.nationId) ??
|
(resolution.created.nations as Nation[]).find((n) => n.id === currentGeneral.nationId) ??
|
||||||
currentNation;
|
currentNation;
|
||||||
}
|
}
|
||||||
|
if (kind === 'nation' && !usedFallback && currentNation) {
|
||||||
|
const metaKey = nationLastTurnKey(currentGeneral.officerLevel);
|
||||||
|
const nextMeta: Record<string, unknown> = {
|
||||||
|
...currentNation.meta,
|
||||||
|
[metaKey]: {
|
||||||
|
command: definition.name,
|
||||||
|
...(Object.keys(actionArgsRecord).length > 0 ? { arg: actionArgsRecord } : undefined),
|
||||||
|
term: 0,
|
||||||
|
} satisfies NationLastTurn,
|
||||||
|
};
|
||||||
|
if (postReqTurn > 0) {
|
||||||
|
nextMeta[`next_execute_${definition.name}`] =
|
||||||
|
joinYearMonth(context.world.currentYear, context.world.currentMonth) +
|
||||||
|
postReqTurn -
|
||||||
|
preReqTurn;
|
||||||
|
}
|
||||||
|
currentNation = {
|
||||||
|
...currentNation,
|
||||||
|
meta: nextMeta as Nation['meta'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
logs.push(...resolution.logs);
|
logs.push(...resolution.logs);
|
||||||
if (worldOverlay) {
|
if (worldOverlay) {
|
||||||
@@ -738,15 +862,16 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
|
|
||||||
if (resolution.effects.length > 0) {
|
if (resolution.effects.length > 0) {
|
||||||
for (const effect of resolution.effects) {
|
for (const effect of resolution.effects) {
|
||||||
if (effect.type !== 'diplomacy:patch') {
|
if (effect.type === 'message:add') {
|
||||||
continue;
|
messages.push(effect.draft);
|
||||||
|
} else if (effect.type === 'diplomacy:patch') {
|
||||||
|
diplomacyPatches.push({
|
||||||
|
srcNationId: effect.srcNationId,
|
||||||
|
destNationId: effect.destNationId,
|
||||||
|
patch: effect.patch,
|
||||||
|
});
|
||||||
|
worldOverlay?.applyDiplomacyPatch(effect.srcNationId, effect.destNationId, effect.patch);
|
||||||
}
|
}
|
||||||
diplomacyPatches.push({
|
|
||||||
srcNationId: effect.srcNationId,
|
|
||||||
destNationId: effect.destNationId,
|
|
||||||
patch: effect.patch,
|
|
||||||
});
|
|
||||||
worldOverlay?.applyDiplomacyPatch(effect.srcNationId, effect.destNationId, effect.patch);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,7 +951,12 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined, actionKey, usedFallback, blockedReason };
|
return {
|
||||||
|
nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined,
|
||||||
|
actionKey,
|
||||||
|
usedFallback,
|
||||||
|
blockedReason,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentNation && currentGeneral.officerLevel >= 5) {
|
if (currentNation && currentGeneral.officerLevel >= 5) {
|
||||||
@@ -860,7 +990,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
}
|
}
|
||||||
nationAiState = ai.getDebugState();
|
nationAiState = ai.getDebugState();
|
||||||
}
|
}
|
||||||
const nationResult = runAction(nationDefinitions, nationFallback, nationCommand, false);
|
const nationResult = runAction('nation', nationDefinitions, nationFallback, nationCommand, false);
|
||||||
options.onActionResolved?.({
|
options.onActionResolved?.({
|
||||||
kind: 'nation',
|
kind: 'nation',
|
||||||
generalId: currentGeneral.id,
|
generalId: currentGeneral.id,
|
||||||
@@ -900,7 +1030,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
}
|
}
|
||||||
generalAiState = ai.getDebugState();
|
generalAiState = ai.getDebugState();
|
||||||
}
|
}
|
||||||
const generalResult = runAction(generalDefinitions, generalFallback, generalCommand, true);
|
const generalResult = runAction('general', generalDefinitions, generalFallback, generalCommand, true);
|
||||||
options.onActionResolved?.({
|
options.onActionResolved?.({
|
||||||
kind: 'general',
|
kind: 'general',
|
||||||
generalId: currentGeneral.id,
|
generalId: currentGeneral.id,
|
||||||
@@ -935,6 +1065,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
nation: currentNation,
|
nation: currentNation,
|
||||||
nextTurnAt,
|
nextTurnAt,
|
||||||
logs,
|
logs,
|
||||||
|
...(messages.length > 0 ? { messages } : undefined),
|
||||||
patches,
|
patches,
|
||||||
...(diplomacyPatches.length > 0 ? { diplomacyPatches } : undefined),
|
...(diplomacyPatches.length > 0 ? { diplomacyPatches } : undefined),
|
||||||
created:
|
created:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createGatewayAdminActionConsumer } from './gatewayAdminActions.js';
|
|||||||
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||||
import { composeCalendarHandlers } from './calendarHandlers.js';
|
import { composeCalendarHandlers } from './calendarHandlers.js';
|
||||||
import { createIncomeHandler } from './incomeHandler.js';
|
import { createIncomeHandler } from './incomeHandler.js';
|
||||||
|
import { createNationTurnMonthlyHandler } from './nationTurnMonthlyHandler.js';
|
||||||
import { createFrontStateHandler } from './frontStateHandler.js';
|
import { createFrontStateHandler } from './frontStateHandler.js';
|
||||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||||
@@ -124,6 +125,9 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
scenarioConfig: snapshot.scenarioConfig,
|
scenarioConfig: snapshot.scenarioConfig,
|
||||||
nationTraits: nationTraitMap,
|
nationTraits: nationTraitMap,
|
||||||
});
|
});
|
||||||
|
const nationTurnMonthlyHandler = createNationTurnMonthlyHandler({
|
||||||
|
getWorld: () => worldRef,
|
||||||
|
});
|
||||||
const frontStateHandler = createFrontStateHandler({
|
const frontStateHandler = createFrontStateHandler({
|
||||||
getWorld: () => worldRef,
|
getWorld: () => worldRef,
|
||||||
map: snapshot.map ?? null,
|
map: snapshot.map ?? null,
|
||||||
@@ -141,6 +145,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
});
|
});
|
||||||
const calendarHandler = composeCalendarHandlers(
|
const calendarHandler = composeCalendarHandlers(
|
||||||
options.calendarHandler ?? unification?.handler,
|
options.calendarHandler ?? unification?.handler,
|
||||||
|
nationTurnMonthlyHandler,
|
||||||
incomeHandler,
|
incomeHandler,
|
||||||
frontStateHandler,
|
frontStateHandler,
|
||||||
tournamentAutoStartHandler,
|
tournamentAutoStartHandler,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { composeCalendarHandlers } from '../../src/turn/calendarHandlers.js';
|
|||||||
import { createIncomeHandler } from '../../src/turn/incomeHandler.js';
|
import { createIncomeHandler } from '../../src/turn/incomeHandler.js';
|
||||||
import { createNpcTaxHandler } from '../../src/turn/npcTaxHandler.js';
|
import { createNpcTaxHandler } from '../../src/turn/npcTaxHandler.js';
|
||||||
import { createFrontStateHandler } from '../../src/turn/frontStateHandler.js';
|
import { createFrontStateHandler } from '../../src/turn/frontStateHandler.js';
|
||||||
|
import { createNationTurnMonthlyHandler } from '../../src/turn/nationTurnMonthlyHandler.js';
|
||||||
|
|
||||||
export const createMockPrisma = (initialGeneralRows: any[] = []) => {
|
export const createMockPrisma = (initialGeneralRows: any[] = []) => {
|
||||||
let generalRows = [...initialGeneralRows];
|
let generalRows = [...initialGeneralRows];
|
||||||
@@ -126,8 +127,12 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
getWorld: () => worldRef.current,
|
getWorld: () => worldRef.current,
|
||||||
map: options.map,
|
map: options.map,
|
||||||
});
|
});
|
||||||
|
const nationTurnMonthlyHandler = createNationTurnMonthlyHandler({
|
||||||
|
getWorld: () => worldRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
const calendarHandler = composeCalendarHandlers(
|
const calendarHandler = composeCalendarHandlers(
|
||||||
|
nationTurnMonthlyHandler,
|
||||||
incomeHandler,
|
incomeHandler,
|
||||||
npcTaxHandler,
|
npcTaxHandler,
|
||||||
frontStateHandler,
|
frontStateHandler,
|
||||||
@@ -194,8 +199,7 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
runUntil,
|
runUntil,
|
||||||
getCollectedLogs: () => [...collectedLogs],
|
getCollectedLogs: () => [...collectedLogs],
|
||||||
getCollectedLogsCount: () => collectedLogs.length,
|
getCollectedLogsCount: () => collectedLogs.length,
|
||||||
getCollectedLogsRange: (start: number, end?: number) =>
|
getCollectedLogsRange: (start: number, end?: number) => collectedLogs.slice(start, end ?? collectedLogs.length),
|
||||||
collectedLogs.slice(start, end ?? collectedLogs.length),
|
|
||||||
getAndClearCollectedLogs: () => collectedLogs.splice(0, collectedLogs.length),
|
getAndClearCollectedLogs: () => collectedLogs.splice(0, collectedLogs.length),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -238,10 +242,7 @@ const formatCity = (city: ReturnType<InMemoryTurnWorld['getCityById']>) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createWorldDebugger = (
|
export const createWorldDebugger = (getWorld: () => InMemoryTurnWorld | null, watchTargets: DebugWatchTargets = {}) => {
|
||||||
getWorld: () => InMemoryTurnWorld | null,
|
|
||||||
watchTargets: DebugWatchTargets = {}
|
|
||||||
) => {
|
|
||||||
const dumpWorldSummary = (label = 'WORLD') => {
|
const dumpWorldSummary = (label = 'WORLD') => {
|
||||||
const world = getWorld();
|
const world = getWorld();
|
||||||
if (!world) {
|
if (!world) {
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||||
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||||
|
import { createNationTurnMonthlyHandler } from '../src/turn/nationTurnMonthlyHandler.js';
|
||||||
|
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
||||||
|
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
||||||
|
|
||||||
|
const mockDate = new Date('0189-01-01T00:00:00Z');
|
||||||
|
|
||||||
|
const createChief = (): TurnGeneral => ({
|
||||||
|
id: 1,
|
||||||
|
name: '군주',
|
||||||
|
nationId: 1,
|
||||||
|
cityId: 1,
|
||||||
|
troopId: 0,
|
||||||
|
stats: { leadership: 90, strength: 80, intelligence: 70 },
|
||||||
|
turnTime: mockDate,
|
||||||
|
role: {
|
||||||
|
items: { horse: null, weapon: null, book: null, item: null },
|
||||||
|
personality: null,
|
||||||
|
specialDomestic: null,
|
||||||
|
specialWar: null,
|
||||||
|
},
|
||||||
|
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||||
|
meta: { killturn: 800 },
|
||||||
|
officerLevel: 12,
|
||||||
|
experience: 0,
|
||||||
|
dedication: 0,
|
||||||
|
injury: 0,
|
||||||
|
gold: 100_000,
|
||||||
|
rice: 100_000,
|
||||||
|
crew: 0,
|
||||||
|
crewTypeId: 0,
|
||||||
|
train: 0,
|
||||||
|
atmos: 0,
|
||||||
|
age: 30,
|
||||||
|
npcState: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('레거시 사령부 턴 실행 호환성', () => {
|
||||||
|
it('화시병 연구는 선행 11턴을 누적한 뒤 12번째 턴에만 완료한다', async () => {
|
||||||
|
const cities = buildLargeTestCities();
|
||||||
|
for (const city of cities) {
|
||||||
|
city.nationId = city.id === 1 ? 1 : city.id === 2 ? 2 : 0;
|
||||||
|
}
|
||||||
|
const snapshot: TurnWorldSnapshot = {
|
||||||
|
generals: [createChief()],
|
||||||
|
cities,
|
||||||
|
nations: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '테스트국',
|
||||||
|
color: '#aa0000',
|
||||||
|
capitalCityId: 1,
|
||||||
|
chiefGeneralId: 1,
|
||||||
|
gold: 1_000_000,
|
||||||
|
rice: 1_000_000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'large_test_map_def',
|
||||||
|
meta: { can_화시병사용: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '상대국',
|
||||||
|
color: '#0000aa',
|
||||||
|
capitalCityId: 2,
|
||||||
|
chiefGeneralId: null,
|
||||||
|
gold: 1_000_000,
|
||||||
|
rice: 1_000_000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'large_test_map_def',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
troops: [],
|
||||||
|
diplomacy: [
|
||||||
|
{ fromNationId: 1, toNationId: 2, state: 0, term: 1200, dead: 0, meta: {} },
|
||||||
|
{ fromNationId: 2, toNationId: 1, state: 0, term: 1200, dead: 0, meta: {} },
|
||||||
|
],
|
||||||
|
events: [],
|
||||||
|
initialEvents: [],
|
||||||
|
map: LARGE_TEST_MAP,
|
||||||
|
scenarioConfig: {
|
||||||
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||||
|
iconPath: '',
|
||||||
|
map: {},
|
||||||
|
const: {
|
||||||
|
openingPartYear: 3,
|
||||||
|
develCost: 10,
|
||||||
|
baseGold: 1000,
|
||||||
|
baseRice: 1000,
|
||||||
|
maxResourceActionAmount: 10000,
|
||||||
|
maxTechLevel: 12000,
|
||||||
|
},
|
||||||
|
environment: { mapName: 'large_test_map', unitSet: 'default' },
|
||||||
|
},
|
||||||
|
scenarioMeta: { startYear: 189 } as never,
|
||||||
|
unitSet: {} as never,
|
||||||
|
};
|
||||||
|
const state: TurnWorldState = {
|
||||||
|
id: 1,
|
||||||
|
currentYear: 189,
|
||||||
|
currentMonth: 1,
|
||||||
|
tickSeconds: 600,
|
||||||
|
lastTurnTime: mockDate,
|
||||||
|
meta: { seed: 1 },
|
||||||
|
};
|
||||||
|
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
||||||
|
const resolvedActions: Array<{ requestedAction: string; actionKey: string; blockedReason?: string }> = [];
|
||||||
|
const { world, reservedTurnStore, runOneTick } = await createTurnTestHarness({
|
||||||
|
snapshot,
|
||||||
|
state,
|
||||||
|
schedule,
|
||||||
|
map: LARGE_TEST_MAP,
|
||||||
|
reservedTurnStoreOptions: { maxGeneralTurns: 12, maxNationTurns: 12 },
|
||||||
|
onActionResolved: (event) => {
|
||||||
|
if (event.kind === 'nation') {
|
||||||
|
resolvedActions.push(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const turns = reservedTurnStore.getNationTurns(1, 12);
|
||||||
|
turns.fill({ action: 'event_화시병연구', args: {} });
|
||||||
|
|
||||||
|
for (let index = 1; index <= 11; index += 1) {
|
||||||
|
await runOneTick();
|
||||||
|
const nation = world.getNationById(1)!;
|
||||||
|
expect(nation.meta.can_화시병사용 ?? 0).toBe(0);
|
||||||
|
expect(nation.meta.turn_last_12).toMatchObject({
|
||||||
|
command: '화시병 연구',
|
||||||
|
term: index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await runOneTick();
|
||||||
|
const nation = world.getNationById(1)!;
|
||||||
|
expect(nation.meta.can_화시병사용).toBe(1);
|
||||||
|
expect(nation.meta.turn_last_12).toMatchObject({
|
||||||
|
command: '화시병 연구',
|
||||||
|
term: 0,
|
||||||
|
});
|
||||||
|
expect(world.getGeneralById(1)!.meta.inherit_active_action).toBe(1);
|
||||||
|
expect(world.getGeneralById(1)!.experience).toBe(60);
|
||||||
|
expect(world.getGeneralById(1)!.dedication).toBe(60);
|
||||||
|
|
||||||
|
reservedTurnStore.getNationTurns(1, 12)[0] = {
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationId: 2 },
|
||||||
|
};
|
||||||
|
await runOneTick();
|
||||||
|
expect(resolvedActions.at(-1)?.blockedReason).toBeUndefined();
|
||||||
|
expect(resolvedActions.at(-1)).toMatchObject({
|
||||||
|
requestedAction: 'che_종전제의',
|
||||||
|
actionKey: 'che_종전제의',
|
||||||
|
});
|
||||||
|
expect(world.peekDirtyState().messages).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
dest: expect.objectContaining({ nationId: 2 }),
|
||||||
|
option: { action: 'stopWar', deletable: false },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('월 경계마다 전략·외교 제한을 1씩 감소시키고 0 아래로 내리지 않는다', () => {
|
||||||
|
const updates: Array<{ id: number; patch: Record<string, unknown> }> = [];
|
||||||
|
const nations = [
|
||||||
|
{ id: 1, meta: { strategic_cmd_limit: 2, surlimit: '1', keep: true } },
|
||||||
|
{ id: 2, meta: { strategic_cmd_limit: 0, surlimit: 0 } },
|
||||||
|
];
|
||||||
|
const handler = createNationTurnMonthlyHandler({
|
||||||
|
getWorld: () =>
|
||||||
|
({
|
||||||
|
listNations: () => nations,
|
||||||
|
updateNation: (id: number, patch: Record<string, unknown>) => updates.push({ id, patch }),
|
||||||
|
}) as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.onMonthChanged?.({} as never);
|
||||||
|
|
||||||
|
expect(updates).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
patch: { meta: { strategic_cmd_limit: 1, surlimit: 0, keep: true } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
patch: { meta: { strategic_cmd_limit: 0, surlimit: 0 } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+3
-1
@@ -15,7 +15,8 @@
|
|||||||
"dev": "turbo dev",
|
"dev": "turbo dev",
|
||||||
"build:server": "pnpm --filter ./tools/build-scripts build:server --",
|
"build:server": "pnpm --filter ./tools/build-scripts build:server --",
|
||||||
"generate:resource-schemas": "pnpm --filter @sammo-ts/tools-scripts generate:resource-schemas",
|
"generate:resource-schemas": "pnpm --filter @sammo-ts/tools-scripts generate:resource-schemas",
|
||||||
"validate:resources": "pnpm --filter @sammo-ts/tools-scripts validate:resources"
|
"validate:resources": "pnpm --filter @sammo-ts/tools-scripts validate:resources",
|
||||||
|
"check:legacy:nation": "node tools/compare-command-constraints.mjs --include '^Nation/' --check && node tools/compare-command-logs.mjs --include '^Nation/' --mode action --check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"tsdown": "^0.22.14",
|
"tsdown": "^0.22.14",
|
||||||
"turbo": "^2.10.6",
|
"turbo": "^2.10.6",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
|
"typescript-legacy": "npm:typescript@5.9.3",
|
||||||
"typescript-eslint": "^8.65.0",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vue-eslint-parser": "^10.4.1"
|
"vue-eslint-parser": "^10.4.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,10 @@ export interface GeneralActionDefinition<
|
|||||||
// 커맨드 입력 단계에서 최소 조건만 평가할 때 사용한다.
|
// 커맨드 입력 단계에서 최소 조건만 평가할 때 사용한다.
|
||||||
buildMinConstraints?(ctx: ConstraintContext, args: Args): Constraint[];
|
buildMinConstraints?(ctx: ConstraintContext, args: Args): Constraint[];
|
||||||
buildConstraints(ctx: ConstraintContext, args: Args): Constraint[];
|
buildConstraints(ctx: ConstraintContext, args: Args): Constraint[];
|
||||||
|
// NationCommand::addTermStack()/setNextAvailable() 호환 실행 메타데이터.
|
||||||
|
getPreReqTurn?(context: Context, args: Args): number;
|
||||||
|
getPostReqTurn?(context: Context, args: Args): number;
|
||||||
|
getStackSequence?(context: Context, args: Args): number | null;
|
||||||
|
readonly countsAsInheritanceActiveAction?: boolean;
|
||||||
resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>;
|
resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||||
import { getNextTurnAt, type TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
|
import { getNextTurnAt, type TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
|
||||||
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
|
import type { MessageDraft } from '@sammo-ts/logic/messages/message.js';
|
||||||
|
|
||||||
enablePatches();
|
enablePatches();
|
||||||
|
|
||||||
@@ -86,6 +87,11 @@ export interface NextTurnOverrideEffect {
|
|||||||
nextTurnAt: Date;
|
nextTurnAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessageAddEffect {
|
||||||
|
type: 'message:add';
|
||||||
|
draft: MessageDraft;
|
||||||
|
}
|
||||||
|
|
||||||
export type GeneralActionEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
|
export type GeneralActionEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
|
||||||
| GeneralPatchEffect<TriggerState>
|
| GeneralPatchEffect<TriggerState>
|
||||||
| GeneralAddEffect<TriggerState>
|
| GeneralAddEffect<TriggerState>
|
||||||
@@ -94,6 +100,7 @@ export type GeneralActionEffect<TriggerState extends GeneralTriggerState = Gener
|
|||||||
| NationAddEffect
|
| NationAddEffect
|
||||||
| DiplomacyPatchEffect
|
| DiplomacyPatchEffect
|
||||||
| LogEffect
|
| LogEffect
|
||||||
|
| MessageAddEffect
|
||||||
| NextTurnOverrideEffect;
|
| NextTurnOverrideEffect;
|
||||||
|
|
||||||
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
@@ -203,6 +210,11 @@ export const createLogEffect = (message: string, options: Partial<Omit<LogEntryD
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createMessageEffect = (draft: MessageDraft): MessageAddEffect => ({
|
||||||
|
type: 'message:add',
|
||||||
|
draft,
|
||||||
|
});
|
||||||
|
|
||||||
export const createNextTurnOverrideEffect = (nextTurnAt: Date): NextTurnOverrideEffect => ({
|
export const createNextTurnOverrideEffect = (nextTurnAt: Date): NextTurnOverrideEffect => ({
|
||||||
type: 'schedule:override',
|
type: 'schedule:override',
|
||||||
nextTurnAt,
|
nextTurnAt,
|
||||||
@@ -301,6 +313,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
|
|||||||
createdNations.push(effect.nation as Nation);
|
createdNations.push(effect.nation as Nation);
|
||||||
break;
|
break;
|
||||||
case 'diplomacy:patch':
|
case 'diplomacy:patch':
|
||||||
|
case 'message:add':
|
||||||
pendingEffects.push(effect);
|
pendingEffects.push(effect);
|
||||||
break;
|
break;
|
||||||
case 'general:patch':
|
case 'general:patch':
|
||||||
|
|||||||
@@ -47,14 +47,15 @@ const requireCapitalCity = (reason: string): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const reqDestCityValue = (
|
const reqDestCityValue = (comp: '>' | '<' | '>=' | '<=', required: number | 'origin', reason: string): Constraint => ({
|
||||||
comp: '>' | '<' | '>=' | '<=',
|
|
||||||
required: number | 'origin',
|
|
||||||
reason: string
|
|
||||||
): Constraint => ({
|
|
||||||
name: 'reqDestCityValue',
|
name: 'reqDestCityValue',
|
||||||
requires: (ctx) =>
|
requires: (ctx) =>
|
||||||
ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }, { kind: 'env', key: 'map' }] : [],
|
ctx.nationId !== undefined
|
||||||
|
? [
|
||||||
|
{ kind: 'nation', id: ctx.nationId },
|
||||||
|
{ kind: 'env', key: 'map' },
|
||||||
|
]
|
||||||
|
: [],
|
||||||
test: (ctx: ConstraintContext, view: StateView) => {
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
if (ctx.nationId === undefined) {
|
if (ctx.nationId === undefined) {
|
||||||
return { kind: 'deny', reason };
|
return { kind: 'deny', reason };
|
||||||
@@ -71,8 +72,7 @@ const reqDestCityValue = (
|
|||||||
required === 'origin'
|
required === 'origin'
|
||||||
? ((view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined)?.cities.find(
|
? ((view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined)?.cities.find(
|
||||||
(mapCity) => mapCity.id === nation.capitalCityId
|
(mapCity) => mapCity.id === nation.capitalCityId
|
||||||
)?.level ??
|
)?.level ?? 0)
|
||||||
0)
|
|
||||||
: required;
|
: required;
|
||||||
const level = city.level;
|
const level = city.level;
|
||||||
const allow =
|
const allow =
|
||||||
@@ -95,6 +95,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, ReduceCityArgs, ReduceCityResolveContext<TriggerState>> {
|
> implements GeneralActionDefinition<TriggerState, ReduceCityArgs, ReduceCityResolveContext<TriggerState>> {
|
||||||
public readonly key = 'che_감축';
|
public readonly key = 'che_감축';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
constructor(private readonly env: TurnCommandEnv) {}
|
constructor(private readonly env: TurnCommandEnv) {}
|
||||||
|
|
||||||
@@ -122,6 +123,15 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStackSequence(context: ReduceCityResolveContext<TriggerState>): number {
|
||||||
|
const value = context.nation?.meta.capset;
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: ReduceCityResolveContext<TriggerState>,
|
context: ReduceCityResolveContext<TriggerState>,
|
||||||
_args: ReduceCityArgs
|
_args: ReduceCityArgs
|
||||||
@@ -163,6 +173,10 @@ export class ActionDefinition<
|
|||||||
{
|
{
|
||||||
gold: nation.gold + recoverAmount,
|
gold: nation.gold + recoverAmount,
|
||||||
rice: nation.rice + recoverAmount,
|
rice: nation.rice + recoverAmount,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nation.id
|
nation.id
|
||||||
),
|
),
|
||||||
@@ -194,6 +208,11 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ const NATION_COLORS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
colorType: z.number().int().min(0).max(NATION_COLORS.length - 1),
|
colorType: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(NATION_COLORS.length - 1),
|
||||||
});
|
});
|
||||||
export type ChangeFlagArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type ChangeFlagArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, ChangeFlagArgs> {
|
> implements GeneralActionDefinition<TriggerState, ChangeFlagArgs> {
|
||||||
public readonly key = 'che_국기변경';
|
public readonly key = 'che_국기변경';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
parseArgs(raw: unknown): ChangeFlagArgs | null {
|
parseArgs(raw: unknown): ChangeFlagArgs | null {
|
||||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||||
@@ -141,6 +146,11 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경하였습니다`, {
|
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경하였습니다`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, ChangeNationNameArgs> {
|
> implements GeneralActionDefinition<TriggerState, ChangeNationNameArgs> {
|
||||||
public readonly key = 'che_국호변경';
|
public readonly key = 'che_국호변경';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
parseArgs(raw: unknown): ChangeNationNameArgs | null {
|
parseArgs(raw: unknown): ChangeNationNameArgs | null {
|
||||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||||
@@ -101,6 +102,11 @@ export class ActionDefinition<
|
|||||||
category: LogCategory.HISTORY,
|
category: LogCategory.HISTORY,
|
||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}),
|
}),
|
||||||
|
createLogEffect(`국호를 <D><b>${newNationName}</b></>${josaRo} 변경`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`국호를 <D><b>${newNationName}</b></>${josaRo} 변경합니다.`, {
|
createLogEffect(`국호를 <D><b>${newNationName}</b></>${josaRo} 변경합니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
|
|||||||
@@ -53,10 +53,19 @@ const TERM_REDUCE = 3;
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 16) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
|
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -69,8 +78,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_급습';
|
readonly key = 'che_급습';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context: RaidResolveContext<TriggerState>, _args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: RaidResolveContext<TriggerState>, _args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||||
@@ -167,8 +180,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): RaidArgs | null {
|
parseArgs(raw: unknown): RaidArgs | null {
|
||||||
@@ -191,6 +204,10 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(context: RaidResolveContext<TriggerState>, args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: RaidResolveContext<TriggerState>, args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
@@ -235,5 +252,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destNationId: 0 },
|
availabilityArgs: { destNationId: 0 },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { GeneralTriggerState, City, General } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState, City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import { beLord, occupiedCity, suppliedCity, beOpeningPart, reqNationAuxValue } from '@sammo-ts/logic/constraints/presets.js';
|
import {
|
||||||
|
beLord,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
beOpeningPart,
|
||||||
|
reqNationAuxValue,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type {
|
import type {
|
||||||
GeneralActionEffect,
|
GeneralActionEffect,
|
||||||
@@ -39,6 +45,7 @@ export class ActionDefinition<
|
|||||||
> {
|
> {
|
||||||
public readonly key = 'che_무작위수도이전';
|
public readonly key = 'che_무작위수도이전';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
parseArgs(_raw: unknown): RandomMoveCapitalArgs | null {
|
parseArgs(_raw: unknown): RandomMoveCapitalArgs | null {
|
||||||
return {};
|
return {};
|
||||||
@@ -57,6 +64,10 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: RandomMoveCapitalResolveContext<TriggerState>,
|
context: RandomMoveCapitalResolveContext<TriggerState>,
|
||||||
_args: RandomMoveCapitalArgs
|
_args: RandomMoveCapitalArgs
|
||||||
@@ -142,6 +153,11 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} 국가를 옮겼습니다.`, {
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} 국가를 옮겼습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
beChief,
|
beChief,
|
||||||
@@ -27,16 +27,23 @@ import { clamp } from 'es-toolkit';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z
|
||||||
destNationId: z.number(),
|
.object({
|
||||||
amountList: z.tuple([z.number(), z.number()]),
|
destNationId: z.number().int().positive(),
|
||||||
});
|
amountList: z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]),
|
||||||
|
})
|
||||||
|
.refine(({ amountList }) => amountList[0] > 0 || amountList[1] > 0, {
|
||||||
|
message: '지원량은 0보다 커야 합니다.',
|
||||||
|
path: ['amountList'],
|
||||||
|
});
|
||||||
export type MaterialAidArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type MaterialAidArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
export interface MaterialAidResolveContext<
|
export interface MaterialAidResolveContext<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> extends GeneralActionResolveContext<TriggerState> {
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
destNation: Nation;
|
destNation: Nation;
|
||||||
|
friendlyChiefs: Array<General<TriggerState>>;
|
||||||
|
destNationChiefs: Array<General<TriggerState>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '원조';
|
const ACTION_NAME = '원조';
|
||||||
@@ -75,7 +82,12 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildMinConstraints(_ctx: ConstraintContext, _args: MaterialAidArgs): Constraint[] {
|
buildMinConstraints(_ctx: ConstraintContext, _args: MaterialAidArgs): Constraint[] {
|
||||||
return [occupiedCity(), beChief(), suppliedCity(), reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.')];
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] {
|
buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] {
|
||||||
@@ -111,11 +123,24 @@ export class ActionDefinition<
|
|||||||
|
|
||||||
const goldText = actualGold.toLocaleString();
|
const goldText = actualGold.toLocaleString();
|
||||||
const riceText = actualRice.toLocaleString();
|
const riceText = actualRice.toLocaleString();
|
||||||
|
const nationName = nation.name;
|
||||||
const josaUlRice = JosaUtil.pick(riceText, '을');
|
const josaUlRice = JosaUtil.pick(riceText, '을');
|
||||||
const josaRo = JosaUtil.pick(destNation.name, '로');
|
const josaRo = JosaUtil.pick(destNation.name, '로');
|
||||||
const nationName = nation.name;
|
const josaRoSrc = JosaUtil.pick(nationName, '로');
|
||||||
|
|
||||||
const broadcastMessage = `<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>을 지원했습니다.`;
|
const broadcastMessage = `<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>을 지원했습니다.`;
|
||||||
|
const recvAssist =
|
||||||
|
typeof destNation.meta.recv_assist === 'object' && destNation.meta.recv_assist !== null
|
||||||
|
? { ...destNation.meta.recv_assist }
|
||||||
|
: {};
|
||||||
|
const recvKey = `n${nation.id}`;
|
||||||
|
const priorEntry =
|
||||||
|
typeof recvAssist[recvKey] === 'object' && recvAssist[recvKey] !== null ? recvAssist[recvKey] : {};
|
||||||
|
const priorAmount = Number(priorEntry['1'] ?? 0);
|
||||||
|
recvAssist[recvKey] = {
|
||||||
|
0: nation.id,
|
||||||
|
1: (Number.isFinite(priorAmount) ? priorAmount : 0) + actualGold + actualRice,
|
||||||
|
};
|
||||||
|
|
||||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
createNationPatchEffect(
|
createNationPatchEffect(
|
||||||
@@ -133,6 +158,10 @@ export class ActionDefinition<
|
|||||||
{
|
{
|
||||||
gold: destNation.gold + actualGold,
|
gold: destNation.gold + actualGold,
|
||||||
rice: destNation.rice + actualRice,
|
rice: destNation.rice + actualRice,
|
||||||
|
meta: {
|
||||||
|
...destNation.meta,
|
||||||
|
recv_assist: recvAssist,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
destNation.id
|
destNation.id
|
||||||
),
|
),
|
||||||
@@ -146,6 +175,14 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
// Actor Nation History Log
|
// Actor Nation History Log
|
||||||
|
createLogEffect(
|
||||||
|
`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`,
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}
|
||||||
|
),
|
||||||
createLogEffect(
|
createLogEffect(
|
||||||
`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`,
|
`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`,
|
||||||
{
|
{
|
||||||
@@ -157,7 +194,7 @@ export class ActionDefinition<
|
|||||||
),
|
),
|
||||||
// Dest Nation History Log
|
// Dest Nation History Log
|
||||||
createLogEffect(
|
createLogEffect(
|
||||||
`<D><b>${nationName}</b></>${JosaUtil.pick(nationName, '부터')} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원 받음`,
|
`<D><b>${nationName}</b></>${josaRoSrc}부터 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원 받음`,
|
||||||
{
|
{
|
||||||
scope: LogScope.NATION,
|
scope: LogScope.NATION,
|
||||||
nationId: destNation.id,
|
nationId: destNation.id,
|
||||||
@@ -178,6 +215,30 @@ export class ActionDefinition<
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
for (const chief of context.friendlyChiefs) {
|
||||||
|
if (chief.id !== general.id) {
|
||||||
|
effects.push(
|
||||||
|
createLogEffect(broadcastMessage, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
generalId: chief.id,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const destBroadcastMessage = `<D><b>${nationName}</b></>에서 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 원조했습니다.`;
|
||||||
|
for (const chief of context.destNationChiefs) {
|
||||||
|
effects.push(
|
||||||
|
createLogEffect(destBroadcastMessage, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
generalId: chief.id,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
general.experience += 5;
|
general.experience += 5;
|
||||||
general.dedication += 5;
|
general.dedication += 5;
|
||||||
|
|
||||||
@@ -194,10 +255,15 @@ export const actionContextBuilder: ActionContextBuilder<MaterialAidArgs> = (base
|
|||||||
|
|
||||||
const destNation = worldRef.getNationById(destNationId);
|
const destNation = worldRef.getNationById(destNationId);
|
||||||
if (!destNation) return null;
|
if (!destNation) return null;
|
||||||
|
const generals = worldRef.listGenerals();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
destNation,
|
destNation,
|
||||||
|
friendlyChiefs: generals.filter(
|
||||||
|
(general) => general.nationId === base.general.nationId && general.officerLevel >= 5
|
||||||
|
),
|
||||||
|
destNationChiefs: generals.filter((general) => general.nationId === destNationId && general.officerLevel >= 5),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,19 @@ const DEFENCE_RATE = 0.8;
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
|
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -64,8 +73,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_백성동원';
|
readonly key = 'che_백성동원';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
@@ -144,8 +157,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): MobilizePeopleArgs | null {
|
parseArgs(raw: unknown): MobilizePeopleArgs | null {
|
||||||
@@ -162,6 +175,10 @@ export class ActionDefinition<
|
|||||||
return [occupiedCity(), beChief(), occupiedDestCity(), availableStrategicCommand()];
|
return [occupiedCity(), beChief(), occupiedDestCity(), availableStrategicCommand()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: MobilizePeopleResolveContext<TriggerState>,
|
context: MobilizePeopleResolveContext<TriggerState>,
|
||||||
args: MobilizePeopleArgs
|
args: MobilizePeopleArgs
|
||||||
@@ -198,5 +215,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destCityId: 0 },
|
availabilityArgs: { destCityId: 0 },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
beChief,
|
beChief,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
@@ -32,6 +32,14 @@ const ARGS_SCHEMA = z.object({
|
|||||||
});
|
});
|
||||||
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
|
interface NonAggressionProposalContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destNation: Nation;
|
||||||
|
messageValidMinutes: number;
|
||||||
|
messageTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '불가침 제의';
|
const ACTION_NAME = '불가침 제의';
|
||||||
const MIN_TERM_MONTHS = 6;
|
const MIN_TERM_MONTHS = 6;
|
||||||
|
|
||||||
@@ -90,7 +98,11 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
|||||||
// 불가침 제의를 처리하는 국가 커맨드.
|
// 불가침 제의를 처리하는 국가 커맨드.
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<TriggerState, NonAggressionProposalArgs> {
|
> implements GeneralActionDefinition<
|
||||||
|
TriggerState,
|
||||||
|
NonAggressionProposalArgs,
|
||||||
|
NonAggressionProposalContext<TriggerState>
|
||||||
|
> {
|
||||||
public readonly key = 'che_불가침제의';
|
public readonly key = 'che_불가침제의';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
@@ -117,14 +129,42 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
_context: GeneralActionResolveContext<TriggerState>,
|
context: NonAggressionProposalContext<TriggerState>,
|
||||||
args: NonAggressionProposalArgs
|
args: NonAggressionProposalArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
const destNationName =
|
const { general, nation, destNation } = context;
|
||||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||||
|
}
|
||||||
|
const destNationName = destNation.name;
|
||||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||||
|
const josaWa = JosaUtil.pick(nation.name, '와');
|
||||||
|
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||||
return {
|
return {
|
||||||
effects: [
|
effects: [
|
||||||
|
createMessageEffect({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
src: {
|
||||||
|
generalId: general.id,
|
||||||
|
generalName: general.name,
|
||||||
|
nationId: nation.id,
|
||||||
|
nationName: nation.name,
|
||||||
|
color: nation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
generalId: 0,
|
||||||
|
generalName: '',
|
||||||
|
nationId: destNation.id,
|
||||||
|
nationName: destNation.name,
|
||||||
|
color: destNation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
text: `${nation.name}${josaWa} ${args.year}년 ${args.month}월까지 불가침 제의 서신`,
|
||||||
|
time: context.messageTime,
|
||||||
|
validUntil,
|
||||||
|
option: { action: 'noAggression', year: args.year, month: args.month },
|
||||||
|
}),
|
||||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 제의 서신을 보냈습니다.`, {
|
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 제의 서신을 보냈습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
@@ -136,11 +176,18 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 예약 턴 실행에 필요한 날짜 정보를 제공한다.
|
// 예약 턴 실행에 필요한 날짜 정보를 제공한다.
|
||||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
|
export const actionContextBuilder: ActionContextBuilder<NonAggressionProposalArgs> = (base, options) => {
|
||||||
...base,
|
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||||
currentYear: options.world.currentYear,
|
if (!destNation) {
|
||||||
currentMonth: options.world.currentMonth,
|
return null;
|
||||||
});
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destNation,
|
||||||
|
messageTime: base.general.turnTime,
|
||||||
|
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const commandSpec: NationTurnCommandSpec = {
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
key: 'che_불가침제의',
|
key: 'che_불가침제의',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
allowDiplomacyBetweenStatus,
|
allowDiplomacyBetweenStatus,
|
||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { NationTurnCommandSpec } from './index.js';
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
@@ -27,13 +27,25 @@ const ARGS_SCHEMA = z.object({
|
|||||||
});
|
});
|
||||||
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
|
interface NonAggressionCancelProposalContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destNation: Nation;
|
||||||
|
messageValidMinutes: number;
|
||||||
|
messageTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '불가침 파기 제의';
|
const ACTION_NAME = '불가침 파기 제의';
|
||||||
const DIPLOMACY_NON_AGGRESSION = 7;
|
const DIPLOMACY_NON_AGGRESSION = 7;
|
||||||
|
|
||||||
// 불가침 파기 제의를 처리하는 국가 커맨드.
|
// 불가침 파기 제의를 처리하는 국가 커맨드.
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs> {
|
> implements GeneralActionDefinition<
|
||||||
|
TriggerState,
|
||||||
|
NonAggressionCancelProposalArgs,
|
||||||
|
NonAggressionCancelProposalContext<TriggerState>
|
||||||
|
> {
|
||||||
public readonly key = 'che_불가침파기제의';
|
public readonly key = 'che_불가침파기제의';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
@@ -57,14 +69,41 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
_context: GeneralActionResolveContext<TriggerState>,
|
context: NonAggressionCancelProposalContext<TriggerState>,
|
||||||
args: NonAggressionCancelProposalArgs
|
_args: NonAggressionCancelProposalArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
const destNationName =
|
const { general, nation, destNation } = context;
|
||||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||||
|
}
|
||||||
|
const destNationName = destNation.name;
|
||||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||||
|
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||||
return {
|
return {
|
||||||
effects: [
|
effects: [
|
||||||
|
createMessageEffect({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
src: {
|
||||||
|
generalId: general.id,
|
||||||
|
generalName: general.name,
|
||||||
|
nationId: nation.id,
|
||||||
|
nationName: nation.name,
|
||||||
|
color: nation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
generalId: 0,
|
||||||
|
generalName: '',
|
||||||
|
nationId: destNation.id,
|
||||||
|
nationName: destNation.name,
|
||||||
|
color: destNation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
text: `${nation.name}의 불가침 파기 제의 서신`,
|
||||||
|
time: context.messageTime,
|
||||||
|
validUntil,
|
||||||
|
option: { action: 'cancelNA', deletable: false },
|
||||||
|
}),
|
||||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 파기 제의 서신을 보냈습니다.`, {
|
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 파기 제의 서신을 보냈습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
@@ -76,7 +115,18 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
||||||
export const actionContextBuilder = defaultActionContextBuilder;
|
export const actionContextBuilder: ActionContextBuilder<NonAggressionCancelProposalArgs> = (base, options) => {
|
||||||
|
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||||
|
if (!destNation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destNation,
|
||||||
|
messageTime: base.general.turnTime,
|
||||||
|
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const commandSpec: NationTurnCommandSpec = {
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
key: 'che_불가침파기제의',
|
key: 'che_불가침파기제의',
|
||||||
|
|||||||
@@ -58,10 +58,19 @@ const battleGroundCity = (): Constraint => ({
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
|
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -74,8 +83,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_수몰';
|
readonly key = 'che_수몰';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context: FloodResolveContext<TriggerState>, _args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: FloodResolveContext<TriggerState>, _args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||||
@@ -175,8 +188,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): FloodArgs | null {
|
parseArgs(raw: unknown): FloodArgs | null {
|
||||||
@@ -200,6 +213,14 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(context: FloodResolveContext<TriggerState>, args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: FloodResolveContext<TriggerState>, args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
@@ -238,5 +259,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destCityId: 0 },
|
availabilityArgs: { destCityId: 0 },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,6 +221,11 @@ export class ActionResolver<
|
|||||||
this.command = new CommandResolver(modules, env);
|
this.command = new CommandResolver(modules, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: VolunteerRecruitResolveContext<TriggerState>): number {
|
||||||
|
const value = context.nation ? readMetaNumber(context.nation.meta, 'gennum') : null;
|
||||||
|
return this.command.getPostDelay(context, value ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: VolunteerRecruitResolveContext<TriggerState>,
|
context: VolunteerRecruitResolveContext<TriggerState>,
|
||||||
_args: VolunteerRecruitArgs
|
_args: VolunteerRecruitArgs
|
||||||
@@ -363,6 +368,14 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return DEFAULT_PRE_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: VolunteerRecruitResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: VolunteerRecruitResolveContext<TriggerState>,
|
context: VolunteerRecruitResolveContext<TriggerState>,
|
||||||
args: VolunteerRecruitArgs
|
args: VolunteerRecruitArgs
|
||||||
|
|||||||
@@ -54,10 +54,19 @@ const resolveNextTerm = (state: number, term: number): number => (state === DIPL
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 16) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
|
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -70,8 +79,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_이호경식';
|
readonly key = 'che_이호경식';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
@@ -174,8 +187,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): DegradeRelationsArgs | null {
|
parseArgs(raw: unknown): DegradeRelationsArgs | null {
|
||||||
@@ -198,6 +211,10 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: DegradeRelationsResolveContext<TriggerState>,
|
context: DegradeRelationsResolveContext<TriggerState>,
|
||||||
args: DegradeRelationsArgs
|
args: DegradeRelationsArgs
|
||||||
@@ -245,5 +262,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destNationId: 0 },
|
availabilityArgs: { destNationId: 0 },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
allowDiplomacyBetweenStatus,
|
allowDiplomacyBetweenStatus,
|
||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { NationTurnCommandSpec } from './index.js';
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -27,12 +27,20 @@ const ARGS_SCHEMA = z.object({
|
|||||||
});
|
});
|
||||||
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
|
interface StopWarProposalContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destNation: Nation;
|
||||||
|
messageValidMinutes: number;
|
||||||
|
messageTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '종전 제의';
|
const ACTION_NAME = '종전 제의';
|
||||||
|
|
||||||
// 종전 제의를 처리하는 국가 커맨드.
|
// 종전 제의를 처리하는 국가 커맨드.
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<TriggerState, StopWarProposalArgs> {
|
> implements GeneralActionDefinition<TriggerState, StopWarProposalArgs, StopWarProposalContext<TriggerState>> {
|
||||||
public readonly key = 'che_종전제의';
|
public readonly key = 'che_종전제의';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
@@ -56,14 +64,41 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
_context: GeneralActionResolveContext<TriggerState>,
|
context: StopWarProposalContext<TriggerState>,
|
||||||
args: StopWarProposalArgs
|
_args: StopWarProposalArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
const destNationName =
|
const { general, nation, destNation } = context;
|
||||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||||
|
}
|
||||||
|
const destNationName = destNation.name;
|
||||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||||
|
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||||
return {
|
return {
|
||||||
effects: [
|
effects: [
|
||||||
|
createMessageEffect({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
src: {
|
||||||
|
generalId: general.id,
|
||||||
|
generalName: general.name,
|
||||||
|
nationId: nation.id,
|
||||||
|
nationName: nation.name,
|
||||||
|
color: nation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
generalId: 0,
|
||||||
|
generalName: '',
|
||||||
|
nationId: destNation.id,
|
||||||
|
nationName: destNation.name,
|
||||||
|
color: destNation.color,
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
text: `${nation.name}의 종전 제의 서신`,
|
||||||
|
time: context.messageTime,
|
||||||
|
validUntil,
|
||||||
|
option: { action: 'stopWar', deletable: false },
|
||||||
|
}),
|
||||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 종전 제의 서신을 보냈습니다.`, {
|
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 종전 제의 서신을 보냈습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
@@ -75,7 +110,18 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
||||||
export const actionContextBuilder = defaultActionContextBuilder;
|
export const actionContextBuilder: ActionContextBuilder<StopWarProposalArgs> = (base, options) => {
|
||||||
|
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||||
|
if (!destNation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destNation,
|
||||||
|
messageTime: base.general.turnTime,
|
||||||
|
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const commandSpec: NationTurnCommandSpec = {
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
key: 'che_종전제의',
|
key: 'che_종전제의',
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, ExpandCityArgs, ExpandCityResolveContext<TriggerState>> {
|
> implements GeneralActionDefinition<TriggerState, ExpandCityArgs, ExpandCityResolveContext<TriggerState>> {
|
||||||
public readonly key = 'che_증축';
|
public readonly key = 'che_증축';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
constructor(private readonly env: TurnCommandEnv) {}
|
constructor(private readonly env: TurnCommandEnv) {}
|
||||||
|
|
||||||
@@ -118,6 +119,15 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStackSequence(context: ExpandCityResolveContext<TriggerState>): number {
|
||||||
|
const value = context.nation?.meta.capset;
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: ExpandCityResolveContext<TriggerState>,
|
context: ExpandCityResolveContext<TriggerState>,
|
||||||
_args: ExpandCityArgs
|
_args: ExpandCityArgs
|
||||||
@@ -153,6 +163,10 @@ export class ActionDefinition<
|
|||||||
{
|
{
|
||||||
gold: nation.gold - cost,
|
gold: nation.gold - cost,
|
||||||
rice: nation.rice - cost,
|
rice: nation.rice - cost,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nation.id
|
nation.id
|
||||||
),
|
),
|
||||||
@@ -184,6 +198,11 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
|
|||||||
@@ -36,35 +36,43 @@ export interface MoveCapitalResolveContext<
|
|||||||
> extends GeneralActionResolveContext<TriggerState> {
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
destCity: City;
|
destCity: City;
|
||||||
map: MapDefinition;
|
map: MapDefinition;
|
||||||
|
nationCities: City[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '천도';
|
const ACTION_NAME = '천도';
|
||||||
|
|
||||||
const hasRouteToDestCity = (destCityID: number, develCost: number): Constraint => ({
|
const hasRouteToDestCity = (destCityID: number): Constraint => ({
|
||||||
name: 'hasRouteToDestCity',
|
name: 'hasRouteToDestCity',
|
||||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }, { kind: 'env', key: 'map' }],
|
requires: (ctx) => [
|
||||||
|
{ kind: 'nation', id: ctx.nationId! },
|
||||||
|
{ kind: 'env', key: 'map' },
|
||||||
|
{ kind: 'env', key: 'cities' },
|
||||||
|
],
|
||||||
test: (ctx: ConstraintContext, view: StateView) => {
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
||||||
|
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | undefined;
|
||||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||||
return { kind: 'allow' };
|
return { kind: 'allow' };
|
||||||
}
|
}
|
||||||
const dist = calcDistance(nation.capitalCityId, destCityID, map);
|
const allowedCityIds = new Set(
|
||||||
if (dist >= 50) {
|
(cities ?? []).filter((city) => city.nationId === nation.id).map((city) => city.id)
|
||||||
|
);
|
||||||
|
const dist = calcDistance(nation.capitalCityId, destCityID, map, allowedCityIds);
|
||||||
|
if (dist === null) {
|
||||||
return { kind: 'deny', reason: '천도 대상으로 도달할 방법이 없습니다.' };
|
return { kind: 'deny', reason: '천도 대상으로 도달할 방법이 없습니다.' };
|
||||||
}
|
}
|
||||||
const cost = develCost * 5 * Math.pow(2, dist);
|
|
||||||
if (nation.gold < cost + 1000) {
|
|
||||||
return { kind: 'allow' };
|
|
||||||
}
|
|
||||||
if (nation.rice < cost + 1000) {
|
|
||||||
return { kind: 'allow' };
|
|
||||||
}
|
|
||||||
return { kind: 'allow' };
|
return { kind: 'allow' };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition): number => {
|
const calcDistance = (
|
||||||
|
fromCityId: number,
|
||||||
|
toCityId: number,
|
||||||
|
map: MapDefinition,
|
||||||
|
allowedCityIds?: ReadonlySet<number>
|
||||||
|
): number | null => {
|
||||||
|
if (allowedCityIds && !allowedCityIds.has(toCityId)) return null;
|
||||||
if (fromCityId === toCityId) return 0;
|
if (fromCityId === toCityId) return 0;
|
||||||
|
|
||||||
const connections = new Map<number, number[]>();
|
const connections = new Map<number, number[]>();
|
||||||
@@ -82,6 +90,9 @@ const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition):
|
|||||||
|
|
||||||
const nextNodes = connections.get(current) ?? [];
|
const nextNodes = connections.get(current) ?? [];
|
||||||
for (const next of nextNodes) {
|
for (const next of nextNodes) {
|
||||||
|
if (allowedCityIds && !allowedCityIds.has(next)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!visited.has(next)) {
|
if (!visited.has(next)) {
|
||||||
visited.add(next);
|
visited.add(next);
|
||||||
queue.push([next, dist + 1]);
|
queue.push([next, dist + 1]);
|
||||||
@@ -89,7 +100,7 @@ const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 50;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
@@ -97,6 +108,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, MoveCapitalArgs, MoveCapitalResolveContext<TriggerState>> {
|
> implements GeneralActionDefinition<TriggerState, MoveCapitalArgs, MoveCapitalResolveContext<TriggerState>> {
|
||||||
public readonly key = 'che_천도';
|
public readonly key = 'che_천도';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
constructor(private readonly env: TurnCommandEnv) {}
|
constructor(private readonly env: TurnCommandEnv) {}
|
||||||
|
|
||||||
@@ -119,8 +131,12 @@ export class ActionDefinition<
|
|||||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | undefined;
|
||||||
if (dist >= 50) {
|
const allowedCityIds = new Set(
|
||||||
|
(cities ?? []).filter((city) => city.nationId === nation.id).map((city) => city.id)
|
||||||
|
);
|
||||||
|
const dist = calcDistance(nation.capitalCityId, args.destCityID, map, allowedCityIds);
|
||||||
|
if (dist === null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return develcost * 5 * Math.pow(2, dist);
|
return develcost * 5 * Math.pow(2, dist);
|
||||||
@@ -132,13 +148,38 @@ export class ActionDefinition<
|
|||||||
beChief(),
|
beChief(),
|
||||||
suppliedCity(),
|
suppliedCity(),
|
||||||
suppliedDestCity(),
|
suppliedDestCity(),
|
||||||
hasRouteToDestCity(args.destCityID, develcost),
|
hasRouteToDestCity(args.destCityID),
|
||||||
reqNationValue('capitalCityId', '수도', '!=', args.destCityID, '이미 수도입니다.'),
|
reqNationValue('capitalCityId', '수도', '!=', args.destCityID, '이미 수도입니다.'),
|
||||||
reqNationGold((ctx, view) => baseGold + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
reqNationGold(
|
||||||
reqNationRice((ctx, view) => baseRice + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
(ctx, view) => baseGold + getRequiredCost(ctx, view),
|
||||||
|
[
|
||||||
|
{ kind: 'env', key: 'map' },
|
||||||
|
{ kind: 'env', key: 'cities' },
|
||||||
|
]
|
||||||
|
),
|
||||||
|
reqNationRice(
|
||||||
|
(ctx, view) => baseRice + getRequiredCost(ctx, view),
|
||||||
|
[
|
||||||
|
{ kind: 'env', key: 'map' },
|
||||||
|
{ kind: 'env', key: 'cities' },
|
||||||
|
]
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(context: MoveCapitalResolveContext<TriggerState>, args: MoveCapitalArgs): number {
|
||||||
|
if (!context.nation?.capitalCityId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const allowedCityIds = new Set(context.nationCities.map((city) => city.id));
|
||||||
|
return (calcDistance(context.nation.capitalCityId, args.destCityID, context.map, allowedCityIds) ?? 0) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStackSequence(context: MoveCapitalResolveContext<TriggerState>): number {
|
||||||
|
const value = context.nation?.meta.capset;
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: MoveCapitalResolveContext<TriggerState>,
|
context: MoveCapitalResolveContext<TriggerState>,
|
||||||
args: MoveCapitalArgs
|
args: MoveCapitalArgs
|
||||||
@@ -148,9 +189,12 @@ export class ActionDefinition<
|
|||||||
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
const nationCities = context.nationCities ?? [];
|
||||||
const cost = this.env.develCost * 5 * Math.pow(2, dist);
|
const allowedCityIds = nationCities.length > 0 ? new Set(nationCities.map((city) => city.id)) : undefined;
|
||||||
|
const dist = calcDistance(nation.capitalCityId, args.destCityID, map, allowedCityIds);
|
||||||
|
if (dist === null) {
|
||||||
|
return { effects: [createLogEffect('천도 대상으로 도달할 방법이 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
const generalName = general.name;
|
const generalName = general.name;
|
||||||
const nationName = nation.name;
|
const nationName = nation.name;
|
||||||
const destCityName = destCity.name;
|
const destCityName = destCity.name;
|
||||||
@@ -163,8 +207,11 @@ export class ActionDefinition<
|
|||||||
createNationPatchEffect(
|
createNationPatchEffect(
|
||||||
{
|
{
|
||||||
capitalCityId: args.destCityID,
|
capitalCityId: args.destCityID,
|
||||||
gold: nation.gold - cost,
|
// ref는 비용 보유를 제약에서 검사하지만 실행 시 차감하지 않는다.
|
||||||
rice: nation.rice - cost,
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nation.id
|
nation.id
|
||||||
),
|
),
|
||||||
@@ -196,6 +243,11 @@ export class ActionDefinition<
|
|||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>명령`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
// General Action Log
|
// General Action Log
|
||||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} ${ACTION_NAME}했습니다.`, {
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} ${ACTION_NAME}했습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
@@ -226,6 +278,7 @@ export const actionContextBuilder: ActionContextBuilder<MoveCapitalArgs> = (base
|
|||||||
...base,
|
...base,
|
||||||
destCity,
|
destCity,
|
||||||
map,
|
map,
|
||||||
|
nationCities: worldRef.listCities().filter((city) => city.nationId === base.general.nationId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, ScorchedEarthArgs, ScorchedEarthResolveContext<TriggerState>> {
|
> implements GeneralActionDefinition<TriggerState, ScorchedEarthArgs, ScorchedEarthResolveContext<TriggerState>> {
|
||||||
public readonly key = 'che_초토화';
|
public readonly key = 'che_초토화';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
parseArgs(raw: unknown): ScorchedEarthArgs | null {
|
parseArgs(raw: unknown): ScorchedEarthArgs | null {
|
||||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||||
@@ -102,6 +103,10 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: ScorchedEarthResolveContext<TriggerState>,
|
context: ScorchedEarthResolveContext<TriggerState>,
|
||||||
_args: ScorchedEarthArgs
|
_args: ScorchedEarthArgs
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
occupiedCity,
|
occupiedCity,
|
||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||||
|
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type {
|
import type {
|
||||||
GeneralActionEffect,
|
GeneralActionEffect,
|
||||||
@@ -15,7 +16,7 @@ import type {
|
|||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
GeneralActionResolver,
|
GeneralActionResolver,
|
||||||
} from '@sammo-ts/logic/actions/engine.js';
|
} from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
@@ -32,10 +33,12 @@ export interface CounterStrategyResolveContext<
|
|||||||
destNation: Nation;
|
destNation: Nation;
|
||||||
friendlyGenerals: Array<General<TriggerState>>;
|
friendlyGenerals: Array<General<TriggerState>>;
|
||||||
destNationGenerals: Array<General<TriggerState>>;
|
destNationGenerals: Array<General<TriggerState>>;
|
||||||
|
currentYearMonth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '피장파장';
|
const ACTION_NAME = '피장파장';
|
||||||
const DEFAULT_GLOBAL_DELAY = 8;
|
const DEFAULT_GLOBAL_DELAY = 8;
|
||||||
|
const TARGET_DELAY = 60;
|
||||||
const PRE_REQ_TURN = 1;
|
const PRE_REQ_TURN = 1;
|
||||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||||
|
|
||||||
@@ -89,11 +92,53 @@ const reqValidStrategicCommandType = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const alwaysFail = (commandType: CounterStrategyArgs['commandType']): Constraint => ({
|
||||||
|
// Legacy inserts AlwaysFail when the selected strategy is still cooling down.
|
||||||
|
name: 'alwaysFail',
|
||||||
|
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||||
|
test: (ctx, view) => {
|
||||||
|
if (ctx.nationId === undefined) {
|
||||||
|
return { kind: 'deny', reason: '국가 정보가 없습니다.' };
|
||||||
|
}
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined;
|
||||||
|
if (!nation) {
|
||||||
|
return { kind: 'deny', reason: '국가 정보가 없습니다.' };
|
||||||
|
}
|
||||||
|
const raw = nation.meta[`next_execute_${STRATEGIC_COMMANDS[commandType]}`];
|
||||||
|
const nextAvailable = typeof raw === 'number' ? raw : Number(raw ?? 0);
|
||||||
|
const year = Number(ctx.env.currentYear ?? ctx.env.year);
|
||||||
|
const month = Number(ctx.env.currentMonth ?? ctx.env.month);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month)) {
|
||||||
|
return { kind: 'unknown', missing: [{ kind: 'env', key: 'currentYear' }] };
|
||||||
|
}
|
||||||
|
const currentYearMonth = Math.floor(year) * 12 + Math.floor(month) - 1;
|
||||||
|
if (Number.isFinite(nextAvailable) && nextAvailable > currentYearMonth) {
|
||||||
|
return { kind: 'deny', reason: '해당 전략을 아직 사용할 수 없습니다' };
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 피장파장 실행 결과를 계산한다.
|
// 피장파장 실행 결과를 계산한다.
|
||||||
export class ActionResolver<
|
export class ActionResolver<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionResolver<TriggerState, CounterStrategyArgs> {
|
> implements GeneralActionResolver<TriggerState, CounterStrategyArgs> {
|
||||||
readonly key = 'che_피장파장';
|
readonly key = 'che_피장파장';
|
||||||
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined> = [],
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTargetPostReqTurn(context: CounterStrategyResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 2) * 10);
|
||||||
|
const triggered = Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
return Math.max(triggered, Math.round(TARGET_DELAY * 1.2));
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: CounterStrategyResolveContext<TriggerState>,
|
context: CounterStrategyResolveContext<TriggerState>,
|
||||||
@@ -105,6 +150,7 @@ export class ActionResolver<
|
|||||||
const nationName = nation?.name ?? '아국';
|
const nationName = nation?.name ?? '아국';
|
||||||
const destNationName = destNation.name;
|
const destNationName = destNation.name;
|
||||||
const targetCommandName = STRATEGIC_COMMANDS[args.commandType] ?? args.commandType;
|
const targetCommandName = STRATEGIC_COMMANDS[args.commandType] ?? args.commandType;
|
||||||
|
const currentYearMonth = Number.isFinite(context.currentYearMonth) ? context.currentYearMonth : 0;
|
||||||
const actionName = ACTION_NAME;
|
const actionName = ACTION_NAME;
|
||||||
const actionJosa = JosaUtil.pick(actionName, '을');
|
const actionJosa = JosaUtil.pick(actionName, '을');
|
||||||
|
|
||||||
@@ -117,8 +163,8 @@ export class ActionResolver<
|
|||||||
context.addLog(
|
context.addLog(
|
||||||
`<D><b>${destNationName}</b></>에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
`<D><b>${destNationName}</b></>에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||||
{
|
{
|
||||||
category: LogCategory.HISTORY,
|
category: LogCategory.HISTORY,
|
||||||
format: LogFormat.YEAR_MONTH,
|
format: LogFormat.YEAR_MONTH,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -159,6 +205,7 @@ export class ActionResolver<
|
|||||||
nation.meta = {
|
nation.meta = {
|
||||||
...(nation.meta as object),
|
...(nation.meta as object),
|
||||||
strategic_cmd_limit: globalDelay,
|
strategic_cmd_limit: globalDelay,
|
||||||
|
[`next_execute_${targetCommandName}`]: currentYearMonth + this.getTargetPostReqTurn(context),
|
||||||
};
|
};
|
||||||
effects.push(
|
effects.push(
|
||||||
createLogEffect(broadcastMessage, {
|
createLogEffect(broadcastMessage, {
|
||||||
@@ -170,6 +217,22 @@ export class ActionResolver<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const destMeta = destNation.meta;
|
||||||
|
const destKey = `next_execute_${targetCommandName}`;
|
||||||
|
const destRaw = destMeta[destKey];
|
||||||
|
const destNext = typeof destRaw === 'number' ? destRaw : Number(destRaw ?? 0);
|
||||||
|
effects.push(
|
||||||
|
createNationPatchEffect(
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
...destMeta,
|
||||||
|
[destKey]: Math.max(Number.isFinite(destNext) ? destNext : 0, currentYearMonth) + TARGET_DELAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destNation.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
effects.push(
|
effects.push(
|
||||||
createLogEffect(
|
createLogEffect(
|
||||||
`<D><b>${nationName}</b></>의 <Y>${generalName}</>${generalJosa} 아국에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
`<D><b>${nationName}</b></>의 <Y>${generalName}</>${generalJosa} 아국에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||||
@@ -206,7 +269,11 @@ export class ActionDefinition<
|
|||||||
> implements GeneralActionDefinition<TriggerState, CounterStrategyArgs, CounterStrategyResolveContext<TriggerState>> {
|
> implements GeneralActionDefinition<TriggerState, CounterStrategyArgs, CounterStrategyResolveContext<TriggerState>> {
|
||||||
public readonly key = 'che_피장파장';
|
public readonly key = 'che_피장파장';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver = new ActionResolver<TriggerState>();
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined> = [], initialNationGenLimit = 10) {
|
||||||
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): CounterStrategyArgs | null {
|
parseArgs(raw: unknown): CounterStrategyArgs | null {
|
||||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||||
@@ -226,10 +293,22 @@ export class ActionDefinition<
|
|||||||
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
||||||
availableStrategicCommand(),
|
availableStrategicCommand(),
|
||||||
reqValidStrategicCommandType(),
|
reqValidStrategicCommandType(),
|
||||||
|
alwaysFail(_args.commandType),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context: CounterStrategyResolveContext<TriggerState>, args: CounterStrategyArgs): GeneralActionOutcome<TriggerState> {
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(): number {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: CounterStrategyResolveContext<TriggerState>,
|
||||||
|
args: CounterStrategyArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,6 +335,7 @@ export const actionContextBuilder: ActionContextBuilder<CounterStrategyArgs> = (
|
|||||||
destNation,
|
destNation,
|
||||||
friendlyGenerals,
|
friendlyGenerals,
|
||||||
destNationGenerals,
|
destNationGenerals,
|
||||||
|
currentYearMonth: options.world.currentYear * 12 + options.world.currentMonth - 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,5 +345,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destNationId: 0, commandType: '' },
|
availabilityArgs: { destNationId: 0, commandType: '' },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,10 +40,19 @@ const ATMOS_CAP = 100;
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.nationGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 8) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
|
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -56,8 +65,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_필사즉생';
|
readonly key = 'che_필사즉생';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
@@ -142,8 +155,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(_raw: unknown): DesperateFightArgs | null {
|
parseArgs(_raw: unknown): DesperateFightArgs | null {
|
||||||
@@ -162,6 +175,14 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: DesperateFightResolveContext<TriggerState>,
|
context: DesperateFightResolveContext<TriggerState>,
|
||||||
args: DesperateFightArgs
|
args: DesperateFightArgs
|
||||||
@@ -188,5 +209,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
category: '전략',
|
category: '전략',
|
||||||
reqArg: false,
|
reqArg: false,
|
||||||
|
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,10 +65,19 @@ const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: num
|
|||||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(
|
||||||
|
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||||
|
private readonly initialNationGenLimit = 10
|
||||||
|
) {
|
||||||
this.pipeline = new GeneralActionPipeline(modules);
|
this.pipeline = new GeneralActionPipeline(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||||
|
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||||
|
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||||
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
|
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
|
||||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||||
}
|
}
|
||||||
@@ -81,8 +90,12 @@ export class ActionResolver<
|
|||||||
readonly key = 'che_허보';
|
readonly key = 'che_허보';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.command = new CommandResolver(modules);
|
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||||
|
return this.command.getPostReqTurn(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context: DeceptionResolveContext<TriggerState>, _args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: DeceptionResolveContext<TriggerState>, _args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||||
@@ -176,8 +189,8 @@ export class ActionDefinition<
|
|||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
private readonly resolver: ActionResolver<TriggerState>;
|
private readonly resolver: ActionResolver<TriggerState>;
|
||||||
|
|
||||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||||
this.resolver = new ActionResolver(modules);
|
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseArgs(raw: unknown): DeceptionArgs | null {
|
parseArgs(raw: unknown): DeceptionArgs | null {
|
||||||
@@ -201,6 +214,14 @@ export class ActionDefinition<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||||
|
return this.resolver.getPostReqTurn(context);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(context: DeceptionResolveContext<TriggerState>, args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
resolve(context: DeceptionResolveContext<TriggerState>, args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
@@ -245,5 +266,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
|||||||
reqArg: true,
|
reqArg: true,
|
||||||
availabilityArgs: { destCityId: 0 },
|
availabilityArgs: { destCityId: 0 },
|
||||||
argsSchema: ARGS_SCHEMA,
|
argsSchema: ARGS_SCHEMA,
|
||||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/c
|
|||||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||||
import { beChief, occupiedCity, reqNationGold, reqNationRice } from '@sammo-ts/logic/constraints/presets.js';
|
import { beChief, occupiedCity, reqNationGold, reqNationRice } from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type {
|
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
GeneralActionOutcome,
|
|
||||||
GeneralActionResolveContext,
|
|
||||||
} from '@sammo-ts/logic/actions/engine.js';
|
|
||||||
import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
@@ -43,7 +40,9 @@ const reqNationAuxValue = (auxKey: string, actionName: string): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createEventResearchCommand = (config: EventResearchConfig): {
|
export const createEventResearchCommand = (
|
||||||
|
config: EventResearchConfig
|
||||||
|
): {
|
||||||
ActionDefinition: new <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
ActionDefinition: new <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||||
env: TurnCommandEnv
|
env: TurnCommandEnv
|
||||||
) => GeneralActionDefinition<TriggerState, Record<string, never>>;
|
) => GeneralActionDefinition<TriggerState, Record<string, never>>;
|
||||||
@@ -61,6 +60,7 @@ export const createEventResearchCommand = (config: EventResearchConfig): {
|
|||||||
> implements GeneralActionDefinition<TriggerState, Record<string, never>> {
|
> implements GeneralActionDefinition<TriggerState, Record<string, never>> {
|
||||||
public readonly key = config.key;
|
public readonly key = config.key;
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
public readonly countsAsInheritanceActiveAction = true;
|
||||||
|
|
||||||
constructor(private readonly env: TurnCommandEnv) {}
|
constructor(private readonly env: TurnCommandEnv) {}
|
||||||
|
|
||||||
@@ -93,6 +93,10 @@ export const createEventResearchCommand = (config: EventResearchConfig): {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreReqTurn(): number {
|
||||||
|
return PRE_REQ_TURN;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: GeneralActionResolveContext<TriggerState>,
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
_args: Record<string, never>
|
_args: Record<string, never>
|
||||||
|
|||||||
@@ -21,20 +21,15 @@ export type TriggerDomesticActionType =
|
|||||||
|
|
||||||
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
||||||
|
|
||||||
export type TriggerStrategicActionType = '의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습';
|
export type TriggerStrategicActionType =
|
||||||
|
'의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습' | '피장파장';
|
||||||
|
|
||||||
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
||||||
|
|
||||||
export type TriggerNationalIncomeType = 'gold' | 'rice' | 'pop';
|
export type TriggerNationalIncomeType = 'gold' | 'rice' | 'pop';
|
||||||
|
|
||||||
export type GeneralStatName =
|
export type GeneralStatName =
|
||||||
| 'leadership'
|
'leadership' | 'strength' | 'intelligence' | 'experience' | 'dedication' | 'sabotageDefence' | 'sabotageAttack';
|
||||||
| 'strength'
|
|
||||||
| 'intelligence'
|
|
||||||
| 'experience'
|
|
||||||
| 'dedication'
|
|
||||||
| 'sabotageDefence'
|
|
||||||
| 'sabotageAttack';
|
|
||||||
|
|
||||||
export type WarStatName =
|
export type WarStatName =
|
||||||
| GeneralStatName
|
| GeneralStatName
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
|
|||||||
import { ActionDefinition as StopWarProposalAction } from '../../../src/actions/turn/nation/che_종전제의.js';
|
import { ActionDefinition as StopWarProposalAction } from '../../../src/actions/turn/nation/che_종전제의.js';
|
||||||
import { ActionDefinition as ScorchedEarthAction } from '../../../src/actions/turn/nation/che_초토화.js';
|
import { ActionDefinition as ScorchedEarthAction } from '../../../src/actions/turn/nation/che_초토화.js';
|
||||||
import { ActionDefinition as CounterStrategyAction } from '../../../src/actions/turn/nation/che_피장파장.js';
|
import { ActionDefinition as CounterStrategyAction } from '../../../src/actions/turn/nation/che_피장파장.js';
|
||||||
|
import { ActionDefinition as MaterialAidAction } from '../../../src/actions/turn/nation/che_물자원조.js';
|
||||||
import { ActionDefinition as PopulationMoveAction } from '../../../src/actions/turn/nation/cr_인구이동.js';
|
import { ActionDefinition as PopulationMoveAction } from '../../../src/actions/turn/nation/cr_인구이동.js';
|
||||||
import { ActionDefinition as EventWonyungAction } from '../../../src/actions/turn/nation/event_원융노병연구.js';
|
import { ActionDefinition as EventWonyungAction } from '../../../src/actions/turn/nation/event_원융노병연구.js';
|
||||||
import { ActionDefinition as EventHwasibyeongAction } from '../../../src/actions/turn/nation/event_화시병연구.js';
|
import { ActionDefinition as EventHwasibyeongAction } from '../../../src/actions/turn/nation/event_화시병연구.js';
|
||||||
@@ -205,13 +206,7 @@ const buildEnv = (): TurnCommandEnv => ({
|
|||||||
maxResourceActionAmount: 100000,
|
maxResourceActionAmount: 100000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupDiplomacy = (
|
const setupDiplomacy = (view: TestStateView, srcNationId: number, destNationId: number, state: number, term = 0) => {
|
||||||
view: TestStateView,
|
|
||||||
srcNationId: number,
|
|
||||||
destNationId: number,
|
|
||||||
state: number,
|
|
||||||
term = 0
|
|
||||||
) => {
|
|
||||||
view.set(
|
view.set(
|
||||||
{
|
{
|
||||||
kind: 'diplomacy',
|
kind: 'diplomacy',
|
||||||
@@ -267,6 +262,9 @@ describe('Nation Missing Actions', () => {
|
|||||||
general,
|
general,
|
||||||
city,
|
city,
|
||||||
nation,
|
nation,
|
||||||
|
destNation,
|
||||||
|
messageTime: new Date('2026-01-01T00:00:00Z'),
|
||||||
|
messageValidMinutes: 30,
|
||||||
rng: {} as any,
|
rng: {} as any,
|
||||||
addLog: () => {},
|
addLog: () => {},
|
||||||
} as any,
|
} as any,
|
||||||
@@ -275,6 +273,15 @@ describe('Nation Missing Actions', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(resolution.logs.some((log) => log.text.includes('종전 제의'))).toBe(true);
|
expect(resolution.logs.some((log) => log.text.includes('종전 제의'))).toBe(true);
|
||||||
|
expect(resolution.effects).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'message:add',
|
||||||
|
draft: expect.objectContaining({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
option: { action: 'stopWar', deletable: false },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('che_피장파장: blocks when not at war/declare', () => {
|
it('che_피장파장: blocks when not at war/declare', () => {
|
||||||
@@ -323,6 +330,7 @@ describe('Nation Missing Actions', () => {
|
|||||||
destNation,
|
destNation,
|
||||||
friendlyGenerals: [general, otherGeneral],
|
friendlyGenerals: [general, otherGeneral],
|
||||||
destNationGenerals: [enemyGeneral],
|
destNationGenerals: [enemyGeneral],
|
||||||
|
currentYearMonth: 155,
|
||||||
rng: {} as any,
|
rng: {} as any,
|
||||||
addLog: () => {},
|
addLog: () => {},
|
||||||
} as any,
|
} as any,
|
||||||
@@ -332,6 +340,54 @@ describe('Nation Missing Actions', () => {
|
|||||||
|
|
||||||
expect(resolution.general.experience).toBeGreaterThan(100);
|
expect(resolution.general.experience).toBeGreaterThan(100);
|
||||||
expect(resolution.nation?.meta?.strategic_cmd_limit).toBe(8);
|
expect(resolution.nation?.meta?.strategic_cmd_limit).toBe(8);
|
||||||
|
expect(resolution.nation?.meta?.next_execute_허보).toBe(227);
|
||||||
|
expect(resolution.patches?.nations).toContainEqual({
|
||||||
|
id: destNation.id,
|
||||||
|
patch: expect.objectContaining({
|
||||||
|
meta: expect.objectContaining({ next_execute_허보: 215 }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('che_물자원조: validates amounts and records the legacy receive-assist accumulator', () => {
|
||||||
|
const definition = new MaterialAidAction(buildEnv());
|
||||||
|
expect(definition.parseArgs({ destNationId: 2, amountList: [0, 0] })).toBeNull();
|
||||||
|
expect(definition.parseArgs({ destNationId: 2, amountList: [-1, 10] })).toBeNull();
|
||||||
|
|
||||||
|
const general = buildGeneral(1, 1, 1);
|
||||||
|
const nation = { ...buildNation(1), gold: 1000, rice: 1000 };
|
||||||
|
const destNation = { ...buildNation(2), gold: 100, rice: 100 };
|
||||||
|
const resolution = resolveGeneralAction(
|
||||||
|
definition,
|
||||||
|
{
|
||||||
|
general,
|
||||||
|
city: buildCity(1, 1),
|
||||||
|
nation,
|
||||||
|
destNation,
|
||||||
|
friendlyChiefs: [general],
|
||||||
|
destNationChiefs: [],
|
||||||
|
rng: {} as any,
|
||||||
|
addLog: () => {},
|
||||||
|
} as any,
|
||||||
|
{ now: new Date(), schedule },
|
||||||
|
{ destNationId: 2, amountList: [100, 50] }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolution.nation).toMatchObject({
|
||||||
|
gold: 900,
|
||||||
|
rice: 950,
|
||||||
|
meta: { surlimit: 12 },
|
||||||
|
});
|
||||||
|
expect(resolution.patches?.nations).toContainEqual({
|
||||||
|
id: 2,
|
||||||
|
patch: expect.objectContaining({
|
||||||
|
gold: 200,
|
||||||
|
rice: 150,
|
||||||
|
meta: expect.objectContaining({
|
||||||
|
recv_assist: { n1: { 0: 1, 1: 150 } },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('che_초토화: blocks when diplomacy limit exists', () => {
|
it('che_초토화: blocks when diplomacy limit exists', () => {
|
||||||
|
|||||||
Generated
+43
-27
@@ -40,7 +40,7 @@ importers:
|
|||||||
version: 3.9.6
|
version: 3.9.6
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.22.14
|
specifier: ^0.22.14
|
||||||
version: 0.22.14(@volar/typescript@2.4.27)(tsx@4.21.0)(typescript@7.0.2)(unrun@0.2.22(synckit@0.11.13))(vue-tsc@3.2.2(typescript@7.0.2))
|
version: 0.22.14(@volar/typescript@2.4.27)(tsx@4.21.0)(typescript@7.0.2)(unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13))(vue-tsc@3.2.2(typescript@7.0.2))
|
||||||
turbo:
|
turbo:
|
||||||
specifier: ^2.10.6
|
specifier: ^2.10.6
|
||||||
version: 2.10.6
|
version: 2.10.6
|
||||||
@@ -50,6 +50,9 @@ importers:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.65.0
|
specifier: ^8.65.0
|
||||||
version: 8.65.0(eslint@10.8.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2)
|
version: 8.65.0(eslint@10.8.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2)
|
||||||
|
typescript-legacy:
|
||||||
|
specifier: npm:typescript@5.9.3
|
||||||
|
version: typescript@5.9.3
|
||||||
vue-eslint-parser:
|
vue-eslint-parser:
|
||||||
specifier: ^10.4.1
|
specifier: ^10.4.1
|
||||||
version: 10.4.1(eslint@10.8.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
version: 10.4.1(eslint@10.8.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||||
@@ -95,7 +98,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2))
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
||||||
@@ -126,7 +129,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
||||||
@@ -266,7 +269,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2))
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(supports-color@7.2.0)(typescript@7.0.2)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
version: 6.0.3(supports-color@7.2.0)(typescript@7.0.2)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
||||||
@@ -355,7 +358,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.16
|
specifier: ^4.0.16
|
||||||
version: 4.0.16(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
|
version: 4.0.16(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
|
||||||
@@ -392,7 +395,7 @@ importers:
|
|||||||
version: 7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
|
version: 7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
||||||
|
|
||||||
packages/logic:
|
packages/logic:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -411,7 +414,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.18.4
|
specifier: ^0.18.4
|
||||||
version: 0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
version: 0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3))
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
version: 6.0.3(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
||||||
@@ -5230,9 +5233,12 @@ snapshots:
|
|||||||
'@napi-rs/wasm-runtime': 1.1.1
|
'@napi-rs/wasm-runtime': 1.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.58':
|
'@rolldown/binding-wasm32-wasi@1.0.0-beta.58(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/wasm-runtime': 1.1.1
|
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.2.0':
|
'@rolldown/binding-wasm32-wasi@1.2.0':
|
||||||
@@ -6036,7 +6042,7 @@ snapshots:
|
|||||||
alien-signals: 3.1.2
|
alien-signals: 3.1.2
|
||||||
muggle-string: 0.4.1
|
muggle-string: 0.4.1
|
||||||
path-browserify: 1.0.1
|
path-browserify: 1.0.1
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.5
|
||||||
|
|
||||||
'@vue/reactivity@3.5.26':
|
'@vue/reactivity@3.5.26':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6290,7 +6296,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
confbox: 0.2.2
|
confbox: 0.2.2
|
||||||
defu: 6.1.4
|
defu: 6.1.7
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
exsolve: 1.0.8
|
exsolve: 1.0.8
|
||||||
giget: 2.0.0
|
giget: 2.0.0
|
||||||
@@ -6778,7 +6784,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
defu: 6.1.4
|
defu: 6.1.7
|
||||||
node-fetch-native: 1.6.7
|
node-fetch-native: 1.6.7
|
||||||
nypm: 0.6.2
|
nypm: 0.6.2
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -7111,7 +7117,7 @@ snapshots:
|
|||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
tinyexec: 1.0.2
|
tinyexec: 1.2.4
|
||||||
|
|
||||||
obug@2.1.1: {}
|
obug@2.1.1: {}
|
||||||
|
|
||||||
@@ -7547,7 +7553,7 @@ snapshots:
|
|||||||
|
|
||||||
rc9@2.1.2:
|
rc9@2.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
defu: 6.1.4
|
defu: 6.1.7
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
|
|
||||||
react-dom@19.2.3(react@19.2.3):
|
react-dom@19.2.3(react@19.2.3):
|
||||||
@@ -7678,7 +7684,7 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57
|
||||||
|
|
||||||
rolldown@1.0.0-beta.58:
|
rolldown@1.0.0-beta.58(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.106.0
|
'@oxc-project/types': 0.106.0
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.58
|
'@rolldown/pluginutils': 1.0.0-beta.58
|
||||||
@@ -7693,9 +7699,12 @@ snapshots:
|
|||||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.58
|
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.58
|
||||||
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.58
|
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.58
|
||||||
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.58
|
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.58
|
||||||
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.58
|
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.58(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
|
||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
|
||||||
rolldown@1.2.0:
|
rolldown@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7965,7 +7974,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 7.0.2
|
typescript: 7.0.2
|
||||||
|
|
||||||
tsdown@0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3)):
|
tsdown@0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.2.0
|
ansis: 4.2.0
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@@ -7982,17 +7991,19 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
unconfig-core: 7.4.2
|
unconfig-core: 7.4.2
|
||||||
unrun: 0.2.22(synckit@0.11.13)
|
unrun: 0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
- '@ts-macro/tsc'
|
- '@ts-macro/tsc'
|
||||||
- '@typescript/native-preview'
|
- '@typescript/native-preview'
|
||||||
- oxc-resolver
|
- oxc-resolver
|
||||||
- synckit
|
- synckit
|
||||||
- vue-tsc
|
- vue-tsc
|
||||||
|
|
||||||
tsdown@0.18.4(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2)):
|
tsdown@0.18.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)(typescript@5.9.3)(vue-tsc@3.2.2(typescript@7.0.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.2.0
|
ansis: 4.2.0
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@@ -8009,17 +8020,19 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
unconfig-core: 7.4.2
|
unconfig-core: 7.4.2
|
||||||
unrun: 0.2.22(synckit@0.11.13)
|
unrun: 0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
- '@ts-macro/tsc'
|
- '@ts-macro/tsc'
|
||||||
- '@typescript/native-preview'
|
- '@typescript/native-preview'
|
||||||
- oxc-resolver
|
- oxc-resolver
|
||||||
- synckit
|
- synckit
|
||||||
- vue-tsc
|
- vue-tsc
|
||||||
|
|
||||||
tsdown@0.22.14(@volar/typescript@2.4.27)(tsx@4.21.0)(typescript@7.0.2)(unrun@0.2.22(synckit@0.11.13))(vue-tsc@3.2.2(typescript@7.0.2)):
|
tsdown@0.22.14(@volar/typescript@2.4.27)(tsx@4.21.0)(typescript@7.0.2)(unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13))(vue-tsc@3.2.2(typescript@7.0.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.3.1
|
ansis: 4.3.1
|
||||||
cac: 7.0.0
|
cac: 7.0.0
|
||||||
@@ -8039,7 +8052,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
typescript: 7.0.2
|
typescript: 7.0.2
|
||||||
unrun: 0.2.22(synckit@0.11.13)
|
unrun: 0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@typescript/native-preview'
|
- '@typescript/native-preview'
|
||||||
- '@volar/typescript'
|
- '@volar/typescript'
|
||||||
@@ -8131,11 +8144,14 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@8.3.0: {}
|
undici-types@8.3.0: {}
|
||||||
|
|
||||||
unrun@0.2.22(synckit@0.11.13):
|
unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(synckit@0.11.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
rolldown: 1.0.0-beta.58
|
rolldown: 1.0.0-beta.58(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
synckit: 0.11.13
|
synckit: 0.11.13
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
|
||||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8200,11 +8216,11 @@ snapshots:
|
|||||||
vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0):
|
vite@7.3.1(@types/node@26.1.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.2
|
esbuild: 0.27.2
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.5)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.5
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
rollup: 4.55.1
|
rollup: 4.55.1
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.1
|
'@types/node': 26.1.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import ts from 'typescript';
|
import ts from 'typescript-legacy';
|
||||||
|
|
||||||
const ROOT_DIR = process.cwd();
|
const ROOT_DIR = process.cwd();
|
||||||
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
|
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
|
||||||
@@ -25,6 +25,7 @@ Options:
|
|||||||
--json Print JSON report.
|
--json Print JSON report.
|
||||||
--show-matches Print matched command keys.
|
--show-matches Print matched command keys.
|
||||||
--show-compat Print compatibility-matched pairs.
|
--show-compat Print compatibility-matched pairs.
|
||||||
|
--check Exit non-zero when a command is missing or mismatched.
|
||||||
--no-compat Disable compatibility alias rules (default: enabled).
|
--no-compat Disable compatibility alias rules (default: enabled).
|
||||||
--compat-file <path> Compatibility rules JSON file (default: tools/compare-command-constraints.compat.json).
|
--compat-file <path> Compatibility rules JSON file (default: tools/compare-command-constraints.compat.json).
|
||||||
--similarity <0..1> Near-match threshold for non-strict mode (default: 0.6).
|
--similarity <0..1> Near-match threshold for non-strict mode (default: 0.6).
|
||||||
@@ -32,6 +33,7 @@ Options:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
const check = args.includes('--check');
|
||||||
if (args.includes('--help')) {
|
if (args.includes('--help')) {
|
||||||
console.log(HELP_TEXT.trim());
|
console.log(HELP_TEXT.trim());
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@@ -114,7 +116,7 @@ const maskPhpComments = (text) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -205,7 +207,7 @@ const scanToDelimiter = (text, startIndex, delimiter) => {
|
|||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -242,7 +244,7 @@ const scanToParenEnd = (text, startIndex) => {
|
|||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -280,7 +282,7 @@ const splitTopLevel = (text, delimiter) => {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
current += ch;
|
current += ch;
|
||||||
continue;
|
continue;
|
||||||
@@ -389,7 +391,7 @@ const stripWrappingQuotes = (value) => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(text.startsWith('\'') && text.endsWith('\'')) ||
|
(text.startsWith("'") && text.endsWith("'")) ||
|
||||||
(text.startsWith('"') && text.endsWith('"')) ||
|
(text.startsWith('"') && text.endsWith('"')) ||
|
||||||
(text.startsWith('`') && text.endsWith('`'))
|
(text.startsWith('`') && text.endsWith('`'))
|
||||||
) {
|
) {
|
||||||
@@ -830,8 +832,7 @@ const extractPhpFileConstraints = (file, text) => {
|
|||||||
regex.lastIndex = endIndex;
|
regex.lastIndex = endIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classMatch =
|
const classMatch = /class\s+([\\\p{L}_][\\\p{L}\p{N}_]*)\s+extends\s+([\\\p{L}_][\\\p{L}\p{N}_]*)/mu.exec(text);
|
||||||
/class\s+([\\\p{L}_][\\\p{L}\p{N}_]*)\s+extends\s+([\\\p{L}_][\\\p{L}\p{N}_]*)/mu.exec(text);
|
|
||||||
const parentClass = classMatch?.[2]?.split('\\').pop() ?? null;
|
const parentClass = classMatch?.[2]?.split('\\').pop() ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -902,11 +903,7 @@ const readStringLikeProperty = (objLiteral, keyName) => {
|
|||||||
if (!ts.isPropertyAssignment(prop)) {
|
if (!ts.isPropertyAssignment(prop)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const key = ts.isIdentifier(prop.name)
|
const key = ts.isIdentifier(prop.name) ? prop.name.text : ts.isStringLiteral(prop.name) ? prop.name.text : null;
|
||||||
? prop.name.text
|
|
||||||
: ts.isStringLiteral(prop.name)
|
|
||||||
? prop.name.text
|
|
||||||
: null;
|
|
||||||
if (key !== keyName) {
|
if (key !== keyName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1337,7 +1334,12 @@ const extractTsFileConstraints = (file, text) => {
|
|||||||
let parentFile = null;
|
let parentFile = null;
|
||||||
|
|
||||||
const visit = (node) => {
|
const visit = (node) => {
|
||||||
if (!parentFile && ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
if (
|
||||||
|
!parentFile &&
|
||||||
|
ts.isVariableDeclaration(node) &&
|
||||||
|
node.initializer &&
|
||||||
|
ts.isCallExpression(node.initializer)
|
||||||
|
) {
|
||||||
const callee = node.initializer.expression;
|
const callee = node.initializer.expression;
|
||||||
if (ts.isIdentifier(callee)) {
|
if (ts.isIdentifier(callee)) {
|
||||||
const moduleText = imports.get(callee.text);
|
const moduleText = imports.get(callee.text);
|
||||||
@@ -1373,15 +1375,11 @@ const extractTsFileConstraints = (file, text) => {
|
|||||||
const name = node.name.text;
|
const name = node.name.text;
|
||||||
if (name === 'buildConstraints') {
|
if (name === 'buildConstraints') {
|
||||||
assigned.full = true;
|
assigned.full = true;
|
||||||
byKind.full.push(
|
byKind.full.push(...extractTsMethodConstraints(node, sourceFile, 'ts', 'full', relFile, factorySet));
|
||||||
...extractTsMethodConstraints(node, sourceFile, 'ts', 'full', relFile, factorySet)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (name === 'buildMinConstraints') {
|
if (name === 'buildMinConstraints') {
|
||||||
assigned.min = true;
|
assigned.min = true;
|
||||||
byKind.min.push(
|
byKind.min.push(...extractTsMethodConstraints(node, sourceFile, 'ts', 'min', relFile, factorySet));
|
||||||
...extractTsMethodConstraints(node, sourceFile, 'ts', 'min', relFile, factorySet)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ts.forEachChild(node, visit);
|
ts.forEachChild(node, visit);
|
||||||
@@ -1834,7 +1832,9 @@ const printReport = (report) => {
|
|||||||
if (result.near.length > 0) {
|
if (result.near.length > 0) {
|
||||||
console.log(` Near match (${result.near.length}):`);
|
console.log(` Near match (${result.near.length}):`);
|
||||||
for (const pair of result.near) {
|
for (const pair of result.near) {
|
||||||
console.log(` - PHP "${pair.phpName}" ~ TS "${pair.tsName}" (score ${pair.score.toFixed(2)})`);
|
console.log(
|
||||||
|
` - PHP "${pair.phpName}" ~ TS "${pair.tsName}" (score ${pair.score.toFixed(2)})`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.argDiffs.length > 0) {
|
if (result.argDiffs.length > 0) {
|
||||||
@@ -1904,9 +1904,18 @@ const main = async () => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
console.log(JSON.stringify(jsonFriendly, null, 2));
|
console.log(JSON.stringify(jsonFriendly, null, 2));
|
||||||
return;
|
} else {
|
||||||
|
printReport(report);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
check &&
|
||||||
|
(report.totals.mismatch > 0 ||
|
||||||
|
report.totals.nearMatch > 0 ||
|
||||||
|
report.totals.missingCommandInTs > 0 ||
|
||||||
|
report.totals.missingCommandInPhp > 0)
|
||||||
|
) {
|
||||||
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
printReport(report);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import ts from 'typescript';
|
import ts from 'typescript-legacy';
|
||||||
|
|
||||||
const ROOT_DIR = process.cwd();
|
const ROOT_DIR = process.cwd();
|
||||||
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
|
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
|
||||||
@@ -24,6 +24,7 @@ Options:
|
|||||||
--keep-date Keep <1>...</> date markers in normalized output.
|
--keep-date Keep <1>...</> date markers in normalized output.
|
||||||
--ignore-file <path> JSON ignore list file (default: tools/compare-command-logs.ignore.json).
|
--ignore-file <path> JSON ignore list file (default: tools/compare-command-logs.ignore.json).
|
||||||
--checklist Output a markdown checklist for mismatches.
|
--checklist Output a markdown checklist for mismatches.
|
||||||
|
--check Exit non-zero when a command is missing or mismatched.
|
||||||
--json Output JSON report.
|
--json Output JSON report.
|
||||||
--help Show this help.
|
--help Show this help.
|
||||||
`;
|
`;
|
||||||
@@ -40,6 +41,7 @@ const includeGuards = args.includes('--include-guards');
|
|||||||
const includeTarget = args.includes('--include-target');
|
const includeTarget = args.includes('--include-target');
|
||||||
const countSensitive = args.includes('--count-sensitive');
|
const countSensitive = args.includes('--count-sensitive');
|
||||||
const checklist = args.includes('--checklist');
|
const checklist = args.includes('--checklist');
|
||||||
|
const check = args.includes('--check');
|
||||||
const ignoreFileIndex = args.indexOf('--ignore-file');
|
const ignoreFileIndex = args.indexOf('--ignore-file');
|
||||||
const ignoreFile = ignoreFileIndex >= 0 ? args[ignoreFileIndex + 1] : DEFAULT_IGNORE_FILE;
|
const ignoreFile = ignoreFileIndex >= 0 ? args[ignoreFileIndex + 1] : DEFAULT_IGNORE_FILE;
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ const guardPatterns = [
|
|||||||
/병종 정보를 확인할 수 없어/,
|
/병종 정보를 확인할 수 없어/,
|
||||||
/현재 선택할 수 없는 병종입니다/,
|
/현재 선택할 수 없는 병종입니다/,
|
||||||
/도시 정보가 없어/,
|
/도시 정보가 없어/,
|
||||||
|
/도달할 방법이 없습니다/,
|
||||||
];
|
];
|
||||||
|
|
||||||
const excludeGuards = DEFAULT_EXCLUDE_GUARDS && !includeGuards;
|
const excludeGuards = DEFAULT_EXCLUDE_GUARDS && !includeGuards;
|
||||||
@@ -127,7 +130,7 @@ const scanToDelimiter = (text, startIndex, delimiter) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -166,7 +169,7 @@ const scanToFirstArgumentEnd = (text, startIndex) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -208,7 +211,7 @@ const scanToParenEnd = (text, startIndex) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -249,7 +252,7 @@ const splitTopLevel = (text, delimiter) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch === '\'' || ch === '"') {
|
if (ch === "'" || ch === '"') {
|
||||||
quote = ch;
|
quote = ch;
|
||||||
current += ch;
|
current += ch;
|
||||||
continue;
|
continue;
|
||||||
@@ -280,7 +283,7 @@ const splitTopLevel = (text, delimiter) => {
|
|||||||
const parsePhpStringLiteral = (segment) => {
|
const parsePhpStringLiteral = (segment) => {
|
||||||
const trimmed = segment.trim();
|
const trimmed = segment.trim();
|
||||||
const quote = trimmed[0];
|
const quote = trimmed[0];
|
||||||
if (quote !== '\'' && quote !== '"') {
|
if (quote !== "'" && quote !== '"') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +508,8 @@ const extractPhpLogCalls = (text, assignments) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parsePhpExprToTemplate(value, assignments, match.index);
|
const parsed = parsePhpExprToTemplate(value, assignments, match.index);
|
||||||
const hasGeneralId = methodMeta.generalMethod && !isPhpActorLoggerExpr(calleeExpr, actorGeneralVars, actorLoggerVars);
|
const hasGeneralId =
|
||||||
|
methodMeta.generalMethod && !isPhpActorLoggerExpr(calleeExpr, actorGeneralVars, actorLoggerVars);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
pos: match.index,
|
pos: match.index,
|
||||||
@@ -615,10 +619,7 @@ const extractTsLogs = (filePath, text) => {
|
|||||||
if (!ts.isIdentifier(decl.name) || !decl.initializer) {
|
if (!ts.isIdentifier(decl.name) || !decl.initializer) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (ts.isStringLiteral(decl.initializer) || ts.isNoSubstitutionTemplateLiteral(decl.initializer)) {
|
||||||
ts.isStringLiteral(decl.initializer) ||
|
|
||||||
ts.isNoSubstitutionTemplateLiteral(decl.initializer)
|
|
||||||
) {
|
|
||||||
constants.set(decl.name.text, decl.initializer.text);
|
constants.set(decl.name.text, decl.initializer.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,8 +685,7 @@ const extractTsLogs = (filePath, text) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nameProp = arg.properties.find(
|
const nameProp = arg.properties.find(
|
||||||
(prop) =>
|
(prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name'
|
||||||
ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name'
|
|
||||||
);
|
);
|
||||||
if (!nameProp || !ts.isPropertyAssignment(nameProp)) {
|
if (!nameProp || !ts.isPropertyAssignment(nameProp)) {
|
||||||
ts.forEachChild(node, visitFactory);
|
ts.forEachChild(node, visitFactory);
|
||||||
@@ -780,8 +780,7 @@ const loadIgnoreConfig = async () => {
|
|||||||
const compileIgnoreRules = (config) => {
|
const compileIgnoreRules = (config) => {
|
||||||
const global = config.Global ?? {};
|
const global = config.Global ?? {};
|
||||||
const normalizeList = (list) => (Array.isArray(list) ? list.map((item) => normalizeTemplate(String(item))) : []);
|
const normalizeList = (list) => (Array.isArray(list) ? list.map((item) => normalizeTemplate(String(item))) : []);
|
||||||
const compileRegex = (list) =>
|
const compileRegex = (list) => (Array.isArray(list) ? list.map((item) => new RegExp(String(item))) : []);
|
||||||
Array.isArray(list) ? list.map((item) => new RegExp(String(item))) : [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalTemplates: new Set(normalizeList(global.templates)),
|
globalTemplates: new Set(normalizeList(global.templates)),
|
||||||
@@ -1127,69 +1126,71 @@ const main = async () => {
|
|||||||
|
|
||||||
if (asJson) {
|
if (asJson) {
|
||||||
console.log(JSON.stringify(report, null, 2));
|
console.log(JSON.stringify(report, null, 2));
|
||||||
return;
|
} else {
|
||||||
}
|
console.log(
|
||||||
|
`Compare command logs (mode: ${mode}, strict: ${strict ? 'on' : 'off'}, keepDate: ${keepDate ? 'on' : 'off'}, excludeGuards: ${excludeGuards ? 'on' : 'off'}, excludeTarget: ${excludeTarget ? 'on' : 'off'}, countSensitive: ${countSensitive ? 'on' : 'off'})`
|
||||||
|
);
|
||||||
|
console.log(`PHP commands: ${report.totals.phpCommands}`);
|
||||||
|
console.log(`TS commands: ${report.totals.tsCommands}`);
|
||||||
|
console.log(`Matched commands: ${report.totals.matches}`);
|
||||||
|
console.log(`Mismatched commands: ${report.totals.mismatches}`);
|
||||||
|
console.log(`Missing in TS: ${report.missingInTs.length}`);
|
||||||
|
console.log(`Missing in PHP: ${report.missingInPhp.length}`);
|
||||||
|
console.log(`Ignored mismatches: ${report.totals.ignored}`);
|
||||||
|
|
||||||
console.log(
|
if (report.missingInTs.length > 0) {
|
||||||
`Compare command logs (mode: ${mode}, strict: ${strict ? 'on' : 'off'}, keepDate: ${keepDate ? 'on' : 'off'}, excludeGuards: ${excludeGuards ? 'on' : 'off'}, excludeTarget: ${excludeTarget ? 'on' : 'off'}, countSensitive: ${countSensitive ? 'on' : 'off'})`
|
console.log('\nMissing in TS:');
|
||||||
);
|
for (const key of report.missingInTs) {
|
||||||
console.log(`PHP commands: ${report.totals.phpCommands}`);
|
console.log(`- ${key}`);
|
||||||
console.log(`TS commands: ${report.totals.tsCommands}`);
|
|
||||||
console.log(`Matched commands: ${report.totals.matches}`);
|
|
||||||
console.log(`Mismatched commands: ${report.totals.mismatches}`);
|
|
||||||
console.log(`Missing in TS: ${report.missingInTs.length}`);
|
|
||||||
console.log(`Missing in PHP: ${report.missingInPhp.length}`);
|
|
||||||
console.log(`Ignored mismatches: ${report.totals.ignored}`);
|
|
||||||
|
|
||||||
if (report.missingInTs.length > 0) {
|
|
||||||
console.log('\nMissing in TS:');
|
|
||||||
for (const key of report.missingInTs) {
|
|
||||||
console.log(`- ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (report.missingInPhp.length > 0) {
|
|
||||||
console.log('\nMissing in PHP:');
|
|
||||||
for (const key of report.missingInPhp) {
|
|
||||||
console.log(`- ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (report.mismatches.length > 0) {
|
|
||||||
console.log('\nMismatch Details:');
|
|
||||||
for (const mismatch of report.mismatches) {
|
|
||||||
console.log(`\n== ${mismatch.key} ==`);
|
|
||||||
if (mismatch.missingDetails.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`PHP only: ${mismatch.missingDetails
|
|
||||||
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
|
|
||||||
.join(' | ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (mismatch.extraDetails.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`TS only: ${mismatch.extraDetails
|
|
||||||
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
|
|
||||||
.join(' | ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const phpLines = formatEntries(mismatch.phpEntries);
|
|
||||||
const tsLines = formatEntries(mismatch.tsEntries);
|
|
||||||
|
|
||||||
console.log('PHP:');
|
|
||||||
for (const line of phpLines) {
|
|
||||||
console.log(line);
|
|
||||||
}
|
|
||||||
console.log('TS:');
|
|
||||||
for (const line of tsLines) {
|
|
||||||
console.log(line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (checklist) {
|
if (report.missingInPhp.length > 0) {
|
||||||
console.log('\nChecklist:');
|
console.log('\nMissing in PHP:');
|
||||||
console.log(renderChecklist(report));
|
for (const key of report.missingInPhp) {
|
||||||
|
console.log(`- ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.mismatches.length > 0) {
|
||||||
|
console.log('\nMismatch Details:');
|
||||||
|
for (const mismatch of report.mismatches) {
|
||||||
|
console.log(`\n== ${mismatch.key} ==`);
|
||||||
|
if (mismatch.missingDetails.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`PHP only: ${mismatch.missingDetails
|
||||||
|
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
|
||||||
|
.join(' | ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mismatch.extraDetails.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`TS only: ${mismatch.extraDetails
|
||||||
|
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
|
||||||
|
.join(' | ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const phpLines = formatEntries(mismatch.phpEntries);
|
||||||
|
const tsLines = formatEntries(mismatch.tsEntries);
|
||||||
|
|
||||||
|
console.log('PHP:');
|
||||||
|
for (const line of phpLines) {
|
||||||
|
console.log(line);
|
||||||
|
}
|
||||||
|
console.log('TS:');
|
||||||
|
for (const line of tsLines) {
|
||||||
|
console.log(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checklist) {
|
||||||
|
console.log('\nChecklist:');
|
||||||
|
console.log(renderChecklist(report));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (check && (report.totals.mismatches > 0 || report.missingInTs.length > 0 || report.missingInPhp.length > 0)) {
|
||||||
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user