Extract event and race setup logic into module
This commit is contained in:
785
src/event_race_logic.js
Normal file
785
src/event_race_logic.js
Normal file
@@ -0,0 +1,785 @@
|
||||
export function normalizeRaceTeam(team, { uid }) {
|
||||
return {
|
||||
id: String(team?.id || uid("team")),
|
||||
name: String(team?.name || "").trim(),
|
||||
driverIds: Array.isArray(team?.driverIds) ? team.driverIds.filter(Boolean) : [],
|
||||
carIds: Array.isArray(team?.carIds) ? team.carIds.filter(Boolean) : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function normalizeStoredRacePreset(preset, { uid }) {
|
||||
return {
|
||||
id: String(preset?.id || uid("preset")),
|
||||
name: String(preset?.name || "").trim(),
|
||||
values: preset?.values && typeof preset.values === "object" ? { ...preset.values } : {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getRaceFormatPresets({ state, t, uid, normalizeStoredRacePreset }) {
|
||||
const builtins = [
|
||||
{
|
||||
id: "custom",
|
||||
label: t("events.preset_custom"),
|
||||
values: {},
|
||||
},
|
||||
{
|
||||
id: "short_technical",
|
||||
label: t("events.preset_short_technical"),
|
||||
values: {
|
||||
qualifyingScoring: "points",
|
||||
qualifyingRounds: 3,
|
||||
carsPerHeat: 6,
|
||||
qualDurationMin: 5,
|
||||
qualStartMode: "staggered",
|
||||
qualSeedLapCount: 3,
|
||||
qualSeedMethod: "best_sum",
|
||||
countedQualRounds: 2,
|
||||
qualifyingPointsTable: "rank_low",
|
||||
qualifyingTieBreak: "best_lap",
|
||||
carsPerFinal: 8,
|
||||
finalLegs: 3,
|
||||
countedFinalLegs: 2,
|
||||
finalDurationMin: 5,
|
||||
finalStartMode: "position",
|
||||
followUpSec: 10,
|
||||
minLapMs: 11000,
|
||||
maxLapMs: 60000,
|
||||
bumpCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "club_qualifying",
|
||||
label: t("events.preset_club_qualifying"),
|
||||
values: {
|
||||
qualifyingScoring: "points",
|
||||
qualifyingRounds: 4,
|
||||
carsPerHeat: 8,
|
||||
qualDurationMin: 5,
|
||||
qualStartMode: "staggered",
|
||||
qualSeedLapCount: 3,
|
||||
qualSeedMethod: "best_sum",
|
||||
countedQualRounds: 2,
|
||||
qualifyingPointsTable: "rank_low",
|
||||
qualifyingTieBreak: "rounds",
|
||||
carsPerFinal: 8,
|
||||
finalLegs: 3,
|
||||
countedFinalLegs: 2,
|
||||
finalDurationMin: 5,
|
||||
finalStartMode: "position",
|
||||
followUpSec: 15,
|
||||
minLapMs: 12000,
|
||||
maxLapMs: 60000,
|
||||
bumpCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ifmar",
|
||||
label: t("events.preset_ifmar"),
|
||||
values: {
|
||||
qualifyingScoring: "points",
|
||||
qualifyingRounds: 5,
|
||||
carsPerHeat: 10,
|
||||
qualDurationMin: 5,
|
||||
qualStartMode: "staggered",
|
||||
qualSeedLapCount: 3,
|
||||
qualSeedMethod: "best_sum",
|
||||
countedQualRounds: 3,
|
||||
qualifyingPointsTable: "ifmar",
|
||||
qualifyingTieBreak: "best_lap",
|
||||
carsPerFinal: 10,
|
||||
finalLegs: 3,
|
||||
countedFinalLegs: 2,
|
||||
finalDurationMin: 5,
|
||||
finalStartMode: "position",
|
||||
followUpSec: 15,
|
||||
minLapMs: 12000,
|
||||
maxLapMs: 70000,
|
||||
bumpCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "endurance",
|
||||
label: t("events.preset_endurance"),
|
||||
values: {
|
||||
qualifyingScoring: "best",
|
||||
qualifyingRounds: 1,
|
||||
carsPerHeat: 12,
|
||||
qualDurationMin: 10,
|
||||
qualStartMode: "mass",
|
||||
qualSeedLapCount: 0,
|
||||
qualSeedMethod: "best_sum",
|
||||
countedQualRounds: 1,
|
||||
qualifyingPointsTable: "rank_low",
|
||||
qualifyingTieBreak: "best_round",
|
||||
carsPerFinal: 12,
|
||||
finalLegs: 1,
|
||||
countedFinalLegs: 1,
|
||||
finalDurationMin: 240,
|
||||
finalStartMode: "mass",
|
||||
followUpSec: 60,
|
||||
minLapMs: 10000,
|
||||
maxLapMs: 120000,
|
||||
bumpCount: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
const customPresets = Array.isArray(state.settings?.racePresets)
|
||||
? state.settings.racePresets
|
||||
.map((preset) => normalizeStoredRacePreset(preset))
|
||||
.filter((preset) => preset.name)
|
||||
.map((preset) => ({
|
||||
id: preset.id,
|
||||
label: preset.name,
|
||||
custom: true,
|
||||
values: { ...preset.values },
|
||||
}))
|
||||
: [];
|
||||
return [...builtins, ...customPresets];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function applyRaceFormatPreset(event, presetId, { getRaceFormatPresets }) {
|
||||
const preset = getRaceFormatPresets().find((item) => item.id === presetId);
|
||||
if (!preset || preset.id === "custom") {
|
||||
event.raceConfig.presetId = "custom";
|
||||
return;
|
||||
}
|
||||
Object.assign(event.raceConfig, preset.values, { presetId: preset.id });
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function buildRaceFormatConfigFromForm(form, event, { normalizeStartMode, getEventTeams }) {
|
||||
return {
|
||||
presetId: String(form.get("presetId") || "custom").trim() || "custom",
|
||||
qualifyingScoring: String(form.get("qualifyingScoring") || "points") === "best" ? "best" : "points",
|
||||
qualifyingRounds: Math.max(1, Number(form.get("qualifyingRounds") || 3) || 3),
|
||||
carsPerHeat: Math.max(2, Number(form.get("carsPerHeat") || 8) || 8),
|
||||
qualDurationMin: Math.max(1, Number(form.get("qualDurationMin") || 5) || 5),
|
||||
qualStartMode: normalizeStartMode(String(form.get("qualStartMode") || "staggered")),
|
||||
qualSeedLapCount: Math.max(0, Number(form.get("qualSeedLapCount") || 2) || 0),
|
||||
qualSeedMethod: ["best_sum", "average", "consecutive"].includes(String(form.get("qualSeedMethod") || "").toLowerCase())
|
||||
? String(form.get("qualSeedMethod")).toLowerCase()
|
||||
: "best_sum",
|
||||
countedQualRounds: Math.max(1, Number(form.get("countedQualRounds") || 1) || 1),
|
||||
qualifyingPointsTable: ["rank_low", "field_desc", "ifmar"].includes(String(form.get("qualifyingPointsTable") || "").toLowerCase())
|
||||
? String(form.get("qualifyingPointsTable")).toLowerCase()
|
||||
: "rank_low",
|
||||
qualifyingTieBreak: ["rounds", "best_lap", "best_round"].includes(String(form.get("qualifyingTieBreak") || "").toLowerCase())
|
||||
? String(form.get("qualifyingTieBreak")).toLowerCase()
|
||||
: "rounds",
|
||||
carsPerFinal: Math.max(2, Number(form.get("carsPerFinal") || 8) || 8),
|
||||
finalLegs: Math.max(1, Number(form.get("finalLegs") || 1) || 1),
|
||||
countedFinalLegs: Math.max(1, Number(form.get("countedFinalLegs") || 1) || 1),
|
||||
finalDurationMin: Math.max(1, Number(form.get("finalDurationMin") || 5) || 5),
|
||||
finalStartMode: normalizeStartMode(String(form.get("finalStartMode") || "position")),
|
||||
followUpSec: Math.max(0, Number(form.get("followUpSec") || 0) || 0),
|
||||
minLapMs: Math.max(0, Math.round((Number(form.get("minLapSec") || 0) || 0) * 1000)),
|
||||
maxLapMs: Math.max(1000, Math.round((Number(form.get("maxLapSec") || 60) || 60) * 1000)),
|
||||
bumpCount: Math.max(0, Number(form.get("bumpCount") || 0) || 0),
|
||||
reserveBumpSlots: form.get("reserveBumpSlots") === "on",
|
||||
driverIds: event.raceConfig.driverIds || [],
|
||||
participantsConfigured: event.raceConfig.participantsConfigured !== false,
|
||||
finalsSource: String(form.get("finalsSource") || "qualifying") === "practice" ? "practice" : "qualifying",
|
||||
teams: getEventTeams(event),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function normalizeEvent(event, { normalizeBrandingConfig, normalizeStartMode, normalizeRaceTeam }) {
|
||||
return {
|
||||
...event,
|
||||
branding: normalizeBrandingConfig(event?.branding),
|
||||
raceConfig: {
|
||||
presetId: String(event?.raceConfig?.presetId || "custom").trim() || "custom",
|
||||
qualifyingScoring: event?.raceConfig?.qualifyingScoring === "best" ? "best" : "points",
|
||||
qualifyingRounds: Math.max(1, Number(event?.raceConfig?.qualifyingRounds || 3) || 3),
|
||||
carsPerHeat: Math.max(2, Number(event?.raceConfig?.carsPerHeat || 8) || 8),
|
||||
qualDurationMin: Math.max(1, Number(event?.raceConfig?.qualDurationMin || 5) || 5),
|
||||
qualStartMode: normalizeStartMode(event?.raceConfig?.qualStartMode || "staggered"),
|
||||
qualSeedLapCount: Math.max(0, Number(event?.raceConfig?.qualSeedLapCount || 2) || 2),
|
||||
qualSeedMethod: ["best_sum", "average", "consecutive"].includes(String(event?.raceConfig?.qualSeedMethod || "").toLowerCase())
|
||||
? String(event.raceConfig.qualSeedMethod).toLowerCase()
|
||||
: "best_sum",
|
||||
countedQualRounds: Math.max(1, Number(event?.raceConfig?.countedQualRounds || 1) || 1),
|
||||
qualifyingPointsTable: ["rank_low", "field_desc", "ifmar"].includes(String(event?.raceConfig?.qualifyingPointsTable || "").toLowerCase())
|
||||
? String(event.raceConfig.qualifyingPointsTable).toLowerCase()
|
||||
: "rank_low",
|
||||
qualifyingTieBreak: ["rounds", "best_lap", "best_round"].includes(String(event?.raceConfig?.qualifyingTieBreak || "").toLowerCase())
|
||||
? String(event.raceConfig.qualifyingTieBreak).toLowerCase()
|
||||
: "rounds",
|
||||
carsPerFinal: Math.max(2, Number(event?.raceConfig?.carsPerFinal || 8) || 8),
|
||||
finalLegs: Math.max(1, Number(event?.raceConfig?.finalLegs || 1) || 1),
|
||||
countedFinalLegs: Math.max(1, Number(event?.raceConfig?.countedFinalLegs || 1) || 1),
|
||||
finalDurationMin: Math.max(1, Number(event?.raceConfig?.finalDurationMin || 5) || 5),
|
||||
finalStartMode: normalizeStartMode(event?.raceConfig?.finalStartMode || "position"),
|
||||
followUpSec: Math.max(0, Number(event?.raceConfig?.followUpSec || 0) || 0),
|
||||
minLapMs: Math.max(0, Number(event?.raceConfig?.minLapMs || 0) || 0),
|
||||
maxLapMs: Math.max(0, Number(event?.raceConfig?.maxLapMs || 60000) || 60000),
|
||||
bumpCount: Math.max(0, Number(event?.raceConfig?.bumpCount || 0) || 0),
|
||||
reserveBumpSlots: event?.raceConfig?.reserveBumpSlots !== false,
|
||||
driverIds: Array.isArray(event?.raceConfig?.driverIds) ? event.raceConfig.driverIds : [],
|
||||
participantsConfigured: Boolean(event?.raceConfig?.participantsConfigured),
|
||||
finalsSource: event?.raceConfig?.finalsSource === "practice" ? "practice" : "qualifying",
|
||||
teams: Array.isArray(event?.raceConfig?.teams) ? event.raceConfig.teams.map((team) => normalizeRaceTeam(team)).filter((team) => team.name) : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function normalizeBrandingConfig(branding) {
|
||||
const theme = ["classic", "minimal", "motorsport"].includes(String(branding?.pdfTheme || "").toLowerCase())
|
||||
? String(branding.pdfTheme).toLowerCase()
|
||||
: "";
|
||||
return {
|
||||
brandName: String(branding?.brandName || "").trim(),
|
||||
brandTagline: String(branding?.brandTagline || "").trim(),
|
||||
pdfFooter: String(branding?.pdfFooter || "").trim(),
|
||||
pdfTheme: theme,
|
||||
logoDataUrl: String(branding?.logoDataUrl || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function resolveEventBranding(event, { normalizeBrandingConfig, state }) {
|
||||
const local = normalizeBrandingConfig(event?.branding);
|
||||
return {
|
||||
brandName: local.brandName || state.settings.clubName || "JMK RB",
|
||||
brandTagline: local.brandTagline || state.settings.clubTagline || "Live Event",
|
||||
pdfFooter: local.pdfFooter || state.settings.pdfFooter || "Generated by JMK RB RaceController",
|
||||
pdfTheme: local.pdfTheme || state.settings.pdfTheme || "classic",
|
||||
logoDataUrl: local.logoDataUrl || state.settings.logoDataUrl || "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset, { t, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings }) {
|
||||
const cfg = event.raceConfig || {};
|
||||
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
|
||||
const isEndurancePreset = selectedPreset?.id === "endurance";
|
||||
const practiceCount = sessions.filter((session) => ["practice", "free_practice", "open_practice"].includes(session.type)).length;
|
||||
const qualCount = sessions.filter((session) => session.type === "qualification").length;
|
||||
const finalCount = sessions.filter((session) => session.type === "final").length;
|
||||
const teamCount = sessions.filter((session) => session.type === "team_race").length;
|
||||
const hasQualifying = qualCount > 0;
|
||||
const hasFinals = finalCount > 0;
|
||||
const hasTeamRace = teamCount > 0;
|
||||
const startedCount = sessions.filter((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt).length;
|
||||
const practiceRows = buildPracticeStandings(event);
|
||||
const qualifyingRows = buildQualifyingStandings(event);
|
||||
const finalRows = buildFinalStandings(event);
|
||||
const lapWindowValid = Math.max(0, Number(cfg.maxLapMs || 0) || 0) > Math.max(0, Number(cfg.minLapMs || 0) || 0);
|
||||
const resultSourceCount = [practiceRows, qualifyingRows, finalRows].filter((rows) => rows.length > 0).length;
|
||||
|
||||
const setupStatus = participantCount > 0 && (!isEndurancePreset || raceTeams.length > 0)
|
||||
? "complete"
|
||||
: participantCount > 0 || raceTeams.length > 0
|
||||
? "attention"
|
||||
: "pending";
|
||||
|
||||
const formatStatus = lapWindowValid ? "complete" : "attention";
|
||||
|
||||
const generationReady = isEndurancePreset ? hasTeamRace : hasQualifying || hasFinals;
|
||||
const generationStatus = generationReady ? "complete" : sessions.length > 0 ? "attention" : "pending";
|
||||
|
||||
const liveStatus = startedCount > 0 || resultSourceCount > 0 ? "complete" : generationReady ? "pending" : "pending";
|
||||
|
||||
return {
|
||||
setup: {
|
||||
status: setupStatus,
|
||||
detail: `${participantCount} ${t("events.detail_drivers")} · ${raceTeams.length} ${t("events.detail_teams")}`,
|
||||
},
|
||||
format: {
|
||||
status: formatStatus,
|
||||
detail: `${selectedPreset?.label || t("events.preset_custom")} · ${((cfg.minLapMs || 0) / 1000).toFixed(1)} / ${((cfg.maxLapMs || 0) / 1000).toFixed(1)}s`,
|
||||
},
|
||||
generation: {
|
||||
status: generationStatus,
|
||||
detail: isEndurancePreset
|
||||
? `P ${practiceCount} · T ${teamCount} · ${sessions.length} ${t("events.detail_sessions")}`
|
||||
: `Q ${qualCount} · F ${finalCount} · ${sessions.length} ${t("events.detail_sessions")}`,
|
||||
},
|
||||
live: {
|
||||
status: liveStatus,
|
||||
detail: `${startedCount} ${t("events.detail_active")} · ${resultSourceCount} ${t("events.detail_results")}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function buildDefaultRaceWizardDraft({ state, getDriversForClass }) {
|
||||
const classId = state.classes[0]?.id || "";
|
||||
const presetId = "club_qualifying";
|
||||
const classDrivers = getDriversForClass(classId);
|
||||
return {
|
||||
name: "",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
classId,
|
||||
presetId,
|
||||
driverIds: classDrivers.map((driver) => driver.id),
|
||||
createPractice: true,
|
||||
practiceSessions: 1,
|
||||
createQualifying: true,
|
||||
qualifyingRounds: 4,
|
||||
createTeamRace: false,
|
||||
teamRaceDurationMin: 240,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getRaceWizardPreset(presetId, { getRaceFormatPresets }) {
|
||||
return getRaceFormatPresets().find((preset) => preset.id === presetId) || getRaceFormatPresets()[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function applyRaceWizardPresetDefaults(draft, presetId, { getRaceWizardPreset }) {
|
||||
const preset = getRaceWizardPreset(presetId);
|
||||
const endurance = preset.id === "endurance";
|
||||
draft.presetId = preset.id;
|
||||
draft.createPractice = !endurance;
|
||||
draft.practiceSessions = endurance ? 0 : Math.max(1, Number(draft.practiceSessions || 1));
|
||||
draft.createQualifying = !endurance;
|
||||
draft.qualifyingRounds = Math.max(1, Number(preset.values?.qualifyingRounds || draft.qualifyingRounds || 3));
|
||||
draft.createTeamRace = endurance;
|
||||
draft.teamRaceDurationMin = Math.max(1, Number(preset.values?.finalDurationMin || draft.teamRaceDurationMin || 240));
|
||||
return draft;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function ensureRaceParticipantsConfigured(event, { state }) {
|
||||
if (!event || event.mode !== "race") {
|
||||
return;
|
||||
}
|
||||
if (event.raceConfig?.participantsConfigured) {
|
||||
return;
|
||||
}
|
||||
const classDrivers = state.drivers.filter((driver) => !event.classId || driver.classId === event.classId);
|
||||
event.raceConfig.driverIds = classDrivers.map((driver) => driver.id);
|
||||
event.raceConfig.participantsConfigured = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function buildRaceSession(eventId, name, type, durationMin, overrides = {}, { normalizeSession, uid }) {
|
||||
return normalizeSession({
|
||||
id: uid("session"),
|
||||
eventId,
|
||||
name,
|
||||
type,
|
||||
durationMin,
|
||||
maxCars: null,
|
||||
mode: "race",
|
||||
status: "ready",
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
finishedByTimer: false,
|
||||
assignments: [],
|
||||
driverIds: overrides.driverIds || [],
|
||||
startMode: overrides.startMode || "mass",
|
||||
seedBestLapCount: overrides.seedBestLapCount || 0,
|
||||
seedMethod: overrides.seedMethod || "best_sum",
|
||||
followUpSec: overrides.followUpSec || 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildTrackSession(eventId, name, type, durationMin, { normalizeSession, uid }) {
|
||||
return normalizeSession({
|
||||
id: uid("session"),
|
||||
eventId,
|
||||
name,
|
||||
type,
|
||||
durationMin,
|
||||
maxCars: null,
|
||||
mode: "track",
|
||||
status: "ready",
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
finishedByTimer: false,
|
||||
assignments: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function createSponsorRounds(eventId, config, { state, t, buildTrackSession }) {
|
||||
const qualificationRounds = Math.max(0, Math.floor(config.qualificationRounds || 0));
|
||||
const heatRounds = Math.max(0, Math.floor(config.heatRounds || 0));
|
||||
const finalRounds = Math.max(0, Math.floor(config.finalRounds || 0));
|
||||
const durationMin = Math.max(1, Math.floor(config.durationMin || 5));
|
||||
|
||||
for (let i = 1; i <= qualificationRounds; i += 1) {
|
||||
state.sessions.push(buildTrackSession(eventId, `${t("session.qualification")} ${i}`, "qualification", durationMin));
|
||||
}
|
||||
for (let i = 1; i <= heatRounds; i += 1) {
|
||||
state.sessions.push(buildTrackSession(eventId, `${t("session.heat")} ${i}`, "heat", durationMin));
|
||||
}
|
||||
for (let i = 1; i <= finalRounds; i += 1) {
|
||||
state.sessions.push(buildTrackSession(eventId, `${t("session.final")} ${i}`, "final", durationMin));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function buildRaceSessionsFromWizard(event, draft, { t, buildRaceSession }) {
|
||||
const driverIds = event.raceConfig.driverIds || [];
|
||||
const created = [];
|
||||
const practiceCount = Math.max(0, Number(draft.practiceSessions || 0) || 0);
|
||||
const qualRounds = Math.max(0, Number(draft.qualifyingRounds || 0) || 0);
|
||||
if (draft.createPractice && practiceCount > 0) {
|
||||
for (let index = 1; index <= practiceCount; index += 1) {
|
||||
created.push(
|
||||
buildRaceSession(event.id, `${t("session.practice")} ${index}`, "practice", event.raceConfig.qualDurationMin, {
|
||||
driverIds,
|
||||
startMode: "mass",
|
||||
followUpSec: 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (draft.createQualifying && qualRounds > 0) {
|
||||
for (let index = 1; index <= qualRounds; index += 1) {
|
||||
created.push(
|
||||
buildRaceSession(event.id, `${t("session.qualification")} ${index}`, "qualification", event.raceConfig.qualDurationMin, {
|
||||
driverIds,
|
||||
startMode: event.raceConfig.qualStartMode,
|
||||
seedBestLapCount: event.raceConfig.qualSeedLapCount,
|
||||
seedMethod: event.raceConfig.qualSeedMethod,
|
||||
followUpSec: event.raceConfig.followUpSec,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (draft.createTeamRace) {
|
||||
created.push(
|
||||
buildRaceSession(event.id, t("events.team_race"), "team_race", Math.max(1, Number(draft.teamRaceDurationMin || event.raceConfig.finalDurationMin || 1)), {
|
||||
driverIds,
|
||||
startMode: "mass",
|
||||
followUpSec: event.raceConfig.followUpSec,
|
||||
})
|
||||
);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getRaceWizardSessionPlan(draft, { t }) {
|
||||
const plan = [];
|
||||
if (draft.createPractice) {
|
||||
plan.push(`${Math.max(1, Number(draft.practiceSessions || 1))} x ${t("session.practice")}`);
|
||||
}
|
||||
if (draft.createQualifying) {
|
||||
plan.push(`${Math.max(1, Number(draft.qualifyingRounds || 1))} x ${t("session.qualification")}`);
|
||||
}
|
||||
if (draft.createTeamRace) {
|
||||
plan.push(`${t("events.team_race")} ${Math.max(1, Number(draft.teamRaceDurationMin || 1))} min`);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getEventDrivers(event, { state }) {
|
||||
const classDrivers = state.drivers.filter((driver) => !event?.classId || driver.classId === event.classId);
|
||||
if (!event?.raceConfig?.participantsConfigured) {
|
||||
return classDrivers;
|
||||
}
|
||||
return classDrivers.filter((driver) => (event.raceConfig.driverIds || []).includes(driver.id));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getEventTeams(event, { normalizeRaceTeam }) {
|
||||
return Array.isArray(event?.raceConfig?.teams) ? event.raceConfig.teams.map((team) => normalizeRaceTeam(team)).filter((team) => team.name) : [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getTeamDriverPool(event, { getEventDrivers, state }) {
|
||||
const scopedDrivers = getEventDrivers(event);
|
||||
if (scopedDrivers.length) {
|
||||
return { drivers: scopedDrivers, fallback: false };
|
||||
}
|
||||
return {
|
||||
drivers: [...state.drivers],
|
||||
fallback: state.drivers.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function findEventTeamForPassing(event, driverId, carId, { getEventTeams }) {
|
||||
return getEventTeams(event).find((team) => {
|
||||
const driverMatch = driverId && Array.isArray(team.driverIds) && team.driverIds.includes(driverId);
|
||||
const carMatch = carId && Array.isArray(team.carIds) && team.carIds.includes(carId);
|
||||
return Boolean(driverMatch || carMatch);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function generateQualifyingForRace(event, { buildPracticeStandings, getEventDrivers, state, normalizeStartMode, normalizeSession, uid, chunkArray, clearGeneratedQualifying }) {
|
||||
const sourceRows = buildPracticeStandings(event);
|
||||
const fallbackRows =
|
||||
sourceRows.length > 0
|
||||
? sourceRows
|
||||
: getEventDrivers(event).map((driver, index) => ({
|
||||
rank: index + 1,
|
||||
driverId: driver.id,
|
||||
driverName: driver.name,
|
||||
}));
|
||||
|
||||
if (!fallbackRows.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
clearGeneratedQualifying(event.id);
|
||||
|
||||
const qualifyingRounds = Math.max(1, Number(event.raceConfig?.qualifyingRounds || 1) || 1);
|
||||
const carsPerHeat = Math.max(2, Number(event.raceConfig?.carsPerHeat || 8) || 8);
|
||||
const qualDurationMin = Math.max(1, Number(event.raceConfig?.qualDurationMin || 5) || 5);
|
||||
const qualStartMode = normalizeStartMode(event.raceConfig?.qualStartMode || "staggered");
|
||||
const qualSeedLapCount = Math.max(0, Number(event.raceConfig?.qualSeedLapCount || 2) || 0);
|
||||
const qualSeedMethod = ["best_sum", "average", "consecutive"].includes(String(event.raceConfig?.qualSeedMethod || "").toLowerCase())
|
||||
? String(event.raceConfig.qualSeedMethod).toLowerCase()
|
||||
: "best_sum";
|
||||
const followUpSec = Math.max(0, Number(event.raceConfig?.followUpSec || 0) || 0);
|
||||
const heats = chunkArray(fallbackRows, carsPerHeat);
|
||||
let created = 0;
|
||||
|
||||
heats.forEach((heatRows, heatIndex) => {
|
||||
const heatNumber = heatIndex + 1;
|
||||
const driverIds = heatRows.map((row) => row.driverId).filter(Boolean);
|
||||
for (let round = 1; round <= qualifyingRounds; round += 1) {
|
||||
state.sessions.push(
|
||||
normalizeSession({
|
||||
id: uid("session"),
|
||||
eventId: event.id,
|
||||
name: `Q${round} H${heatNumber}`,
|
||||
type: "qualification",
|
||||
durationMin: qualDurationMin,
|
||||
maxCars: carsPerHeat,
|
||||
mode: "race",
|
||||
status: "ready",
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
finishedByTimer: false,
|
||||
followUpSec,
|
||||
followUpStartedAt: null,
|
||||
startMode: qualStartMode,
|
||||
seedBestLapCount: qualSeedLapCount,
|
||||
seedMethod: qualSeedMethod,
|
||||
staggerGapSec: 3,
|
||||
driverIds,
|
||||
manualGridIds: [...driverIds],
|
||||
gridCustomized: false,
|
||||
generated: true,
|
||||
assignments: [],
|
||||
})
|
||||
);
|
||||
created += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function reseedUpcomingQualifying(event, { buildQualifyingStandings, buildPracticeStandings, getEventDrivers, getSessionsForEvent, chunkArray }) {
|
||||
const standings = buildQualifyingStandings(event);
|
||||
const sourceRows =
|
||||
standings.length > 0
|
||||
? standings
|
||||
: buildPracticeStandings(event).length > 0
|
||||
? buildPracticeStandings(event)
|
||||
: getEventDrivers(event).map((driver, index) => ({
|
||||
rank: index + 1,
|
||||
driverId: driver.id,
|
||||
driverName: driver.name,
|
||||
}));
|
||||
|
||||
if (!sourceRows.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const carsPerHeat = Math.max(2, Number(event.raceConfig?.carsPerHeat || 8) || 8);
|
||||
const heats = chunkArray(sourceRows, carsPerHeat);
|
||||
const sessionsByRound = new Map();
|
||||
getSessionsForEvent(event.id)
|
||||
.filter((session) => session.type === "qualification" && session.generated && session.status === "ready")
|
||||
.forEach((session) => {
|
||||
const match = String(session.name || "").match(/^Q(\d+)\s+H(\d+)/i);
|
||||
const round = match ? Number(match[1]) : 0;
|
||||
if (!sessionsByRound.has(round)) {
|
||||
sessionsByRound.set(round, []);
|
||||
}
|
||||
sessionsByRound.get(round).push(session);
|
||||
});
|
||||
|
||||
let updated = 0;
|
||||
let locked = 0;
|
||||
[...sessionsByRound.entries()].forEach(([, roundSessions]) => {
|
||||
roundSessions.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
roundSessions.forEach((session, heatIndex) => {
|
||||
if (session.gridCustomized) {
|
||||
locked += 1;
|
||||
return;
|
||||
}
|
||||
const heatRows = heats[heatIndex] || [];
|
||||
session.driverIds = heatRows.map((row) => row.driverId).filter(Boolean);
|
||||
session.manualGridIds = [...session.driverIds];
|
||||
updated += 1;
|
||||
});
|
||||
});
|
||||
|
||||
return { updated, locked };
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function generateFinalsForRace(event, { buildPracticeStandings, buildQualifyingStandings, clearGeneratedFinals, state, normalizeStartMode, normalizeSession, uid, chunkArray, t }) {
|
||||
const sourceRows =
|
||||
event.raceConfig?.finalsSource === "practice"
|
||||
? buildPracticeStandings(event)
|
||||
: buildQualifyingStandings(event);
|
||||
|
||||
if (!sourceRows.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
clearGeneratedFinals(event.id);
|
||||
|
||||
const finalLegs = Math.max(1, Number(event.raceConfig?.finalLegs || 1) || 1);
|
||||
const carsPerFinal = Math.max(2, Number(event.raceConfig?.carsPerFinal || 8) || 8);
|
||||
const finalDurationMin = Math.max(1, Number(event.raceConfig?.finalDurationMin || 5) || 5);
|
||||
const finalStartMode = normalizeStartMode(event.raceConfig?.finalStartMode || "position");
|
||||
const followUpSec = Math.max(0, Number(event.raceConfig?.followUpSec || 0) || 0);
|
||||
const bumpCount = Math.max(0, Number(event.raceConfig?.bumpCount || 0) || 0);
|
||||
const reserveBumpSlots = Boolean(event.raceConfig?.reserveBumpSlots && bumpCount > 0);
|
||||
const seededSlotsPerMain = reserveBumpSlots ? Math.max(1, carsPerFinal - bumpCount) : carsPerFinal;
|
||||
const mains = chunkArray(sourceRows, seededSlotsPerMain);
|
||||
let created = 0;
|
||||
|
||||
mains.forEach((mainRows, mainIndex) => {
|
||||
const mainLetter = String.fromCharCode(65 + mainIndex);
|
||||
const driverIds = mainRows.map((row) => row.driverId).filter(Boolean);
|
||||
for (let leg = 1; leg <= finalLegs; leg += 1) {
|
||||
state.sessions.push(
|
||||
normalizeSession({
|
||||
id: uid("session"),
|
||||
eventId: event.id,
|
||||
name: `${mainLetter} ${t("session.final")} ${leg}`,
|
||||
type: "final",
|
||||
durationMin: finalDurationMin,
|
||||
maxCars: carsPerFinal,
|
||||
mode: "race",
|
||||
status: "ready",
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
finishedByTimer: false,
|
||||
followUpSec,
|
||||
followUpStartedAt: null,
|
||||
startMode: finalStartMode,
|
||||
seedBestLapCount: 0,
|
||||
staggerGapSec: 0,
|
||||
driverIds,
|
||||
manualGridIds: [...driverIds],
|
||||
gridCustomized: false,
|
||||
reservedBumpSlots: reserveBumpSlots && mainIndex < mains.length - 1 ? bumpCount : 0,
|
||||
generated: true,
|
||||
assignments: [],
|
||||
})
|
||||
);
|
||||
created += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function applyBumpsForRace(event, { buildFinalStandings, getSessionsForEvent }) {
|
||||
const bumpCount = Math.max(0, Number(event.raceConfig?.bumpCount || 0) || 0);
|
||||
if (bumpCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const standings = buildFinalStandings(event);
|
||||
if (!standings.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const grouped = new Map();
|
||||
standings.forEach((row) => {
|
||||
const mainKey = String(row.rank || "").charAt(0).toUpperCase();
|
||||
if (!grouped.has(mainKey)) {
|
||||
grouped.set(mainKey, []);
|
||||
}
|
||||
grouped.get(mainKey).push(row);
|
||||
});
|
||||
|
||||
const mainKeys = [...grouped.keys()].sort();
|
||||
let applied = 0;
|
||||
|
||||
for (let index = 1; index < mainKeys.length; index += 1) {
|
||||
const upperMainKey = mainKeys[index - 1];
|
||||
const lowerMainKey = mainKeys[index];
|
||||
const lowerRows = grouped.get(lowerMainKey) || [];
|
||||
const bumpRows = lowerRows.slice(0, bumpCount);
|
||||
if (!bumpRows.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const upperSessions = getSessionsForEvent(event.id).filter((session) => {
|
||||
return session.type === "final" && String(session.name || "").toUpperCase().startsWith(upperMainKey) && session.status === "ready";
|
||||
});
|
||||
|
||||
if (!upperSessions.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bumpDriverIds = bumpRows.map((row) => row.driverId).filter(Boolean);
|
||||
if (!bumpDriverIds.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
upperSessions.forEach((session) => {
|
||||
session.driverIds = Array.isArray(session.driverIds) ? session.driverIds : [];
|
||||
session.manualGridIds = Array.isArray(session.manualGridIds) ? session.manualGridIds : [...session.driverIds];
|
||||
const reservedSlots = Math.max(0, Number(session.reservedBumpSlots || 0) || 0);
|
||||
const capacity = Math.max(0, Number(session.maxCars || event.raceConfig?.carsPerFinal || 0) || 0);
|
||||
const allowedSize = reservedSlots > 0 ? Math.min(capacity, session.driverIds.length + reservedSlots) : capacity || Infinity;
|
||||
let addedHere = 0;
|
||||
bumpDriverIds.forEach((driverId) => {
|
||||
if (!session.driverIds.includes(driverId) && session.driverIds.length < allowedSize) {
|
||||
session.driverIds.push(driverId);
|
||||
if (!session.manualGridIds.includes(driverId)) {
|
||||
session.manualGridIds.push(driverId);
|
||||
}
|
||||
applied += 1;
|
||||
addedHere += 1;
|
||||
}
|
||||
});
|
||||
session.reservedBumpSlots = Math.max(0, reservedSlots - addedHere);
|
||||
});
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user