use crate::models::{achievement::AchievementDefinition, profile::LevelUpEvent}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum MatchOutcome { Win, Draw, Loss, } /// Canonical, game-independent economic result of a completed match. The game /// adapter maps its own wire (FIFA17 `endReason`, score, …) onto this — Core /// never sees a game-specific reason string. /// /// * `Win` / `Draw` / `Loss` — a finished match; standard reward tiers. /// * `Dnf` — did-not-finish (abandon/quit). Economically a loss, but tallied in /// its own statistics bucket and never in `matches_lost`. /// * `NoContest` — a voided match. Zero economic effect: no coins, XP, or /// W/D/L/DNF change; recorded only for history + idempotency. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MatchResultKind { Win, Draw, Loss, Dnf, NoContest, } impl MatchResultKind { /// The canonical lowercase token persisted in `matches.outcome` and /// `match_completions.result`. pub fn as_str(self) -> &'static str { match self { MatchResultKind::Win => "win", MatchResultKind::Draw => "draw", MatchResultKind::Loss => "loss", MatchResultKind::Dnf => "dnf", MatchResultKind::NoContest => "no_contest", } } /// Whether this result applies any economic effect (coins / XP / statistics / /// objectives / achievements). `NoContest` is the only non-economic result. pub fn is_economic(self) -> bool { !matches!(self, MatchResultKind::NoContest) } } #[derive(Debug, Deserialize)] pub struct SubmitMatchRequest { pub squad_id: String, pub opponent_name: String, pub goals_for: i64, pub goals_against: i64, pub mode: String, pub goal_positions: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Match { pub id: String, pub profile_id: String, pub squad_id: String, pub opponent_name: String, pub goals_for: i64, pub goals_against: i64, pub outcome: String, pub coins_awarded: i64, pub xp_awarded: i64, pub mode: String, pub played_at: String, } impl Match { #[allow(clippy::too_many_arguments)] pub fn new( profile_id: &str, squad_id: &str, opponent_name: &str, goals_for: i64, goals_against: i64, mode: &str, coins_awarded: i64, xp_awarded: i64, ) -> Self { let outcome = if goals_for > goals_against { "win" } else if goals_for == goals_against { "draw" } else { "loss" }; Self { id: Uuid::new_v4().to_string(), profile_id: profile_id.to_string(), squad_id: squad_id.to_string(), opponent_name: opponent_name.to_string(), goals_for, goals_against, outcome: outcome.to_string(), coins_awarded, xp_awarded, mode: mode.to_string(), played_at: chrono::Utc::now().to_rfc3339(), } } } #[derive(Debug, Serialize)] pub struct MatchRewardResult { pub match_record: Match, pub coins_awarded: i64, pub xp_awarded: i64, pub objectives_updated: Vec, /// Owned card IDs removed because the loan expired this match. pub expired_loans: Vec, /// Present when this match completed the current season. pub season_end: Option, /// Non-empty when the player levelled up one or more times from this match's XP. pub level_ups: Vec, /// Achievements unlocked as a result of this match. pub achievements_unlocked: Vec, } /// Request to atomically complete a match exactly once. `match_identity` is the /// opaque, host-supplied per-match token that keys durable economic idempotency /// (persona/profile + match_identity). `result` is the canonical outcome the /// game adapter derived from its wire; `goals_for`/`goals_against` are recorded /// for history and statistics (0-0 is normal for a DNF/no-contest). #[derive(Debug, Clone, Deserialize)] pub struct CompleteMatchRequest { pub match_identity: String, pub result: MatchResultKind, pub squad_id: String, pub opponent_name: String, pub goals_for: i64, pub goals_against: i64, pub mode: String, #[serde(default)] pub goal_positions: Option>, } /// Outcome of [`crate::services::match_service::complete_match`]. #[derive(Debug, Serialize)] pub struct MatchCompletionResult { /// `true` when THIS call applied the economic effect; `false` on an /// idempotent replay of an already-completed match (the persisted canonical /// result is echoed unchanged). pub applied: bool, pub match_identity: String, pub result: MatchResultKind, pub coins_awarded: i64, pub xp_awarded: i64, /// Club balance after completion — echoed so the host can render the wire /// reward body without a second round-trip. pub coins_balance: i64, /// Objectives completed by this match (empty on a replay). pub objectives_updated: Vec, /// Level-ups gained from this match's XP (empty on a replay). pub level_ups: Vec, /// Achievements unlocked by this match (empty on a replay). pub achievements_unlocked: Vec, pub match_record: Match, }