NPC와 알림 메시지의 장수 아이콘 경로를 공통 처리
This commit is contained in:
@@ -7,8 +7,6 @@ const DEFAULT_NATION = {
|
|||||||
color: '#000000',
|
color: '#000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SHARED_ICON_PUBLIC_URL = 'https://sam-image.hided.net/icons';
|
|
||||||
|
|
||||||
export const resolveNationInfo = async (
|
export const resolveNationInfo = async (
|
||||||
db: DatabaseClient,
|
db: DatabaseClient,
|
||||||
nationId: number
|
nationId: number
|
||||||
@@ -25,14 +23,13 @@ export const resolveNationInfo = async (
|
|||||||
|
|
||||||
export const buildTargetFromGeneral = async (db: DatabaseClient, general: GeneralRow): Promise<MessageTarget> => {
|
export const buildTargetFromGeneral = async (db: DatabaseClient, general: GeneralRow): Promise<MessageTarget> => {
|
||||||
const nation = await resolveNationInfo(db, general.nationId);
|
const nation = await resolveNationInfo(db, general.nationId);
|
||||||
const picture = general.picture?.trim() || 'default.jpg';
|
|
||||||
return {
|
return {
|
||||||
generalId: general.id,
|
generalId: general.id,
|
||||||
generalName: general.name,
|
generalName: general.name,
|
||||||
nationId: general.nationId,
|
nationId: general.nationId,
|
||||||
nationName: nation.name,
|
nationName: nation.name,
|
||||||
color: nation.color,
|
color: nation.color,
|
||||||
icon: general.imageServer ? `d_pic/${picture}` : `${DEFAULT_SHARED_ICON_PUBLIC_URL}/${picture}`,
|
icon: resolveMessageTargetIcon({ picture: general.picture, imageServer: general.imageServer }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { createGamePostgresConnector, GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
|
||||||
@@ -106,7 +107,7 @@ export const buildAuctionOutbidRefundMessage = (options: {
|
|||||||
nationId: options.bidder.nationId,
|
nationId: options.bidder.nationId,
|
||||||
nationName: options.nation?.name ?? '재야',
|
nationName: options.nation?.name ?? '재야',
|
||||||
color: options.nation?.color ?? '#000000',
|
color: options.nation?.color ?? '#000000',
|
||||||
icon: options.bidder.picture ?? '',
|
icon: resolveMessageTargetIcon(options.bidder),
|
||||||
},
|
},
|
||||||
text: `${options.auctionId}번 ${options.title ?? '경매'}에 상회입찰자가 나타났습니다.`,
|
text: `${options.auctionId}번 ${options.title ?? '경매'}에 상회입찰자가 나타났습니다.`,
|
||||||
time: new Date(options.time.getTime()),
|
time: new Date(options.time.getTime()),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import { createGamePostgresConnector, GamePrisma } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, GamePrisma } from '@sammo-ts/infra';
|
||||||
import { ActionLogger, ItemLoader, LogFormat, isItemKey, type MessageDraft } from '@sammo-ts/logic';
|
import { ActionLogger, ItemLoader, LogFormat, isItemKey, type MessageDraft } from '@sammo-ts/logic';
|
||||||
import { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
import { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
||||||
@@ -124,7 +125,7 @@ export const buildAuctionBidderSystemMessage = (options: {
|
|||||||
nationId: options.bidder.nationId,
|
nationId: options.bidder.nationId,
|
||||||
nationName: options.nation?.name ?? '재야',
|
nationName: options.nation?.name ?? '재야',
|
||||||
color: options.nation?.color ?? '#000000',
|
color: options.nation?.color ?? '#000000',
|
||||||
icon: options.bidder.picture ?? '',
|
icon: resolveMessageTargetIcon(options.bidder),
|
||||||
},
|
},
|
||||||
text: options.text,
|
text: options.text,
|
||||||
time: new Date(options.time.getTime()),
|
time: new Date(options.time.getTime()),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||||
@@ -127,7 +128,7 @@ export const createOpenNationBettingHandler = (options: {
|
|||||||
nationId: general.nationId,
|
nationId: general.nationId,
|
||||||
nationName: nation?.name ?? '재야',
|
nationName: nation?.name ?? '재야',
|
||||||
color: nation?.color ?? '#000000',
|
color: nation?.color ?? '#000000',
|
||||||
icon: general.picture ?? '',
|
icon: resolveMessageTargetIcon(general),
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
time: now,
|
time: now,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import type {
|
import type {
|
||||||
ActionContextBase,
|
ActionContextBase,
|
||||||
ActionContextBuilder,
|
ActionContextBuilder,
|
||||||
@@ -2121,7 +2122,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
nationId: currentGeneral.nationId,
|
nationId: currentGeneral.nationId,
|
||||||
nationName: currentNation?.name ?? '재야',
|
nationName: currentNation?.name ?? '재야',
|
||||||
color: currentNation?.color ?? '#000000',
|
color: currentNation?.color ?? '#000000',
|
||||||
icon: currentGeneral.picture ?? '',
|
icon: resolveMessageTargetIcon(currentGeneral),
|
||||||
};
|
};
|
||||||
messages.push({
|
messages.push({
|
||||||
msgType: 'public',
|
msgType: 'public',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
|
|
||||||
import { asNumber, asRecord, JosaUtil } from '@sammo-ts/common';
|
import { asNumber, asRecord, JosaUtil } from '@sammo-ts/common';
|
||||||
@@ -156,7 +157,7 @@ export const createUnificationHandler = (options: {
|
|||||||
nationId: winner.id,
|
nationId: winner.id,
|
||||||
nationName: winner.name,
|
nationName: winner.name,
|
||||||
color: winner.color,
|
color: winner.color,
|
||||||
icon: recipient.picture ?? '',
|
icon: resolveMessageTargetIcon(recipient),
|
||||||
},
|
},
|
||||||
text: `이벤트 게임으로 이민족[${invader.difficulty}]을 소환`,
|
text: `이벤트 게임으로 이민족[${invader.difficulty}]을 소환`,
|
||||||
time: context.turnTime,
|
time: context.turnTime,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveMessageTargetIcon } from '@sammo-ts/logic';
|
||||||
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
||||||
import { asRecord, HALL_OF_FAME_TYPES, resolveLegacyTextColor, type HallOfFameType } from '@sammo-ts/common';
|
import { asRecord, HALL_OF_FAME_TYPES, resolveLegacyTextColor, type HallOfFameType } from '@sammo-ts/common';
|
||||||
import {
|
import {
|
||||||
@@ -246,7 +247,7 @@ const cancelPendingUniqueAuctions = async (
|
|||||||
nationId: bidder.nationId,
|
nationId: bidder.nationId,
|
||||||
nationName: nation?.name ?? '재야',
|
nationName: nation?.name ?? '재야',
|
||||||
color: nation?.color ?? '#000000',
|
color: nation?.color ?? '#000000',
|
||||||
icon: bidder.picture ?? '',
|
icon: resolveMessageTargetIcon(bidder),
|
||||||
},
|
},
|
||||||
text: `${planned.auctionId}번 ${planned.title}가 취소되었습니다.`,
|
text: `${planned.auctionId}번 ${planned.title}가 취소되었습니다.`,
|
||||||
time: input.completedAt,
|
time: input.completedAt,
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ describe('NPC 일반 내정 턴', () => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'NPC_무장',
|
name: 'NPC_무장',
|
||||||
|
picture: '롤시나리오/다이애나.png',
|
||||||
|
imageServer: 0,
|
||||||
nationId: 1,
|
nationId: 1,
|
||||||
cityId: 1,
|
cityId: 1,
|
||||||
troopId: 0,
|
troopId: 0,
|
||||||
@@ -297,7 +299,12 @@ describe('NPC 일반 내정 턴', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
msgType: 'public',
|
msgType: 'public',
|
||||||
text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다',
|
text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다',
|
||||||
src: expect.objectContaining({ generalId: 1, generalName: 'NPC_무장', nationId: 1 }),
|
src: expect.objectContaining({
|
||||||
|
generalId: 1,
|
||||||
|
generalName: 'NPC_무장',
|
||||||
|
nationId: 1,
|
||||||
|
icon: 'https://sam-image.hided.net/icons/롤시나리오/다이애나.png',
|
||||||
|
}),
|
||||||
time: logicalGameNow,
|
time: logicalGameNow,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6302,3 +6302,89 @@ for (const viewport of [
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const viewport of [
|
||||||
|
{ width: 1200, height: 900 },
|
||||||
|
{ width: 390, height: 844 },
|
||||||
|
]) {
|
||||||
|
test(`NPC message portraits resolve stored and new scenario icons at ${viewport.width}px`, async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const picture = '롤시나리오/다이애나.png';
|
||||||
|
const iconUrl = `https://sam-image.hided.net/icons/${picture.split('/').map(encodeURIComponent).join('/')}`;
|
||||||
|
const asset = await readFile(resolve(process.cwd(), '../../../image/icons', picture));
|
||||||
|
const defaultAsset = await readFile(resolve(process.cwd(), '../../../image/icons/default.jpg'));
|
||||||
|
const state: NavigationFixture = {
|
||||||
|
officerLevel: 1,
|
||||||
|
permission: 0,
|
||||||
|
nationLevel: 1,
|
||||||
|
stage: 0,
|
||||||
|
npcMode: 1,
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
messages: {
|
||||||
|
...emptyMessages(0),
|
||||||
|
public: [picture, `https://sam-image.hided.net/icons/${picture}`, ''].map((icon, index) => ({
|
||||||
|
id: 801 + index,
|
||||||
|
text: '새로운 달이 떠오르고 있다.',
|
||||||
|
time: '2026-09-07 12:00:00',
|
||||||
|
msgType: 'public',
|
||||||
|
src: {
|
||||||
|
generalId: 22 + index,
|
||||||
|
generalName: index === 2 ? '유저' : '다이애나',
|
||||||
|
nationId: 1,
|
||||||
|
nationName: '위',
|
||||||
|
color: '#008000',
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
dest: null,
|
||||||
|
option: {},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await installFixture(page, state);
|
||||||
|
await page.route('https://sam-image.hided.net/icons/**', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
contentType: route.request().url() === iconUrl ? 'image/png' : 'image/jpeg',
|
||||||
|
body: route.request().url() === iconUrl ? asset : defaultAsset,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await waitForMain(page);
|
||||||
|
const measurements = [];
|
||||||
|
for (const id of [801, 802, 803]) {
|
||||||
|
const icon = page.locator(`.msg-plate[data-id="${id}"]:visible img.general-icon`).first();
|
||||||
|
await expect(icon).toBeVisible();
|
||||||
|
await expect
|
||||||
|
.poll(() => icon.evaluate((element) => (element as HTMLImageElement).naturalWidth))
|
||||||
|
.toBe(id === 803 ? 64 : 128);
|
||||||
|
const result = await icon.evaluate((element) => {
|
||||||
|
const image = element as HTMLImageElement;
|
||||||
|
const rect = image.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
src: image.src,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
naturalWidth: image.naturalWidth,
|
||||||
|
naturalHeight: image.naturalHeight,
|
||||||
|
objectFit: getComputedStyle(image).objectFit,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(result.src).toBe(id === 803 ? 'https://sam-image.hided.net/icons/default.jpg' : iconUrl);
|
||||||
|
expect(result.width).toBe(64);
|
||||||
|
expect(result.height).toBe(64);
|
||||||
|
measurements.push(result);
|
||||||
|
}
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('.msg-plate[data-id="801"]:visible img').first()).toHaveAttribute('src', iconUrl);
|
||||||
|
await writeFile(testInfo.outputPath('icon-geometry.json'), JSON.stringify({ viewport, measurements }, null, 2));
|
||||||
|
await writeFile(
|
||||||
|
testInfo.outputPath('messages.html'),
|
||||||
|
await page
|
||||||
|
.locator('.msg-plate[data-id="801"]:visible')
|
||||||
|
.first()
|
||||||
|
.evaluate((el) => el.outerHTML)
|
||||||
|
);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('npc-message-icons.png'), fullPage: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DEFAULT_USER_ICON_PUBLIC_URL,
|
DEFAULT_USER_ICON_PUBLIC_URL,
|
||||||
externalizeLegacyImageUrl,
|
externalizeLegacyImageUrl,
|
||||||
} from './imageAssets.ts';
|
} from './imageAssets.ts';
|
||||||
import { gameFrontendRuntimeConfig } from '../config/runtimeConfig.ts';
|
|
||||||
|
|
||||||
export const DEFAULT_GENERAL_ICON_URL = `${configuredSharedIconPublicUrl()}/default.jpg`;
|
export const DEFAULT_GENERAL_ICON_URL = `${configuredSharedIconPublicUrl()}/default.jpg`;
|
||||||
export const DEFAULT_GATEWAY_USER_ICON_BASE_URL = DEFAULT_USER_ICON_PUBLIC_URL;
|
export const DEFAULT_GATEWAY_USER_ICON_BASE_URL = DEFAULT_USER_ICON_PUBLIC_URL;
|
||||||
@@ -69,7 +68,8 @@ export const resolveMessageGeneralIconUrl = (
|
|||||||
if (normalized.startsWith('/') || /^https?:\/\//iu.test(normalized)) {
|
if (normalized.startsWith('/') || /^https?:\/\//iu.test(normalized)) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return `${gameFrontendRuntimeConfig.appBasePath}${normalized.replace(/^\/+/u, '')}`;
|
// 기존 NPC 대사와 알림에는 URL 대신 picture 상대 경로가 저장되어 있다.
|
||||||
|
return resolveGeneralIconUrl({ picture: normalized, imageServer: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDefaultGeneralIcon = (event: Event): void => {
|
export const useDefaultGeneralIcon = (event: Event): void => {
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ void describe('generalIcon', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void it('resolves stored NPC message pictures like directory icons across scenarios', () => {
|
||||||
|
for (const picture of ['롤시나리오/다이애나.png', '장수/관우 1.png', '22.jpg', 'default.jpg']) {
|
||||||
|
assert.equal(resolveMessageGeneralIconUrl(picture), resolveGeneralIconUrl({ picture, imageServer: 0 }));
|
||||||
|
}
|
||||||
|
assert.equal(resolveMessageGeneralIconUrl(null), resolveGeneralIconUrl({ picture: null }));
|
||||||
|
});
|
||||||
|
|
||||||
void it('translates legacy message d_pic references without changing absolute or external icons', () => {
|
void it('translates legacy message d_pic references without changing absolute or external icons', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveMessageGeneralIconUrl('d_pic/users/core2026/user name.jpg', '/gateway/api/user-icons/'),
|
resolveMessageGeneralIconUrl('d_pic/users/core2026/user name.jpg', '/gateway/api/user-icons/'),
|
||||||
|
|||||||
Reference in New Issue
Block a user