fix: 커맨드 차등 생명주기와 로그 그래프를 보강
장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다. 실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
@@ -2,6 +2,33 @@ export type MessageType = 'public' | 'private' | 'national' | 'diplomacy';
|
||||
|
||||
export const MESSAGE_MAILBOX_PUBLIC = 9999;
|
||||
export const MESSAGE_MAILBOX_NATIONAL_BASE = 9000;
|
||||
export const DEFAULT_MESSAGE_SHARED_ICON_BASE_URL = 'https://sam-image.hided.net/icons';
|
||||
|
||||
export interface MessageIconSource {
|
||||
picture?: unknown;
|
||||
imageServer?: unknown;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match Ref GetImageURL for message payloads. Shared icons are absolute;
|
||||
* user icons retain the legacy d_pic marker consumed by the frontend.
|
||||
*/
|
||||
export const resolveMessageTargetIcon = (
|
||||
source: MessageIconSource | null = null,
|
||||
sharedIconBaseUrl = DEFAULT_MESSAGE_SHARED_ICON_BASE_URL
|
||||
): string => {
|
||||
const rawPicture = source?.picture ?? source?.meta?.picture;
|
||||
const picture =
|
||||
(typeof rawPicture === 'string' && rawPicture.trim() !== '') || typeof rawPicture === 'number'
|
||||
? String(rawPicture)
|
||||
: 'default.jpg';
|
||||
const imageServer = source?.imageServer ?? source?.meta?.imageServer;
|
||||
if (typeof imageServer === 'number' && imageServer !== 0) {
|
||||
return `d_pic/${picture}`;
|
||||
}
|
||||
return `${sharedIconBaseUrl.replace(/\/+$/u, '')}/${picture}`;
|
||||
};
|
||||
|
||||
export interface MessageTarget {
|
||||
generalId: number;
|
||||
@@ -80,7 +107,7 @@ const buildPayload = (draft: MessageDraft, optionOverride?: MessageOption | null
|
||||
src: draft.src,
|
||||
dest: draft.dest,
|
||||
text: draft.text,
|
||||
option: optionOverride ?? draft.option ?? {},
|
||||
option: optionOverride !== undefined ? optionOverride : (draft.option ?? {}),
|
||||
});
|
||||
|
||||
const buildRecord = (
|
||||
@@ -110,15 +137,18 @@ const buildRecord = (
|
||||
};
|
||||
};
|
||||
|
||||
const buildSenderOption = (draft: MessageDraft, receiverId: number): MessageOption => {
|
||||
const buildSenderOption = (draft: MessageDraft, receiverId: number): MessageOption | null => {
|
||||
const option = {
|
||||
...(draft.option ?? {}),
|
||||
receiverMessageID: receiverId,
|
||||
};
|
||||
|
||||
if (draft.msgType === 'diplomacy' && 'action' in option) {
|
||||
const { action: _action, ...rest } = option;
|
||||
return rest;
|
||||
// Ref Message::sendToSender temporarily replaces the entire actionable
|
||||
// diplomacy option with null. Keeping year/month/deletable or the
|
||||
// receiver row id would make the sender copy actionable in a way Ref is
|
||||
// deliberately not.
|
||||
return null;
|
||||
}
|
||||
|
||||
return option;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { resolveMessageTargetIcon, type MessageDraft } from './message.js';
|
||||
|
||||
type ScoutGeneral = Pick<General, 'id' | 'name' | 'nationId' | 'officerLevel'>;
|
||||
type ScoutNation = Pick<Nation, 'id' | 'name' | 'color'>;
|
||||
|
||||
export interface ScoutMessageDraftInput {
|
||||
srcGeneral: ScoutGeneral;
|
||||
destGeneral: ScoutGeneral;
|
||||
srcNation: ScoutNation | null;
|
||||
destNation: ScoutNation | null;
|
||||
time: Date;
|
||||
sharedIconBaseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure equivalent of Ref ScoutMessage::buildScoutMessage(). Ref constructs
|
||||
* both targets without a general picture, so MessageTarget supplies the shared
|
||||
* default icon, and every caller persists the receiver copy only via send(true).
|
||||
*/
|
||||
export const buildScoutMessageDraft = (input: ScoutMessageDraftInput): MessageDraft | null => {
|
||||
const { srcGeneral, destGeneral, srcNation, destNation } = input;
|
||||
if (
|
||||
srcGeneral.id === destGeneral.id ||
|
||||
destGeneral.officerLevel === 12 ||
|
||||
srcGeneral.nationId === 0 ||
|
||||
srcGeneral.nationId === destGeneral.nationId ||
|
||||
!srcNation ||
|
||||
srcNation.id !== srcGeneral.nationId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedDestNation =
|
||||
destGeneral.nationId === 0
|
||||
? { id: 0, name: '재야', color: '#000000' }
|
||||
: destNation?.id === destGeneral.nationId
|
||||
? destNation
|
||||
: null;
|
||||
if (!resolvedDestNation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const josaRo = JosaUtil.pick(srcNation.name, '로');
|
||||
const icon = resolveMessageTargetIcon(null, input.sharedIconBaseUrl);
|
||||
return {
|
||||
msgType: 'private',
|
||||
src: {
|
||||
generalId: srcGeneral.id,
|
||||
generalName: srcGeneral.name,
|
||||
nationId: srcGeneral.nationId,
|
||||
nationName: srcNation.name,
|
||||
color: srcNation.color,
|
||||
icon,
|
||||
},
|
||||
dest: {
|
||||
generalId: destGeneral.id,
|
||||
generalName: destGeneral.name,
|
||||
nationId: destGeneral.nationId,
|
||||
nationName: resolvedDestNation.name,
|
||||
color: resolvedDestNation.color,
|
||||
icon,
|
||||
},
|
||||
text: `${srcNation.name}${josaRo} 망명 권유 서신`,
|
||||
time: input.time,
|
||||
validUntil: new Date('9999-12-31T12:59:59.000Z'),
|
||||
option: { action: 'scout' },
|
||||
sendDestOnly: true,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user