"Add Windows installer and driver training rewards"

This commit is contained in:
larssand
2026-08-24 14:48:49 +02:00
parent 4c0e94bc50
commit de7b4adfe0
21 changed files with 2068 additions and 210 deletions

113
src/training_logic.js Normal file
View File

@@ -0,0 +1,113 @@
export function normalizeTrainingState(training, { uid }) {
const source = training && typeof training === "object" ? training : {};
const goals = Array.isArray(source.goals)
? source.goals
.map((goal) => ({
id: String(goal?.id || uid("goal")),
driverId: String(goal?.driverId || ""),
title: String(goal?.title || "").trim(),
reward: String(goal?.reward || "").trim(),
targetLaps: Math.max(1, Math.round(Number(goal?.targetLaps || 0) || 0)),
createdAt: String(goal?.createdAt || new Date().toISOString()),
claimedAt: goal?.claimedAt ? String(goal.claimedAt) : "",
}))
.filter((goal) => goal.driverId && goal.targetLaps > 0)
: [];
return { goals };
}
export function getDriverTrainingLaps(state, driverId) {
const laps = [];
Object.entries(state?.resultsBySession || {}).forEach(([sessionId, result]) => {
(Array.isArray(result?.passings) ? result.passings : []).forEach((passing) => {
const lapMs = Number(passing?.lapMs || 0);
if (
String(passing?.driverId || "") === String(driverId || "") &&
passing?.validLap !== false &&
lapMs > 500
) {
laps.push({
sessionId,
timestamp: Number(passing.timestamp || 0) || 0,
lapMs,
});
}
});
});
return laps.sort((left, right) => left.timestamp - right.timestamp);
}
function percentile(values, ratio) {
if (!values.length) return 0;
const sorted = [...values].sort((left, right) => left - right);
const index = Math.min(sorted.length - 1, Math.max(0, Math.round((sorted.length - 1) * ratio)));
return sorted[index];
}
export function buildDriverTrainingStats(state, driverId) {
const laps = getDriverTrainingLaps(state, driverId);
const lapTimes = laps.map((lap) => lap.lapMs);
const recentLapTimes = lapTimes.slice(-30);
const totalLaps = laps.length;
const bestLapMs = totalLaps ? Math.min(...lapTimes) : 0;
const averageLapMs = totalLaps ? Math.round(lapTimes.reduce((sum, value) => sum + value, 0) / totalLaps) : 0;
const recentAverageMs = recentLapTimes.length
? Math.round(recentLapTimes.reduce((sum, value) => sum + value, 0) / recentLapTimes.length)
: 0;
const recentP10Ms = percentile(recentLapTimes, 0.1);
const recentP90Ms = percentile(recentLapTimes, 0.9);
const consistencySpreadPct = recentAverageMs
? ((recentP90Ms - recentP10Ms) / recentAverageMs) * 100
: 0;
return {
totalLaps,
totalSessions: new Set(laps.map((lap) => lap.sessionId)).size,
bestLapMs,
averageLapMs,
recentAverageMs,
consistencySpreadPct,
recentLapCount: recentLapTimes.length,
lastLapAt: laps.at(-1)?.timestamp || 0,
};
}
export function buildTrainingRecommendation(stats) {
if (!stats || stats.totalLaps < 10) {
return {
level: "baseline",
titleKey: "training.plan_baseline_title",
bodyKey: "training.plan_baseline_body",
targetLapMs: 0,
};
}
if (stats.recentLapCount >= 10 && stats.consistencySpreadPct > 8) {
return {
level: "consistency",
titleKey: "training.plan_consistency_title",
bodyKey: "training.plan_consistency_body",
targetLapMs: Math.round(stats.bestLapMs * 1.08),
};
}
return {
level: "progression",
titleKey: "training.plan_progression_title",
bodyKey: "training.plan_progression_body",
targetLapMs: Math.round(stats.bestLapMs * 1.05),
};
}
export function getTrainingGoalProgress(goal, stats) {
const targetLaps = Math.max(1, Number(goal?.targetLaps || 1));
const currentLaps = Math.max(0, Number(stats?.totalLaps || 0));
return {
currentLaps,
targetLaps,
remainingLaps: Math.max(0, targetLaps - currentLaps),
percent: Math.min(100, Math.max(0, (currentLaps / targetLaps) * 100)),
reached: currentLaps >= targetLaps,
};
}