use crate::{ db::Pool, error::AppResult, models::{ match_result::{Match, MatchRewardResult, SubmitMatchRequest}, objective::ObjectiveDefinition, }, services::{card_db::CardDb, club, objective, profile, statistics}, }; use rand::{seq::SliceRandom, Rng}; const FORMATIONS: &[&str] = &[ "4-3-3", "4-4-2", "4-2-3-1", "4-1-2-1-2", "3-5-2", "5-3-2", ]; /// Generate a random AI opponent squad for Squad Battles. /// /// Difficulty bands: /// beginner — overall 55+ (same as "any") /// professional — overall 70+ /// world_class — overall 78+ /// legendary — overall 85+ /// ultimate — overall 90+ pub fn generate_opponent(card_db: &CardDb, difficulty: &str) -> serde_json::Value { let (min_overall, names): (u8, &[&str]) = match difficulty { "professional" => ( 70, &["Athletic CF", "City Wanderers", "The Rovers", "United Select", "Blue Stars FC"], ), "world_class" => ( 78, &["Elite Stars FC", "Champions Select", "Premier XI", "Galaxy United", "Titan FC"], ), "legendary" => ( 85, &["Legends United", "Ultimate XI", "Gold Standard FC", "The Icons", "Heritage FC"], ), "ultimate" => ( 90, &["Apex XI", "Pantheon FC", "Gods of FUT", "Invincibles Select", "Eternal XI"], ), _ => ( 55, &["Amateur Town FC", "Sunday League XI", "Park FC", "Village Stars", "Reserve XI"], ), }; let mut rng = rand::thread_rng(); let all = card_db.by_min_overall(min_overall); // Fall back to lower overall band if not enough cards at this difficulty let pool: Vec<_> = if all.len() >= 11 { all } else { card_db.by_min_overall(55) }; let mut indices: Vec = (0..pool.len()).collect(); indices.shuffle(&mut rng); let cards: Vec<_> = indices.into_iter().take(11).map(|i| pool[i].clone()).collect(); let squad_rating = if cards.is_empty() { 0 } else { cards.iter().map(|c| c.overall as i64).sum::() / cards.len() as i64 }; let name = names[rng.gen_range(0..names.len())]; let formation = FORMATIONS[rng.gen_range(0..FORMATIONS.len())]; serde_json::json!({ "opponent_name": name, "difficulty": difficulty, "squad_rating": squad_rating, "formation": formation, "cards": cards, }) } const COINS_WIN: i64 = 400; const COINS_DRAW: i64 = 150; const COINS_LOSS: i64 = 75; const XP_WIN: i64 = 200; const XP_DRAW: i64 = 75; const XP_LOSS: i64 = 30; pub async fn process_match( pool: &Pool, profile_id: &str, club_id: &str, req: &SubmitMatchRequest, obj_defs: &[ObjectiveDefinition], ) -> AppResult { let outcome = if req.goals_for > req.goals_against { "win" } else if req.goals_for == req.goals_against { "draw" } else { "loss" }; let (coins, xp) = match outcome { "win" => (COINS_WIN, XP_WIN), "draw" => (COINS_DRAW, XP_DRAW), _ => (COINS_LOSS, XP_LOSS), }; let match_record = Match::new( profile_id, &req.squad_id, &req.opponent_name, req.goals_for, req.goals_against, &req.mode, coins, xp, ); sqlx::query( "INSERT INTO matches (id, profile_id, squad_id, opponent_name, goals_for, goals_against, outcome, coins_awarded, xp_awarded, mode, played_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ) .bind(&match_record.id) .bind(&match_record.profile_id) .bind(&match_record.squad_id) .bind(&match_record.opponent_name) .bind(match_record.goals_for) .bind(match_record.goals_against) .bind(&match_record.outcome) .bind(match_record.coins_awarded) .bind(match_record.xp_awarded) .bind(&match_record.mode) .bind(&match_record.played_at) .execute(pool) .await?; club::add_coins(pool, club_id, coins).await?; profile::add_xp(pool, profile_id, xp).await?; statistics::record_match( pool, profile_id, outcome, req.goals_for, req.goals_against, coins, ) .await?; if let Some(positions) = &req.goal_positions { statistics::record_position_goals(pool, profile_id, positions).await?; } let mut objectives_updated = Vec::new(); let mut completed = objective::increment_metric(pool, profile_id, obj_defs, "matchesplayed", 1).await?; objectives_updated.append(&mut completed); if outcome == "win" { let mut c = objective::increment_metric(pool, profile_id, obj_defs, "matcheswon", 1).await?; objectives_updated.append(&mut c); } let mut c = objective::increment_metric(pool, profile_id, obj_defs, "goalsscored", req.goals_for) .await?; objectives_updated.append(&mut c); let mut c = objective::increment_metric(pool, profile_id, obj_defs, "coinsearned", coins).await?; objectives_updated.append(&mut c); Ok(MatchRewardResult { match_record, coins_awarded: coins, xp_awarded: xp, objectives_updated, }) }