Add Daily Rewards & Time Trials features
NEW FEATURES:
- Daily login rewards with Gold and Cash
- Daily time trial racing events
- FREE gold purchase system
- Streak tracking for consecutive days
- Web panel for managing rewards and events
ENDPOINTS ADDED:
- GET/POST /synergy/rewards/daily/{id} - Daily rewards
- POST /synergy/rewards/gold/purchase - Buy gold (FREE)
- GET /synergy/rewards/timetrials - Active events
- POST /synergy/rewards/timetrials/{id}/submit - Submit times
DATABASE:
- DailyReward table - tracks claims and streaks
- TimeTrial table - racing events with rewards
- TimeTrialResult table - player submissions
- User.Gold and User.Cash - currency tracking
WEB PANEL:
- /admin/rewards - Manage all reward features
- Create/edit/activate time trial events
- View reward statistics and history
- Gold packages in catalog (100, 500, 1000, 5000)
FOCUS:
- Single-player progression features
- NO race teams or multiplayer
- Perfect for offline play
- All purchases are FREE in community server
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
289
RR3CommunityServer/Controllers/RewardsController.cs
Normal file
289
RR3CommunityServer/Controllers/RewardsController.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Services;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("synergy/[controller]")]
|
||||
public class RewardsController : ControllerBase
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
private readonly ILogger<RewardsController> _logger;
|
||||
|
||||
public RewardsController(RR3DbContext context, ILogger<RewardsController> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get daily reward status for a user
|
||||
/// </summary>
|
||||
[HttpGet("daily/{synergyId}")]
|
||||
public async Task<IActionResult> GetDailyReward(string synergyId)
|
||||
{
|
||||
_logger.LogInformation("Getting daily reward for {SynergyId}", synergyId);
|
||||
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { error = "User not found" });
|
||||
}
|
||||
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var reward = await _context.DailyRewards
|
||||
.FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today);
|
||||
|
||||
if (reward == null)
|
||||
{
|
||||
// Create new daily reward
|
||||
reward = new DailyReward
|
||||
{
|
||||
UserId = user.Id,
|
||||
RewardDate = DateTime.UtcNow,
|
||||
GoldAmount = 50, // Daily gold reward
|
||||
CashAmount = 5000, // Daily cash reward
|
||||
Claimed = false,
|
||||
Streak = await CalculateStreak(user.Id)
|
||||
};
|
||||
|
||||
_context.DailyRewards.Add(reward);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
available = !reward.Claimed,
|
||||
gold = reward.GoldAmount,
|
||||
cash = reward.CashAmount,
|
||||
streak = reward.Streak,
|
||||
nextRewardIn = reward.Claimed ? (24 - (DateTime.UtcNow - reward.RewardDate).TotalHours) : 0
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Claim daily reward
|
||||
/// </summary>
|
||||
[HttpPost("daily/{synergyId}/claim")]
|
||||
public async Task<IActionResult> ClaimDailyReward(string synergyId)
|
||||
{
|
||||
_logger.LogInformation("Claiming daily reward for {SynergyId}", synergyId);
|
||||
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { error = "User not found" });
|
||||
}
|
||||
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var reward = await _context.DailyRewards
|
||||
.FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today);
|
||||
|
||||
if (reward == null || reward.Claimed)
|
||||
{
|
||||
return BadRequest(new { error = "No reward available to claim" });
|
||||
}
|
||||
|
||||
// Mark as claimed
|
||||
reward.Claimed = true;
|
||||
reward.ClaimedAt = DateTime.UtcNow;
|
||||
|
||||
// Update user's gold and cash
|
||||
if (user.Gold == null) user.Gold = 0;
|
||||
if (user.Cash == null) user.Cash = 0;
|
||||
|
||||
user.Gold += reward.GoldAmount;
|
||||
user.Cash += reward.CashAmount;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
goldEarned = reward.GoldAmount,
|
||||
cashEarned = reward.CashAmount,
|
||||
totalGold = user.Gold,
|
||||
totalCash = user.Cash,
|
||||
streak = reward.Streak
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purchase gold with real money (free in community server)
|
||||
/// </summary>
|
||||
[HttpPost("gold/purchase")]
|
||||
public async Task<IActionResult> PurchaseGold([FromBody] GoldPurchaseRequest request)
|
||||
{
|
||||
_logger.LogInformation("Processing gold purchase for {SynergyId}: {Amount} gold",
|
||||
request.SynergyId, request.GoldAmount);
|
||||
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { error = "User not found" });
|
||||
}
|
||||
|
||||
// In community server, all gold purchases are FREE!
|
||||
if (user.Gold == null) user.Gold = 0;
|
||||
user.Gold += request.GoldAmount;
|
||||
|
||||
// Log the purchase
|
||||
var purchase = new Purchase
|
||||
{
|
||||
SynergyId = request.SynergyId,
|
||||
UserId = user.Id,
|
||||
ItemId = $"gold_{request.GoldAmount}",
|
||||
Sku = request.Sku ?? $"com.ea.rr3.gold_{request.GoldAmount}",
|
||||
OrderId = Guid.NewGuid().ToString(),
|
||||
PurchaseTime = DateTime.UtcNow,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Price = 0, // FREE in community server
|
||||
Status = "approved"
|
||||
};
|
||||
|
||||
_context.Purchases.Add(purchase);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
goldPurchased = request.GoldAmount,
|
||||
totalGold = user.Gold,
|
||||
orderId = purchase.OrderId,
|
||||
message = "Gold added to your account (FREE in community server!)"
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get time trial events
|
||||
/// </summary>
|
||||
[HttpGet("timetrials")]
|
||||
public async Task<IActionResult> GetTimeTrials()
|
||||
{
|
||||
_logger.LogInformation("Getting time trial events");
|
||||
|
||||
var trials = await _context.TimeTrials
|
||||
.Where(t => t.Active)
|
||||
.OrderBy(t => t.StartDate)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
events = trials.Select(t => new
|
||||
{
|
||||
id = t.Id,
|
||||
name = t.Name,
|
||||
track = t.TrackName,
|
||||
car = t.CarName,
|
||||
startDate = t.StartDate,
|
||||
endDate = t.EndDate,
|
||||
goldReward = t.GoldReward,
|
||||
cashReward = t.CashReward,
|
||||
targetTime = t.TargetTime
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit time trial result
|
||||
/// </summary>
|
||||
[HttpPost("timetrials/{trialId}/submit")]
|
||||
public async Task<IActionResult> SubmitTimeTrial(int trialId, [FromBody] TimeTrialSubmission submission)
|
||||
{
|
||||
_logger.LogInformation("Submitting time trial {TrialId} for {SynergyId}: {Time}s",
|
||||
trialId, submission.SynergyId, submission.TimeSeconds);
|
||||
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == submission.SynergyId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { error = "User not found" });
|
||||
}
|
||||
|
||||
var trial = await _context.TimeTrials.FindAsync(trialId);
|
||||
if (trial == null || !trial.Active)
|
||||
{
|
||||
return NotFound(new { error = "Time trial not found or inactive" });
|
||||
}
|
||||
|
||||
// Check if beat target time
|
||||
bool beatTarget = submission.TimeSeconds <= trial.TargetTime;
|
||||
int goldEarned = beatTarget ? trial.GoldReward : 0;
|
||||
int cashEarned = beatTarget ? trial.CashReward : trial.CashReward / 2; // Half cash for participation
|
||||
|
||||
// Save result
|
||||
var result = new TimeTrialResult
|
||||
{
|
||||
UserId = user.Id,
|
||||
TimeTrialId = trialId,
|
||||
TimeSeconds = submission.TimeSeconds,
|
||||
SubmittedAt = DateTime.UtcNow,
|
||||
BeatTarget = beatTarget,
|
||||
GoldEarned = goldEarned,
|
||||
CashEarned = cashEarned
|
||||
};
|
||||
|
||||
_context.TimeTrialResults.Add(result);
|
||||
|
||||
// Award currency
|
||||
if (user.Gold == null) user.Gold = 0;
|
||||
if (user.Cash == null) user.Cash = 0;
|
||||
user.Gold += goldEarned;
|
||||
user.Cash += cashEarned;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
beatTarget = beatTarget,
|
||||
timeSeconds = submission.TimeSeconds,
|
||||
targetTime = trial.TargetTime,
|
||||
goldEarned = goldEarned,
|
||||
cashEarned = cashEarned,
|
||||
totalGold = user.Gold,
|
||||
totalCash = user.Cash,
|
||||
message = beatTarget ? "🏆 Target time beaten!" : "Good try! Keep racing!"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<int> CalculateStreak(int userId)
|
||||
{
|
||||
var rewards = await _context.DailyRewards
|
||||
.Where(r => r.UserId == userId && r.Claimed)
|
||||
.OrderByDescending(r => r.RewardDate)
|
||||
.ToListAsync();
|
||||
|
||||
int streak = 0;
|
||||
var currentDate = DateTime.UtcNow.Date;
|
||||
|
||||
foreach (var reward in rewards)
|
||||
{
|
||||
if (reward.RewardDate.Date == currentDate.AddDays(-streak))
|
||||
{
|
||||
streak++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
}
|
||||
|
||||
public class GoldPurchaseRequest
|
||||
{
|
||||
public string SynergyId { get; set; } = string.Empty;
|
||||
public int GoldAmount { get; set; }
|
||||
public string? Sku { get; set; }
|
||||
}
|
||||
|
||||
public class TimeTrialSubmission
|
||||
{
|
||||
public string SynergyId { get; set; } = string.Empty;
|
||||
public double TimeSeconds { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user