diff --git a/record-daemon/src/lqp/events.rs b/record-daemon/src/lqp/events.rs index ede4798..39b8170 100644 --- a/record-daemon/src/lqp/events.rs +++ b/record-daemon/src/lqp/events.rs @@ -124,16 +124,34 @@ pub fn describe_event(event_type: &str, raw_data: &serde_json::Value) -> String format!("Phase changed to: {}", phase) } EVENT_TYPE_LP_CHANGE => { - let lp_change = raw_data - .get("lpChange") + // Prefer leaguePointsDelta from raw_data (actual LP change amount) + let lp_delta = raw_data + .get("leaguePointsDelta") .and_then(|v| v.as_i64()) + .or_else(|| raw_data.get("lpChange").and_then(|v| v.as_i64())) .unwrap_or(0); let tier = raw_data .get("tier") .and_then(|v| v.as_str()) .unwrap_or("UNRANKED"); - let sign = if lp_change >= 0 { "+" } else { "" }; - format!("LP Change: {}{} LP ({})", sign, lp_change, tier) + let division = raw_data + .get("division") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let lp_after = raw_data + .get("leaguePoints") + .and_then(|v| v.as_i64()) + .unwrap_or(0); + let sign = if lp_delta >= 0 { "+" } else { "" }; + let rank = if division.is_empty() { + tier.to_string() + } else { + format!("{} {}", tier, division) + }; + format!( + "LP Change: {}{} LP ({} → {} LP)", + sign, lp_delta, rank, lp_after + ) } _ => "Unknown event".to_string(), } diff --git a/tauri-app/src/components/GameHistory.vue b/tauri-app/src/components/GameHistory.vue index 8181ff2..cd36ce5 100644 --- a/tauri-app/src/components/GameHistory.vue +++ b/tauri-app/src/components/GameHistory.vue @@ -30,6 +30,9 @@ import { getFinalStats, getSummonerSpells, getItems, + getLpChange, + formatLpDelta, + formatRank, } from "../types/timeline"; // Helper to get video timestamp in seconds from tuple format @@ -123,6 +126,13 @@ onMounted(() => {
{{ getGameResult(game) }} + + {{ formatLpDelta(getLpChange(game)!.leaguePointsDelta) }} LP +
@@ -178,6 +188,10 @@ onMounted(() => {
{{ formatRelativeTime(game.start_time) }}
+
+ {{ formatRank(getLpChange(game)!.tier, getLpChange(game)!.division) }} + · {{ getLpChange(game)!.leaguePoints }} LP +
@@ -288,6 +302,22 @@ onMounted(() => { Summoner: {{ getSummonerName(selectedGame) }} +
+ Rank: + + {{ formatRank(getLpChange(selectedGame)!.tier, getLpChange(selectedGame)!.division) }} + · {{ getLpChange(selectedGame)!.leaguePoints }} LP + +
+
+ LP Change: + + {{ formatLpDelta(getLpChange(selectedGame)!.leaguePointsDelta) }} LP + +
@@ -537,6 +567,7 @@ onMounted(() => { padding: 0.35rem 0.75rem; display: flex; align-items: center; + gap: 0.5rem; } .result-text { @@ -546,6 +577,33 @@ onMounted(() => { letter-spacing: 0.05em; } +.lp-badge { + font-size: 0.7rem; + font-weight: 700; + padding: 0.1rem 0.4rem; + border-radius: 3px; + margin-left: auto; +} + +.lp-badge.gain { + color: #4ade80; + background: rgba(74, 222, 128, 0.15); +} + +.lp-badge.loss { + color: #f87171; + background: rgba(248, 113, 113, 0.15); +} + +.game-rank { + font-size: 0.7rem; + color: #c8aa6e; +} + +.game-rank-lp { + color: #888; +} + /* Game Content */ .game-content { display: flex; @@ -908,6 +966,16 @@ onMounted(() => { font-weight: 500; } +.detail-value.lp-change.gain { + color: #4ade80; + font-weight: 700; +} + +.detail-value.lp-change.loss { + color: #f87171; + font-weight: 700; +} + .stats-highlight { display: flex; justify-content: center; diff --git a/tauri-app/src/components/GameReview.vue b/tauri-app/src/components/GameReview.vue index 4cc8e66..566a937 100644 --- a/tauri-app/src/components/GameReview.vue +++ b/tauri-app/src/components/GameReview.vue @@ -14,6 +14,9 @@ import { getQueueId, getItemImageUrl, getResultColor, + getLpChange, + formatLpDelta, + formatRank, } from "../types/timeline"; // Props @@ -508,6 +511,18 @@ onUnmounted(() => { {{ getGameResult(game) }} + + {{ formatLpDelta(getLpChange(game)!.leaguePointsDelta) }} LP + + +
+ {{ formatRank(getLpChange(game)!.tier, getLpChange(game)!.division) }} + · {{ getLpChange(game)!.leaguePoints }} LP + In Promos
@@ -958,6 +973,40 @@ onUnmounted(() => { font-weight: 600; } +.lp-change-badge { + font-size: 0.8rem; + font-weight: 700; + padding: 0.1rem 0.5rem; + border-radius: 4px; +} + +.lp-change-badge.gain { + color: #4ade80; + background: rgba(74, 222, 128, 0.15); +} + +.lp-change-badge.loss { + color: #f87171; + background: rgba(248, 113, 113, 0.15); +} + +.header-rank { + font-size: 0.8rem; + color: #c8aa6e; + margin-top: 0.15rem; +} + +.promo-badge { + font-size: 0.65rem; + font-weight: 700; + color: #fbbf24; + background: rgba(251, 191, 36, 0.15); + padding: 0.1rem 0.35rem; + border-radius: 3px; + margin-left: 0.35rem; + text-transform: uppercase; +} + .toggle-stats-btn { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); diff --git a/tauri-app/src/types/timeline.ts b/tauri-app/src/types/timeline.ts index 30cc132..a10ec2b 100644 --- a/tauri-app/src/types/timeline.ts +++ b/tauri-app/src/types/timeline.ts @@ -23,6 +23,30 @@ export interface TimestampedEvent { uri: string; } +/** + * LP change information extracted from an lp_change event's raw_data. + */ +export interface LpChangeInfo { + /** LP delta (positive = gain, negative = loss). */ + leaguePointsDelta: number; + /** Current league points after the change. */ + leaguePoints: number; + /** Tier (e.g. "EMERALD", "DIAMOND", "GOLD"). */ + tier: string; + /** Division (e.g. "I", "II", "III", "IV"). */ + division: string; + /** Queue type (e.g. "RANKED_SOLO_5x5", "RANKED_FLEX_SR"). */ + queueType: string; + /** Total wins. */ + wins: number; + /** Total losses. */ + losses: number; + /** Current win streak. */ + winStreak: number; + /** Whether the player is in promos. */ + inPromos: boolean; +} + /** * Final game statistics for the player. */ @@ -377,6 +401,75 @@ export function getItems(game: GameHistoryItem): (ItemInfo | null)[] { return result; } +/** + * Extract LP change info from lp_change events in a game's timeline. + * Multiple lp_change events may exist — we pick the one with a ranked tier + * (non-UNRANKED) or a non-zero leaguePointsDelta, since the others record zeros. + * Returns null if no lp_change event is found or data is incomplete. + */ +export function getLpChange(game: GameHistoryItem): LpChangeInfo | null { + const lpEvents = game.events.filter(e => e.event_type === 'lp_change'); + if (lpEvents.length === 0) return null; + + // Prefer the event with a non-UNRANKED tier (the actual ranked one) + let lpEvent = lpEvents.find(e => { + const raw = e.raw_data as Record; + return raw?.tier && raw.tier !== 'UNRANKED'; + }); + + // Fallback: pick the event with a non-zero leaguePointsDelta + if (!lpEvent) { + lpEvent = lpEvents.find(e => { + const raw = e.raw_data as Record; + return typeof raw?.leaguePointsDelta === 'number' && raw.leaguePointsDelta !== 0; + }); + } + + // Last resort: use the first event + if (!lpEvent) { + lpEvent = lpEvents[0]; + } + + const raw = lpEvent.raw_data as Record; + if (!raw) return null; + + // Use leaguePointsDelta from raw_data (the actual LP change amount) + const leaguePointsDelta = typeof raw.leaguePointsDelta === 'number' ? raw.leaguePointsDelta : null; + if (leaguePointsDelta === null) return null; + + return { + leaguePointsDelta, + leaguePoints: typeof raw.leaguePoints === 'number' ? raw.leaguePoints : 0, + tier: typeof raw.tier === 'string' ? raw.tier : 'UNRANKED', + division: typeof raw.division === 'string' ? raw.division : '', + queueType: typeof raw.queueType === 'string' ? raw.queueType : '', + wins: typeof raw.wins === 'number' ? raw.wins : 0, + losses: typeof raw.losses === 'number' ? raw.losses : 0, + winStreak: typeof raw.winStreak === 'number' ? raw.winStreak : 0, + inPromos: typeof raw.miniseriesProgress === 'string' ? raw.miniseriesProgress.length > 0 : false, + }; +} + +/** + * Format an LP delta as a signed string (e.g. "+19", "-15"). + */ +export function formatLpDelta(delta: number): string { + if (delta > 0) return `+${delta}`; + return `${delta}`; +} + +/** + * Format a rank string from tier and division (e.g. "Emerald II", "Diamond I"). + */ +export function formatRank(tier: string, division: string): string { + if (tier === 'UNRANKED' || !tier) return 'Unranked'; + // Capitalize first letter of tier, lowercase rest + const tierDisplay = tier.charAt(0) + tier.slice(1).toLowerCase(); + if (!division) return tierDisplay; + // Convert Roman numerals: I, II, III, IV + return `${tierDisplay} ${division}`; +} + /** * Get the result color class. */