Files
OpenFUT-Core/src/models/squad.rs
T

103 lines
3.1 KiB
Rust

use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Squad {
pub id: String,
pub club_id: String,
pub name: String,
pub formation: String,
pub created_at: String,
pub updated_at: String,
}
impl Squad {
pub fn new(
club_id: impl Into<String>,
name: impl Into<String>,
formation: impl Into<String>,
) -> Self {
let now = chrono::Utc::now().to_rfc3339();
Self {
id: Uuid::new_v4().to_string(),
club_id: club_id.into(),
name: name.into(),
formation: formation.into(),
created_at: now.clone(),
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct SquadPlayer {
pub id: String,
pub squad_id: String,
pub owned_card_id: String,
pub position_index: i64,
pub is_captain: bool,
pub is_on_bench: bool,
}
#[derive(Debug, Deserialize)]
pub struct SaveSquadRequest {
pub squad_id: Option<String>,
pub name: Option<String>,
pub formation: Option<String>,
pub players: Vec<SquadPlayerInput>,
}
#[derive(Debug, Deserialize)]
pub struct SquadPlayerInput {
pub owned_card_id: String,
pub position_index: i64,
pub is_captain: bool,
pub is_on_bench: bool,
}
/// A complete squad, as a game client sends it.
///
/// # Why replacement rather than edits
///
/// Retail FIFA 17 sends the WHOLE squad on every save — roughly 2 KB carrying
/// every slot, item id and kit number — and a user swapping two players
/// produced nine changed slots across two saves. Slot deltas therefore do not
/// describe what the user did, and any attempt to derive `swap_players` or
/// `move_player` from them would be inventing intent the wire never carried.
///
/// So the only honest semantic operation is: *this is the squad now*.
///
/// Empty slots are simply absent from `slots`; a client that models an empty
/// slot as a zero item id must drop it at the adapter boundary rather than
/// sending a player Core would have to special-case.
#[derive(Debug, Clone, Default)]
pub struct SquadReplacement {
pub name: Option<String>,
pub formation: Option<String>,
pub slots: Vec<SlotAssignment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlotAssignment {
pub owned_card_id: String,
/// Core's slot numbering. The adapter maps the game's numbering onto it.
pub slot: i64,
pub is_captain: bool,
pub is_on_bench: bool,
}
/// Outcome of a replacement.
///
/// Carries the server's own evaluation and, separately, any disagreement with
/// what the client claimed — never a merged value.
#[derive(Debug, Clone)]
pub struct SquadReplaced {
pub squad: Squad,
pub slots_written: usize,
pub evaluation: crate::services::squad_rules::SquadEvaluation,
pub client_disagreements: Vec<crate::services::squad_rules::EvaluationComparison>,
/// Server-computed deterministic fingerprint of the committed canonical squad
/// (anchors any opaque game extension against stale projection).
pub canonical_fingerprint: String,
}