XP thresholds (500→1200→2000→…→11000→+2500/level) drive automatic level increases. add_xp_with_levelup() replaces bare add_xp() in match processing: for each level gained it grants level×500 coins and milestone packs (bronze@5, silver@10, gold@15, rare_gold@20, gold every 5 after). GET /profile now returns computed level (recalculated from XP so it stays consistent), xp_to_next_level, and xp_for_next_level so the dashboard can render a progress bar without a second call. POST /matches/result response gains level_ups array (empty when no level-up occurred) with new_level, coins_granted, pack_granted per event. Four new tests: profile level fields, level_for_xp boundary checks, level-up event in match result, milestone pack unit test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::models::profile::LevelUpEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -77,4 +78,10 @@ pub struct MatchRewardResult {
|
||||
pub coins_awarded: i64,
|
||||
pub xp_awarded: i64,
|
||||
pub objectives_updated: Vec<String>,
|
||||
/// Owned card IDs removed because the loan expired this match.
|
||||
pub expired_loans: Vec<String>,
|
||||
/// Present when this match completed the current season.
|
||||
pub season_end: Option<crate::models::season::SeasonEndSummary>,
|
||||
/// Non-empty when the player levelled up one or more times from this match's XP.
|
||||
pub level_ups: Vec<LevelUpEvent>,
|
||||
}
|
||||
|
||||
@@ -30,3 +30,59 @@ impl Profile {
|
||||
pub struct CreateProfileRequest {
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
/// XP required to reach each level (cumulative total from level 1).
|
||||
/// Level 1 starts at 0 XP. Level 2 needs 500 total XP, etc.
|
||||
pub const XP_THRESHOLDS: &[i64] = &[
|
||||
0, // level 1
|
||||
500, // level 2
|
||||
1200, // level 3
|
||||
2000, // level 4
|
||||
3000, // level 5
|
||||
4200, // level 6
|
||||
5600, // level 7
|
||||
7200, // level 8
|
||||
9000, // level 9
|
||||
11000, // level 10
|
||||
];
|
||||
|
||||
/// Compute the level for a given cumulative XP total.
|
||||
pub fn level_for_xp(total_xp: i64) -> i64 {
|
||||
let base = XP_THRESHOLDS
|
||||
.iter()
|
||||
.rposition(|&t| total_xp >= t)
|
||||
.map(|i| i as i64 + 1)
|
||||
.unwrap_or(1);
|
||||
// Beyond the table: every 2500 XP is another level.
|
||||
let overflow_xp = total_xp - XP_THRESHOLDS.last().copied().unwrap_or(0);
|
||||
if overflow_xp > 0 {
|
||||
let table_max = XP_THRESHOLDS.len() as i64;
|
||||
table_max + (overflow_xp / 2500)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
/// Coins awarded per new level gained.
|
||||
pub fn coins_for_level(new_level: i64) -> i64 {
|
||||
new_level * 500
|
||||
}
|
||||
|
||||
/// Pack granted at milestone levels (5, 10, 15, 20, …).
|
||||
pub fn pack_for_level(new_level: i64) -> Option<&'static str> {
|
||||
match new_level {
|
||||
5 => Some("bronze_pack"),
|
||||
10 => Some("silver_pack"),
|
||||
15 => Some("gold_pack"),
|
||||
20 => Some("rare_gold_pack"),
|
||||
l if l > 20 && l % 5 == 0 => Some("gold_pack"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct LevelUpEvent {
|
||||
pub new_level: i64,
|
||||
pub coins_granted: i64,
|
||||
pub pack_granted: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user