merge: 유저 장수 preset 아이콘 차단을 main에 반영한다

This commit is contained in:
2026-08-24 08:41:28 +00:00
13 changed files with 227 additions and 76 deletions
+25 -7
View File
@@ -54,6 +54,18 @@ const DEFAULT_CREW_TYPE_ID = 1100;
const MAX_GENERAL_TURNS = 30;
const DEFAULT_TURN_ACTION = '휴식';
export const resolveSelectionPoolUserIcon = (options: {
showImgLevel: number;
ownerPicture?: string;
ownerImageServer?: number;
}): { picture: string; imageServer: number } => {
const useOwnerPicture =
options.showImgLevel >= 1 && typeof options.ownerPicture === 'string' && options.ownerPicture !== 'default.jpg';
return useOwnerPicture
? { picture: options.ownerPicture!, imageServer: options.ownerImageServer ?? 1 }
: { picture: 'default.jpg', imageServer: 0 };
};
const zCandidateInfo = z.object({
uniqueName: z.string().min(1),
generalName: z.string().min(1),
@@ -756,11 +768,14 @@ export const createGeneralFromSelectionPool = async (options: {
now.getTime() + resolveTurnTermMinutes(worldState) * RESELECTION_TURN_MULTIPLIER * 60_000
);
const prestartDeleteAfter = buildPrestartDeleteAfter(options.operationalAcceptedAt, worldState.tickSeconds, config);
const showImgLevel = asNumber(config.showImgLevel, 0);
const useOwnerPicture =
showImgLevel >= 1 && typeof options.ownerPicture === 'string' && options.ownerPicture !== 'default.jpg';
const picture = useOwnerPicture ? options.ownerPicture! : showImgLevel >= 3 ? info.picture : 'default.jpg';
const imageServer = useOwnerPicture ? (options.ownerImageServer ?? 1) : info.imgsvr;
// 후보 picture는 NPC용 preset이다. 후보가 사람 장수(npcState=0)가 되는
// 순간부터는 명시적으로 선택한 계정 전용 아이콘 또는 기본 아이콘만 허용한다.
const { picture, imageServer } = resolveSelectionPoolUserIcon({
showImgLevel: asNumber(config.showImgLevel, 0),
ownerPicture: options.ownerPicture,
ownerImageServer: options.ownerImageServer,
});
const useOwnerPicture = picture !== 'default.jpg';
const defaultSpecialWar =
typeof configConst.defaultSpecialWar === 'string' ? configConst.defaultSpecialWar : 'None';
const defaultSpecialDomestic =
@@ -1023,6 +1038,7 @@ export const reselectGeneralFromSelectionPool = async (options: {
now
),
};
const reselectionIcon = resolveSelectionPoolUserIcon({ showImgLevel: 0 });
const updated = world.updateGeneral(general.id, {
name: info.generalName,
stats: centennialGrowth?.stats ?? {
@@ -1036,8 +1052,10 @@ export const reselectGeneralFromSelectionPool = async (options: {
specialDomestic: info.specialDomestic,
specialWar: info.specialWar ?? general.role.specialWar,
},
picture: info.picture,
imageServer: info.imgsvr,
// 재선택 후보의 preset은 유저 장수에 이어 붙이지 않는다. 전용 아이콘을
// 다시 고르는 UI가 없는 현재 경로는 안전한 기본 아이콘으로 되돌린다.
picture: reselectionIcon.picture,
imageServer: reselectionIcon.imageServer,
meta: updatedMeta,
});
if (!updated) {
@@ -4,7 +4,7 @@ import { GAME_TICKS_PER_TURN } from '@sammo-ts/common';
import { parseScenarioGeneralPoolCandidate } from '@sammo-ts/logic';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { reserveSelectionPool } from '../src/turn/selectPoolService.js';
import { reserveSelectionPool, resolveSelectionPoolUserIcon } from '../src/turn/selectPoolService.js';
import type { TurnGeneralPoolEntry, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
interface TestPoolRow {
@@ -171,6 +171,27 @@ const worldState = {
};
describe('selection-pool reservation command state', () => {
it('uses only an explicitly selected owner icon for a human general', () => {
expect(resolveSelectionPoolUserIcon({ showImgLevel: 3 })).toEqual({
picture: 'default.jpg',
imageServer: 0,
});
expect(
resolveSelectionPoolUserIcon({
showImgLevel: 3,
ownerPicture: 'uploaded/user.png',
ownerImageServer: 1,
})
).toEqual({ picture: 'uploaded/user.png', imageServer: 1 });
expect(
resolveSelectionPoolUserIcon({
showImgLevel: 0,
ownerPicture: 'uploaded/user.png',
ownerImageServer: 1,
})
).toEqual({ picture: 'default.jpg', imageServer: 0 });
});
it('excludes current reservations and keeps serialized users disjoint in DB and memory', async () => {
const rows = buildRows();
const world = buildWorld(rows);