replace items record with full end-of-game stats record
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m5s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m5s
- record-daemon: remove items parsing module - tauri-app: add items parsing from recorded end-of-game stats
This commit is contained in:
@@ -16,8 +16,7 @@ use super::api_types::{
|
|||||||
};
|
};
|
||||||
use super::auth::LockfileCredentials;
|
use super::auth::LockfileCredentials;
|
||||||
use super::endpoints;
|
use super::endpoints;
|
||||||
use super::events::{GameEvent, ItemBuild};
|
use super::events::GameEvent;
|
||||||
use super::items::{parse_items_from_game_stats, parse_items_from_live_client};
|
|
||||||
use super::mappings::{champion_id_to_name, spell_id_to_name};
|
use super::mappings::{champion_id_to_name, spell_id_to_name};
|
||||||
use super::state::{ClientState, GameflowPhase};
|
use super::state::{ClientState, GameflowPhase};
|
||||||
use super::tls::create_insecure_tls_config;
|
use super::tls::create_insecure_tls_config;
|
||||||
@@ -725,122 +724,6 @@ impl LqpClient {
|
|||||||
|
|
||||||
Ok(players)
|
Ok(players)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch final items from end-of-game stats or live client data.
|
|
||||||
pub async fn fetch_final_items(&self) -> Result<Option<ItemBuild>> {
|
|
||||||
info!("[ITEMS] Fetching final items...");
|
|
||||||
|
|
||||||
// First try live client data (typed)
|
|
||||||
match self.get_live_client_player_list_typed().await {
|
|
||||||
Ok(player_list) => {
|
|
||||||
info!(
|
|
||||||
"[ITEMS] Live client player list response received with {} players",
|
|
||||||
player_list.0.len()
|
|
||||||
);
|
|
||||||
for player in &player_list.0 {
|
|
||||||
if player.is_local_player == Some(true) {
|
|
||||||
info!("[ITEMS] Found local player in live client data");
|
|
||||||
if let Some(ref items) = player.items {
|
|
||||||
info!("[ITEMS] Items array has {} items", items.len());
|
|
||||||
// Convert LiveClientItem to serde_json::Value for parsing
|
|
||||||
let items_json: Vec<serde_json::Value> = items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| {
|
|
||||||
item.item_id.map(|id| {
|
|
||||||
serde_json::json!({"itemId": id, "displayName": item.display_name})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let item_build = parse_items_from_live_client(&items_json);
|
|
||||||
if item_build.is_some() {
|
|
||||||
info!("[ITEMS] Successfully parsed items from live client data");
|
|
||||||
return Ok(item_build);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
info!("[ITEMS] Failed to get live client player list: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try end-of-game stats (typed)
|
|
||||||
match self.get_game_stats_typed().await {
|
|
||||||
Ok(stats) => {
|
|
||||||
info!("[ITEMS] Game stats response received");
|
|
||||||
|
|
||||||
// Try local player first
|
|
||||||
if let Some(local_player) = stats.get_local_player() {
|
|
||||||
info!("[ITEMS] Found localPlayer in game stats");
|
|
||||||
if let Some(ref items) = local_player.items {
|
|
||||||
info!("[ITEMS] localPlayer.items array has {} items", items.len());
|
|
||||||
// Convert item IDs to serde_json::Value for parsing (as raw numbers)
|
|
||||||
let items_json: Vec<serde_json::Value> =
|
|
||||||
items.iter().map(|id| serde_json::json!(*id)).collect();
|
|
||||||
let item_build = parse_items_from_game_stats(&items_json);
|
|
||||||
if item_build.is_some() {
|
|
||||||
info!("[ITEMS] Successfully parsed items from localPlayer");
|
|
||||||
return Ok(item_build);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try teams
|
|
||||||
if let Some(ref teams) = stats.teams {
|
|
||||||
info!("[ITEMS] Found {} teams in game stats", teams.len());
|
|
||||||
for team in teams {
|
|
||||||
if let Some(ref players) = team.players {
|
|
||||||
for player in players {
|
|
||||||
if player.is_local_player == Some(true) {
|
|
||||||
info!("[ITEMS] Found local player in teams[].players[]");
|
|
||||||
if let Some(ref items) = player.items {
|
|
||||||
info!(
|
|
||||||
"[ITEMS] Player items array has {} items",
|
|
||||||
items.len()
|
|
||||||
);
|
|
||||||
let items_json: Vec<serde_json::Value> =
|
|
||||||
items.iter().map(|id| serde_json::json!(*id)).collect();
|
|
||||||
let item_build = parse_items_from_game_stats(&items_json);
|
|
||||||
if item_build.is_some() {
|
|
||||||
info!("[ITEMS] Successfully parsed items from teams[].players[]");
|
|
||||||
return Ok(item_build);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try legacy players array
|
|
||||||
if let Some(ref players) = stats.players {
|
|
||||||
info!(
|
|
||||||
"[ITEMS] Found {} players in game stats (legacy)",
|
|
||||||
players.len()
|
|
||||||
);
|
|
||||||
if let Some(player) = players.first() {
|
|
||||||
if let Some(ref items) = player.items {
|
|
||||||
let items_json: Vec<serde_json::Value> =
|
|
||||||
items.iter().map(|id| serde_json::json!(*id)).collect();
|
|
||||||
let item_build = parse_items_from_game_stats(&items_json);
|
|
||||||
if item_build.is_some() {
|
|
||||||
return Ok(item_build);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("[ITEMS] Could not find items in game stats structure");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
info!("[ITEMS] Failed to get game stats: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("[ITEMS] Could not fetch final items from any source");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LqpClient {
|
impl Default for LqpClient {
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
//! Item parsing utilities for LQP client.
|
|
||||||
//!
|
|
||||||
//! Provides functions for parsing item builds from different data formats.
|
|
||||||
|
|
||||||
use super::events::{ItemBuild, ItemInfo};
|
|
||||||
|
|
||||||
/// Parse items from live client data format.
|
|
||||||
///
|
|
||||||
/// Live client data items are objects with `itemID` and `displayName` fields.
|
|
||||||
pub fn parse_items_from_live_client(items: &[serde_json::Value]) -> Option<ItemBuild> {
|
|
||||||
let mut item_list = Vec::new();
|
|
||||||
let mut trinket = None;
|
|
||||||
|
|
||||||
for (slot, item) in items.iter().enumerate() {
|
|
||||||
if let Some(item_id) = item.get("itemID").and_then(|id| id.as_u64()) {
|
|
||||||
if item_id > 0 {
|
|
||||||
let item_info = ItemInfo {
|
|
||||||
item_id: item_id as u32,
|
|
||||||
name: item
|
|
||||||
.get("displayName")
|
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.map(|s| s.to_string()),
|
|
||||||
slot: slot as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Slot 6 is typically the trinket
|
|
||||||
if slot == 6 {
|
|
||||||
trinket = Some(item_info);
|
|
||||||
} else {
|
|
||||||
item_list.push(item_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !item_list.is_empty() || trinket.is_some() {
|
|
||||||
Some(ItemBuild {
|
|
||||||
items: item_list,
|
|
||||||
trinket,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse items from game stats format.
|
|
||||||
///
|
|
||||||
/// Game stats items are just numbers (item IDs) in an array.
|
|
||||||
pub fn parse_items_from_game_stats(items: &[serde_json::Value]) -> Option<ItemBuild> {
|
|
||||||
let mut item_list = Vec::new();
|
|
||||||
let mut trinket = None;
|
|
||||||
|
|
||||||
for (slot, item) in items.iter().enumerate() {
|
|
||||||
// Items in game stats are just numbers (item IDs), not objects
|
|
||||||
if let Some(item_id) = item.as_u64() {
|
|
||||||
if item_id > 0 {
|
|
||||||
let item_info = ItemInfo {
|
|
||||||
item_id: item_id as u32,
|
|
||||||
name: None, // Item names would need a separate mapping
|
|
||||||
slot: slot as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Slot 6 is typically the trinket
|
|
||||||
if slot == 6 {
|
|
||||||
trinket = Some(item_info);
|
|
||||||
} else {
|
|
||||||
item_list.push(item_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !item_list.is_empty() || trinket.is_some() {
|
|
||||||
Some(ItemBuild {
|
|
||||||
items: item_list,
|
|
||||||
trinket,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_items_from_live_client_empty() {
|
|
||||||
let items = vec![];
|
|
||||||
let result = parse_items_from_live_client(&items);
|
|
||||||
assert!(result.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_items_from_live_client_with_items() {
|
|
||||||
let items = vec![
|
|
||||||
serde_json::json!({"itemID": 1001, "displayName": "Boots"}),
|
|
||||||
serde_json::json!({"itemID": 0, "displayName": ""}),
|
|
||||||
];
|
|
||||||
let result = parse_items_from_live_client(&items);
|
|
||||||
assert!(result.is_some());
|
|
||||||
let build = result.unwrap();
|
|
||||||
assert_eq!(build.items.len(), 1);
|
|
||||||
assert_eq!(build.items[0].item_id, 1001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_items_from_game_stats_empty() {
|
|
||||||
let items = vec![];
|
|
||||||
let result = parse_items_from_game_stats(&items);
|
|
||||||
assert!(result.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_items_from_game_stats_with_items() {
|
|
||||||
let items = vec![
|
|
||||||
serde_json::json!(1001),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(3020),
|
|
||||||
];
|
|
||||||
let result = parse_items_from_game_stats(&items);
|
|
||||||
assert!(result.is_some());
|
|
||||||
let build = result.unwrap();
|
|
||||||
assert_eq!(build.items.len(), 2);
|
|
||||||
assert_eq!(build.items[0].item_id, 1001);
|
|
||||||
assert_eq!(build.items[1].item_id, 3020);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_items_with_trinket() {
|
|
||||||
// Slot 6 is trinket
|
|
||||||
let items = vec![
|
|
||||||
serde_json::json!(1001),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(0),
|
|
||||||
serde_json::json!(3340), // Trinket in slot 6
|
|
||||||
];
|
|
||||||
let result = parse_items_from_game_stats(&items);
|
|
||||||
assert!(result.is_some());
|
|
||||||
let build = result.unwrap();
|
|
||||||
assert_eq!(build.items.len(), 1);
|
|
||||||
assert!(build.trinket.is_some());
|
|
||||||
assert_eq!(build.trinket.unwrap().item_id, 3340);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ mod auth;
|
|||||||
mod client;
|
mod client;
|
||||||
mod endpoints;
|
mod endpoints;
|
||||||
mod events;
|
mod events;
|
||||||
mod items;
|
|
||||||
mod mappings;
|
mod mappings;
|
||||||
mod state;
|
mod state;
|
||||||
mod tls;
|
mod tls;
|
||||||
@@ -34,7 +33,6 @@ pub use events::{
|
|||||||
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
|
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
|
||||||
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
||||||
};
|
};
|
||||||
pub use items::{parse_items_from_game_stats, parse_items_from_live_client};
|
|
||||||
pub use mappings::{champion_id_to_name, map_id_to_name, spell_id_to_name};
|
pub use mappings::{champion_id_to_name, map_id_to_name, spell_id_to_name};
|
||||||
pub use state::{ClientState, GameflowPhase};
|
pub use state::{ClientState, GameflowPhase};
|
||||||
pub use websocket::{parse_event_from_uri, parse_websocket_message};
|
pub use websocket::{parse_event_from_uri, parse_websocket_message};
|
||||||
|
|||||||
@@ -503,10 +503,6 @@ impl Daemon {
|
|||||||
StateTransition::GameEnded => {
|
StateTransition::GameEnded => {
|
||||||
info!("[EVENT_HANDLER] GameEnded transition");
|
info!("[EVENT_HANDLER] GameEnded transition");
|
||||||
|
|
||||||
// Fetch final items before stopping
|
|
||||||
let fetched_final_items =
|
|
||||||
self.lqp_client.fetch_final_items().await.ok().flatten();
|
|
||||||
|
|
||||||
// Fetch end-of-game stats from API
|
// Fetch end-of-game stats from API
|
||||||
let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok();
|
let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok();
|
||||||
|
|
||||||
@@ -515,10 +511,7 @@ impl Daemon {
|
|||||||
game_end_stats
|
game_end_stats
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self.stop_recording_with_metadata(game_end_stats).await {
|
||||||
.stop_recording_with_metadata(game_end_stats, fetched_final_items)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("[EVENT_HANDLER] Failed to stop recording: {}", e);
|
error!("[EVENT_HANDLER] Failed to stop recording: {}", e);
|
||||||
|
|
||||||
// Don't propagate error - keep daemon running
|
// Don't propagate error - keep daemon running
|
||||||
@@ -608,14 +601,13 @@ impl Daemon {
|
|||||||
|
|
||||||
/// Stop recording.
|
/// Stop recording.
|
||||||
async fn stop_recording(&self) -> Result<()> {
|
async fn stop_recording(&self) -> Result<()> {
|
||||||
self.stop_recording_with_metadata(None, None).await
|
self.stop_recording_with_metadata(None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop recording with optional game end stats.
|
/// Stop recording with optional game end stats.
|
||||||
async fn stop_recording_with_metadata(
|
async fn stop_recording_with_metadata(
|
||||||
&self,
|
&self,
|
||||||
game_end_stats: Option<record_daemon::lqp::EndOfGameStatsResponse>,
|
game_end_stats: Option<record_daemon::lqp::EndOfGameStatsResponse>,
|
||||||
final_items: Option<record_daemon::lqp::ItemBuild>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("Stopping recording");
|
info!("Stopping recording");
|
||||||
|
|
||||||
@@ -672,6 +664,9 @@ impl Daemon {
|
|||||||
update.victory = Some(stats.is_victory());
|
update.victory = Some(stats.is_victory());
|
||||||
update.match_id = stats.match_id.map(|id| id.to_string());
|
update.match_id = stats.match_id.map(|id| id.to_string());
|
||||||
|
|
||||||
|
// Store raw end-of-game stats as JSON
|
||||||
|
update.raw_end_game_stats = serde_json::to_value(&stats).ok();
|
||||||
|
|
||||||
// Get local player stats
|
// Get local player stats
|
||||||
if let Some(player) = stats.get_local_player() {
|
if let Some(player) = stats.get_local_player() {
|
||||||
if let Some(player_stats) = &player.stats {
|
if let Some(player_stats) = &player.stats {
|
||||||
@@ -692,9 +687,6 @@ impl Daemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add final items
|
|
||||||
update.final_items = final_items;
|
|
||||||
|
|
||||||
// Apply the update
|
// Apply the update
|
||||||
if let Err(e) = timeline_store.write().update_metadata(recording_id, update) {
|
if let Err(e) = timeline_store.write().update_metadata(recording_id, update) {
|
||||||
warn!("Failed to update recording metadata: {}", e);
|
warn!("Failed to update recording metadata: {}", e);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use chrono::{DateTime, Duration, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::lqp::{GameEvent, ItemBuild, RunePage, SummonerSpells};
|
use crate::lqp::{GameEvent, RunePage, SummonerSpells};
|
||||||
|
|
||||||
/// A timeline of events for a recording.
|
/// A timeline of events for a recording.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -67,9 +67,9 @@ pub struct Timeline {
|
|||||||
/// Summoner spells.
|
/// Summoner spells.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub summoner_spells: Option<SummonerSpells>,
|
pub summoner_spells: Option<SummonerSpells>,
|
||||||
/// Final item build at game end.
|
/// Raw end-of-game stats JSON from the API.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub final_items: Option<ItemBuild>,
|
pub raw_end_game_stats: Option<serde_json::Value>,
|
||||||
/// All players in the game (puuid to summoner name mapping).
|
/// All players in the game (puuid to summoner name mapping).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub all_players: Vec<PlayerIdentityInfo>,
|
pub all_players: Vec<PlayerIdentityInfo>,
|
||||||
@@ -104,7 +104,7 @@ impl Timeline {
|
|||||||
final_stats: None,
|
final_stats: None,
|
||||||
runes: None,
|
runes: None,
|
||||||
summoner_spells: None,
|
summoner_spells: None,
|
||||||
final_items: None,
|
raw_end_game_stats: None,
|
||||||
all_players: Vec::new(),
|
all_players: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::error::{Result, TimelineError};
|
use crate::error::{Result, TimelineError};
|
||||||
use crate::lqp::{GameEvent, ItemBuild, RunePage, SummonerSpells};
|
use crate::lqp::{GameEvent, RunePage, SummonerSpells};
|
||||||
use crate::recording::RecordingResult;
|
use crate::recording::RecordingResult;
|
||||||
|
|
||||||
/// A timestamped event in the timeline.
|
/// A timestamped event in the timeline.
|
||||||
@@ -69,9 +69,9 @@ pub struct RecordingMetadata {
|
|||||||
/// Player's summoner spells.
|
/// Player's summoner spells.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub summoner_spells: Option<SummonerSpells>,
|
pub summoner_spells: Option<SummonerSpells>,
|
||||||
/// Final item build at game end.
|
/// Raw end-of-game stats JSON from the API.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub final_items: Option<ItemBuild>,
|
pub raw_end_game_stats: Option<serde_json::Value>,
|
||||||
/// All players in the game (puuid -> summoner name mapping).
|
/// All players in the game (puuid -> summoner name mapping).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub all_players: Vec<PlayerIdentityInfo>,
|
pub all_players: Vec<PlayerIdentityInfo>,
|
||||||
@@ -148,7 +148,7 @@ pub struct MetadataUpdate {
|
|||||||
pub final_stats: Option<GameFinalStats>,
|
pub final_stats: Option<GameFinalStats>,
|
||||||
pub runes: Option<RunePage>,
|
pub runes: Option<RunePage>,
|
||||||
pub summoner_spells: Option<SummonerSpells>,
|
pub summoner_spells: Option<SummonerSpells>,
|
||||||
pub final_items: Option<ItemBuild>,
|
pub raw_end_game_stats: Option<serde_json::Value>,
|
||||||
pub all_players: Vec<PlayerIdentityInfo>,
|
pub all_players: Vec<PlayerIdentityInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ impl RecordingMetadata {
|
|||||||
final_stats: None,
|
final_stats: None,
|
||||||
runes: None,
|
runes: None,
|
||||||
summoner_spells: None,
|
summoner_spells: None,
|
||||||
final_items: None,
|
raw_end_game_stats: None,
|
||||||
all_players: Vec::new(),
|
all_players: Vec::new(),
|
||||||
start_time: result.start_time,
|
start_time: result.start_time,
|
||||||
end_time: Some(result.end_time),
|
end_time: Some(result.end_time),
|
||||||
@@ -239,7 +239,7 @@ impl TimelineStore {
|
|||||||
final_stats: None,
|
final_stats: None,
|
||||||
runes: None,
|
runes: None,
|
||||||
summoner_spells: None,
|
summoner_spells: None,
|
||||||
final_items: None,
|
raw_end_game_stats: None,
|
||||||
all_players: Vec::new(),
|
all_players: Vec::new(),
|
||||||
start_time: Utc::now(),
|
start_time: Utc::now(),
|
||||||
end_time: None,
|
end_time: None,
|
||||||
@@ -300,7 +300,7 @@ impl TimelineStore {
|
|||||||
final_stats: None,
|
final_stats: None,
|
||||||
runes: None,
|
runes: None,
|
||||||
summoner_spells: None,
|
summoner_spells: None,
|
||||||
final_items: None,
|
raw_end_game_stats: None,
|
||||||
all_players: Vec::new(),
|
all_players: Vec::new(),
|
||||||
start_time: result.start_time,
|
start_time: result.start_time,
|
||||||
end_time: Some(result.end_time),
|
end_time: Some(result.end_time),
|
||||||
@@ -386,8 +386,8 @@ impl TimelineStore {
|
|||||||
if let Some(summoner_spells) = update.summoner_spells {
|
if let Some(summoner_spells) = update.summoner_spells {
|
||||||
metadata.summoner_spells = Some(summoner_spells);
|
metadata.summoner_spells = Some(summoner_spells);
|
||||||
}
|
}
|
||||||
if let Some(final_items) = update.final_items {
|
if let Some(raw_end_game_stats) = update.raw_end_game_stats {
|
||||||
metadata.final_items = Some(final_items);
|
metadata.raw_end_game_stats = Some(raw_end_game_stats);
|
||||||
}
|
}
|
||||||
if !update.all_players.is_empty() {
|
if !update.all_players.is_empty() {
|
||||||
metadata.all_players = update.all_players;
|
metadata.all_players = update.all_players;
|
||||||
@@ -442,7 +442,7 @@ impl TimelineStore {
|
|||||||
final_stats: metadata.final_stats.clone(),
|
final_stats: metadata.final_stats.clone(),
|
||||||
runes: metadata.runes.clone(),
|
runes: metadata.runes.clone(),
|
||||||
summoner_spells: metadata.summoner_spells.clone(),
|
summoner_spells: metadata.summoner_spells.clone(),
|
||||||
final_items: metadata.final_items.clone(),
|
raw_end_game_stats: metadata.raw_end_game_stats.clone(),
|
||||||
all_players: metadata.all_players.clone(),
|
all_players: metadata.all_players.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ impl TimelineStore {
|
|||||||
final_stats: metadata.final_stats,
|
final_stats: metadata.final_stats,
|
||||||
runes: metadata.runes,
|
runes: metadata.runes,
|
||||||
summoner_spells: metadata.summoner_spells,
|
summoner_spells: metadata.summoner_spells,
|
||||||
final_items: metadata.final_items,
|
raw_end_game_stats: metadata.raw_end_game_stats,
|
||||||
all_players: metadata.all_players,
|
all_players: metadata.all_players,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -535,7 +535,7 @@ impl TimelineStore {
|
|||||||
final_stats: None,
|
final_stats: None,
|
||||||
runes: None,
|
runes: None,
|
||||||
summoner_spells: None,
|
summoner_spells: None,
|
||||||
final_items: None,
|
raw_end_game_stats: None,
|
||||||
all_players: Vec::new(),
|
all_players: Vec::new(),
|
||||||
start_time: timeline.start_time,
|
start_time: timeline.start_time,
|
||||||
end_time: timeline.end_time,
|
end_time: timeline.end_time,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { GameHistoryItem, GameResult, TimestampedEvent, ItemInfo } from "../types/timeline";
|
import type { GameHistoryItem, TimestampedEvent, ItemInfo, RawEndGameStats, EndGamePlayer } from "../types/timeline";
|
||||||
import {
|
import {
|
||||||
getGameResult,
|
getGameResult,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
@@ -49,21 +49,57 @@ function closeDetail() {
|
|||||||
selectedGame.value = null;
|
selectedGame.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get items array for display (6 slots + trinket)
|
// Helper to find local player from raw end game stats
|
||||||
function getItemsArray(game: GameHistoryItem): (ItemInfo | null)[] {
|
function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
|
||||||
const result: (ItemInfo | null)[] = [null, null, null, null, null, null, null];
|
if (!stats) return null;
|
||||||
if (game.final_items) {
|
|
||||||
// Fill main items (slots 0-5)
|
// Try local_player field first
|
||||||
for (const item of game.final_items.items) {
|
if (stats.local_player) {
|
||||||
if (item.slot >= 0 && item.slot <= 5) {
|
return stats.local_player;
|
||||||
result[item.slot] = item;
|
}
|
||||||
|
|
||||||
|
// Try teams
|
||||||
|
if (stats.teams) {
|
||||||
|
for (const team of stats.teams) {
|
||||||
|
if (team.players) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
if (player.is_local_player) {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fill trinket (slot 6)
|
}
|
||||||
if (game.final_items.trinket) {
|
|
||||||
result[6] = game.final_items.trinket;
|
// Try legacy players array
|
||||||
|
if (stats.players && stats.players.length > 0) {
|
||||||
|
return stats.players[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get items array for display (6 slots + trinket)
|
||||||
|
// Items are now stored as raw item IDs in raw_end_game_stats
|
||||||
|
function getItemsArray(game: GameHistoryItem): (ItemInfo | null)[] {
|
||||||
|
const result: (ItemInfo | null)[] = [null, null, null, null, null, null, null];
|
||||||
|
|
||||||
|
const localPlayer = getLocalPlayer(game.raw_end_game_stats);
|
||||||
|
if (localPlayer && localPlayer.items) {
|
||||||
|
// Items are stored as an array of item IDs (up to 7 items: 6 main + 1 trinket)
|
||||||
|
for (let i = 0; i < Math.min(localPlayer.items.length, 7); i++) {
|
||||||
|
const itemId = localPlayer.items[i];
|
||||||
|
if (itemId && itemId > 0) {
|
||||||
|
// Slot 6 is trinket, slots 0-5 are main items
|
||||||
|
result[i] = {
|
||||||
|
itemId: itemId,
|
||||||
|
name: null,
|
||||||
|
slot: i
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +253,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="item-slot trinket">
|
<div class="item-slot trinket">
|
||||||
<img
|
<img
|
||||||
v-if="getItemsArray(game)[6] && getItemsArray(game)[6].itemId"
|
v-if="getItemsArray(game)[6]?.itemId"
|
||||||
:src="getItemImageUrl(getItemsArray(game)[6].itemId)"
|
:src="getItemImageUrl(getItemsArray(game)[6]!.itemId)"
|
||||||
:alt="getItemsArray(game)[6].name || 'Trinket'"
|
:alt="getItemsArray(game)[6]?.name || 'Trinket'"
|
||||||
class="item-image"
|
class="item-image"
|
||||||
/>
|
/>
|
||||||
<div v-else class="item-empty"></div>
|
<div v-else class="item-empty"></div>
|
||||||
|
|||||||
@@ -169,14 +169,83 @@ export interface Timeline {
|
|||||||
runes: RunePage | null;
|
runes: RunePage | null;
|
||||||
/** Summoner spells. */
|
/** Summoner spells. */
|
||||||
summoner_spells: SummonerSpells | null;
|
summoner_spells: SummonerSpells | null;
|
||||||
/** Final item build at game end. */
|
/** Raw end-of-game stats JSON from the API. */
|
||||||
final_items: ItemBuild | null;
|
raw_end_game_stats: RawEndGameStats | null;
|
||||||
|
|
||||||
// All players in the game (puuid to summoner name mapping)
|
// All players in the game (puuid to summoner name mapping)
|
||||||
/** All players in the game. */
|
/** All players in the game. */
|
||||||
all_players: PlayerIdentityInfo[];
|
all_players: PlayerIdentityInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw end-of-game stats from the League Client API.
|
||||||
|
* This is the full response from the end-of-game stats endpoint.
|
||||||
|
*/
|
||||||
|
export interface RawEndGameStats {
|
||||||
|
/** Game ID. */
|
||||||
|
game_id?: number;
|
||||||
|
/** Game length in seconds. */
|
||||||
|
game_length?: number;
|
||||||
|
/** Match ID. */
|
||||||
|
match_id?: number;
|
||||||
|
/** Game result. */
|
||||||
|
game_result?: string;
|
||||||
|
/** Local player data. */
|
||||||
|
local_player?: EndGamePlayer;
|
||||||
|
/** Teams data. */
|
||||||
|
teams?: EndGameTeam[];
|
||||||
|
/** Players (legacy format). */
|
||||||
|
players?: EndGamePlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player in end-of-game stats.
|
||||||
|
*/
|
||||||
|
export interface EndGamePlayer {
|
||||||
|
/** Whether this is the local player. */
|
||||||
|
is_local_player?: boolean;
|
||||||
|
/** Player stats. */
|
||||||
|
stats?: PlayerStats;
|
||||||
|
/** Items (array of item IDs). */
|
||||||
|
items?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team in end-of-game stats.
|
||||||
|
*/
|
||||||
|
export interface EndGameTeam {
|
||||||
|
/** Whether this is the player's team. */
|
||||||
|
is_player_team?: boolean;
|
||||||
|
/** Whether this is the winning team. */
|
||||||
|
is_winning_team?: boolean;
|
||||||
|
/** Players on the team. */
|
||||||
|
players?: EndGamePlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player stats in end-of-game.
|
||||||
|
*/
|
||||||
|
export interface PlayerStats {
|
||||||
|
/** Kills. */
|
||||||
|
CHAMPIONS_KILLED?: number;
|
||||||
|
/** Deaths. */
|
||||||
|
NUM_DEATHS?: number;
|
||||||
|
/** Assists. */
|
||||||
|
ASSISTS?: number;
|
||||||
|
/** Minions killed (CS). */
|
||||||
|
MINIONS_KILLED?: number;
|
||||||
|
/** Gold earned. */
|
||||||
|
GOLD_EARNED?: number;
|
||||||
|
/** Total damage dealt to champions. */
|
||||||
|
TOTAL_DAMAGE_DEALT_TO_CHAMPIONS?: number;
|
||||||
|
/** Total damage taken. */
|
||||||
|
TOTAL_DAMAGE_TAKEN?: number;
|
||||||
|
/** Vision score. */
|
||||||
|
VISION_SCORE?: number;
|
||||||
|
/** Win status (1 = win). */
|
||||||
|
WIN?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game history item for display in the UI.
|
* Game history item for display in the UI.
|
||||||
* Alias for Timeline since the backend returns full timeline data.
|
* Alias for Timeline since the backend returns full timeline data.
|
||||||
|
|||||||
Reference in New Issue
Block a user