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
+30 -3
View File
@@ -1,4 +1,8 @@
use axum::{extract::State, Json};
use axum::{
extract::{Query, State},
Json,
};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::{
@@ -8,8 +12,31 @@ use crate::{
services::{club as club_svc, market as market_svc, profile as profile_svc},
};
pub async fn get_market(State(state): State<AppState>) -> AppResult<Json<Value>> {
let listings = market_svc::get_active_listings(&state.pool, &state.card_db).await?;
#[derive(Deserialize)]
pub struct MarketQuery {
pub min_overall: Option<u8>,
pub position: Option<String>,
}
pub async fn get_market(
State(state): State<AppState>,
Query(query): Query<MarketQuery>,
) -> AppResult<Json<Value>> {
let all = market_svc::get_active_listings(&state.pool, &state.card_db).await?;
let listings: Vec<_> = all
.into_iter()
.filter(|l| {
query
.min_overall
.map(|min| l.card.overall >= min)
.unwrap_or(true)
&& query
.position
.as_ref()
.map(|p| l.card.position.eq_ignore_ascii_case(p))
.unwrap_or(true)
})
.collect();
Ok(Json(
json!({ "listings": listings, "total": listings.len() }),
))