From 0e968d31d773103d264bd6e81db71fdc28942c63 Mon Sep 17 00:00:00 2001 From: Juan Date: Sat, 13 Jun 2026 04:42:29 +0200 Subject: [PATCH] feat(draft): overhaul pick/ban phase with meta tiers, UI pivots, and flex-pick visibility Implements issue #302. 1. Meta tier system: added MetaTier enum (S/A/B/C/D) to champion domain model, parsed from champion-list.json or defaulted to B. Exposed to frontend via ChampionData.meta_tier. Badge shown on draft suggestion tips. 2. UI pivot fix: reworked tip mixing logic so ALL player tips are always visible (one per role). When a player changes their request, other players' bubbles no longer get hidden. Pre-slice cap raised to 6-7. 3. Flex-pick visibility: champion suggestion tips now render secondary role badges when a champion can flex into other positions (e.g. Corki shown as MID + ADC). Uses the existing roleHints/role_icons infra. --- src-tauri/crates/olm_core/src/champions.rs | 8 ++ .../src/db/repositories/champion_repo.rs | 4 + .../crates/olm_core/src/domain/champion.rs | 34 +++++++++ src-tauri/src/commands/game.rs | 1 + src/store/types.ts | 1 + .../components/match/ChampionDraft.tsx | 74 ++++++++++++++----- 6 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src-tauri/crates/olm_core/src/champions.rs b/src-tauri/crates/olm_core/src/champions.rs index 3bb2819b3..6d0c57814 100644 --- a/src-tauri/crates/olm_core/src/champions.rs +++ b/src-tauri/crates/olm_core/src/champions.rs @@ -1604,6 +1604,8 @@ pub struct ChampionListEntry { pub name: String, pub tags: Vec, pub image: String, + /// Patch meta tier (S/A/B/C/D). Defaults to "B" when absent. + pub meta_tier: Option, } /// Load the champion catalog from `assets/draft/champion-list.json`. @@ -1636,6 +1638,11 @@ pub fn load_champion_catalog_from_path(path: &Path) -> Vec Vec Result Result, String> { synergies_json: row.get(5)?, image_tile_url: row.get(6)?, image_splash_url: row.get(7)?, + meta_tier: crate::domain::champion::MetaTier::B, }) }) .map_err(|e| format!("Failed to query champions: {}", e))?; @@ -203,6 +205,7 @@ pub fn get_champion_by_id(conn: &Connection, id: i64) -> Result synergies_json: row.get(5)?, image_tile_url: row.get(6)?, image_splash_url: row.get(7)?, + meta_tier: crate::domain::champion::MetaTier::B, }) }) .map_err(|e| format!("Failed to query champion: {}", e))?; @@ -234,6 +237,7 @@ pub fn get_champion_by_key(conn: &Connection, key: &str) -> Result i32 { + *self as i32 + } + + pub fn from_name(name: &str) -> Option { + match name.to_uppercase().as_str() { + "S" => Some(Self::S), + "A" => Some(Self::A), + "B" => Some(Self::B), + "C" => Some(Self::C), + "D" => Some(Self::D), + _ => None, + } + } +} + /// Represents a League of Legends champion stored in the database. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(TS))] @@ -15,6 +45,9 @@ pub struct Champion { pub synergies_json: Option, pub image_tile_url: Option, pub image_splash_url: Option, + /// Current-patch meta tier (S = must-ban/pick, D = niche). + /// Populated from the patch champion list; defaults to B if unknown. + pub meta_tier: MetaTier, } /// Input for creating a new champion (without id, which is auto-generated). @@ -29,4 +62,5 @@ pub struct NewChampion { pub synergies_json: Option, pub image_tile_url: Option, pub image_splash_url: Option, + pub meta_tier: MetaTier, } diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index adaae179e..0635102fe 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -689,6 +689,7 @@ pub async fn get_champions() -> Result synergies_json: None, image_tile_url: Some(format!("/champion-tiles/{}.webp", champion_key)), image_splash_url: Some(format!("/champion-splash/{}.jpg", champion_key)), + meta_tier: olm_core::domain::champion::MetaTier::B, } }) .collect(); diff --git a/src/store/types.ts b/src/store/types.ts index e6fff49e9..52122ba38 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -457,6 +457,7 @@ export interface ChampionData { synergies_json: string | null; image_tile_url: string | null; image_splash_url: string | null; + meta_tier: string; } export interface TransferOfferData { diff --git a/src/ui-v2/_legacy/components/match/ChampionDraft.tsx b/src/ui-v2/_legacy/components/match/ChampionDraft.tsx index 58029069e..30b5157df 100644 --- a/src/ui-v2/_legacy/components/match/ChampionDraft.tsx +++ b/src/ui-v2/_legacy/components/match/ChampionDraft.tsx @@ -27,6 +27,8 @@ interface ChampionData { image: string; tags: string[]; roleHints: Role[]; + /// Patch meta tier for draft priority (S/A/B/C/D). Defaults to "B". + meta_tier: string; } interface DraftAction { @@ -780,7 +782,7 @@ export default function ChampionDraft({ useEffect(() => { try { - const data = championListSeed as { champions: Array<{ id: string; key: number; name: string; tags: string[]; image: string }> }; + const data = championListSeed as { champions: Array<{ id: string; key: number; name: string; tags: string[]; image: string; meta_tier?: string }> }; const list = (data.champions ?? []) .map((champion) => ({ id: champion.id, @@ -789,6 +791,7 @@ export default function ChampionDraft({ image: `/champion-tiles/${champion.id}.webp`, tags: champion.tags, roleHints: inferRoleHintsFromSeed(champion.id, champion.name, champion.tags), + meta_tier: champion.meta_tier ?? "B", })) .sort((a, b) => a.name.localeCompare(b.name)); @@ -2408,28 +2411,18 @@ export default function ChampionDraft({ const playerTips = tips.filter((tip) => tip.sourceType === "player"); if (draftAdviceStage === "pick") { + // Always show ALL player pick tips (one per role) so that when a player + // pivots their request the rest of the squad's preferences stay visible. const playerPickTips = playerTips.filter((tip) => tip.type === "pick").slice(0, 5); const coachWarnTips = coachTips.filter((tip) => tip.type !== "pick").slice(0, 1); - return [...playerPickTips, ...coachWarnTips].slice(0, 5); + return [...playerPickTips, ...coachWarnTips].slice(0, 6); } - const mixed: DraftAdviceTip[] = []; - let coachIdx = 0; - let playerIdx = 0; - - while (mixed.length < 4 && (coachIdx < coachTips.length || playerIdx < playerTips.length)) { - if (coachIdx < coachTips.length) { - mixed.push(coachTips[coachIdx]); - coachIdx += 1; - if (mixed.length >= 4) break; - } - if (playerIdx < playerTips.length) { - mixed.push(playerTips[playerIdx]); - playerIdx += 1; - } - } - - return mixed.slice(0, 4); + // Ban stage: show all player ban-request tips plus top coach tip. + // Player tips are rendered first and always visible. + const playerBanTips = playerTips.filter((tip) => tip.type === "ban").slice(0, 5); + const coachBanTips = coachTips.filter((tip) => tip.type === "ban").slice(0, 2); + return [...playerBanTips, ...coachBanTips].slice(0, 7); }, [ champions, gameState, @@ -2837,7 +2830,7 @@ export default function ChampionDraft({

{tip.champion ? ( -
+
{tip.champion.name} {tip.champion.name} + + {/* Meta tier badge */} + {tip.champion.meta_tier && ( + + {tip.champion.meta_tier} + + )} + + {/* Flex pick: show secondary roles if champion has multiple */} + {(() => { + const roleHints = tip.champion!.roleHints ?? []; + const secondaryRoles = roleHints.filter( + (r) => tip.sourceRole && r !== tip.sourceRole && ROLE_ORDER.includes(r as Role), + ); + if (secondaryRoles.length > 0) { + return secondaryRoles.map((role) => ( + + {role} + {role} + + )); + } + return null; + })()}
) : null}