Files
gridpilot.gg/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts
2025-12-10 18:28:32 +01:00

149 lines
4.4 KiB
TypeScript

import type { ChampionshipConfig } from '@gridpilot/racing/domain/value-objects/ChampionshipConfig';
import type { BonusRule } from '@gridpilot/racing/domain/value-objects/BonusRule';
import type {
ILeagueScoringConfigPresenter,
LeagueScoringConfigData,
LeagueScoringConfigViewModel,
LeagueScoringChampionshipViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueScoringConfigPresenter';
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
private viewModel: LeagueScoringConfigViewModel | null = null;
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
const championships: LeagueScoringChampionshipViewModel[] =
data.championships.map((champ) => this.mapChampionship(champ));
const dropPolicySummary =
data.preset?.dropPolicySummary ??
this.deriveDropPolicyDescriptionFromChampionships(data.championships);
this.viewModel = {
leagueId: data.leagueId,
seasonId: data.seasonId,
gameId: data.gameId,
gameName: data.gameName,
scoringPresetId: data.scoringPresetId,
scoringPresetName: data.preset?.name,
dropPolicySummary,
championships,
};
return this.viewModel;
}
getViewModel(): LeagueScoringConfigViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
const bonusSummary = this.buildBonusSummary(
championship.bonusRulesBySessionType ?? {},
);
const dropPolicyDescription = this.deriveDropPolicyDescription(
championship.dropScorePolicy,
);
return {
id: championship.id,
name: championship.name,
type: championship.type,
sessionTypes,
pointsPreview,
bonusSummary,
dropPolicyDescription,
};
}
private buildPointsPreview(
tables: Record<string, any>,
): Array<{ sessionType: string; position: number; points: number }> {
const preview: Array<{
sessionType: string;
position: number;
points: number;
}> = [];
const maxPositions = 10;
for (const [sessionType, table] of Object.entries(tables)) {
for (let pos = 1; pos <= maxPositions; pos++) {
const points = table.getPointsForPosition(pos);
if (points && points !== 0) {
preview.push({
sessionType,
position: pos,
points,
});
}
}
}
return preview;
}
private buildBonusSummary(
bonusRulesBySessionType: Record<string, BonusRule[]>,
): string[] {
const summaries: string[] = [];
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
for (const rule of rules) {
if (rule.type === 'fastestLap') {
const base = `Fastest lap in ${sessionType}`;
if (rule.requiresFinishInTopN) {
summaries.push(
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
);
} else {
summaries.push(`${base} +${rule.points} points`);
}
} else {
summaries.push(
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
);
}
}
}
return summaries;
}
private deriveDropPolicyDescriptionFromChampionships(
championships: ChampionshipConfig[],
): string {
const first = championships[0];
if (!first) {
return 'All results count';
}
return this.deriveDropPolicyDescription(first.dropScorePolicy);
}
private deriveDropPolicyDescription(policy: {
strategy: string;
count?: number;
dropCount?: number;
}): string {
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count towards the championship`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped from the championship total`;
}
return 'Custom drop score rules apply';
}
}