diff --git a/src-tauri/crates/olm_core/src/domain/team.rs b/src-tauri/crates/olm_core/src/domain/team.rs index e3db0447..cf29f910 100644 --- a/src-tauri/crates/olm_core/src/domain/team.rs +++ b/src-tauri/crates/olm_core/src/domain/team.rs @@ -113,6 +113,13 @@ pub struct Team { #[serde(default, alias = "starting_xi_ids")] pub active_lineup_ids: Vec, + /// Per-role substitute player IDs (index 0..4 matching active_lineup_ids role order: + /// TOP, JUNGLE, MID, ADC, SUPPORT). An empty string means no sub is assigned for that role. + /// When the starter has critically low condition (< 30 %), the match engine will + /// automatically promote the sub into the active lineup for that match. + #[serde(default)] + pub role_sub_ids: Vec, + #[serde(default)] pub team_roles: TeamRoles, @@ -1357,6 +1364,7 @@ impl Team { secondary: "#ffffff".to_string(), }, active_lineup_ids: Vec::new(), + role_sub_ids: Vec::new(), team_roles: TeamRoles::default(), form: Vec::new(), history: Vec::new(), diff --git a/src-tauri/src/commands/squad.rs b/src-tauri/src/commands/squad.rs index 2da5ab6d..99390c9c 100644 --- a/src-tauri/src/commands/squad.rs +++ b/src-tauri/src/commands/squad.rs @@ -511,6 +511,34 @@ fn apply_active_lineup(game: &mut Game, team_id: &str, player_ids: Vec) } } +/// Assign per-role substitutes for the active lineup. +/// +/// `sub_ids` must be a Vec of 5 strings (one per role in the order TOP, JUNGLE, +/// MID, ADC, SUPPORT), where each entry is either a valid player ID or empty. +#[tauri::command] +pub fn set_role_subs( + state: State<'_, StateManager>, + sub_ids: Vec, +) -> Result { + info!("[cmd] set_role_subs: {} subs", sub_ids.len()); + let mut game = state + .get_game(|g| g.clone()) + .ok_or("No active game session".to_string())?; + + let team_id = game + .manager + .team_id + .clone() + .ok_or("No team assigned".to_string())?; + + if let Some(team) = game.teams.iter_mut().find(|t| t.id == team_id) { + team.role_sub_ids = sub_ids; + } + + state.set_game(game.clone()); + Ok(game) +} + #[tauri::command] pub fn set_draft_strategy( state: State<'_, StateManager>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7d13eed3..3f2ddf92 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -155,6 +155,7 @@ pub fn run() { delegate_renewals, preview_renewal_financial_impact, set_active_lineup, + set_role_subs, set_starting_xi, set_draft_strategy, set_lol_tactics, diff --git a/src/store/types.ts b/src/store/types.ts index e6fff49e..1a46e9d9 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -202,6 +202,8 @@ export interface TeamData { sponsorship?: SponsorshipData | null; /** Preferred LoL terminology. Serialized by current saves/API responses. */ active_lineup_ids?: string[]; + /** Per-role substitute player IDs (indexed same as active_lineup_ids). */ + role_sub_ids?: string[]; /** @deprecated Compatibility for older saves/API payloads. Use active_lineup_ids. */ starting_xi_ids?: string[]; team_roles?: TeamRolesData; diff --git a/src/ui-v2/dashboard/tabs/SquadTabV2.tsx b/src/ui-v2/dashboard/tabs/SquadTabV2.tsx index 11d2c6c1..ee307b5e 100644 --- a/src/ui-v2/dashboard/tabs/SquadTabV2.tsx +++ b/src/ui-v2/dashboard/tabs/SquadTabV2.tsx @@ -8,6 +8,7 @@ import { ShoppingCart, User, Loader2, + RefreshCw, } from "lucide-react"; import type { GameStateData, PlayerSelectionOptions } from "@/store/gameStore"; @@ -77,6 +78,21 @@ export function SquadTabV2({ const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [search, setSearch] = useState(""); const [savingSlot, setSavingSlot] = useState(null); + const [swappingSlot, setSwappingSlot] = useState(null); + + const roleSubIds: string[] = myTeam.role_sub_ids ?? []; + + const handleSetRoleSub = useCallback(async (roleIndex: number, playerId: string) => { + const newSubs = [...roleSubIds]; + while (newSubs.length < 5) newSubs.push(""); + newSubs[roleIndex] = playerId; + try { + const updated = await invoke("set_role_subs", { subIds: newSubs }); + onGameUpdate(updated); + } catch (err) { + console.error("[SquadTab] Failed to set role sub:", err); + } + }, [roleSubIds, onGameUpdate]); const handleToggleTransfer = useCallback(async (playerId: string) => { try { @@ -308,8 +324,10 @@ export function SquadTabV2({ const morale = player.morale; const annualWage = player.wage; - return ( -
{ if (!(e.target as HTMLElement).closest("select,button")) onSelectPlayer(player.id); }} className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/30"> + return ( +
{ if (!(e.target as HTMLElement).closest("select,button")) onSelectPlayer(player.id); }} className={cn("flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/30", + condition < 30 && "border-l-2 border-l-red-500/60 bg-red-500/5" + )}>
{roleLabel}
@@ -326,6 +344,11 @@ export function SquadTabV2({ )} + {condition < 30 && ( + + + + )}
handleSetRoleSub(slot.index, e.target.value)} + className="max-w-[160px] rounded-md border border-border/60 bg-muted/40 pl-1.5 pr-6 py-0.5 text-[11px] text-foreground" + > + + {benchPlayers + .filter((bp) => !activeLineupIds.includes(bp.id)) + .sort((a, b) => { + const aMatch = resolvePlayerLolRole(a) === slot.role ? 0 : 1; + const bMatch = resolvePlayerLolRole(b) === slot.role ? 0 : 1; + return aMatch - bMatch || calculateLolOvr(b) - calculateLolOvr(a); + }) + .map((bp) => ( + + ))} + + {sub && sub.condition < 30 && ( + + + + )} + + ); + })()} +
); })} diff --git a/src/ui-v2/dashboard/tabs/YouthTabV2.tsx b/src/ui-v2/dashboard/tabs/YouthTabV2.tsx index 322bec4e..06d6a840 100644 --- a/src/ui-v2/dashboard/tabs/YouthTabV2.tsx +++ b/src/ui-v2/dashboard/tabs/YouthTabV2.tsx @@ -9,12 +9,14 @@ import { Info, Loader2, Search, + ShoppingCart, Sparkles, Star, TrendingUp, Users, } from "lucide-react"; +import { invoke } from "@tauri-apps/api/core"; import type { GameStateData, PlayerData, AcademyAcquisitionOptionData } from "@/store/gameStore"; import { findAcademyTeamForParent, getTeamAcademyRoster } from "@/store/academySelectors"; import { @@ -92,6 +94,7 @@ export function YouthTabV2({ gameState, onSelectPlayer, onSelectTeam, onGameUpda ); const [promotingPlayerId, setPromotingPlayerId] = useState(null); + const [transferListingPlayerId, setTransferListingPlayerId] = useState(null); const [acquisitionOptions, setAcquisitionOptions] = useState([]); const [acquisitionBlockedReason, setAcquisitionBlockedReason] = useState(null); const [acquisitionLoading, setAcquisitionLoading] = useState(false); @@ -621,35 +624,68 @@ export function YouthTabV2({ gameState, onSelectPlayer, onSelectTeam, onGameUpda - + > + {transferListingPlayerId === player.id ? ( + + ) : ( + + )} + + + );