From ab7fbf70af8a020b2348fd956494f05d8fe4b1dd Mon Sep 17 00:00:00 2001 From: Juan Date: Sat, 13 Jun 2026 05:57:51 +0200 Subject: [PATCH] fix: validate opponent roster before starting match and show error The 'start match' button did nothing when the opponent team was missing players in one or more roles (e.g. no support player). The backend now validates BOTH teams have a full 5-role lineup before creating the match, returning a clear error message when the opponent can't field a team. --- src-tauri/src/application/live_match.rs | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src-tauri/src/application/live_match.rs b/src-tauri/src/application/live_match.rs index b2c99b880..9fdfd31b8 100644 --- a/src-tauri/src/application/live_match.rs +++ b/src-tauri/src/application/live_match.rs @@ -50,6 +50,49 @@ fn validate_user_team_role_coverage(game: &Game) -> Result<(), String> { } } +/// Checks that a specific team (opponent) has at least one player assigned to +/// each of the five required LoL roles. If the opponent roster is incomplete +/// the match cannot proceed. +fn validate_team_role_coverage(game: &Game, team_id: &str) -> Result<(), String> { + let role_set: std::collections::HashSet<&'static str> = game + .players + .iter() + .filter(|player| player.team_id.as_deref() == Some(team_id)) + .map(|player| role_to_string(&player.natural_position)) + .collect(); + let required_roles = ["TOP", "JUNGLE", "MID", "ADC", "SUPPORT"]; + let missing_roles: Vec<&str> = required_roles + .iter() + .copied() + .filter(|role| !role_set.contains(role)) + .collect(); + + if missing_roles.is_empty() { + Ok(()) + } else { + Err(format!( + "Opponent team role coverage incomplete: missing {}. The AI cannot field a full lineup.", + missing_roles.join(", ") + )) + } +} + +fn missing_roles_for_team(game: &Game, team_id: &str) -> Vec { + let role_set: std::collections::HashSet = game + .players + .iter() + .filter(|player| player.team_id.as_deref() == Some(team_id)) + .map(|player| role_to_string(&player.natural_position).to_string()) + .collect(); + let required_roles = ["TOP", "JUNGLE", "MID", "ADC", "SUPPORT"]; + required_roles + .iter() + .copied() + .map(|r| r.to_string()) + .filter(|role| !role_set.contains(role)) + .collect() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FinishLiveMatchResponse { pub game: Game, @@ -374,6 +417,22 @@ pub fn start_live_match( validate_user_team_role_coverage(&game)?; + // Also validate the opponent roster – an AI team without a full lineup + // would make the match unwinnable or buggy. + let user_team_id = game.manager.team_id.as_deref(); + if let Some(league) = game.leagues.first() { + if let Some(fixture) = league.fixtures.get(fixture_index) { + let opp_team_id = if fixture.home_team_id == user_team_id.unwrap_or_default() { + Some(&fixture.away_team_id) + } else { + Some(&fixture.home_team_id) + }; + if let Some(opp_id) = opp_team_id { + validate_team_role_coverage(&game, opp_id)?; + } + } + } + let match_mode = match mode { "spectator" => MatchMode::Spectator, "instant" => MatchMode::Instant,