merge: 최종 main을 인게임 폰트 통일에 반영
This commit is contained in:
@@ -7,6 +7,7 @@ import type { TournamentState } from '../../tournament/types.js';
|
|||||||
|
|
||||||
import { TournamentStore } from '../../tournament/store.js';
|
import { TournamentStore } from '../../tournament/store.js';
|
||||||
import { buildTournamentKeys } from '../../tournament/keys.js';
|
import { buildTournamentKeys } from '../../tournament/keys.js';
|
||||||
|
import { assignManualApplicantGroup } from '../../tournament/workerHelpers.js';
|
||||||
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
|
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
|
||||||
import { getMyGeneral } from '../shared/general.js';
|
import { getMyGeneral } from '../shared/general.js';
|
||||||
import { loadCurrentGameTime } from '../../services/gameClock.js';
|
import { loadCurrentGameTime } from '../../services/gameClock.js';
|
||||||
@@ -412,52 +413,31 @@ export const tournamentRouter = router({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingResult = await ctx.turnDaemon.requestCommand({
|
const meta = asRecord(general.meta);
|
||||||
type: 'setMySetting',
|
const level = typeof meta.explevel === 'number' ? meta.explevel : 0;
|
||||||
generalId: general.id,
|
const applicant = assignManualApplicantGroup({
|
||||||
settings: { tnmt: 1 },
|
state,
|
||||||
|
baseSeed: String(asRecord(worldState?.meta).hiddenSeed ?? 'tournament'),
|
||||||
|
current: participants,
|
||||||
|
applicant: {
|
||||||
|
id: general.id,
|
||||||
|
name: general.name,
|
||||||
|
leadership: general.leadership,
|
||||||
|
strength: general.strength,
|
||||||
|
intel: general.intel,
|
||||||
|
level,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!settingResult || settingResult.type !== 'setMySetting' || !settingResult.ok) {
|
const next = participants.concat(applicant);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.setParticipants(next);
|
||||||
|
} catch (error) {
|
||||||
await ctx.turnDaemon.requestCommand({
|
await ctx.turnDaemon.requestCommand({
|
||||||
type: 'adjustGeneralResources',
|
type: 'adjustGeneralResources',
|
||||||
reason: 'tournamentJoinRollback',
|
reason: 'tournamentJoinRollback',
|
||||||
adjustments: [{ generalId: general.id, goldDelta: develCost }],
|
adjustments: [{ generalId: general.id, goldDelta: develCost }],
|
||||||
});
|
});
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message:
|
|
||||||
settingResult && settingResult.type === 'setMySetting'
|
|
||||||
? (settingResult.reason ?? '요청에 실패했습니다.')
|
|
||||||
: 'Unexpected response',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = asRecord(general.meta);
|
|
||||||
const level = typeof meta.explevel === 'number' ? meta.explevel : 0;
|
|
||||||
const next = participants.concat({
|
|
||||||
id: general.id,
|
|
||||||
name: general.name,
|
|
||||||
leadership: general.leadership,
|
|
||||||
strength: general.strength,
|
|
||||||
intel: general.intel,
|
|
||||||
level,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await store.setParticipants(next);
|
|
||||||
} catch (error) {
|
|
||||||
await Promise.all([
|
|
||||||
ctx.turnDaemon.requestCommand({
|
|
||||||
type: 'adjustGeneralResources',
|
|
||||||
reason: 'tournamentJoinRollback',
|
|
||||||
adjustments: [{ generalId: general.id, goldDelta: develCost }],
|
|
||||||
}),
|
|
||||||
ctx.turnDaemon.requestCommand({
|
|
||||||
type: 'setMySetting',
|
|
||||||
generalId: general.id,
|
|
||||||
settings: { tnmt: 0 },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
return { ok: true, count: next.length };
|
return { ok: true, count: next.length };
|
||||||
|
|||||||
@@ -155,6 +155,60 @@ export const assignGroupSlots = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref assigns a manual applicant to one uniformly selected non-full preliminary
|
||||||
|
* group as part of the join request. Keeping that assignment in the persisted
|
||||||
|
* participant projection lets the applicant see the group immediately while
|
||||||
|
* the later participant-fill pass can still balance automatic applicants.
|
||||||
|
*/
|
||||||
|
export const assignManualApplicantGroup = (options: {
|
||||||
|
state: TournamentState;
|
||||||
|
baseSeed: string;
|
||||||
|
current: TournamentParticipantEntry[];
|
||||||
|
applicant: TournamentParticipantEntry;
|
||||||
|
groupCount?: number;
|
||||||
|
groupSize?: number;
|
||||||
|
}): TournamentParticipantEntry => {
|
||||||
|
const groupCount = options.groupCount ?? 8;
|
||||||
|
const groupSize = options.groupSize ?? 8;
|
||||||
|
const groupCounts = Array.from({ length: groupCount }, () => 0);
|
||||||
|
|
||||||
|
for (const participant of options.current) {
|
||||||
|
const groupId = participant.groupId;
|
||||||
|
if (groupId !== undefined && groupId >= 0 && groupId < groupCount) {
|
||||||
|
groupCounts[groupId] = (groupCounts[groupId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openGroupIds = groupCounts.flatMap((count, groupId) => (count < groupSize ? [groupId] : []));
|
||||||
|
if (openGroupIds.length === 0) {
|
||||||
|
throw new Error('참가 인원이 가득 찼습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rng = createTournamentRng(options.baseSeed, {
|
||||||
|
openYear: options.state.openYear,
|
||||||
|
openMonth: options.state.openMonth,
|
||||||
|
stage: 1,
|
||||||
|
phase: options.state.phase,
|
||||||
|
matchIndex: options.applicant.id,
|
||||||
|
participantIndex: options.current.length,
|
||||||
|
extraSeed: `manual-group:${options.current.map((entry) => entry.id).join('-')}:${openGroupIds.join('-')}`,
|
||||||
|
});
|
||||||
|
const groupId = rng.choice(openGroupIds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options.applicant,
|
||||||
|
groupId,
|
||||||
|
groupNo: groupCounts[groupId] ?? 0,
|
||||||
|
win: 0,
|
||||||
|
draw: 0,
|
||||||
|
lose: 0,
|
||||||
|
gl: 0,
|
||||||
|
seedRank: 0,
|
||||||
|
finalRank: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const selectWeighted = <T>(rng: ReturnType<typeof createTournamentRng>, pool: Array<{ item: T; weight: number }>): T =>
|
const selectWeighted = <T>(rng: ReturnType<typeof createTournamentRng>, pool: Array<{ item: T; weight: number }>): T =>
|
||||||
rng.choiceUsingWeightPair(pool.map((entry) => [entry.item, entry.weight]));
|
rng.choiceUsingWeightPair(pool.map((entry) => [entry.item, entry.weight]));
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,20 @@ describe('tournament router permissions and mutations', () => {
|
|||||||
|
|
||||||
expect(transport.gold.get(general.id)).toBe(1_800);
|
expect(transport.gold.get(general.id)).toBe(1_800);
|
||||||
expect(transport.commands.filter((command) => command.type === 'adjustGeneralResources')).toHaveLength(1);
|
expect(transport.commands.filter((command) => command.type === 'adjustGeneralResources')).toHaveLength(1);
|
||||||
|
expect(transport.commands.filter((command) => command.type === 'setMySetting')).toHaveLength(0);
|
||||||
|
const snapshot = await caller.tournament.getSnapshot();
|
||||||
|
expect(snapshot.participants).toHaveLength(1);
|
||||||
|
expect(snapshot.participants[0]).toMatchObject({
|
||||||
|
id: general.id,
|
||||||
|
groupId: expect.any(Number),
|
||||||
|
groupNo: 0,
|
||||||
|
win: 0,
|
||||||
|
draw: 0,
|
||||||
|
lose: 0,
|
||||||
|
gl: 0,
|
||||||
|
});
|
||||||
|
expect(snapshot.participants[0]!.groupId).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(snapshot.participants[0]!.groupId).toBeLessThan(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serializes concurrent bets and enforces the legacy per-user 1000 limit', async () => {
|
it('serializes concurrent bets and enforces the legacy per-user 1000 limit', async () => {
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import type {
|
|||||||
TournamentState,
|
TournamentState,
|
||||||
} from '../src/tournament/types.js';
|
} from '../src/tournament/types.js';
|
||||||
import { applyBattle, applyPreBattleStage, settleTournamentOutcome } from '../src/tournament/worker.js';
|
import { applyBattle, applyPreBattleStage, settleTournamentOutcome } from '../src/tournament/worker.js';
|
||||||
import { buildBettingPayouts, resolveBettingCloseAt, resolveNextAt } from '../src/tournament/workerHelpers.js';
|
import {
|
||||||
|
assignManualApplicantGroup,
|
||||||
|
buildBettingPayouts,
|
||||||
|
resolveBettingCloseAt,
|
||||||
|
resolveNextAt,
|
||||||
|
} from '../src/tournament/workerHelpers.js';
|
||||||
import type { TurnDaemonTransport } from '../src/daemon/transport.js';
|
import type { TurnDaemonTransport } from '../src/daemon/transport.js';
|
||||||
|
|
||||||
class MemoryRedis {
|
class MemoryRedis {
|
||||||
@@ -226,6 +231,45 @@ const runTournamentToCompletion = async (options: {
|
|||||||
const delayTick = async (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
const delayTick = async (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
describe('tournament worker schedule compatibility', () => {
|
describe('tournament worker schedule compatibility', () => {
|
||||||
|
it('수동 참가자를 즉시 남은 예선 조의 다음 슬롯에 배치한다', () => {
|
||||||
|
const current = Array.from({ length: 63 }, (_, index): TournamentParticipantEntry => {
|
||||||
|
const groupId = index < 47 ? index % 8 : (index + 1) % 8;
|
||||||
|
const groupNo = Math.floor(index / 8);
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
name: `참가자${index + 1}`,
|
||||||
|
leadership: 70,
|
||||||
|
strength: 70,
|
||||||
|
intel: 70,
|
||||||
|
level: 10,
|
||||||
|
groupId,
|
||||||
|
groupNo,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const groupCounts = Array.from({ length: 8 }, (_, groupId) =>
|
||||||
|
current.filter((entry) => entry.groupId === groupId).length
|
||||||
|
);
|
||||||
|
const openGroupId = groupCounts.findIndex((count) => count === 7);
|
||||||
|
expect(openGroupId).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(groupCounts.filter((count) => count === 7)).toHaveLength(1);
|
||||||
|
|
||||||
|
const applicant = assignManualApplicantGroup({
|
||||||
|
state: createTournamentState(),
|
||||||
|
baseSeed: 'manual-join-seed',
|
||||||
|
current,
|
||||||
|
applicant: {
|
||||||
|
id: 100,
|
||||||
|
name: '즉시배치',
|
||||||
|
leadership: 80,
|
||||||
|
strength: 81,
|
||||||
|
intel: 82,
|
||||||
|
level: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(applicant).toMatchObject({ groupId: openGroupId, groupNo: 7, win: 0, draw: 0, lose: 0, gl: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
it('catches up from the stored schedule instead of discarding elapsed legacy phases', () => {
|
it('catches up from the stored schedule instead of discarding elapsed legacy phases', () => {
|
||||||
const state = createTournamentState({
|
const state = createTournamentState({
|
||||||
termSeconds: 600,
|
termSeconds: 600,
|
||||||
@@ -600,6 +644,14 @@ describe('tournament worker (in-memory)', () => {
|
|||||||
expect(participants.some((entry) => entry.id === 99)).toBe(false);
|
expect(participants.some((entry) => entry.id === 99)).toBe(false);
|
||||||
expect(participants.some((entry) => entry.id === 1001)).toBe(true);
|
expect(participants.some((entry) => entry.id === 1001)).toBe(true);
|
||||||
expect(participants.some((entry) => entry.id < 0)).toBe(true);
|
expect(participants.some((entry) => entry.id < 0)).toBe(true);
|
||||||
|
expect(participants.every((entry) => entry.groupId !== undefined && entry.groupNo !== undefined)).toBe(true);
|
||||||
|
expect(participants.find((entry) => entry.id === 1)).toMatchObject({ groupId: expect.any(Number) });
|
||||||
|
expect(participants.find((entry) => entry.id === 1001)).toMatchObject({ groupId: expect.any(Number) });
|
||||||
|
expect(
|
||||||
|
Array.from({ length: 8 }, (_, groupId) =>
|
||||||
|
participants.filter((entry) => entry.groupId === groupId).length
|
||||||
|
)
|
||||||
|
).toEqual(Array.from({ length: 8 }, () => 8));
|
||||||
|
|
||||||
await store.setState(afterJoin);
|
await store.setState(afterJoin);
|
||||||
const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' });
|
const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' });
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string)
|
|||||||
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const installFixture = async (page: Page) => {
|
const installFixture = async (page: Page, options: { applicationOpen?: boolean } = {}) => {
|
||||||
|
let joined = false;
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
||||||
window.localStorage.setItem('sammo-game-profile', profile);
|
window.localStorage.setItem('sammo-game-profile', profile);
|
||||||
@@ -141,7 +142,7 @@ const installFixture = async (page: Page) => {
|
|||||||
if (operation === 'tournament.getSnapshot') {
|
if (operation === 'tournament.getSnapshot') {
|
||||||
return response({
|
return response({
|
||||||
state: {
|
state: {
|
||||||
stage: 0,
|
stage: options.applicationOpen ? 1 : 0,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
type: 0,
|
type: 0,
|
||||||
auto: false,
|
auto: false,
|
||||||
@@ -151,11 +152,32 @@ const installFixture = async (page: Page) => {
|
|||||||
nextAt: '2026-08-02T00:00:00.000Z',
|
nextAt: '2026-08-02T00:00:00.000Z',
|
||||||
winnerId: 1,
|
winnerId: 1,
|
||||||
},
|
},
|
||||||
participants,
|
participants:
|
||||||
|
options.applicationOpen && !joined
|
||||||
|
? []
|
||||||
|
: options.applicationOpen
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
...participants[0],
|
||||||
|
groupId: 0,
|
||||||
|
groupNo: 0,
|
||||||
|
win: 0,
|
||||||
|
draw: 0,
|
||||||
|
lose: 0,
|
||||||
|
gl: 0,
|
||||||
|
seedRank: 0,
|
||||||
|
finalRank: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: participants,
|
||||||
matches,
|
matches,
|
||||||
betCount: 16,
|
betCount: 16,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (operation === 'tournament.join') {
|
||||||
|
joined = true;
|
||||||
|
return response({ ok: true, count: 1 });
|
||||||
|
}
|
||||||
if (operation === 'tournament.getBettingSummary') {
|
if (operation === 'tournament.getBettingSummary') {
|
||||||
return response({
|
return response({
|
||||||
totals: Object.fromEntries(
|
totals: Object.fromEntries(
|
||||||
@@ -245,9 +267,67 @@ test('desktop bracket connects every real general slot to the next round', async
|
|||||||
expect(geometry.horizontalIdentities).toBe(true);
|
expect(geometry.horizontalIdentities).toBe(true);
|
||||||
expect(Math.abs(geometry.firstParentY - geometry.firstPairAverageY)).toBeLessThan(1);
|
expect(Math.abs(geometry.firstParentY - geometry.firstPairAverageY)).toBeLessThan(1);
|
||||||
|
|
||||||
|
const controls = await page.locator('#tournament-container').evaluate((container) => {
|
||||||
|
const bounds = (selector: string) => container.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
|
||||||
|
const refresh = bounds('.toolbar button:first-child');
|
||||||
|
const join = bounds('.join-button');
|
||||||
|
const close = bounds('.close-button');
|
||||||
|
return {
|
||||||
|
refresh: { width: refresh.width, height: refresh.height },
|
||||||
|
join: { width: join.width, height: join.height },
|
||||||
|
close: { width: close.width, height: close.height },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(controls.refresh).toEqual({ width: 72, height: 44 });
|
||||||
|
expect(controls.join).toEqual({ width: 72, height: 44 });
|
||||||
|
expect(controls.close).toEqual({ width: 88, height: 44 });
|
||||||
|
|
||||||
|
const firstSlot = page.locator('.desktop-bracket-name').first();
|
||||||
|
const oddsContainment = await firstSlot.evaluate((slot) => {
|
||||||
|
const card = slot.getBoundingClientRect();
|
||||||
|
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
cardTop: card.top,
|
||||||
|
cardBottom: card.bottom,
|
||||||
|
oddsTop: odds.top,
|
||||||
|
oddsBottom: odds.bottom,
|
||||||
|
cardHeight: card.height,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
|
||||||
|
expect(oddsContainment.oddsTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
|
||||||
|
expect(oddsContainment.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
|
||||||
|
|
||||||
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('join refresh shows the assigned preliminary group immediately with accessible controls', async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await installFixture(page, { applicationOpen: true });
|
||||||
|
await page.goto('tournament');
|
||||||
|
|
||||||
|
const refresh = page.getByRole('button', { name: '갱신' });
|
||||||
|
const join = page.getByRole('button', { name: '참가' });
|
||||||
|
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||||
|
await expect(join).toBeEnabled();
|
||||||
|
await join.click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다.');
|
||||||
|
await expect(join).toBeDisabled();
|
||||||
|
await expect(page.locator('.preliminary-grid .general-identity', { hasText: names[0] })).toBeVisible();
|
||||||
|
|
||||||
|
for (const control of [refresh, join, close]) {
|
||||||
|
const box = await control.boundingBox();
|
||||||
|
expect(box?.height).toBe(44);
|
||||||
|
expect(box?.width).toBeGreaterThanOrEqual(72);
|
||||||
|
}
|
||||||
|
await refresh.focus();
|
||||||
|
await expect(refresh).toBeFocused();
|
||||||
|
await refresh.hover();
|
||||||
|
await expect(refresh).toHaveCSS('filter', 'brightness(1.25)');
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||||
|
});
|
||||||
|
|
||||||
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
|
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
@@ -325,6 +405,14 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
|
|||||||
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight - 1);
|
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight - 1);
|
||||||
expect(identity.nameTop).toBeLessThan(identity.iconBottom);
|
expect(identity.nameTop).toBeLessThan(identity.iconBottom);
|
||||||
expect(identity.nameBottom).toBeGreaterThan(identity.iconTop);
|
expect(identity.nameBottom).toBeGreaterThan(identity.iconTop);
|
||||||
|
const firstMobileSlot = bracket.locator('.mobile-bracket-name').first();
|
||||||
|
const mobileOddsContainment = await firstMobileSlot.evaluate((slot) => {
|
||||||
|
const card = slot.getBoundingClientRect();
|
||||||
|
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
|
||||||
|
return { cardBottom: card.bottom, oddsBottom: odds.bottom, cardHeight: card.height };
|
||||||
|
});
|
||||||
|
expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
|
||||||
|
expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
|
||||||
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
|
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
|
||||||
await page.getByRole('tab', { name: '二조' }).first().click();
|
await page.getByRole('tab', { name: '二조' }).first().click();
|
||||||
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ const roundColumns = computed(() => [
|
|||||||
]);
|
]);
|
||||||
const desktopX = [110, 355, 600, 845, 1090];
|
const desktopX = [110, 355, 600, 845, 1090];
|
||||||
const cardWidth = 190;
|
const cardWidth = 190;
|
||||||
|
const desktopSlotHeight = 88;
|
||||||
|
const desktopCanvasHeight = desktopSlotHeight * 16;
|
||||||
const slotY = (columnIndex: number, slotIndex: number) => {
|
const slotY = (columnIndex: number, slotIndex: number) => {
|
||||||
const slotHeight = 72 * 2 ** columnIndex;
|
const slotHeight = desktopSlotHeight * 2 ** columnIndex;
|
||||||
return slotHeight / 2 + slotIndex * slotHeight;
|
return slotHeight / 2 + slotIndex * slotHeight;
|
||||||
};
|
};
|
||||||
const connections = computed(() =>
|
const connections = computed(() =>
|
||||||
@@ -77,8 +79,8 @@ const mobilePairs = computed(() => {
|
|||||||
<div class="desktop-round-labels" aria-hidden="true">
|
<div class="desktop-round-labels" aria-hidden="true">
|
||||||
<strong v-for="label in roundLabels" :key="label">{{ label }}</strong>
|
<strong v-for="label in roundLabels" :key="label">{{ label }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop-bracket-canvas">
|
<div class="desktop-bracket-canvas" :style="{ height: `${desktopCanvasHeight}px` }">
|
||||||
<svg viewBox="0 0 1200 1152" aria-hidden="true">
|
<svg :viewBox="`0 0 1200 ${desktopCanvasHeight}`" aria-hidden="true">
|
||||||
<g v-for="connection in connections" :key="connection.id">
|
<g v-for="connection in connections" :key="connection.id">
|
||||||
<path
|
<path
|
||||||
class="bracket-connector"
|
class="bracket-connector"
|
||||||
@@ -177,7 +179,6 @@ const mobilePairs = computed(() => {
|
|||||||
.desktop-bracket-canvas {
|
.desktop-bracket-canvas {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1152px;
|
|
||||||
}
|
}
|
||||||
.desktop-bracket-canvas svg {
|
.desktop-bracket-canvas svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -200,7 +201,7 @@ const mobilePairs = computed(() => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: clamp(140px, 16vw, 190px);
|
width: clamp(140px, 16vw, 190px);
|
||||||
min-height: 68px;
|
min-height: 82px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
@@ -265,7 +266,7 @@ const mobilePairs = computed(() => {
|
|||||||
.mobile-bracket-name {
|
.mobile-bracket-name {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 68px;
|
min-height: 82px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
background: rgb(58 33 24 / 94%);
|
background: rgb(58 33 24 / 94%);
|
||||||
|
|||||||
@@ -304,16 +304,16 @@ const placeBet = async (targetId: number) => {
|
|||||||
background: #142b42 var(--sammo-texture-blue);
|
background: #142b42 var(--sammo-texture-blue);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
height: 55.6875px;
|
min-height: 68px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 19.1875px;
|
line-height: 19.1875px;
|
||||||
}
|
}
|
||||||
.close-button {
|
.close-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 62px;
|
width: 88px;
|
||||||
height: 35.5px;
|
height: 44px;
|
||||||
padding: 8px 12px;
|
padding: 10px 16px;
|
||||||
border: 1px solid #375a7f;
|
border: 1px solid #375a7f;
|
||||||
border-radius: 5.25px;
|
border-radius: 5.25px;
|
||||||
background: #375a7f;
|
background: #375a7f;
|
||||||
@@ -323,10 +323,16 @@ const placeBet = async (targetId: number) => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
min-height: 36.5px;
|
min-height: 46px;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.toolbar button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.error {
|
.error {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|||||||
@@ -385,16 +385,16 @@ const start = async () => {
|
|||||||
background: #142b42 var(--sammo-texture-blue);
|
background: #142b42 var(--sammo-texture-blue);
|
||||||
}
|
}
|
||||||
.legacy-title {
|
.legacy-title {
|
||||||
height: 55.6875px;
|
min-height: 68px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 19.1875px;
|
line-height: 19.1875px;
|
||||||
}
|
}
|
||||||
.close-button {
|
.close-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 62px;
|
width: 88px;
|
||||||
height: 35.5px;
|
height: 44px;
|
||||||
padding: 8px 12px;
|
padding: 10px 16px;
|
||||||
border: 1px solid #375a7f;
|
border: 1px solid #375a7f;
|
||||||
border-radius: 5.25px;
|
border-radius: 5.25px;
|
||||||
background: #375a7f;
|
background: #375a7f;
|
||||||
@@ -404,9 +404,15 @@ const start = async () => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
min-height: 36.5px;
|
min-height: 46px;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
.toolbar button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.operator-row,
|
.operator-row,
|
||||||
.state-row,
|
.state-row,
|
||||||
.error-row,
|
.error-row,
|
||||||
|
|||||||
Reference in New Issue
Block a user