fix(game-ui): 메인 턴과 장수 상태 표시를 보완
This commit is contained in:
@@ -333,6 +333,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
troopLeaderFirstTurn,
|
troopLeaderFirstTurn,
|
||||||
accessLog,
|
accessLog,
|
||||||
rankRows,
|
rankRows,
|
||||||
|
gameTime,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
general.cityId > 0
|
general.cityId > 0
|
||||||
? ctx.db.city.findUnique({
|
? ctx.db.city.findUnique({
|
||||||
@@ -413,6 +414,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
where: { generalId: general.id, type: { in: [...PERSONAL_RECORD_TYPES] } },
|
where: { generalId: general.id, type: { in: [...PERSONAL_RECORD_TYPES] } },
|
||||||
select: { type: true, value: true },
|
select: { type: true, value: true },
|
||||||
}),
|
}),
|
||||||
|
loadCurrentGameTime(ctx.db),
|
||||||
]);
|
]);
|
||||||
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
|
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
|
||||||
|
|
||||||
@@ -588,8 +590,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
|
killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
|
||||||
remainingMinutes: resolveRemainingMinutes(
|
remainingMinutes: resolveRemainingMinutes(
|
||||||
general.turnTime,
|
general.turnTime,
|
||||||
parsedLastExecuted,
|
gameTime.now
|
||||||
worldState?.tickSeconds ?? 0
|
|
||||||
),
|
),
|
||||||
crewTypeId: general.crewTypeId,
|
crewTypeId: general.crewTypeId,
|
||||||
crewTypeName: crewTypeDetails.get(general.crewTypeId)?.name ?? '-',
|
crewTypeName: crewTypeDetails.get(general.crewTypeId)?.name ?? '-',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
resolveRefreshScoreText,
|
resolveRefreshScoreText,
|
||||||
resolveRemainingMinutes,
|
resolveRemainingMinutes,
|
||||||
} from '../../services/generalBasicCardProjection.js';
|
} from '../../services/generalBasicCardProjection.js';
|
||||||
|
import { loadCurrentGameTime } from '../../services/gameClock.js';
|
||||||
import { loadTraitNames } from '../nation/shared.js';
|
import { loadTraitNames } from '../nation/shared.js';
|
||||||
import { getAuthenticatedUserId, getMyGeneral } from '../shared/general.js';
|
import { getAuthenticatedUserId, getMyGeneral } from '../shared/general.js';
|
||||||
import { throwIfCommandRejected } from '../shared/turnDaemon.js';
|
import { throwIfCommandRejected } from '../shared/turnDaemon.js';
|
||||||
@@ -72,7 +73,7 @@ export const troopRouter = router({
|
|||||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '국가에 소속되어 있지 않습니다.' });
|
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '국가에 소속되어 있지 않습니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nation, troops, generals, cities, worldState] = await Promise.all([
|
const [nation, troops, generals, cities, worldState, gameTime] = await Promise.all([
|
||||||
ctx.db.nation.findUnique({
|
ctx.db.nation.findUnique({
|
||||||
where: { id: me.nationId },
|
where: { id: me.nationId },
|
||||||
select: { id: true, name: true, color: true, level: true, meta: true },
|
select: { id: true, name: true, color: true, level: true, meta: true },
|
||||||
@@ -121,6 +122,7 @@ export const troopRouter = router({
|
|||||||
select: { id: true, name: true },
|
select: { id: true, name: true },
|
||||||
}),
|
}),
|
||||||
ctx.db.worldState.findFirst({ select: { tickSeconds: true, config: true, meta: true } }),
|
ctx.db.worldState.findFirst({ select: { tickSeconds: true, config: true, meta: true } }),
|
||||||
|
loadCurrentGameTime(ctx.db),
|
||||||
]);
|
]);
|
||||||
if (!nation) {
|
if (!nation) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: '국가 정보를 찾을 수 없습니다.' });
|
throw new TRPCError({ code: 'NOT_FOUND', message: '국가 정보를 찾을 수 없습니다.' });
|
||||||
@@ -214,15 +216,6 @@ export const troopRouter = router({
|
|||||||
const traitName = (code: string, names: Map<string, { name: string }>): string =>
|
const traitName = (code: string, names: Map<string, { name: string }>): string =>
|
||||||
names.get(code)?.name ?? sanitizeInternalDisplayCode(code);
|
names.get(code)?.name ?? sanitizeInternalDisplayCode(code);
|
||||||
const itemName = (code: string): string => itemNames.get(code) ?? sanitizeInternalDisplayCode(code);
|
const itemName = (code: string): string => itemNames.get(code) ?? sanitizeInternalDisplayCode(code);
|
||||||
const worldMeta = asRecord(worldState?.meta);
|
|
||||||
const rawLastExecuted = worldMeta.lastTurnTime ?? worldMeta.turntime;
|
|
||||||
const lastExecuted =
|
|
||||||
rawLastExecuted instanceof Date
|
|
||||||
? rawLastExecuted
|
|
||||||
: typeof rawLastExecuted === 'string'
|
|
||||||
? new Date(rawLastExecuted)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const mappedTroops = troops
|
const mappedTroops = troops
|
||||||
.map((troop) => {
|
.map((troop) => {
|
||||||
const leader = generalMap.get(troop.troopLeaderId);
|
const leader = generalMap.get(troop.troopLeaderId);
|
||||||
@@ -348,8 +341,7 @@ export const troopRouter = router({
|
|||||||
killTurn: readNumber(meta.killturn ?? meta.killTurn),
|
killTurn: readNumber(meta.killturn ?? meta.killTurn),
|
||||||
remainingMinutes: resolveRemainingMinutes(
|
remainingMinutes: resolveRemainingMinutes(
|
||||||
general.turnTime,
|
general.turnTime,
|
||||||
lastExecuted,
|
gameTime.now
|
||||||
worldState?.tickSeconds ?? 0
|
|
||||||
),
|
),
|
||||||
troopId: general.troopId,
|
troopId: general.troopId,
|
||||||
troop: {
|
troop: {
|
||||||
|
|||||||
@@ -45,15 +45,10 @@ export const resolveRefreshScoreText = (score: number): string => {
|
|||||||
|
|
||||||
export const resolveRemainingMinutes = (
|
export const resolveRemainingMinutes = (
|
||||||
turnTime: Date,
|
turnTime: Date,
|
||||||
lastExecuted: Date | null,
|
currentGameTime: Date | null
|
||||||
turnTermSeconds: number
|
|
||||||
): number | null => {
|
): number | null => {
|
||||||
if (!lastExecuted || !Number.isFinite(lastExecuted.getTime()) || turnTermSeconds <= 0) return null;
|
if (!currentGameTime || !Number.isFinite(currentGameTime.getTime())) return null;
|
||||||
let nextTurnMillis = turnTime.getTime();
|
return Math.floor(Math.max(0, (turnTime.getTime() - currentGameTime.getTime()) / 60_000));
|
||||||
if (nextTurnMillis < lastExecuted.getTime()) {
|
|
||||||
nextTurnMillis += turnTermSeconds * 1_000;
|
|
||||||
}
|
|
||||||
return Math.floor(Math.min(999, Math.max(0, (nextTurnMillis - lastExecuted.getTime()) / 60_000)));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface NextTurnMonthOffsetInput {
|
export interface NextTurnMonthOffsetInput {
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ describe('general basic card Ref projection', () => {
|
|||||||
expect(resolveRefreshScoreText(12_800)).toBe('헐...');
|
expect(resolveRefreshScoreText(12_800)).toBe('헐...');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches the Ref remaining-minute calculation and one-turn rollover', () => {
|
it('matches the Ref remaining-minute calculation against the current game clock', () => {
|
||||||
const lastExecuted = new Date('2026-08-13T00:00:00.000Z');
|
const currentGameTime = new Date('2026-08-13T00:20:00.000Z');
|
||||||
expect(resolveRemainingMinutes(new Date('2026-08-13T00:07:06.000Z'), lastExecuted, 3_600)).toBe(7);
|
expect(resolveRemainingMinutes(new Date('2026-08-13T01:20:00.000Z'), currentGameTime)).toBe(60);
|
||||||
expect(resolveRemainingMinutes(new Date('2026-08-12T23:59:00.000Z'), lastExecuted, 3_600)).toBe(59);
|
expect(resolveRemainingMinutes(new Date('2026-08-13T01:08:59.000Z'), currentGameTime)).toBe(48);
|
||||||
expect(resolveRemainingMinutes(new Date('2026-08-13T00:07:06.000Z'), null, 3_600)).toBeNull();
|
expect(resolveRemainingMinutes(new Date('2026-08-13T00:19:00.000Z'), currentGameTime)).toBe(0);
|
||||||
|
expect(resolveRemainingMinutes(new Date('2026-08-13T00:20:00.000Z'), null)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves the first reserved month only after the general turn bucket has passed', () => {
|
it('moves the first reserved month only after the general turn bucket has passed', () => {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const createContext = (options: {
|
|||||||
troopLeaderAction?: string | null;
|
troopLeaderAction?: string | null;
|
||||||
refreshScore?: number;
|
refreshScore?: number;
|
||||||
refreshScoreTotal?: number;
|
refreshScoreTotal?: number;
|
||||||
|
gameClockNow?: Date;
|
||||||
rankRows?: Array<{ generalId?: number; type: string; value: number }>;
|
rankRows?: Array<{ generalId?: number; type: string; value: number }>;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
transaction?: ReturnType<typeof vi.fn>;
|
transaction?: ReturnType<typeof vi.fn>;
|
||||||
@@ -164,6 +165,10 @@ const createContext = (options: {
|
|||||||
currentMonth: 1,
|
currentMonth: 1,
|
||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
config: { const: { upgradeLimit: 20 } },
|
config: { const: { upgradeLimit: 20 } },
|
||||||
|
clockBaseTime: options.gameClockNow ?? null,
|
||||||
|
clockTick: options.gameClockNow ? 0n : null,
|
||||||
|
clockMode: 'manual',
|
||||||
|
clockWallAnchor: options.gameClockNow ?? null,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
logEntry: {
|
logEntry: {
|
||||||
@@ -485,6 +490,7 @@ describe('in-game my information ownership', () => {
|
|||||||
troopLeaderAction: '휴식',
|
troopLeaderAction: '휴식',
|
||||||
refreshScore: 3,
|
refreshScore: 3,
|
||||||
refreshScoreTotal: 1_141,
|
refreshScoreTotal: 1_141,
|
||||||
|
gameClockNow: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||||
@@ -495,7 +501,7 @@ describe('in-game my information ownership', () => {
|
|||||||
retirementYear: 70,
|
retirementYear: 70,
|
||||||
defenceTrain: 80,
|
defenceTrain: 80,
|
||||||
killTurn: 6,
|
killTurn: 6,
|
||||||
remainingMinutes: null,
|
remainingMinutes: 7,
|
||||||
troop: { name: '정밀검증부대', status: 'inactive', leaderCityName: '업' },
|
troop: { name: '정밀검증부대', status: 'inactive', leaderCityName: '업' },
|
||||||
refreshScore: { current: 3, total: 1_141, text: '열심' },
|
refreshScore: { current: 3, total: 1_141, text: '열심' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -946,7 +946,7 @@ const install = async (
|
|||||||
return requests;
|
return requests;
|
||||||
};
|
};
|
||||||
|
|
||||||
test('offers the Ref repeat range for general turns while keeping the chief range', async ({ page }) => {
|
test('offers 12 repeat turns and six shift turns for general turns while keeping the chief range', async ({ page }) => {
|
||||||
await install(page);
|
await install(page);
|
||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -957,6 +957,11 @@ test('offers the Ref repeat range for general turns while keeping the chief rang
|
|||||||
await expect(generalRepeat.locator('.menu-items > button')).toHaveText(
|
await expect(generalRepeat.locator('.menu-items > button')).toHaveText(
|
||||||
Array.from({ length: 12 }, (_, index) => `${index + 1}턴`)
|
Array.from({ length: 12 }, (_, index) => `${index + 1}턴`)
|
||||||
);
|
);
|
||||||
|
const generalPull = generalEditor.locator('.bottom-shift-menu').first();
|
||||||
|
await generalPull.locator('summary').click();
|
||||||
|
await expect(generalPull.locator('.menu-items > button')).toHaveText(
|
||||||
|
Array.from({ length: 6 }, (_, index) => `${index + 1}턴`)
|
||||||
|
);
|
||||||
|
|
||||||
await page.setViewportSize({ width: 500, height: 900 });
|
await page.setViewportSize({ width: 500, height: 900 });
|
||||||
const mobileGeneralEditor = page.locator('[data-command-scope="general"]:visible');
|
const mobileGeneralEditor = page.locator('[data-command-scope="general"]:visible');
|
||||||
|
|||||||
@@ -1840,7 +1840,11 @@ test('tournament split main action follows recruitment, betting, finals, and tou
|
|||||||
const main = nationMenu.locator('[data-navigation-id="tournament"]');
|
const main = nationMenu.locator('[data-navigation-id="tournament"]');
|
||||||
await expect(main).toHaveText(lifecycle.label);
|
await expect(main).toHaveText(lifecycle.label);
|
||||||
await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`);
|
await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`);
|
||||||
await expect(main).toHaveClass(/highlight/);
|
if (lifecycle.stage === 1 || lifecycle.stage === 6) {
|
||||||
|
await expect(main).toHaveClass(/highlight/);
|
||||||
|
} else {
|
||||||
|
await expect(main).not.toHaveClass(/highlight/);
|
||||||
|
}
|
||||||
|
|
||||||
const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
|
const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
|
||||||
await toggle.click();
|
await toggle.click();
|
||||||
@@ -1851,9 +1855,12 @@ test('tournament split main action follows recruitment, betting, finals, and tou
|
|||||||
if (lifecycle.stage === 6) {
|
if (lifecycle.stage === 6) {
|
||||||
await expect(tournamentItem).not.toHaveClass(/highlight/);
|
await expect(tournamentItem).not.toHaveClass(/highlight/);
|
||||||
await expect(bettingItem).toHaveClass(/highlight/);
|
await expect(bettingItem).toHaveClass(/highlight/);
|
||||||
} else {
|
} else if (lifecycle.stage === 1) {
|
||||||
await expect(tournamentItem).toHaveClass(/highlight/);
|
await expect(tournamentItem).toHaveClass(/highlight/);
|
||||||
await expect(bettingItem).not.toHaveClass(/highlight/);
|
await expect(bettingItem).not.toHaveClass(/highlight/);
|
||||||
|
} else {
|
||||||
|
await expect(tournamentItem).not.toHaveClass(/highlight/);
|
||||||
|
await expect(bettingItem).not.toHaveClass(/highlight/);
|
||||||
}
|
}
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
@@ -1865,9 +1872,12 @@ test('tournament split main action follows recruitment, betting, finals, and tou
|
|||||||
if (lifecycle.stage === 6) {
|
if (lifecycle.stage === 6) {
|
||||||
await expect(mobileTournament).not.toHaveClass(/highlight/);
|
await expect(mobileTournament).not.toHaveClass(/highlight/);
|
||||||
await expect(mobileBetting).toHaveClass(/highlight/);
|
await expect(mobileBetting).toHaveClass(/highlight/);
|
||||||
} else {
|
} else if (lifecycle.stage === 1) {
|
||||||
await expect(mobileTournament).toHaveClass(/highlight/);
|
await expect(mobileTournament).toHaveClass(/highlight/);
|
||||||
await expect(mobileBetting).not.toHaveClass(/highlight/);
|
await expect(mobileBetting).not.toHaveClass(/highlight/);
|
||||||
|
} else {
|
||||||
|
await expect(mobileTournament).not.toHaveClass(/highlight/);
|
||||||
|
await expect(mobileBetting).not.toHaveClass(/highlight/);
|
||||||
}
|
}
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
}
|
}
|
||||||
@@ -2323,6 +2333,12 @@ test('main general card uses local turn time and command clock tracks corrected
|
|||||||
await expect(generalCard).toContainText('7분 남음');
|
await expect(generalCard).toContainText('7분 남음');
|
||||||
await expect(generalCard).toContainText('백마대');
|
await expect(generalCard).toContainText('백마대');
|
||||||
await expect(generalCard).toContainText('보통 120점(3)');
|
await expect(generalCard).toContainText('보통 120점(3)');
|
||||||
|
const leadershipProgress = generalCard.locator('[data-rich-tooltip="stat-leadership"]');
|
||||||
|
await leadershipProgress.hover();
|
||||||
|
const statTooltip = page.locator('.tippy-box[data-theme~="sammo-rich"]');
|
||||||
|
await expect(statTooltip).toBeVisible();
|
||||||
|
await expect(statTooltip).toContainText('통솔 성장');
|
||||||
|
await expect(statTooltip).toContainText('5 / 20');
|
||||||
|
|
||||||
const desktopGeometry = await title.evaluate((element) => {
|
const desktopGeometry = await title.evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
@@ -4239,6 +4255,18 @@ test('all main Lumen button families share the rounded pressed geometry', async
|
|||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generalPullMenu = page.locator('[data-main-target="commands"] .bottom-shift-menu').first();
|
||||||
|
await generalPullMenu.locator('summary').click();
|
||||||
|
await expect(generalPullMenu.locator('.menu-items > button')).toHaveText([
|
||||||
|
'1턴',
|
||||||
|
'2턴',
|
||||||
|
'3턴',
|
||||||
|
'4턴',
|
||||||
|
'5턴',
|
||||||
|
'6턴',
|
||||||
|
]);
|
||||||
|
await generalPullMenu.locator('summary').click();
|
||||||
|
|
||||||
state.permission = 0;
|
state.permission = 0;
|
||||||
await page.locator('.main-turn-controls').getByRole('button', { name: '갱 신' }).click();
|
await page.locator('.main-turn-controls').getByRole('button', { name: '갱 신' }).click();
|
||||||
const disabledSecret = page.locator('.layout-desktop [data-navigation-id="secret-board"]');
|
const disabledSecret = page.locator('.layout-desktop [data-navigation-id="secret-board"]');
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const props = withDefaults(
|
|||||||
storageKey: string;
|
storageKey: string;
|
||||||
editModeStorageKey?: string;
|
editModeStorageKey?: string;
|
||||||
maxPushTurn?: number;
|
maxPushTurn?: number;
|
||||||
|
maxShiftTurn?: number;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -44,6 +45,7 @@ const props = withDefaults(
|
|||||||
{
|
{
|
||||||
editModeStorageKey: undefined,
|
editModeStorageKey: undefined,
|
||||||
maxPushTurn: 6,
|
maxPushTurn: 6,
|
||||||
|
maxShiftTurn: 6,
|
||||||
compact: false,
|
compact: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
title: '',
|
title: '',
|
||||||
@@ -498,7 +500,7 @@ const clickOutsideMenu = (event: Event) => {
|
|||||||
<summary>당기기</summary>
|
<summary>당기기</summary>
|
||||||
<div class="menu-items">
|
<div class="menu-items">
|
||||||
<button
|
<button
|
||||||
v-for="amount in props.maxPushTurn"
|
v-for="amount in props.maxShiftTurn"
|
||||||
:key="amount"
|
:key="amount"
|
||||||
@click="
|
@click="
|
||||||
emit('shift', -amount);
|
emit('shift', -amount);
|
||||||
@@ -513,7 +515,7 @@ const clickOutsideMenu = (event: Event) => {
|
|||||||
<summary>미루기</summary>
|
<summary>미루기</summary>
|
||||||
<div class="menu-items">
|
<div class="menu-items">
|
||||||
<button
|
<button
|
||||||
v-for="amount in props.maxPushTurn"
|
v-for="amount in props.maxShiftTurn"
|
||||||
:key="amount"
|
:key="amount"
|
||||||
@click="
|
@click="
|
||||||
emit('shift', amount);
|
emit('shift', amount);
|
||||||
@@ -695,12 +697,38 @@ const clickOutsideMenu = (event: Event) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!props.compact" class="bottom-actions">
|
<div v-if="!props.compact" class="bottom-actions">
|
||||||
<button class="legacy-button legacy-button--secondary" type="button" @click="emit('shift', -1)">
|
<details class="legacy-menu bottom-shift-menu">
|
||||||
당기기
|
<summary class="legacy-button legacy-button--secondary" role="button">당기기</summary>
|
||||||
</button>
|
<div class="menu-items">
|
||||||
<button class="legacy-button legacy-button--secondary" type="button" @click="emit('shift', 1)">
|
<button
|
||||||
미루기
|
v-for="amount in props.maxShiftTurn"
|
||||||
</button>
|
:key="amount"
|
||||||
|
type="button"
|
||||||
|
@click="
|
||||||
|
emit('shift', -amount);
|
||||||
|
clickOutsideMenu($event);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ amount }}턴
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="legacy-menu bottom-shift-menu">
|
||||||
|
<summary class="legacy-button legacy-button--secondary" role="button">미루기</summary>
|
||||||
|
<div class="menu-items">
|
||||||
|
<button
|
||||||
|
v-for="amount in props.maxShiftTurn"
|
||||||
|
:key="amount"
|
||||||
|
type="button"
|
||||||
|
@click="
|
||||||
|
emit('shift', amount);
|
||||||
|
clickOutsideMenu($event);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ amount }}턴
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<button class="legacy-button legacy-button--secondary" type="button" @click="expanded = !expanded">
|
<button class="legacy-button legacy-button--secondary" type="button" @click="expanded = !expanded">
|
||||||
{{ expanded ? '접기' : '펼치기' }}
|
{{ expanded ? '접기' : '펼치기' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -831,7 +859,8 @@ const clickOutsideMenu = (event: Event) => {
|
|||||||
}
|
}
|
||||||
.control-pad > button,
|
.control-pad > button,
|
||||||
.clock,
|
.clock,
|
||||||
.legacy-menu > summary {
|
.control-pad > .legacy-menu > summary,
|
||||||
|
.advanced-actions .legacy-menu > summary {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -272,11 +272,19 @@ const specialText = computed(() => {
|
|||||||
<strong class="stat-value" :style="{ color: injuryInfo.color }">
|
<strong class="stat-value" :style="{ color: injuryInfo.color }">
|
||||||
<span>{{ stat.value }}</span>
|
<span>{{ stat.value }}</span>
|
||||||
<span v-if="stat.bonus > 0" class="leadership-bonus">+{{ stat.bonus }}</span>
|
<span v-if="stat.bonus > 0" class="leadership-bonus">+{{ stat.bonus }}</span>
|
||||||
<span class="bar-cell" :data-stat-progress="stat.key">
|
<span class="bar-cell">
|
||||||
<LegacyProgressBar
|
<RichTooltip
|
||||||
:percent="stat.percent"
|
:title="`${stat.label} 성장`"
|
||||||
:label="`${stat.label} 성장 ${stat.accumulated} / ${stat.limit}`"
|
:description="`${stat.accumulated} / ${stat.limit}`"
|
||||||
/>
|
:test-id="`stat-${stat.key}`"
|
||||||
|
>
|
||||||
|
<span :data-stat-progress="stat.key">
|
||||||
|
<LegacyProgressBar
|
||||||
|
:percent="stat.percent"
|
||||||
|
:label="`${stat.label} 성장 ${stat.accumulated} / ${stat.limit}`"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</RichTooltip>
|
||||||
</span>
|
</span>
|
||||||
</strong>
|
</strong>
|
||||||
</template>
|
</template>
|
||||||
@@ -539,6 +547,12 @@ const specialText = computed(() => {
|
|||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bar-cell :deep(.rich-tooltip-trigger),
|
||||||
|
.bar-cell [data-stat-progress] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.bar-cell,
|
.bar-cell,
|
||||||
.experience-bar {
|
.experience-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const resolveTournamentMainPresentation = (
|
|||||||
tournamentStage: number,
|
tournamentStage: number,
|
||||||
tournamentType: number | null
|
tournamentType: number | null
|
||||||
): TournamentMainPresentation => {
|
): TournamentMainPresentation => {
|
||||||
const active = tournamentStage > 0 || tournamentType !== null;
|
const active = tournamentStage === 1 || tournamentStage === 6;
|
||||||
const bettingActive = tournamentStage === 6;
|
const bettingActive = tournamentStage === 6;
|
||||||
const tournamentLabel = tournamentType === null ? undefined : tournamentLabels[tournamentType];
|
const tournamentLabel = tournamentType === null ? undefined : tournamentLabels[tournamentType];
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import test from 'node:test';
|
|||||||
|
|
||||||
import { resolveTournamentMainPresentation } from '../src/utils/tournamentNavigation.ts';
|
import { resolveTournamentMainPresentation } from '../src/utils/tournamentNavigation.ts';
|
||||||
|
|
||||||
void test('routes every active tournament stage except betting to its type-specific tournament button', () => {
|
void test('routes every non-betting stage to its type-specific tournament button and highlights only registration', () => {
|
||||||
const expectedLabels = ['전력전', '통솔전', '일기토', '설전'];
|
const expectedLabels = ['전력전', '통솔전', '일기토', '설전'];
|
||||||
for (const [type, expectedLabel] of expectedLabels.entries()) {
|
for (const [type, expectedLabel] of expectedLabels.entries()) {
|
||||||
for (const stage of [1, 2, 3, 4, 5, 7, 8, 9, 10, 0]) {
|
for (const stage of [1, 2, 3, 4, 5, 7, 8, 9, 10, 0]) {
|
||||||
const presentation = resolveTournamentMainPresentation(stage, type);
|
const presentation = resolveTournamentMainPresentation(stage, type);
|
||||||
assert.equal(presentation.compactLabel, expectedLabel);
|
assert.equal(presentation.compactLabel, expectedLabel);
|
||||||
assert.equal(presentation.to, '/tournament');
|
assert.equal(presentation.to, '/tournament');
|
||||||
assert.equal(presentation.active, true);
|
assert.equal(presentation.active, stage === 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user