Skip to content

Commit ccd217e

Browse files
committed
feat: add charts to session stats
1 parent a1b6324 commit ccd217e

File tree

7 files changed

+603
-119
lines changed

7 files changed

+603
-119
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ csv = "1.3.1"
7272
jiff = { version = "0.2.4", features = ["serde"] }
7373
rootcause = "0.11.1"
7474
egui_ltreeview = { version = "0.6.1", features = ["persistence"] }
75+
egui_plot = "0.34.0"
7576

7677
[target.'cfg(target_os = "windows")'.build-dependencies]
7778
winresource = "0.1.20"

src/session_stats.rs

Lines changed: 140 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,138 @@ use std::collections::HashMap;
22
use std::sync::Arc;
33

44
use parking_lot::RwLock;
5+
use wows_replays::analyzer::battle_controller::BattleResult;
56
use wowsunpack::game_params::provider::GameMetadataProvider;
67

78
use crate::personal_rating::PersonalRatingData;
89
use crate::personal_rating::PersonalRatingResult;
910
use crate::personal_rating::ShipBattleStats;
11+
use crate::tab_state::ChartableStat;
1012
use crate::ui::replay_parser::Replay;
1113

12-
/// Performance statistics for a single ship across multiple games
14+
/// Per-game statistics extracted from a single replay
15+
#[derive(Clone)]
16+
pub struct PerGameStat {
17+
pub ship_name: String,
18+
pub ship_id: u64,
19+
#[allow(dead_code)]
20+
pub game_time: String,
21+
pub damage: u64,
22+
pub spotting_damage: u64,
23+
pub frags: i64,
24+
pub raw_xp: i64,
25+
pub base_xp: i64,
26+
pub is_win: bool,
27+
pub is_loss: bool,
28+
}
29+
30+
impl PerGameStat {
31+
/// Create a PerGameStat from a replay
32+
pub fn from_replay(replay: &Replay, metadata_provider: &GameMetadataProvider) -> Option<Self> {
33+
let ui_report = replay.ui_report.as_ref()?;
34+
let self_report = ui_report.player_reports().iter().find(|r| r.relation().is_self())?;
35+
let ship_name = replay.vehicle_name(metadata_provider);
36+
let ship_id = replay.player_vehicle()?.shipId;
37+
let game_time = replay.game_time().to_string();
38+
let battle_result = replay.battle_result();
39+
40+
Some(PerGameStat {
41+
ship_name,
42+
ship_id,
43+
game_time,
44+
damage: self_report.actual_damage().unwrap_or_default(),
45+
spotting_damage: self_report.spotting_damage().unwrap_or_default(),
46+
frags: self_report.kills().unwrap_or_default(),
47+
raw_xp: self_report.raw_xp().unwrap_or_default(),
48+
base_xp: self_report.base_xp().unwrap_or_default(),
49+
is_win: matches!(battle_result, Some(BattleResult::Win(_))),
50+
is_loss: matches!(battle_result, Some(BattleResult::Loss(_))),
51+
})
52+
}
53+
54+
/// Get the value of a specific stat for charting
55+
pub fn get_stat(&self, stat: ChartableStat, pr_data: Option<&PersonalRatingData>) -> f64 {
56+
match stat {
57+
ChartableStat::Damage => self.damage as f64,
58+
ChartableStat::SpottingDamage => self.spotting_damage as f64,
59+
ChartableStat::Frags => self.frags as f64,
60+
ChartableStat::RawXp => self.raw_xp as f64,
61+
ChartableStat::BaseXp => self.base_xp as f64,
62+
ChartableStat::WinRate => 0.0, // Win rate doesn't make sense per-game
63+
ChartableStat::PersonalRating => self.calculate_pr(pr_data).unwrap_or(0.0),
64+
}
65+
}
66+
67+
/// Calculate Personal Rating for this single game
68+
pub fn calculate_pr(&self, pr_data: Option<&PersonalRatingData>) -> Option<f64> {
69+
let pr_data = pr_data?;
70+
let stats = ShipBattleStats {
71+
ship_id: self.ship_id,
72+
battles: 1,
73+
damage: self.damage,
74+
wins: if self.is_win { 1 } else { 0 },
75+
frags: self.frags,
76+
};
77+
pr_data.calculate_pr(&[stats]).map(|r| r.pr)
78+
}
79+
}
80+
81+
/// Performance statistics for a single ship aggregated from multiple games
1382
#[derive(Default)]
1483
pub struct PerformanceInfo {
1584
ship_id: Option<u64>,
1685
wins: usize,
1786
losses: usize,
18-
/// Total frags
1987
total_frags: i64,
20-
/// Highest frags in a single match
2188
max_frags: i64,
2289
total_damage: u64,
2390
max_damage: u64,
2491
total_games: usize,
2592
max_xp: i64,
2693
max_win_adjusted_xp: i64,
27-
total_xp: usize,
28-
total_win_adjusted_xp: usize,
94+
total_xp: i64,
95+
total_win_adjusted_xp: i64,
2996
max_spotting_damage: u64,
3097
total_spotting_damage: u64,
3198
}
3299

33100
impl PerformanceInfo {
101+
/// Create a PerformanceInfo by aggregating multiple PerGameStat instances
102+
pub fn from_games(games: &[&PerGameStat]) -> Self {
103+
let mut info = PerformanceInfo::default();
104+
105+
for game in games {
106+
if info.ship_id.is_none() {
107+
info.ship_id = Some(game.ship_id);
108+
}
109+
110+
if game.is_win {
111+
info.wins += 1;
112+
} else if game.is_loss {
113+
info.losses += 1;
114+
}
115+
116+
info.total_frags += game.frags;
117+
info.max_frags = info.max_frags.max(game.frags);
118+
119+
info.total_damage += game.damage;
120+
info.max_damage = info.max_damage.max(game.damage);
121+
122+
info.total_spotting_damage += game.spotting_damage;
123+
info.max_spotting_damage = info.max_spotting_damage.max(game.spotting_damage);
124+
125+
info.total_xp += game.raw_xp;
126+
info.max_xp = info.max_xp.max(game.raw_xp);
127+
128+
info.total_win_adjusted_xp += game.base_xp;
129+
info.max_win_adjusted_xp = info.max_win_adjusted_xp.max(game.base_xp);
130+
131+
info.total_games += 1;
132+
}
133+
134+
info
135+
}
136+
34137
pub fn wins(&self) -> usize {
35138
self.wins
36139
}
@@ -153,6 +256,32 @@ impl SessionStats {
153256
}
154257

155258
self.session_replays.push(replay);
259+
self.session_replays.sort_by(|a, b| a.read().game_time().cmp(b.read().game_time()));
260+
}
261+
262+
/// Get per-game statistics for all replays in the session
263+
pub fn per_game_stats(&self, metadata_provider: &GameMetadataProvider) -> Vec<PerGameStat> {
264+
self.session_replays
265+
.iter()
266+
.filter_map(|replay| {
267+
let replay = replay.read();
268+
PerGameStat::from_replay(&replay, metadata_provider)
269+
})
270+
.collect()
271+
}
272+
273+
/// Get aggregated ship statistics derived from per-game stats
274+
pub fn ship_stats(&self, metadata_provider: &GameMetadataProvider) -> HashMap<String, PerformanceInfo> {
275+
let per_game = self.per_game_stats(metadata_provider);
276+
277+
// Group by ship name
278+
let mut by_ship: HashMap<String, Vec<&PerGameStat>> = HashMap::new();
279+
for game in &per_game {
280+
by_ship.entry(game.ship_name.clone()).or_default().push(game);
281+
}
282+
283+
// Convert to PerformanceInfo
284+
by_ship.into_iter().map(|(name, games)| (name, PerformanceInfo::from_games(&games))).collect()
156285
}
157286

158287
/// Returns the win rate percentage for this session. Will return `None`
@@ -173,131 +302,27 @@ impl SessionStats {
173302
/// Total number of games won in the current session
174303
pub fn games_won(&self) -> usize {
175304
self.session_replays.iter().fold(0, |accum, replay| {
176-
if let Some(wows_replays::analyzer::battle_controller::BattleResult::Win(_)) = replay.read().battle_result()
177-
{
178-
accum + 1
179-
} else {
180-
accum
181-
}
305+
if let Some(BattleResult::Win(_)) = replay.read().battle_result() { accum + 1 } else { accum }
182306
})
183307
}
184308

185309
/// Total number of games lost in the current session
186310
pub fn games_lost(&self) -> usize {
187311
self.session_replays.iter().fold(0, |accum, replay| {
188-
if let Some(wows_replays::analyzer::battle_controller::BattleResult::Loss(_)) =
189-
replay.read().battle_result()
190-
{
191-
accum + 1
192-
} else {
193-
accum
194-
}
312+
if let Some(BattleResult::Loss(_)) = replay.read().battle_result() { accum + 1 } else { accum }
195313
})
196314
}
197315

198-
pub fn ship_stats(&self, metadata_provider: &GameMetadataProvider) -> HashMap<String, PerformanceInfo> {
199-
let mut results: HashMap<String, PerformanceInfo> = HashMap::new();
200-
201-
for replay in &self.session_replays {
202-
let replay = replay.read();
203-
let Some(battle_result) = replay.battle_result() else {
204-
continue;
205-
};
206-
207-
let ship_name = replay.vehicle_name(metadata_provider);
208-
let ship_id = replay.player_vehicle().map(|v| v.shipId);
209-
let performance_info = results.entry(ship_name).or_default();
210-
211-
// Set ship_id if not already set
212-
if performance_info.ship_id.is_none() {
213-
performance_info.ship_id = ship_id;
214-
}
215-
216-
match battle_result {
217-
wows_replays::analyzer::battle_controller::BattleResult::Win(_) => {
218-
performance_info.wins += 1;
219-
}
220-
wows_replays::analyzer::battle_controller::BattleResult::Loss(_) => {
221-
performance_info.losses += 1;
222-
}
223-
wows_replays::analyzer::battle_controller::BattleResult::Draw => {
224-
// do nothing for draws at the moment
225-
}
226-
}
227-
228-
let Some(ui_report) = replay.ui_report.as_ref() else {
229-
continue;
230-
};
231-
let Some(self_report) = ui_report.player_reports().iter().find(|report| report.relation().is_self()) else {
232-
continue;
233-
};
234-
235-
performance_info.total_frags += self_report.kills().unwrap_or_default();
236-
performance_info.max_frags = performance_info.max_frags.max(self_report.kills().unwrap_or_default());
237-
238-
performance_info.total_damage += self_report.actual_damage().unwrap_or_default();
239-
performance_info.max_damage =
240-
performance_info.max_damage.max(self_report.actual_damage().unwrap_or_default());
241-
242-
performance_info.total_spotting_damage += self_report.spotting_damage().unwrap_or_default();
243-
performance_info.max_spotting_damage =
244-
performance_info.max_spotting_damage.max(self_report.spotting_damage().unwrap_or_default());
245-
246-
performance_info.total_xp += self_report.raw_xp().unwrap_or_default() as usize;
247-
performance_info.max_xp = performance_info.max_xp.max(self_report.raw_xp().unwrap_or_default());
248-
249-
performance_info.total_win_adjusted_xp += self_report.base_xp().unwrap_or_default() as usize;
250-
performance_info.max_win_adjusted_xp =
251-
performance_info.max_win_adjusted_xp.max(self_report.base_xp().unwrap_or_default());
252-
253-
performance_info.total_games += 1;
254-
}
255-
256-
results
257-
}
258-
259316
pub fn max_damage(&self, metadata_provider: &GameMetadataProvider) -> Option<(String, u64)> {
260-
self.session_replays
261-
.iter()
262-
.filter_map(|replay| {
263-
let replay = replay.read();
264-
265-
let ui_report = replay.ui_report.as_ref()?;
266-
let self_report = ui_report.player_reports().iter().find(|report| report.relation().is_self())?;
267-
268-
Some((replay.vehicle_name(metadata_provider), self_report.actual_damage()?))
269-
})
270-
.max_by_key(|result| result.1)
317+
self.per_game_stats(metadata_provider).into_iter().map(|g| (g.ship_name, g.damage)).max_by_key(|r| r.1)
271318
}
272319

273320
pub fn max_frags(&self, metadata_provider: &GameMetadataProvider) -> Option<(String, i64)> {
274-
self.session_replays
275-
.iter()
276-
.filter_map(|replay| {
277-
let replay = replay.read();
278-
279-
let ui_report = replay.ui_report.as_ref()?;
280-
let self_report = ui_report.player_reports().iter().find(|report| report.relation().is_self())?;
281-
282-
Some((replay.vehicle_name(metadata_provider), self_report.kills()?))
283-
})
284-
.max_by_key(|result| result.1)
321+
self.per_game_stats(metadata_provider).into_iter().map(|g| (g.ship_name, g.frags)).max_by_key(|r| r.1)
285322
}
286323

287-
pub fn total_frags(&self) -> i64 {
288-
self.session_replays.iter().fold(0, |accum, replay| {
289-
let replay = replay.read();
290-
291-
let Some(ui_report) = replay.ui_report.as_ref() else {
292-
return accum;
293-
};
294-
295-
let Some(self_report) = ui_report.player_reports().iter().find(|report| report.relation().is_self()) else {
296-
return accum;
297-
};
298-
299-
accum + self_report.kills().unwrap_or_default()
300-
})
324+
pub fn total_frags(&self, metadata_provider: &GameMetadataProvider) -> i64 {
325+
self.per_game_stats(metadata_provider).iter().map(|g| g.frags).sum()
301326
}
302327

303328
/// Calculate overall Personal Rating for this session

0 commit comments

Comments
 (0)