merge: 유산 경매 후보 종류 정렬을 main에 반영

This commit is contained in:
2026-08-20 16:18:47 +00:00
4 changed files with 87 additions and 6 deletions
+7 -2
View File
@@ -12,6 +12,7 @@ import {
isWarTraitKey, isWarTraitKey,
} from '@sammo-ts/logic'; } from '@sammo-ts/logic';
import type { InheritBuffType } from '@sammo-ts/logic'; import type { InheritBuffType } from '@sammo-ts/logic';
import type { ItemSlot } from '@sammo-ts/logic';
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js'; import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
import { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js'; import { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
import { import {
@@ -39,6 +40,8 @@ const BUFF_KEYS: InheritBuffType[] = [
'warMagicTrialProbOppose', 'warMagicTrialProbOppose',
]; ];
const UNIQUE_ITEM_SLOT_ORDER: readonly ItemSlot[] = ['horse', 'weapon', 'book', 'item'];
const BUFF_LABELS: Record<InheritBuffType, string> = { const BUFF_LABELS: Record<InheritBuffType, string> = {
warAvoidRatio: '회피 확률 증가', warAvoidRatio: '회피 확률 증가',
warCriticalRatio: '필살 확률 증가', warCriticalRatio: '필살 확률 증가',
@@ -79,7 +82,8 @@ const loadAvailableUniqueItems = async (worldState: WorldStateRow) => {
const loader = new ItemLoader(); const loader = new ItemLoader();
const { allItems } = await resolveLegacyCompatibleUniqueConfig(configConst, loader); const { allItems } = await resolveLegacyCompatibleUniqueConfig(configConst, loader);
const enabledKeys: Array<Parameters<ItemLoader['load']>[0]> = []; const enabledKeys: Array<Parameters<ItemLoader['load']>[0]> = [];
for (const entries of Object.values(allItems)) { for (const slot of UNIQUE_ITEM_SLOT_ORDER) {
const entries = allItems[slot] ?? {};
for (const [key, amount] of Object.entries(asRecord(entries))) { for (const [key, amount] of Object.entries(asRecord(entries))) {
if (asNumber(amount, 0) !== 0 && isItemKey(key)) { if (asNumber(amount, 0) !== 0 && isItemKey(key)) {
enabledKeys.push(key); enabledKeys.push(key);
@@ -94,10 +98,11 @@ const loadAvailableUniqueItems = async (worldState: WorldStateRow) => {
name: item.name, name: item.name,
rawName: item.rawName, rawName: item.rawName,
info: item.info ?? '', info: item.info ?? '',
slot: item.slot,
}; };
}) })
); );
return items.sort((left, right) => left.name.localeCompare(right.name, 'ko')); return items;
}; };
const resolveWorld = async (ctx: { db: { worldState: { findFirst: () => Promise<unknown> } } }) => { const resolveWorld = async (ctx: { db: { worldState: { findFirst: () => Promise<unknown> } } }) => {
+26
View File
@@ -218,6 +218,32 @@ describe('inherit router actor and permission boundaries', () => {
} }
); );
it('orders unique auction candidates by Ref slot order and preserves order within each slot', async () => {
const fixture = buildContext({
configConst: {
allItems: {
item: { che_보물_도기: 1 },
book: { che_서적_07_논어: 1 },
weapon: { che_무기_12_칠성검: 1 },
horse: {
che_명마_07_백마: 1,
che_명마_07_기주마: 1,
},
},
},
});
const status = await appRouter.createCaller(fixture.context).inherit.getStatus();
expect(status.availableUnique.map(({ key, slot }) => ({ key, slot }))).toEqual([
{ key: 'che_명마_07_백마', slot: 'horse' },
{ key: 'che_명마_07_기주마', slot: 'horse' },
{ key: 'che_무기_12_칠성검', slot: 'weapon' },
{ key: 'che_서적_07_논어', slot: 'book' },
{ key: 'che_보물_도기', slot: 'item' },
]);
});
it('loads the first inheritance-log page without an out-of-range integer cursor', async () => { it('loads the first inheritance-log page without an out-of-range integer cursor', async () => {
const createdAt = new Date('2026-07-26T00:00:00Z'); const createdAt = new Date('2026-07-26T00:00:00Z');
const fixture = buildContext({ const fixture = buildContext({
+23 -3
View File
@@ -6,6 +6,7 @@ import { trpc } from '../utils/trpc';
type InheritStatus = Awaited<ReturnType<typeof trpc.inherit.getStatus.query>>; type InheritStatus = Awaited<ReturnType<typeof trpc.inherit.getStatus.query>>;
type InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number]; type InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number];
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>; type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
type UniqueItemSlot = InheritStatus['availableUnique'][number]['slot'];
type BuffKey = type BuffKey =
| 'warAvoidRatio' | 'warAvoidRatio'
@@ -67,6 +68,14 @@ const pointOrder = [
'betting', 'betting',
] as const; ] as const;
const uniqueItemSlotOrder: readonly UniqueItemSlot[] = ['horse', 'weapon', 'book', 'item'];
const uniqueItemSlotLabels: Record<UniqueItemSlot, string> = {
horse: '명마',
weapon: '무기',
book: '서적',
item: '도구',
};
const pointHelp: Record<string, string> = { const pointHelp: Record<string, string> = {
previous: '이전에 물려받은 포인트입니다.', previous: '이전에 물려받은 포인트입니다.',
lived_month: '살아남은 기간입니다. (1개월 단위)', lived_month: '살아남은 기간입니다. (1개월 단위)',
@@ -196,6 +205,15 @@ const specialNameMap = computed(() => {
const selectedSpecialWarInfo = computed( const selectedSpecialWarInfo = computed(
() => status.value?.availableSpecialWar.find((entry) => entry.key === nextSpecialKey.value)?.info ?? '' () => status.value?.availableSpecialWar.find((entry) => entry.key === nextSpecialKey.value)?.info ?? ''
); );
const availableUniqueGroups = computed(() =>
uniqueItemSlotOrder
.map((slot) => ({
slot,
label: uniqueItemSlotLabels[slot],
items: status.value?.availableUnique.filter((item) => item.slot === slot) ?? [],
}))
.filter((group) => group.items.length > 0)
);
const buffCost = (key: BuffKey, target: number): number => { const buffCost = (key: BuffKey, target: number): number => {
const points = status.value?.inheritConst.inheritBuffPoints ?? [0, 0, 0, 0, 0, 0]; const points = status.value?.inheritConst.inheritBuffPoints ?? [0, 0, 0, 0, 0, 0];
@@ -518,9 +536,11 @@ onMounted(() => {
<label for="specific-unique">유니크 경매</label> <label for="specific-unique">유니크 경매</label>
<select id="specific-unique" v-model="uniqueForm.itemId"> <select id="specific-unique" v-model="uniqueForm.itemId">
<option disabled value="">유니크 선택</option> <option disabled value="">유니크 선택</option>
<option v-for="item in status.availableUnique" :key="item.key" :value="item.key"> <optgroup v-for="group in availableUniqueGroups" :key="group.slot" :label="group.label">
{{ item.name }} <option v-for="item in group.items" :key="item.key" :value="item.key">
</option> {{ item.name }}
</option>
</optgroup>
</select> </select>
</div> </div>
<div class="control-row"> <div class="control-row">
@@ -81,17 +81,33 @@ const statusFixture = {
resetLevels: { resetSpecialWar: 0, resetTurnTime: 0 }, resetLevels: { resetSpecialWar: 0, resetTurnTime: 0 },
availableSpecialWar: [{ key: 'che_선봉', name: '선봉', info: '공격에 유리합니다.' }], availableSpecialWar: [{ key: 'che_선봉', name: '선봉', info: '공격에 유리합니다.' }],
availableUnique: [ availableUnique: [
{
key: 'che_명마_07_백마',
name: '백마(+7)',
rawName: '백마',
info: '기동력을 올려주는 유니크 명마입니다.',
slot: 'horse',
},
{ {
key: 'che_무기_12_칠성검', key: 'che_무기_12_칠성검',
name: '칠성검(+12)', name: '칠성검(+12)',
rawName: '칠성검', rawName: '칠성검',
info: '무력을 올려주는 유니크 무기입니다.', info: '무력을 올려주는 유니크 무기입니다.',
slot: 'weapon',
}, },
{ {
key: 'che_서적_07_논어', key: 'che_서적_07_논어',
name: '논어(+7)', name: '논어(+7)',
rawName: '논어', rawName: '논어',
info: '지력을 올려주는 유니크 서적입니다.', info: '지력을 올려주는 유니크 서적입니다.',
slot: 'book',
},
{
key: 'che_보물_도기',
name: '도기',
rawName: '도기',
info: '전투를 돕는 유니크 도구입니다.',
slot: 'item',
}, },
], ],
availableTargetGenerals: [{ id: 8, name: '조조' }], availableTargetGenerals: [{ id: 8, name: '조조' }],
@@ -197,7 +213,21 @@ test.describe('inheritance management legacy parity', () => {
await page.setViewportSize({ width: 1280, height: 900 }); await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(gameUrl); await page.goto(gameUrl);
await expect(page.locator('#container')).toBeVisible(); await expect(page.locator('#container')).toBeVisible();
await expect(page.locator('#specific-unique')).toHaveValue('che_무기_12_칠성검'); await expect(page.locator('#specific-unique')).toHaveValue('che_명마_07_백마');
await expect(page.locator('#specific-unique optgroup')).toHaveCount(4);
expect(
await page.locator('#specific-unique optgroup').evaluateAll((groups) =>
groups.map((group) => ({
label: group.getAttribute('label'),
values: [...group.querySelectorAll('option')].map((option) => option.value),
}))
)
).toEqual([
{ label: '명마', values: ['che_명마_07_백마'] },
{ label: '무기', values: ['che_무기_12_칠성검'] },
{ label: '서적', values: ['che_서적_07_논어'] },
{ label: '도구', values: ['che_보물_도기'] },
]);
const desktop = await page.evaluate(() => { const desktop = await page.evaluate(() => {
const rect = (selector: string) => { const rect = (selector: string) => {