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
+
+
+
@@ -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.
*/