Phase 17: profile level-up system
CI / Build, lint & test (push) Failing after 57s

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:
funman300
2026-06-25 17:41:13 -07:00
parent 3d50da3589
commit d97695d414
6 changed files with 257 additions and 6 deletions
+64
View File
@@ -1449,3 +1449,67 @@ async fn test_pack_store_buy_then_opens() {
assert_eq!(status, StatusCode::OK, "open failed: {json}");
assert!(json["cards"].as_array().map(|a| !a.is_empty()).unwrap_or(false), "opened pack should contain cards");
}
// ── Phase 17: Level-up system ─────────────────────────────────────────────────
#[tokio::test]
async fn test_profile_returns_level_info() {
let app = build_test_app().await;
auth(&app, "LevelInfoPlayer").await;
let (status, json) = json_get(&app, "/profile").await;
assert_eq!(status, StatusCode::OK, "{json}");
assert!(json["level"].as_i64().unwrap_or(0) >= 1, "level missing");
assert!(json["xp"].is_number(), "xp missing");
assert!(json["xp_to_next_level"].is_number(), "xp_to_next_level missing");
assert!(json["xp_for_next_level"].is_number(), "xp_for_next_level missing");
}
#[tokio::test]
async fn test_level_for_xp_thresholds() {
use openfut_core::models::profile::level_for_xp;
assert_eq!(level_for_xp(0), 1);
assert_eq!(level_for_xp(499), 1);
assert_eq!(level_for_xp(500), 2);
assert_eq!(level_for_xp(1199), 2);
assert_eq!(level_for_xp(1200), 3);
assert_eq!(level_for_xp(11000), 10);
// Beyond table: level 11 at 11000 + 2500 = 13500
assert_eq!(level_for_xp(13500), 11);
}
#[tokio::test]
async fn test_match_result_includes_level_ups_on_first_win() {
let app = build_test_app().await;
auth(&app, "LevelUpWinner").await;
// A single win awards ~300 XP (beginner win); fresh profile is at 0 XP.
// With enough wins we cross the 500 XP threshold (level 2).
let mut level_ups_seen = false;
for _ in 0..5 {
let (status, json) = json_post(&app, "/matches/result", serde_json::json!({
"squad_id": "dummy", "opponent_name": "Bot",
"goals_for": 3, "goals_against": 0, "mode": "squad_battles"
})).await;
assert_eq!(status, StatusCode::OK, "{json}");
if let Some(arr) = json["level_ups"].as_array() {
if !arr.is_empty() {
level_ups_seen = true;
let ev = &arr[0];
assert!(ev["new_level"].as_i64().unwrap_or(0) >= 2, "should be at least level 2");
assert!(ev["coins_granted"].as_i64().unwrap_or(0) > 0, "coins granted on level up");
}
}
}
assert!(level_ups_seen, "expected a level-up event within 5 wins");
}
#[tokio::test]
async fn test_level_up_grants_milestone_pack_at_level_5() {
use openfut_core::models::profile::{level_for_xp, pack_for_level, XP_THRESHOLDS};
// Level 5 requires 3000 XP total
assert_eq!(level_for_xp(3000), 5);
assert_eq!(pack_for_level(5), Some("bronze_pack"));
assert_eq!(pack_for_level(10), Some("silver_pack"));
assert_eq!(pack_for_level(7), None);
}