feat: 응답 가능한 서신과 턴 시간 호환 이관

등용장과 이민족 선택 응답을 turn daemon transaction으로 연결하고 Ref의 통일 이후 상태 전이와 수신자별 메시지 저장 규칙을 보존한다.\n\n유산 턴 시간 변경을 nextTurnTimeBase 기반 결정적 계산으로 바로잡고 API, 엔진, Chromium 회귀를 추가한다.
This commit is contained in:
2026-08-19 18:03:56 +00:00
parent 9390195d5f
commit 81279e76b5
26 changed files with 1405 additions and 224 deletions
@@ -17,10 +17,11 @@ import type {
GeneralActionResolver,
GeneralActionEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogScope } from '@sammo-ts/logic/logging/types.js';
import { z } from 'zod';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/actions/turn/actionContext.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -31,6 +32,7 @@ export interface EmployResolveContext<
> extends GeneralActionResolveContext<TriggerState> {
destGeneral?: General;
env?: TurnCommandEnv;
messageTime: Date;
}
const ACTION_NAME = '등용';
@@ -131,6 +133,38 @@ export class ActionResolver<
});
}
if (ctx.nation) {
const destNation = ctx.worldView
?.listNations?.()
.find((candidate) => candidate.id === destGeneral.nationId);
const josaRo = JosaUtil.pick(ctx.nation.name, '로');
effects.push(
createMessageEffect({
msgType: 'private',
src: {
generalId: general.id,
generalName: general.name,
nationId: ctx.nation.id,
nationName: ctx.nation.name,
color: ctx.nation.color,
icon: '',
},
dest: {
generalId: destGeneral.id,
generalName: destGeneral.name,
nationId: destGeneral.nationId,
nationName: destNation?.name ?? '재야',
color: destNation?.color ?? '#000000',
icon: '',
},
text: `${ctx.nation.name}${josaRo} 망명 권유 서신`,
time: ctx.messageTime,
validUntil: new Date('9999-12-31T12:59:59.000Z'),
option: { action: 'scout' },
})
);
}
effects.push(
createLogEffect(
`<Y>${general.name}</>(${ctx.nation?.name ?? '재야'})로 부터 등용 권유 서신이 도착했습니다.`,
@@ -215,6 +249,10 @@ export const actionContextBuilder = (base: ActionContextBase, options: ActionCon
...base,
destGeneral,
env: options.scenarioConfig.const as unknown as TurnCommandEnv,
messageTime:
(base.general as General & { turnTime?: Date }).turnTime instanceof Date
? (base.general as General & { turnTime: Date }).turnTime
: options.world.lastTurnTime,
};
};
@@ -295,7 +295,17 @@ export class ActionResolver<
format: LogFormat.YEAR_MONTH,
});
return { effects };
const deletedTroopIds: number[] = [];
if (general.troopId === general.id) {
deletedTroopIds.push(general.id);
for (const member of context.worldView?.listGenerals() ?? []) {
if (member.id !== general.id && member.troopId === general.id) {
effects.push(createGeneralPatchEffect({ troopId: 0 }, member.id));
}
}
}
return { effects, deletedTroopIds };
}
}
+3 -1
View File
@@ -22,6 +22,8 @@ export interface MessageDraft {
time: Date;
validUntil: Date;
option?: MessageOption | null;
/** Ref Message::send(true): persist only the receiver copy. */
sendDestOnly?: boolean;
}
export interface MessagePayload {
@@ -139,7 +141,7 @@ export const sendMessage = async (
throw new Error('Failed to send receiver message.');
}
if (options.sendDestOnly) {
if (options.sendDestOnly ?? draft.sendDestOnly) {
return { receiverId };
}