Add motivational training challenges and shared club goals

This commit is contained in:
larssand
2026-08-30 09:16:11 +02:00
parent 27dcbd92aa
commit 694aba5f3d
11 changed files with 799 additions and 13 deletions

View File

@@ -6,7 +6,9 @@ import {
buildDriverTrainingStats,
buildDriverTimingChannelStats,
buildTrainingAchievements,
buildTrainingHighlights,
buildTrainingMarkerComparisons,
buildTrainingMissions,
buildTrainingPaceBuckets,
buildTrainingRecommendation,
buildTrainingStintAnalysis,
@@ -15,6 +17,7 @@ import {
getBestTrainingDurationResult,
getDriverTrainingLaps,
getNextTrainingMilestone,
getClubTrainingGoalProgress,
getTrainingGoalProgress,
normalizeTrainingState,
splitTrainingStints,
@@ -48,6 +51,13 @@ test("normalization removes invalid and duplicate training records", () => {
{ id: "marker_1", driverId: "driver_1", classId: "class_1", timestamp: 5000, type: "tires", note: " New tires " },
{ id: "marker_invalid", driverId: "", classId: "class_1", timestamp: 5000 },
],
clubGoal: {
id: "club_goal_1",
title: " Club to 10000 ",
reward: " Pizza ",
targetLaps: 10000,
baselineLaps: 120,
},
}, { uid: createUid() });
assert.equal(normalized.goals.length, 1);
@@ -57,6 +67,9 @@ test("normalization removes invalid and duplicate training records", () => {
assert.deepEqual(normalized.shares.map((share) => share.token), ["a".repeat(24)]);
assert.equal(normalized.markers.length, 1);
assert.equal(normalized.markers[0].note, "New tires");
assert.equal(normalized.clubGoal.title, "Club to 10000");
assert.equal(normalized.clubGoal.reward, "Pizza");
assert.equal(normalized.clubGoal.baselineLaps, 120);
});
test("ledger sync retains archived laps and rebuilds current session laps", () => {
@@ -382,6 +395,47 @@ test("automatic achievements expose unlocked and upcoming personal goals", () =>
assert.equal(achievements.find((item) => item.id === "laps_1000").percent, 52);
assert.equal(achievements.find((item) => item.id === "days_10").unlocked, false);
assert.equal(achievements.find((item) => item.id === "five_minute").unlocked, true);
assert.equal(achievements.find((item) => item.id === "laps_250").unlocked, true);
assert.equal(achievements.find((item) => item.id === "controlled_10").unlocked, false);
});
test("daily missions recognize volume, control, and completed five-minute stints", () => {
const missions = buildTrainingMissions({
todayLaps: 12,
todayControlledLapStreak: 5,
todayCompletedFiveMinuteStints: 1,
});
assert.equal(missions.find((item) => item.id === "laps_today").percent, 60);
assert.equal(missions.find((item) => item.id === "controlled_today").completed, true);
assert.equal(missions.find((item) => item.id === "five_minute_today").completed, true);
});
test("training highlights prioritize current personal progress", () => {
const highlights = buildTrainingHighlights({
totalLaps: 600,
todayLaps: 24,
personalBestGainMs: 320,
trendSampleReady: true,
averageTrendMs: 180,
consistencyTrendPct: 1.4,
todayControlledLapStreak: 6,
});
assert.deepEqual(highlights.map((item) => item.id), ["today_laps", "personal_best", "pace_gain"]);
});
test("club goal progress starts at its saved baseline and is bounded", () => {
const progress = getClubTrainingGoalProgress({
ledger: Array.from({ length: 175 }, (_, index) => ({ id: `lap_${index}` })),
clubGoal: { targetLaps: 100, baselineLaps: 125 },
});
assert.equal(progress.currentLaps, 50);
assert.equal(progress.remainingLaps, 50);
assert.equal(progress.percent, 50);
assert.equal(progress.reached, false);
assert.equal(getClubTrainingGoalProgress({ ledger: [] }), null);
});
test("pace buckets use a trimmed average and keep classes isolated", () => {