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

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

실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
2026-08-23 21:48:29 +00:00
parent 85591c68ad
commit c63a49bd07
128 changed files with 8615 additions and 848 deletions
+34 -4
View File
@@ -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,
};
};