@@ -114,6 +114,7 @@ export class BracketsViewer {
114
114
. map ( match => ( {
115
115
...match ,
116
116
metadata : {
117
+ stageType : stage . type ,
117
118
games : data . matchGames . filter ( game => game . parent_id === match . id ) ,
118
119
} ,
119
120
} ) ) ,
@@ -194,7 +195,7 @@ export class BracketsViewer {
194
195
throw Error ( `Unknown bracket type: ${ stage . type as string } ` ) ;
195
196
}
196
197
197
- this . renderConsolationMatches ( root , matchesByGroup ) ;
198
+ this . renderConsolationMatches ( root , stage , matchesByGroup ) ;
198
199
}
199
200
200
201
/**
@@ -265,9 +266,10 @@ export class BracketsViewer {
265
266
* Renders a list of consolation matches.
266
267
*
267
268
* @param root The root element.
269
+ * @param stage The stage to render.
268
270
* @param matchesByGroup A list of matches for each group.
269
271
*/
270
- private renderConsolationMatches ( root : DocumentFragment , matchesByGroup : MatchWithMetadata [ ] [ ] ) : void {
272
+ private renderConsolationMatches ( root : DocumentFragment , stage : Stage , matchesByGroup : MatchWithMetadata [ ] [ ] ) : void {
271
273
const consolationMatches = matchesByGroup [ - 1 ] ;
272
274
if ( ! consolationMatches ?. length )
273
275
return ;
@@ -281,6 +283,7 @@ export class BracketsViewer {
281
283
...match ,
282
284
metadata : {
283
285
label : lang . t ( 'match-label.default' , { matchNumber : ++ matchNumber } ) ,
286
+ stageType : stage . type ,
284
287
games : [ ] ,
285
288
} ,
286
289
} , true ) ) ;
@@ -297,15 +300,13 @@ export class BracketsViewer {
297
300
* @param matchesByGroup A list of matches for each group.
298
301
*/
299
302
private renderSingleElimination ( container : HTMLElement , matchesByGroup : MatchWithMetadata [ ] [ ] ) : void {
300
- const hasFinal = matchesByGroup [ 1 ] !== undefined ;
301
303
const bracketMatches = splitBy ( matchesByGroup [ 0 ] , 'round_id' ) . map ( matches => sortBy ( matches , 'number' ) ) ;
304
+ const { hasFinal, connectFinal, finalMatches } = this . getFinalInfoSingleElimination ( matchesByGroup ) ;
302
305
303
- this . renderBracket ( container , bracketMatches , lang . getRoundName , 'single_bracket' ) ;
306
+ this . renderBracket ( container , bracketMatches , lang . getRoundName , 'single_bracket' , connectFinal ) ;
304
307
305
- if ( hasFinal ) {
306
- const finalMatches = sortBy ( matchesByGroup [ 1 ] , 'number' ) ;
308
+ if ( hasFinal )
307
309
this . renderFinal ( container , 'consolation_final' , finalMatches ) ;
308
- }
309
310
}
310
311
311
312
/**
@@ -316,30 +317,77 @@ export class BracketsViewer {
316
317
*/
317
318
private renderDoubleElimination ( container : HTMLElement , matchesByGroup : MatchWithMetadata [ ] [ ] ) : void {
318
319
const hasLoserBracket = matchesByGroup [ 1 ] !== undefined ;
319
- const hasFinal = matchesByGroup [ 2 ] !== undefined ;
320
320
const winnerBracketMatches = splitBy ( matchesByGroup [ 0 ] , 'round_id' ) . map ( matches => sortBy ( matches , 'number' ) ) ;
321
+ const { hasFinal, connectFinal, grandFinalMatches, consolationFinalMatches } = this . getFinalInfoDoubleElimination ( matchesByGroup ) ;
321
322
322
- this . renderBracket ( container , winnerBracketMatches , lang . getWinnerBracketRoundName , 'winner_bracket' , hasFinal ) ;
323
+ this . renderBracket ( container , winnerBracketMatches , lang . getWinnerBracketRoundName , 'winner_bracket' , connectFinal ) ;
323
324
324
325
if ( hasLoserBracket ) {
325
326
const loserBracketMatches = splitBy ( matchesByGroup [ 1 ] , 'round_id' ) . map ( matches => sortBy ( matches , 'number' ) ) ;
326
327
this . renderBracket ( container , loserBracketMatches , lang . getLoserBracketRoundName , 'loser_bracket' ) ;
327
328
}
328
329
329
330
if ( hasFinal ) {
330
- const finalMatches = sortBy ( matchesByGroup [ 2 ] , 'number' ) ;
331
- this . renderFinal ( container , 'grand_final ' , finalMatches ) ;
331
+ this . renderFinal ( container , 'grand_final' , grandFinalMatches ) ;
332
+ this . renderFinal ( container , 'consolation_final ' , consolationFinalMatches ) ;
332
333
}
333
334
}
334
335
336
+ /**
337
+ * Returns information about the final group in single elimination.
338
+ *
339
+ * @param matchesByGroup A list of matches for each group.
340
+ */
341
+ private getFinalInfoSingleElimination ( matchesByGroup : MatchWithMetadata [ ] [ ] ) : {
342
+ hasFinal : boolean ,
343
+ connectFinal : boolean ,
344
+ finalMatches : MatchWithMetadata [ ]
345
+ } {
346
+ const hasFinal = matchesByGroup [ 1 ] !== undefined ;
347
+ const finalMatches = sortBy ( matchesByGroup [ 1 ] ?? [ ] , 'number' ) ;
348
+
349
+ // In single elimination, the only possible type of final is a consolation final,
350
+ // and it has to be disconnected from the bracket because it doesn't directly follows its last match.
351
+ const connectFinal = false ;
352
+
353
+ return { hasFinal, connectFinal, finalMatches } ;
354
+ }
355
+
356
+ /**
357
+ * Returns information about the final group in double elimination.
358
+ *
359
+ * @param matchesByGroup A list of matches for each group.
360
+ */
361
+ private getFinalInfoDoubleElimination ( matchesByGroup : MatchWithMetadata [ ] [ ] ) : {
362
+ hasFinal : boolean ,
363
+ connectFinal : boolean ,
364
+ grandFinalMatches : MatchWithMetadata [ ]
365
+ consolationFinalMatches : MatchWithMetadata [ ]
366
+ } {
367
+ const hasFinal = matchesByGroup [ 2 ] !== undefined ;
368
+ const finalMatches = sortBy ( matchesByGroup [ 2 ] ?? [ ] , 'number' ) ;
369
+
370
+ // All grand final matches have a `number: 1` property. We can have 0, 1 or 2 of them.
371
+ const grandFinalMatches = finalMatches . filter ( match => match . number === 1 ) ;
372
+ // All consolation matches have a `number: 2` property (set by the manager). We can only have 0 or 1 of them.
373
+ const consolationFinalMatches = finalMatches . filter ( match => match . number === 2 ) ;
374
+
375
+ // In double elimination, we can have a grand final, a consolation final, or both.
376
+ // We only want to connect the upper bracket with the final group when we have at least one grand final match.
377
+ // The grand final will always be placed directly next to the bracket.
378
+ const connectFinal = grandFinalMatches . length > 0 ;
379
+
380
+ return { hasFinal, connectFinal, grandFinalMatches, consolationFinalMatches } ;
381
+ }
382
+
335
383
/**
336
384
* Renders a bracket.
337
385
*
338
386
* @param container The container to render into.
339
387
* @param matchesByRound A list of matches for each round.
340
388
* @param getRoundName A function giving a round's name based on its number.
341
389
* @param bracketType Type of the bracket.
342
- * @param connectFinal Whether to connect the last match of the bracket to the final.
390
+ * @param connectFinal Whether to connect the last match of the bracket to the first match of the final group .
343
391
*/
344
392
private renderBracket ( container : HTMLElement , matchesByRound : MatchWithMetadata [ ] [ ] , getRoundName : RoundNameGetter , bracketType : GroupType , connectFinal ?: boolean ) : void {
345
393
const groupId = matchesByRound [ 0 ] [ 0 ] . group_id ;
@@ -392,6 +440,10 @@ export class BracketsViewer {
392
440
* @param matches Matches of the final.
393
441
*/
394
442
private renderFinal ( container : HTMLElement , finalType : FinalType , matches : MatchWithMetadata [ ] ) : void {
443
+ // Double elimination stages can have a grand final, or a consolation final, or both.
444
+ if ( matches . length === 0 )
445
+ return ;
446
+
395
447
const upperBracket = container . querySelector ( '.bracket .rounds' ) ;
396
448
if ( ! upperBracket ) throw Error ( 'Upper bracket not found.' ) ;
397
449
@@ -400,14 +452,16 @@ export class BracketsViewer {
400
452
const finalMatches = matches . slice ( 0 , displayCount ) ;
401
453
const roundCount = finalMatches . length ;
402
454
455
+ const defaultFinalRoundNameGetter : RoundNameGetter = ( { roundNumber, roundCount } ) => lang . getFinalMatchLabel ( finalType , roundNumber , roundCount ) ;
456
+
403
457
for ( let roundIndex = 0 ; roundIndex < finalMatches . length ; roundIndex ++ ) {
404
458
const roundNumber = roundIndex + 1 ;
405
459
const roundName = this . getRoundName ( {
406
460
roundNumber,
407
461
roundCount,
408
462
groupType : lang . toI18nKey ( 'final_group' ) ,
409
463
finalType : lang . toI18nKey ( finalType ) ,
410
- } , lang . getRoundName ) ;
464
+ } , defaultFinalRoundNameGetter ) ;
411
465
412
466
const finalMatch : MatchWithMetadata = {
413
467
...finalMatches [ roundIndex ] ,
@@ -502,18 +556,18 @@ export class BracketsViewer {
502
556
/**
503
557
* Creates a match in a final.
504
558
*
505
- * @param type Type of the final.
559
+ * @param finalType Type of the final.
506
560
* @param match Information about the match.
507
561
*/
508
- private createFinalMatch ( type : FinalType , match : MatchWithMetadata ) : HTMLElement {
562
+ private createFinalMatch ( finalType : FinalType , match : MatchWithMetadata ) : HTMLElement {
509
563
const { roundNumber, roundCount } = match . metadata ;
510
564
511
565
if ( roundNumber === undefined || roundCount === undefined )
512
566
throw Error ( `The match's internal data is missing roundNumber or roundCount: ${ JSON . stringify ( match ) } ` ) ;
513
567
514
- const connection = dom . getFinalConnection ( type , roundNumber , roundCount ) ;
515
- const matchLabel = lang . getFinalMatchLabel ( type , roundNumber , roundCount ) ;
516
- const originHint = lang . getFinalOriginHint ( type , roundNumber ) ;
568
+ const connection = dom . getFinalConnection ( finalType , roundNumber , roundCount ) ;
569
+ const matchLabel = lang . getFinalMatchLabel ( finalType , roundNumber , roundCount ) ;
570
+ const originHint = lang . getFinalOriginHint ( match . metadata . stageType , finalType , roundNumber ) ;
517
571
518
572
match . metadata . connection = connection ;
519
573
match . metadata . label = matchLabel ;
0 commit comments