fix: 유산 유니크 경매 후보를 종류별로 정렬

Ref의 명마, 무기, 서적, 도구 순서와 종류 내부 선언 순서를 보존한다. 선택 목록은 종류별 optgroup으로 구분하고 API·Chromium 회귀를 추가한다.
This commit is contained in:
2026-08-20 16:16:43 +00:00
parent ce7807bb4f
commit b33b2f454b
4 changed files with 87 additions and 6 deletions
+7 -2
View File
@@ -12,6 +12,7 @@ import {
isWarTraitKey,
} 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 { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
import {
@@ -39,6 +40,8 @@ const BUFF_KEYS: InheritBuffType[] = [
'warMagicTrialProbOppose',
];
const UNIQUE_ITEM_SLOT_ORDER: readonly ItemSlot[] = ['horse', 'weapon', 'book', 'item'];
const BUFF_LABELS: Record<InheritBuffType, string> = {
warAvoidRatio: '회피 확률 증가',
warCriticalRatio: '필살 확률 증가',
@@ -79,7 +82,8 @@ const loadAvailableUniqueItems = async (worldState: WorldStateRow) => {
const loader = new ItemLoader();
const { allItems } = await resolveLegacyCompatibleUniqueConfig(configConst, loader);
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))) {
if (asNumber(amount, 0) !== 0 && isItemKey(key)) {
enabledKeys.push(key);
@@ -94,10 +98,11 @@ const loadAvailableUniqueItems = async (worldState: WorldStateRow) => {
name: item.name,
rawName: item.rawName,
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> } } }) => {
+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 () => {
const createdAt = new Date('2026-07-26T00:00:00Z');
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 InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number];
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
type UniqueItemSlot = InheritStatus['availableUnique'][number]['slot'];
type BuffKey =
| 'warAvoidRatio'
@@ -67,6 +68,14 @@ const pointOrder = [
'betting',
] as const;
const uniqueItemSlotOrder: readonly UniqueItemSlot[] = ['horse', 'weapon', 'book', 'item'];
const uniqueItemSlotLabels: Record<UniqueItemSlot, string> = {
horse: '명마',
weapon: '무기',
book: '서적',
item: '도구',
};
const pointHelp: Record<string, string> = {
previous: '이전에 물려받은 포인트입니다.',
lived_month: '살아남은 기간입니다. (1개월 단위)',
@@ -196,6 +205,15 @@ const specialNameMap = computed(() => {
const selectedSpecialWarInfo = computed(
() => 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 points = status.value?.inheritConst.inheritBuffPoints ?? [0, 0, 0, 0, 0, 0];
@@ -518,9 +536,11 @@ onMounted(() => {
<label for="specific-unique">유니크 경매</label>
<select id="specific-unique" v-model="uniqueForm.itemId">
<option disabled value="">유니크 선택</option>
<option v-for="item in status.availableUnique" :key="item.key" :value="item.key">
{{ item.name }}
</option>
<optgroup v-for="group in availableUniqueGroups" :key="group.slot" :label="group.label">
<option v-for="item in group.items" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</optgroup>
</select>
</div>
<div class="control-row">
@@ -81,17 +81,33 @@ const statusFixture = {
resetLevels: { resetSpecialWar: 0, resetTurnTime: 0 },
availableSpecialWar: [{ key: 'che_선봉', name: '선봉', info: '공격에 유리합니다.' }],
availableUnique: [
{
key: 'che_명마_07_백마',
name: '백마(+7)',
rawName: '백마',
info: '기동력을 올려주는 유니크 명마입니다.',
slot: 'horse',
},
{
key: 'che_무기_12_칠성검',
name: '칠성검(+12)',
rawName: '칠성검',
info: '무력을 올려주는 유니크 무기입니다.',
slot: 'weapon',
},
{
key: 'che_서적_07_논어',
name: '논어(+7)',
rawName: '논어',
info: '지력을 올려주는 유니크 서적입니다.',
slot: 'book',
},
{
key: 'che_보물_도기',
name: '도기',
rawName: '도기',
info: '전투를 돕는 유니크 도구입니다.',
slot: 'item',
},
],
availableTargetGenerals: [{ id: 8, name: '조조' }],
@@ -197,7 +213,21 @@ test.describe('inheritance management legacy parity', () => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(gameUrl);
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 rect = (selector: string) => {