Refactor series handling in backend to include player context in Toss, Ban, Pick, and Record actions. Update service methods to accept player parameters, enhancing authorization checks and ensuring proper team actions. Modify integration tests to reflect new method signatures and improve tournament hydration logic in the store. Enhance frontend components to support new series features and improve user experience with live match navigation.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { adaptEvent, adaptPlayer, adaptSeries, api, type RawEvent, type RawPlayer, type RawSeries } from './client'
|
||||
import { adaptEvent, adaptPlayer, adaptSeries, adaptTournament, api, type RawEvent, type RawPlayer, type RawSeries } from './client'
|
||||
|
||||
const player: RawPlayer = {
|
||||
id: 'player-1',
|
||||
@@ -79,6 +79,21 @@ describe('backend DTO adapters', () => {
|
||||
}
|
||||
expect(adaptSeries(series).score).toEqual({ alpha: 0, beta: 1 })
|
||||
expect(adaptSeries({ ...series, phase: 'CoinTossPending', results: null, maps: null, audit: null }).score).toEqual({ alpha: 0, beta: 0 })
|
||||
expect(adaptTournament({ id: 't-1', eventId: 'event-1', name: 'Cup', winnerTeamId: '', teamIds: ['a', 'b'], rounds: [[{ ...series, phase: 'MapBan' }]] }).matches[0].status).toBe('live')
|
||||
const heroDraft = adaptSeries({
|
||||
...series,
|
||||
phase: 'HeroBan',
|
||||
seriesBans: { a: ['Ana'], b: ['Tracer'] },
|
||||
heroDraft: {
|
||||
heroes: [{ name: 'Ana', role: 'Support' }, { name: 'Kiriko', role: 'Support' }],
|
||||
teamIds: ['a', 'b'],
|
||||
firstTeamId: 'a',
|
||||
bansPerTeam: 2,
|
||||
currentBans: [],
|
||||
},
|
||||
})
|
||||
expect(heroDraft.heroBanHistory).toEqual({ alpha: ['Ana'], beta: ['Tracer'] })
|
||||
expect(heroDraft.options.find((option) => option.name === 'Ana')).toMatchObject({ disabled: true })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface DraftAction {
|
||||
}
|
||||
export interface Series {
|
||||
id: string
|
||||
eventId: string
|
||||
tournamentId?: string
|
||||
status: 'scheduled' | 'coin_toss' | 'map_draft' | 'hero_draft' | 'playing' | 'completed'
|
||||
roundLabel: string
|
||||
@@ -97,6 +98,7 @@ export interface Series {
|
||||
version: number
|
||||
}
|
||||
options: DraftOption[]
|
||||
heroBanHistory: Record<TeamSide, string[]>
|
||||
maps: Array<{
|
||||
number: number
|
||||
name: string
|
||||
@@ -109,7 +111,7 @@ export interface Series {
|
||||
}
|
||||
export interface BracketMatch {
|
||||
id: string; round: number; slot: number; label: string
|
||||
teamAlpha?: string; teamBeta?: string; scoreAlpha?: number; scoreBeta?: number
|
||||
teamAlpha?: string; teamBeta?: string; teamAlphaId?: string; teamBetaId?: string; scoreAlpha?: number; scoreBeta?: number
|
||||
winner?: TeamSide; seriesId?: string; status: 'pending' | 'ready' | 'live' | 'completed'
|
||||
}
|
||||
export interface Bracket { id: string; title: string; rounds: string[]; matches: BracketMatch[] }
|
||||
@@ -164,6 +166,7 @@ export interface RawSeries {
|
||||
mapDraft?: { pool: string[] | null; banned: string[] | null; firstTeamId: string; teamIds: [string, string]; actions: RawDraftAction[] | null }
|
||||
heroDraft?: { heroes: Array<{ name: string; role: 'Tank' | 'Damage' | 'Support' }> | null; teamIds: [string, string]; firstTeamId: string; bansPerTeam: number; currentBans: RawDraftAction[] | null }
|
||||
currentMap: string; playedMaps: string[] | null; nextMapPickerId: string; availableMaps: string[] | null
|
||||
seriesBans?: Record<string, string[]> | null
|
||||
maps: Array<{ number: number; name: string; heroBans: RawDraftAction[] | null; result?: RawMapResult }> | null
|
||||
audit: Array<{ kind: DraftAction['kind']; summary: string; teamId?: string; actorAccountId: string; at: string }> | null
|
||||
results: RawMapResult[] | null; version: number
|
||||
@@ -345,10 +348,14 @@ export function adaptSeries(raw: RawSeries, teams: Team[] = []): Series {
|
||||
: phase === 'MapPick'
|
||||
? (raw.availableMaps ?? []).map((name) => ({ id: name, name }))
|
||||
: phase === 'HeroBan' && raw.heroDraft
|
||||
? (raw.heroDraft.heroes ?? []).filter((hero) => !(raw.heroDraft?.currentBans ?? []).some((ban) => ban.value === hero.name)).map((hero) => ({ id: hero.name, name: hero.name, role: roleFromRaw(hero.role) }))
|
||||
? (raw.heroDraft.heroes ?? []).filter((hero) => !(raw.heroDraft?.currentBans ?? []).some((ban) => ban.value === hero.name)).map((hero) => {
|
||||
const repeated = (raw.seriesBans?.[nextHeroTeam ?? ''] ?? []).includes(hero.name)
|
||||
return { id: hero.name, name: hero.name, role: roleFromRaw(hero.role), disabled: repeated, disabledReason: repeated ? 'Already banned by this team in this Bo3' : undefined }
|
||||
})
|
||||
: []
|
||||
return {
|
||||
id: raw.id,
|
||||
eventId: raw.eventId,
|
||||
tournamentId: raw.tournamentId,
|
||||
status,
|
||||
roundLabel: 'Tournament series',
|
||||
@@ -364,13 +371,20 @@ export function adaptSeries(raw: RawSeries, teams: Team[] = []): Series {
|
||||
version: raw.version,
|
||||
},
|
||||
options,
|
||||
heroBanHistory: {
|
||||
alpha: raw.seriesBans?.[raw.teamAId] ?? [],
|
||||
beta: raw.seriesBans?.[raw.teamBId] ?? [],
|
||||
},
|
||||
maps: (raw.maps ?? activeResults.map((result, index) => ({ number: index + 1, name: result.mapName, heroBans: [], result }))).map((map) => ({
|
||||
number: map.number,
|
||||
name: map.name,
|
||||
mode: 'Map',
|
||||
status: map.result ? 'completed' : phase === 'Playing' ? 'playing' : phase === 'HeroBan' ? 'ready' : 'drafting',
|
||||
winner: map.result ? (map.result.outcome === 'TeamAWin' ? 'alpha' : map.result.outcome === 'TeamBWin' ? 'beta' : 'draw') : undefined,
|
||||
heroBans: (map.heroBans ?? []).map((ban) => ({ hero: ban.value, team: ban.teamId === raw.teamAId ? 'alpha' : 'beta', role: 'damage' as PlayerRole })),
|
||||
heroBans: (map.heroBans ?? []).map((ban) => {
|
||||
const heroRole = raw.heroDraft?.heroes?.find((hero) => hero.name === ban.value)?.role
|
||||
return { hero: ban.value, team: ban.teamId === raw.teamAId ? 'alpha' : 'beta', role: heroRole ? roleFromRaw(heroRole) : 'damage' as PlayerRole }
|
||||
}),
|
||||
})),
|
||||
audit: (raw.audit ?? []).map((action, index) => ({ id: `action-${index}`, kind: action.kind, summary: action.summary, actorName: action.actorAccountId, createdAt: action.at })),
|
||||
}
|
||||
@@ -390,11 +404,13 @@ export function adaptTournament(raw: RawTournament, teams: Team[] = []): Bracket
|
||||
label: `Match ${slot + 1}`,
|
||||
teamAlpha: names.get(series.teamAId) ?? (series.teamAId || undefined),
|
||||
teamBeta: names.get(series.teamBId) ?? (series.teamBId || undefined),
|
||||
teamAlphaId: series.teamAId || undefined,
|
||||
teamBetaId: series.teamBId || undefined,
|
||||
scoreAlpha: adapted.score.alpha,
|
||||
scoreBeta: adapted.score.beta,
|
||||
winner: series.winnerTeamId ? (series.winnerTeamId === series.teamAId ? 'alpha' : 'beta') : undefined,
|
||||
seriesId: series.id,
|
||||
status: series.winnerTeamId ? 'completed' : series.teamAId && series.teamBId ? 'ready' : 'pending',
|
||||
status: series.winnerTeamId ? 'completed' : series.teamAId && series.teamBId && series.phase ? 'live' : series.teamAId && series.teamBId ? 'ready' : 'pending',
|
||||
} satisfies BracketMatch
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ export const demoEvents: MixEvent[] = [
|
||||
version: 6,
|
||||
rulesetId: 'standard-control-hybrid-control',
|
||||
activeSeriesId: 'series-1',
|
||||
tournamentId: 'bracket-1',
|
||||
},
|
||||
{
|
||||
id: 'event-4',
|
||||
@@ -224,6 +225,8 @@ export const demoBalanceCandidates: BalanceCandidate[] = [
|
||||
|
||||
export const demoSeries: Series = {
|
||||
id: 'series-1',
|
||||
eventId: 'event-3',
|
||||
tournamentId: 'bracket-1',
|
||||
status: 'hero_draft',
|
||||
roundLabel: 'Semifinal 1',
|
||||
teamAlpha: alpha,
|
||||
@@ -245,6 +248,10 @@ export const demoSeries: Series = {
|
||||
{ id: 'hero-5', name: 'Winston', role: 'tank', disabled: true, disabledReason: 'Already banned by this team' },
|
||||
{ id: 'hero-6', name: 'Sigma', role: 'tank' },
|
||||
],
|
||||
heroBanHistory: {
|
||||
alpha: ['Winston', 'D.Va'],
|
||||
beta: ['Ana', 'Tracer'],
|
||||
},
|
||||
maps: [
|
||||
{
|
||||
number: 1,
|
||||
@@ -282,8 +289,8 @@ export const demoBracket: Bracket = {
|
||||
title: 'Summer Clash #4',
|
||||
rounds: ['Semifinals', 'Grand Final'],
|
||||
matches: [
|
||||
{ id: 'm1', round: 0, slot: 0, label: 'Semifinal 1', teamAlpha: 'Ember Wolves', teamBeta: 'Azure Phantoms', scoreAlpha: 1, scoreBeta: 0, status: 'live', seriesId: 'series-1' },
|
||||
{ id: 'm2', round: 0, slot: 1, label: 'Semifinal 2', teamAlpha: 'Neon Foxes', teamBeta: 'Void Runners', scoreAlpha: 2, scoreBeta: 1, winner: 'alpha', status: 'completed', seriesId: 'series-2' },
|
||||
{ id: 'm1', round: 0, slot: 0, label: 'Semifinal 1', teamAlpha: 'Ember Wolves', teamBeta: 'Azure Phantoms', teamAlphaId: 'team-alpha', teamBetaId: 'team-beta', scoreAlpha: 1, scoreBeta: 0, status: 'live', seriesId: 'series-1' },
|
||||
{ id: 'm2', round: 0, slot: 1, label: 'Semifinal 2', teamAlpha: 'Neon Foxes', teamBeta: 'Void Runners', teamAlphaId: 'team-neon', teamBetaId: 'team-void', scoreAlpha: 0, scoreBeta: 1, status: 'live', seriesId: 'series-2' },
|
||||
{ id: 'm3', round: 1, slot: 0, label: 'Grand Final', teamBeta: 'Neon Foxes', status: 'pending' },
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user