use std::time::Duration;
use crate::{
model::{GameMode, GameMods, Grade},
request::GetUser,
serde::*,
Osu,
};
use serde::Deserialize;
#[cfg(feature = "serialize")]
use serde::Serialize;
use time::OffsetDateTime;
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Score {
#[serde(
default,
deserialize_with = "to_maybe_u32",
skip_serializing_if = "Option::is_none"
)]
pub beatmap_id: Option<u32>,
#[serde(
default,
deserialize_with = "to_maybe_u64",
skip_serializing_if = "Option::is_none"
)]
pub score_id: Option<u64>,
#[serde(
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub score: u32,
#[serde(deserialize_with = "to_u32")]
pub user_id: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count300: u32,
#[serde(
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count100: u32,
#[serde(
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count50: u32,
#[serde(
alias = "countmiss",
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count_miss: u32,
#[serde(
alias = "countgeki",
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count_geki: u32,
#[serde(
alias = "countkatu",
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub count_katu: u32,
#[serde(
alias = "maxcombo",
deserialize_with = "to_u32",
default,
skip_serializing_if = "default_u32"
)]
pub max_combo: u32,
#[serde(
deserialize_with = "to_bool",
default,
skip_serializing_if = "default_bool"
)]
pub perfect: bool,
pub enabled_mods: GameMods,
#[serde(with = "serde_date")]
pub date: OffsetDateTime,
#[serde(alias = "rank")]
pub grade: Grade,
#[serde(
default,
deserialize_with = "to_maybe_f32",
skip_serializing_if = "Option::is_none"
)]
pub pp: Option<f32>,
#[serde(
default,
deserialize_with = "to_maybe_bool",
skip_serializing_if = "Option::is_none"
)]
pub replay_available: Option<bool>,
}
impl Default for Score {
fn default() -> Self {
Self {
beatmap_id: None,
score_id: None,
score: 0,
user_id: 0,
username: None,
count300: 0,
count100: 0,
count50: 0,
count_geki: 0,
count_katu: 0,
count_miss: 0,
max_combo: 0,
perfect: false,
enabled_mods: GameMods::default(),
date: OffsetDateTime::now_utc(),
grade: Grade::F,
pp: None,
replay_available: None,
}
}
}
impl PartialEq for Score {
fn eq(&self, other: &Self) -> bool {
if self.user_id != other.user_id || self.score != other.score {
return false;
}
let duration = if self.date > other.date {
self.date - other.date
} else {
other.date - self.date
};
duration <= Duration::from_secs(2)
}
}
impl Eq for Score {}
impl Score {
pub fn get_user<'o>(&self, osu: &'o Osu) -> GetUser<'o> {
osu.user(self.user_id)
}
pub fn total_hits(&self, mode: GameMode) -> u32 {
let mut amount = self.count300 + self.count100 + self.count_miss;
if mode != GameMode::Taiko {
amount += self.count50;
if mode != GameMode::Osu {
amount += self.count_katu;
amount += (mode != GameMode::Catch) as u32 * self.count_geki;
}
}
amount
}
pub fn accuracy(&self, mode: GameMode) -> f32 {
let amount_objects = self.total_hits(mode) as f32;
let (numerator, denumerator) = match mode {
GameMode::Taiko => (
0.5 * self.count100 as f32 + self.count300 as f32,
amount_objects,
),
GameMode::Catch => (
(self.count300 + self.count100 + self.count50) as f32,
amount_objects,
),
GameMode::Osu | GameMode::Mania => {
let mut n = (self.count50 * 50 + self.count100 * 100 + self.count300 * 300) as f32;
n += ((mode == GameMode::Mania) as u32
* (self.count_katu * 200 + self.count_geki * 300)) as f32;
(n, amount_objects * 300.0)
}
};
(10_000.0 * numerator / denumerator).round() / 100.0
}
pub fn recalculate_grade(&mut self, mode: GameMode, accuracy: Option<f32>) -> Grade {
let passed_objects = self.total_hits(mode);
self.grade = match mode {
GameMode::Osu => self.osu_grade(passed_objects),
GameMode::Mania => self.mania_grade(passed_objects, accuracy),
GameMode::Taiko => self.taiko_grade(passed_objects, accuracy),
GameMode::Catch => self.ctb_grade(accuracy),
};
self.grade
}
fn osu_grade(&self, passed_objects: u32) -> Grade {
if self.count300 == passed_objects {
return if self.enabled_mods.contains(GameMods::Hidden) {
Grade::XH
} else {
Grade::X
};
}
let ratio300 = self.count300 as f32 / passed_objects as f32;
let ratio50 = self.count50 as f32 / passed_objects as f32;
if ratio300 > 0.9 && ratio50 < 0.01 && self.count_miss == 0 {
if self.enabled_mods.contains(GameMods::Hidden) {
Grade::SH
} else {
Grade::S
}
} else if ratio300 > 0.9 || (ratio300 > 0.8 && self.count_miss == 0) {
Grade::A
} else if ratio300 > 0.8 || (ratio300 > 0.7 && self.count_miss == 0) {
Grade::B
} else if ratio300 > 0.6 {
Grade::C
} else {
Grade::D
}
}
fn mania_grade(&self, passed_objects: u32, accuracy: Option<f32>) -> Grade {
if self.count_geki == passed_objects {
return if self.enabled_mods.contains(GameMods::Hidden) {
Grade::XH
} else {
Grade::X
};
}
let accuracy = accuracy.unwrap_or_else(|| self.accuracy(GameMode::Mania));
if accuracy > 95.0 {
if self.enabled_mods.contains(GameMods::Hidden) {
Grade::SH
} else {
Grade::S
}
} else if accuracy > 90.0 {
Grade::A
} else if accuracy > 80.0 {
Grade::B
} else if accuracy > 70.0 {
Grade::C
} else {
Grade::D
}
}
fn taiko_grade(&self, passed_objects: u32, accuracy: Option<f32>) -> Grade {
if self.count300 == passed_objects {
return if self.enabled_mods.contains(GameMods::Hidden) {
Grade::XH
} else {
Grade::X
};
}
let accuracy = accuracy.unwrap_or_else(|| self.accuracy(GameMode::Taiko));
if accuracy > 95.0 {
if self.enabled_mods.contains(GameMods::Hidden) {
Grade::SH
} else {
Grade::S
}
} else if accuracy > 90.0 {
Grade::A
} else if accuracy > 80.0 {
Grade::B
} else {
Grade::C
}
}
fn ctb_grade(&self, accuracy: Option<f32>) -> Grade {
let accuracy = accuracy.unwrap_or_else(|| self.accuracy(GameMode::Catch));
if (100.0 - accuracy).abs() <= std::f32::EPSILON {
if self.enabled_mods.contains(GameMods::Hidden) {
Grade::XH
} else {
Grade::X
}
} else if accuracy > 98.0 {
if self.enabled_mods.contains(GameMods::Hidden) {
Grade::SH
} else {
Grade::S
}
} else if accuracy > 94.0 {
Grade::A
} else if accuracy > 90.0 {
Grade::B
} else if accuracy > 85.0 {
Grade::C
} else {
Grade::D
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn score_total_hits() {
let mut score = Score::default();
score.count_geki = 456;
score.count300 = 123;
score.count_katu = 5;
score.count100 = 50;
score.count50 = 2;
score.count_miss = 1;
assert_eq!(score.total_hits(GameMode::Osu), 123 + 50 + 2 + 1);
}
}