Skip to content

Commit 4185da7

Browse files
committed
feat: add PR calculation (thanks WoWs Numbers)
1 parent 92b6cdc commit 4185da7

File tree

6 files changed

+515
-5
lines changed

6 files changed

+515
-5
lines changed

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"mcp__acp__Edit"
5+
]
6+
}
7+
}

src/app.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::mpsc::Receiver;
1313
use std::sync::mpsc::Sender;
1414
use std::sync::mpsc::TryRecvError;
1515
use std::sync::mpsc::{self};
16-
use std::thread::current;
16+
1717
use std::time::Duration;
1818
use std::time::Instant;
1919

@@ -69,6 +69,7 @@ use wowsunpack::game_params::provider::GameMetadataProvider;
6969
use crate::error::ToolkitError;
7070
use crate::game_params::game_params_bin_path;
7171
use crate::icons;
72+
use crate::personal_rating::PersonalRatingData;
7273
use crate::plaintext_viewer::PlaintextFileViewer;
7374
use crate::task::BackgroundParserThread;
7475
use crate::task::BackgroundTask;
@@ -567,7 +568,14 @@ impl SessionStats {
567568
};
568569

569570
let ship_name = replay.vehicle_name(metadata_provider);
571+
let ship_id = replay.player_vehicle().map(|v| v.shipId as u64);
570572
let performance_info = results.entry(ship_name).or_default();
573+
574+
// Set ship_id if not already set
575+
if performance_info.ship_id.is_none() {
576+
performance_info.ship_id = ship_id;
577+
}
578+
571579
match battle_result {
572580
wows_replays::analyzer::battle_controller::BattleResult::Win(_) => {
573581
performance_info.wins += 1;
@@ -647,10 +655,46 @@ impl SessionStats {
647655
accum + self_report.kills().unwrap_or_default()
648656
})
649657
}
658+
659+
/// Calculate overall Personal Rating for this session
660+
pub fn calculate_pr(&self, pr_data: &crate::personal_rating::PersonalRatingData) -> Option<crate::personal_rating::PersonalRatingResult> {
661+
let stats: Vec<_> = self.session_replays.iter().filter_map(|replay| replay.read().to_battle_stats()).collect();
662+
pr_data.calculate_pr(&stats)
663+
}
664+
665+
/// Calculate Personal Rating per ship for this session
666+
/// Returns a map of ship_id -> PR result
667+
#[allow(dead_code)]
668+
pub fn calculate_pr_per_ship(&self, pr_data: &crate::personal_rating::PersonalRatingData) -> HashMap<u64, crate::personal_rating::PersonalRatingResult> {
669+
use crate::personal_rating::ShipBattleStats;
670+
671+
// Group stats by ship_id
672+
let mut ship_stats: HashMap<u64, ShipBattleStats> = HashMap::new();
673+
674+
for replay in &self.session_replays {
675+
if let Some(stats) = replay.read().to_battle_stats() {
676+
let entry = ship_stats.entry(stats.ship_id).or_insert(ShipBattleStats { ship_id: stats.ship_id, battles: 0, damage: 0, wins: 0, frags: 0 });
677+
entry.battles += stats.battles;
678+
entry.damage += stats.damage;
679+
entry.wins += stats.wins;
680+
entry.frags += stats.frags;
681+
}
682+
}
683+
684+
// Calculate PR for each ship
685+
ship_stats
686+
.into_iter()
687+
.filter_map(|(ship_id, stats)| {
688+
let pr = pr_data.calculate_pr(&[stats])?;
689+
Some((ship_id, pr))
690+
})
691+
.collect()
692+
}
650693
}
651694

652695
#[derive(Default)]
653696
pub struct PerformanceInfo {
697+
ship_id: Option<u64>,
654698
wins: usize,
655699
losses: usize,
656700
/// Total frags
@@ -743,6 +787,19 @@ impl PerformanceInfo {
743787
}
744788
Some(self.total_win_adjusted_xp as f64 / self.total_games as f64)
745789
}
790+
791+
/// Calculate Personal Rating for this ship's performance
792+
pub fn calculate_pr(&self, pr_data: &crate::personal_rating::PersonalRatingData) -> Option<crate::personal_rating::PersonalRatingResult> {
793+
let ship_id = self.ship_id?;
794+
let stats = crate::personal_rating::ShipBattleStats {
795+
ship_id,
796+
battles: self.total_games as u32,
797+
damage: self.total_damage,
798+
wins: self.wins as u32,
799+
frags: self.total_frags,
800+
};
801+
pr_data.calculate_pr(&[stats])
802+
}
746803
}
747804

748805
#[derive(Serialize, Deserialize)]
@@ -842,6 +899,8 @@ pub struct TabState {
842899
pub show_session_stats: bool,
843900
#[serde(skip)]
844901
pub session_stats: SessionStats,
902+
#[serde(skip)]
903+
pub personal_rating_data: Arc<RwLock<PersonalRatingData>>,
845904
}
846905

847906
impl Default for TabState {
@@ -884,6 +943,7 @@ impl Default for TabState {
884943
parser_lock: Arc::new(parking_lot::Mutex::new(())),
885944
show_session_stats: false,
886945
session_stats: Default::default(),
946+
personal_rating_data: Arc::new(RwLock::new(PersonalRatingData::new())),
887947
}
888948
}
889949
}
@@ -1314,6 +1374,9 @@ impl WowsToolkitApp {
13141374
BackgroundTaskKind::ModTask(_task_info) => {
13151375
// do nothing
13161376
}
1377+
BackgroundTaskKind::LoadingPersonalRatingData => {
1378+
// do nothing
1379+
}
13171380
BackgroundTaskKind::UpdateTimedMessage(timed_message) => {
13181381
self.tab_state.timed_message.write().replace(timed_message.clone());
13191382
}
@@ -1369,7 +1432,6 @@ impl WowsToolkitApp {
13691432
// Rename this process
13701433
let rename_process = move || {
13711434
std::fs::rename(current_process.clone(), &current_process_new_path).context("failed to rename current process")?;
1372-
13731435
// Rename the new exe
13741436
std::fs::rename(new_exe, &current_process).context("failed to rename new process")?;
13751437

@@ -1391,6 +1453,9 @@ impl WowsToolkitApp {
13911453
BackgroundTaskCompletion::ConstantsLoaded(constants) => {
13921454
*self.tab_state.game_constants.write() = constants;
13931455
}
1456+
BackgroundTaskCompletion::PersonalRatingDataLoaded(pr_data) => {
1457+
self.tab_state.personal_rating_data.write().load(pr_data);
1458+
}
13941459
#[cfg(feature = "mod_manager")]
13951460
BackgroundTaskCompletion::ModManager(mod_manager_info) => {
13961461
match *mod_manager_info {
@@ -1547,6 +1612,16 @@ impl WowsToolkitApp {
15471612
update_background_task!(self.tab_state.background_tasks, Some(task::load_constants(constants_updates)));
15481613
}
15491614
}
1615+
1616+
// Check and update PR expected values
1617+
if crate::personal_rating::needs_update() {
1618+
if let Ok(pr_data) = self.runtime.block_on(crate::personal_rating::fetch_expected_values()) {
1619+
if crate::personal_rating::save_expected_values(&pr_data).is_ok() {
1620+
update_background_task!(self.tab_state.background_tasks, Some(task::load_personal_rating_data(pr_data)));
1621+
}
1622+
}
1623+
}
1624+
15501625
self.checked_for_updates = true;
15511626
}
15521627

@@ -1880,7 +1955,11 @@ fn build_about_window(ui: &mut egui::Ui) {
18801955
ui.vertical(|ui| {
18811956
ui.label("Made by landaire.");
18821957
ui.label("Thanks to Trackpad, TTaro, lkolbly for their contributions.");
1883-
if ui.button("View on GitHub").clicked() {
1958+
ui.horizontal(|ui| {
1959+
ui.label("Personal rating (PR) calculation data and formula provided by WoWs Numbers.");
1960+
ui.hyperlink_to("More Info.", "https://wows-numbers.com/personal/rating");
1961+
});
1962+
if ui.button("View Project on GitHub").clicked() {
18841963
ui.ctx().open_url(OpenUrl::new_tab("https://github.com/landaire/wows-toolkit"));
18851964
}
18861965

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod error;
77
mod game_params;
88
#[cfg(feature = "mod_manager")]
99
mod mod_manager;
10+
mod personal_rating;
1011
mod plaintext_viewer;
1112
mod replay_export;
1213
mod task;

0 commit comments

Comments
 (0)