Complete Records/Leaderboards + Time Trials systems (100%)

RECORDS & LEADERBOARDS (5/5 endpoints - 100%):
- Created LeaderboardsController with 5 endpoints
- GET /synergy/leaderboards/timetrials/{trialId}
- GET /synergy/leaderboards/career/{series}/{event}
- GET /synergy/leaderboards/global/top100
- GET /synergy/leaderboards/player/{synergyId}/records
- GET /synergy/leaderboards/compare/{synergyId1}/{synergyId2}

Added LeaderboardEntry and PersonalRecord models and database tables.
Migration applied: AddLeaderboardsAndRecords

Updated RewardsController.SubmitTimeTrial to track personal bests,
update leaderboards, and award 50 gold bonus for improvements.

Updated ProgressionController.CompleteCareerEvent similarly for
career event personal records.

TIME TRIALS (6/6 endpoints - 100%):
- GET /synergy/rewards/timetrials - List with time remaining
- GET /synergy/rewards/timetrials/{id} - Details with stats
- POST /synergy/rewards/timetrials/{id}/submit - Submit with PB tracking
- GET /synergy/rewards/timetrials/player/{synergyId}/results - History
- POST /synergy/rewards/timetrials/{id}/claim - Claim bonuses
- GET /synergy/leaderboards/timetrials/{id} - Leaderboards (above)

Added navigation properties to TimeTrialResult for easier queries.

Server progress: 66/73 endpoints (90%)
Two complete systems: Records/Leaderboards + Time Trials

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-22 17:49:23 -08:00
parent e839064b35
commit a6167c8249
11 changed files with 2695 additions and 25 deletions

View File

@@ -300,8 +300,12 @@ public class ProgressionController : ControllerBase
cp.SeriesName == completion.SeriesName &&
cp.EventName == completion.EventName);
bool isFirstCompletion = false;
double? previousBestTime = null;
if (progress == null)
{
isFirstCompletion = true;
progress = new CareerProgress
{
UserId = user.Id,
@@ -312,36 +316,114 @@ public class ProgressionController : ControllerBase
};
_context.CareerProgress.Add(progress);
}
else
{
previousBestTime = progress.BestTime > 0 ? progress.BestTime : null;
}
// Update progress
progress.Completed = true;
progress.StarsEarned = Math.Max(progress.StarsEarned, completion.StarsEarned);
bool isNewBestTime = progress.BestTime == 0 || completion.RaceTime < progress.BestTime;
progress.BestTime = progress.BestTime == 0 ? completion.RaceTime :
Math.Min(progress.BestTime, completion.RaceTime);
progress.CompletedAt = DateTime.UtcNow;
// Track personal record for career event
var recordCategory = $"{completion.SeriesName}/{completion.EventName}";
var personalRecord = await _context.PersonalRecords
.FirstOrDefaultAsync(pr => pr.SynergyId == completion.SynergyId &&
pr.RecordType == "Career" &&
pr.RecordCategory == recordCategory);
bool isNewPersonalBest = false;
double? improvement = null;
if (personalRecord == null)
{
// First attempt
isNewPersonalBest = true;
personalRecord = new Data.PersonalRecord
{
SynergyId = completion.SynergyId,
RecordType = "Career",
RecordCategory = recordCategory,
TrackName = completion.TrackName,
CarName = completion.CarName,
BestTimeSeconds = completion.RaceTime,
AchievedAt = DateTime.UtcNow,
TotalAttempts = 1
};
_context.PersonalRecords.Add(personalRecord);
}
else
{
personalRecord.TotalAttempts++;
if (completion.RaceTime < personalRecord.BestTimeSeconds)
{
isNewPersonalBest = true;
improvement = personalRecord.BestTimeSeconds - completion.RaceTime;
personalRecord.BestTimeSeconds = completion.RaceTime;
personalRecord.AchievedAt = DateTime.UtcNow;
personalRecord.ImprovementSeconds = improvement;
personalRecord.CarName = completion.CarName;
}
}
// Add/update leaderboard entry if new personal best
if (isNewPersonalBest)
{
var leaderboardEntry = new Data.LeaderboardEntry
{
SynergyId = completion.SynergyId,
PlayerName = user.SynergyId,
RecordType = "Career",
RecordCategory = recordCategory,
TrackName = completion.TrackName,
CarName = completion.CarName,
TimeSeconds = completion.RaceTime,
SubmittedAt = DateTime.UtcNow
};
_context.LeaderboardEntries.Add(leaderboardEntry);
}
// Award rewards
int goldReward = completion.StarsEarned * 10; // 10 gold per star
int cashReward = completion.StarsEarned * 2000; // 2000 cash per star
int xpReward = completion.StarsEarned * 100; // 100 XP per star
// Bonus for personal best
if (isNewPersonalBest && previousBestTime.HasValue)
{
goldReward += 50;
}
user.Gold = (user.Gold ?? 0) + goldReward;
user.Cash = (user.Cash ?? 0) + cashReward;
user.Experience = (user.Experience ?? 0) + xpReward;
await _context.SaveChangesAsync();
return Ok(new
// Calculate global rank
int globalRank = await _context.LeaderboardEntries
.Where(e => e.RecordType == "Career" &&
e.RecordCategory == recordCategory &&
e.TimeSeconds < completion.RaceTime)
.CountAsync() + 1;
return Ok(new RecordSubmissionResponse
{
success = true,
stars = completion.StarsEarned,
goldEarned = goldReward,
cashEarned = cashReward,
xpEarned = xpReward,
bestTime = progress.BestTime,
totalGold = user.Gold,
totalCash = user.Cash,
totalExperience = user.Experience
Success = true,
IsNewPersonalBest = isNewPersonalBest,
IsNewGlobalRecord = globalRank == 1,
GlobalRank = globalRank,
PreviousBestTime = previousBestTime,
Improvement = improvement,
GoldEarned = goldReward,
CashEarned = cashReward
});
}