Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src-tauri/crates/olm_core/src/champions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,8 @@ pub struct ChampionListEntry {
pub name: String,
pub tags: Vec<String>,
pub image: String,
/// Patch meta tier (S/A/B/C/D). Defaults to "B" when absent.
pub meta_tier: Option<String>,
}

/// Load the champion catalog from `assets/draft/champion-list.json`.
Expand Down Expand Up @@ -1636,6 +1638,11 @@ pub fn load_champion_catalog_from_path(path: &Path) -> Vec<crate::domain::champi
.map(|(i, entry)| {
let champion_key = entry.id.clone();
let name = entry.name.clone();
let meta_tier = entry
.meta_tier
.as_deref()
.and_then(crate::domain::champion::MetaTier::from_name)
.unwrap_or(crate::domain::champion::MetaTier::B);
crate::domain::champion::Champion {
id: (i + 1) as i64,
name,
Expand All @@ -1645,6 +1652,7 @@ pub fn load_champion_catalog_from_path(path: &Path) -> Vec<crate::domain::champi
synergies_json: None,
image_tile_url: Some(format!("/champion-tiles/{}.webp", entry.id)),
image_splash_url: Some(format!("/champion-splash/{}.jpg", entry.id)),
meta_tier,
}
})
.collect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub fn seed_from_json(conn: &Connection, json_content: &str) -> Result<usize, St
"/champion-splash/{}.webp",
champion_key
)),
meta_tier: crate::domain::champion::MetaTier::B,
};

insert_champion(conn, &new_champ)?;
Expand Down Expand Up @@ -170,6 +171,7 @@ pub fn get_all_champions(conn: &Connection) -> Result<Vec<Champion>, 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))?;
Expand Down Expand Up @@ -203,6 +205,7 @@ pub fn get_champion_by_id(conn: &Connection, id: i64) -> Result<Option<Champion>
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))?;
Expand Down Expand Up @@ -234,6 +237,7 @@ pub fn get_champion_by_key(conn: &Connection, key: &str) -> Result<Option<Champi
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))?;
Expand Down
34 changes: 34 additions & 0 deletions src-tauri/crates/olm_core/src/domain/champion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "typescript")]
use ts_rs::TS;

/// Meta tier for draft priority — how strong a champion is in the current patch.
/// Used by the coach AI to balance meta strength against player comfort/mastery.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(TS))]
#[cfg_attr(feature = "typescript", ts(export))]
pub enum MetaTier {
S = 5,
A = 4,
B = 3,
C = 2,
D = 1,
}

impl MetaTier {
pub fn score(&self) -> i32 {
*self as i32
}

pub fn from_name(name: &str) -> Option<Self> {
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))]
Expand All @@ -15,6 +45,9 @@ pub struct Champion {
pub synergies_json: Option<String>,
pub image_tile_url: Option<String>,
pub image_splash_url: Option<String>,
/// 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).
Expand All @@ -29,4 +62,5 @@ pub struct NewChampion {
pub synergies_json: Option<String>,
pub image_tile_url: Option<String>,
pub image_splash_url: Option<String>,
pub meta_tier: MetaTier,
}
1 change: 1 addition & 0 deletions src-tauri/src/commands/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ pub async fn get_champions() -> Result<Vec<olm_core::domain::champion::Champion>
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();
Expand Down
1 change: 1 addition & 0 deletions src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 54 additions & 20 deletions src/ui-v2/_legacy/components/match/ChampionDraft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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));

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2837,14 +2830,55 @@ export default function ChampionDraft({
</p>

{tip.champion ? (
<div className="mt-2 pt-2 border-t border-white/10 flex items-center gap-2">
<div className="mt-2 pt-2 border-t border-white/10 flex flex-wrap items-center gap-2">
<img
src={tip.champion.image}
alt={tip.champion.name}
className="w-7 h-7 rounded-sm object-cover border border-orange-400/60"
loading="lazy"
/>
<span className="text-xs text-gray-200 truncate">{tip.champion.name}</span>

{/* Meta tier badge */}
{tip.champion.meta_tier && (
<span
className={`inline-flex items-center justify-center rounded px-1 py-0.5 text-[10px] font-bold leading-none ${
tip.champion.meta_tier === "S"
? "bg-red-500/20 text-red-300 border border-red-500/40"
: tip.champion.meta_tier === "A"
? "bg-orange-500/20 text-orange-300 border border-orange-500/40"
: tip.champion.meta_tier === "B"
? "bg-blue-500/20 text-blue-300 border border-blue-500/40"
: "bg-gray-500/20 text-gray-400 border border-gray-500/40"
}`}
>
{tip.champion.meta_tier}
</span>
)}

{/* 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) => (
<span
key={`flex-${role}`}
className="inline-flex items-center gap-0.5 rounded bg-cyan-500/15 px-1 py-0.5 text-[10px] text-cyan-300 border border-cyan-500/30"
>
<img
src={ROLE_ICON_URLS[role as Role]}
alt={role}
className="w-2.5 h-2.5 invert opacity-75"
/>
{role}
</span>
));
}
return null;
})()}
</div>
) : null}
</article>
Expand Down