"Add Windows installer and driver training rewards"
This commit is contained in:
113
src/training_logic.js
Normal file
113
src/training_logic.js
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user