feat: Phase 2 — game feel complete

Chemistry & squads:
- Chemistry calculation on GET /squad (club/league/nation links, max 100)
- Formation validation on POST /squad (exactly 11 starters, exactly 1 GK)

Objectives:
- Weekly objectives JSON (4 objectives: warrior, goals, dedicated, SBC)
- Daily objectives auto-reset at midnight UTC (background task)

SBC validation expanded:
- max_overall per-player enforcement
- required_clubs validation
- min_players_from_same_nation validation
- min_players_from_same_club validation

Market:
- GET /market?min_overall=X&position=Y filtering
- Expiry cleanup runs before every NPC refresh
- NPC market auto-refresh every 24h (background task, runs at startup)

Matches:
- GET /matches/opponent?difficulty=beginner|professional|world_class|legendary
  generates a random AI opponent squad from the card pool

Statistics:
- win_streak and best_win_streak tracking (migration 0002)
- GET /statistics/history?limit=N — last N matches with summary stats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 15:33:09 -07:00
parent 34bce2ce75
commit 0afce0dd59
13 changed files with 516 additions and 55 deletions
+14
View File
@@ -47,6 +47,20 @@ pub async fn get_matches(
Ok(Json(json!({ "matches": matches, "total": matches.len() })))
}
#[derive(Deserialize)]
pub struct OpponentQuery {
pub difficulty: Option<String>,
}
pub async fn get_opponent(
State(state): State<AppState>,
Query(query): Query<OpponentQuery>,
) -> AppResult<Json<Value>> {
let difficulty = query.difficulty.as_deref().unwrap_or("beginner");
let opponent = match_service::generate_opponent(&state.card_db, difficulty);
Ok(Json(opponent))
}
pub async fn post_match_result(
State(state): State<AppState>,
Json(req): Json<SubmitMatchRequest>,