fix lp_change event values and display in front
All checks were successful
record-daemon / Build, check and test (push) Successful in 3m14s

This commit is contained in:
2026-05-07 00:24:02 +02:00
parent fcfa55d0aa
commit ff4d865c2a
4 changed files with 232 additions and 4 deletions

View File

@@ -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(),
}

View File

@@ -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(() => {
<!-- Result Banner -->
<div class="result-banner">
<span class="result-text">{{ getGameResult(game) }}</span>
<span
v-if="getLpChange(game)"
class="lp-badge"
:class="{ gain: getLpChange(game)!.leaguePointsDelta > 0, loss: getLpChange(game)!.leaguePointsDelta < 0 }"
>
{{ formatLpDelta(getLpChange(game)!.leaguePointsDelta) }} LP
</span>
</div>
<!-- Main Content -->
@@ -178,6 +188,10 @@ onMounted(() => {
<div class="game-time">
{{ formatRelativeTime(game.start_time) }}
</div>
<div v-if="getLpChange(game)" class="game-rank">
{{ formatRank(getLpChange(game)!.tier, getLpChange(game)!.division) }}
<span class="game-rank-lp">· {{ getLpChange(game)!.leaguePoints }} LP</span>
</div>
</div>
<!-- Right: KDA Score -->
@@ -288,6 +302,22 @@ onMounted(() => {
<span class="detail-label">Summoner:</span>
<span class="detail-value">{{ getSummonerName(selectedGame) }}</span>
</div>
<div class="detail-row" v-if="getLpChange(selectedGame)">
<span class="detail-label">Rank:</span>
<span class="detail-value">
{{ formatRank(getLpChange(selectedGame)!.tier, getLpChange(selectedGame)!.division) }}
· {{ getLpChange(selectedGame)!.leaguePoints }} LP
</span>
</div>
<div class="detail-row" v-if="getLpChange(selectedGame)">
<span class="detail-label">LP Change:</span>
<span
class="detail-value lp-change"
:class="{ gain: getLpChange(selectedGame)!.leaguePointsDelta > 0, loss: getLpChange(selectedGame)!.leaguePointsDelta < 0 }"
>
{{ formatLpDelta(getLpChange(selectedGame)!.leaguePointsDelta) }} LP
</span>
</div>
</div>
</div>
@@ -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;

View File

@@ -14,6 +14,9 @@ import {
getQueueId,
getItemImageUrl,
getResultColor,
getLpChange,
formatLpDelta,
formatRank,
} from "../types/timeline";
// Props
@@ -508,6 +511,18 @@ onUnmounted(() => {
<span class="result" :style="{ color: getResultColor(getGameResult(game)) }">
{{ getGameResult(game) }}
</span>
<span
v-if="getLpChange(game)"
class="lp-change-badge"
:class="{ gain: getLpChange(game)!.leaguePointsDelta > 0, loss: getLpChange(game)!.leaguePointsDelta < 0 }"
>
{{ formatLpDelta(getLpChange(game)!.leaguePointsDelta) }} LP
</span>
</div>
<div v-if="getLpChange(game)" class="header-rank">
{{ formatRank(getLpChange(game)!.tier, getLpChange(game)!.division) }}
· {{ getLpChange(game)!.leaguePoints }} LP
<span v-if="getLpChange(game)!.inPromos" class="promo-badge">In Promos</span>
</div>
</div>
</div>
@@ -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);

View File

@@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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.
*/