merge: 유산 경매 후보 종류 정렬을 main에 반영
This commit is contained in:
@@ -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> } } }) => {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user