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:
@@ -2,10 +2,11 @@ use crate::{
|
||||
db::Pool,
|
||||
error::AppResult,
|
||||
models::{
|
||||
card::OwnedCard,
|
||||
match_result::{Match, MatchRewardResult, SubmitMatchRequest},
|
||||
objective::ObjectiveDefinition,
|
||||
},
|
||||
services::{card_db::CardDb, club, objective, profile, statistics},
|
||||
services::{card_db::CardDb, club, objective, profile, season as season_svc, statistics},
|
||||
};
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
|
||||
@@ -134,7 +135,7 @@ pub async fn process_match(
|
||||
.await?;
|
||||
|
||||
club::add_coins(pool, club_id, coins).await?;
|
||||
profile::add_xp(pool, profile_id, xp).await?;
|
||||
let level_ups = profile::add_xp_with_levelup(pool, profile_id, club_id, xp).await?;
|
||||
statistics::record_match(
|
||||
pool,
|
||||
profile_id,
|
||||
@@ -170,10 +171,67 @@ pub async fn process_match(
|
||||
objective::increment_metric(pool, profile_id, obj_defs, "coinsearned", coins).await?;
|
||||
objectives_updated.append(&mut c);
|
||||
|
||||
// Decrement loan matches remaining for squad starters; collect expired loan IDs.
|
||||
let expired_loans = process_loan_expiry(pool, club_id, &req.squad_id).await?;
|
||||
|
||||
// Update season progress (creates the season row if it doesn't exist yet).
|
||||
season_svc::get_or_create(pool, profile_id).await?;
|
||||
let (_, season_end) = season_svc::record_match(pool, club_id, profile_id, outcome).await?;
|
||||
|
||||
Ok(MatchRewardResult {
|
||||
match_record,
|
||||
coins_awarded: coins,
|
||||
xp_awarded: xp,
|
||||
objectives_updated,
|
||||
expired_loans,
|
||||
season_end,
|
||||
level_ups,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrement `loan_matches_remaining` for each loan card in the squad's starting XI.
|
||||
/// Removes cards whose remaining count hits 0 and returns their owned_card_ids.
|
||||
async fn process_loan_expiry(pool: &Pool, club_id: &str, squad_id: &str) -> AppResult<Vec<String>> {
|
||||
// Get starters (is_on_bench = 0) for this squad
|
||||
let starters: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT sp.id, sp.owned_card_id FROM squad_players sp \
|
||||
JOIN squads s ON s.id = sp.squad_id \
|
||||
WHERE sp.squad_id = ? AND sp.is_on_bench = 0 AND s.club_id = ?",
|
||||
)
|
||||
.bind(squad_id)
|
||||
.bind(club_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut expired = Vec::new();
|
||||
|
||||
for (_sp_id, owned_id) in starters {
|
||||
let card = sqlx::query_as::<_, OwnedCard>(
|
||||
"SELECT id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at \
|
||||
FROM owned_cards WHERE id = ? AND is_loan = 1",
|
||||
)
|
||||
.bind(&owned_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(c) = card {
|
||||
let remaining = c.loan_matches_remaining.unwrap_or(0);
|
||||
if remaining <= 1 {
|
||||
// Loan expired — remove from collection
|
||||
sqlx::query("DELETE FROM owned_cards WHERE id = ?")
|
||||
.bind(&owned_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
expired.push(owned_id);
|
||||
} else {
|
||||
sqlx::query("UPDATE owned_cards SET loan_matches_remaining = ? WHERE id = ?")
|
||||
.bind(remaining - 1)
|
||||
.bind(&owned_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(expired)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user