feat: 응답 가능한 서신과 턴 시간 호환 이관
등용장과 이민족 선택 응답을 turn daemon transaction으로 연결하고 Ref의 통일 이후 상태 전이와 수신자별 메시지 저장 규칙을 보존한다.\n\n유산 턴 시간 변경을 nextTurnTimeBase 기반 결정적 계산으로 바로잡고 API, 엔진, Chromium 회귀를 추가한다.
This commit is contained in:
@@ -93,6 +93,14 @@ export type TurnDaemonCommand =
|
||||
| { type: 'dieOnPrestart'; requestId?: string; userId: string; generalId: number }
|
||||
| { type: 'buildNationCandidate'; requestId?: string; userId: string; generalId: number }
|
||||
| { type: 'instantRetreat'; requestId?: string; userId: string; generalId: number }
|
||||
| {
|
||||
type: 'messageRespond';
|
||||
requestId?: string;
|
||||
userId: string;
|
||||
generalId: number;
|
||||
messageId: number;
|
||||
response: boolean;
|
||||
}
|
||||
| { type: 'vacation'; requestId?: string; generalId: number }
|
||||
| {
|
||||
type: 'setMySetting';
|
||||
@@ -441,6 +449,14 @@ export type TurnDaemonCommandResult =
|
||||
| { type: 'dieOnPrestart'; ok: boolean; generalId: number; reason?: string }
|
||||
| { type: 'buildNationCandidate'; ok: boolean; generalId: number; reason?: string }
|
||||
| { type: 'instantRetreat'; ok: boolean; generalId: number; reason?: string }
|
||||
| {
|
||||
type: 'messageRespond';
|
||||
ok: boolean;
|
||||
generalId: number;
|
||||
messageId: number;
|
||||
action?: 'scout' | 'raiseInvader';
|
||||
reason: string;
|
||||
}
|
||||
| { type: 'vacation'; ok: boolean; generalId: number; reason?: string }
|
||||
| { type: 'setMySetting'; ok: boolean; generalId: number; reason?: string }
|
||||
| { type: 'dropItem'; ok: boolean; generalId: number; reason?: string }
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { ActionResolver } from '../../../src/actions/turn/general/che_등용.js';
|
||||
|
||||
describe('che_등용 recruitment message', () => {
|
||||
it('queues the Ref scout prompt with sender and receiver snapshots', () => {
|
||||
const logs: unknown[] = [];
|
||||
const general = {
|
||||
id: 1,
|
||||
name: '등용자',
|
||||
nationId: 1,
|
||||
cityId: 1,
|
||||
troopId: 0,
|
||||
gold: 5_000,
|
||||
experience: 100,
|
||||
dedication: 100,
|
||||
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||
role: { items: {} },
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: {},
|
||||
};
|
||||
const destination = {
|
||||
...general,
|
||||
id: 2,
|
||||
name: '수신자',
|
||||
nationId: 2,
|
||||
experience: 300,
|
||||
dedication: 200,
|
||||
};
|
||||
const sourceNation = { id: 1, name: '위', color: '#ffffff' };
|
||||
const destinationNation = { id: 2, name: '촉', color: '#000000' };
|
||||
const messageTime = new Date('0200-01-01T00:10:00.000Z');
|
||||
|
||||
const result = new ActionResolver().resolve(
|
||||
{
|
||||
general,
|
||||
nation: sourceNation,
|
||||
destGeneral: destination,
|
||||
messageTime,
|
||||
env: { develCost: 100 },
|
||||
worldView: { listNations: () => [sourceNation, destinationNation] },
|
||||
addLog: (text: string, options: unknown) => logs.push({ text, options }),
|
||||
} as never,
|
||||
{ destGeneralId: destination.id }
|
||||
);
|
||||
|
||||
expect(result.effects).toContainEqual({
|
||||
type: 'message:add',
|
||||
draft: {
|
||||
msgType: 'private',
|
||||
src: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: sourceNation.id,
|
||||
nationName: sourceNation.name,
|
||||
color: sourceNation.color,
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: destination.id,
|
||||
generalName: destination.name,
|
||||
nationId: destinationNation.id,
|
||||
nationName: destinationNation.name,
|
||||
color: destinationNation.color,
|
||||
icon: '',
|
||||
},
|
||||
text: '위로 망명 권유 서신',
|
||||
time: messageTime,
|
||||
validUntil: new Date('9999-12-31T12:59:59.000Z'),
|
||||
option: { action: 'scout' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user