diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 656648e..a510ec6 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use mods::Stats; use rosu_v2::prelude::GameModIntermode; use serde::{Deserialize, Serialize}; use std::fmt; @@ -55,6 +56,14 @@ pub struct Difficulty { impl Difficulty { // Difficulty calculation is based on // https://www.reddit.com/r/osugame/comments/6phntt/difficulty_settings_table_with_all_values/ + // + + fn override_stats(&mut self, stats: &Stats) { + self.cs = stats.cs.unwrap_or(self.cs); + self.od = stats.od.unwrap_or(self.od); + self.ar = stats.ar.unwrap_or(self.ar); + self.hp = stats.hp.unwrap_or(self.hp); + } fn apply_everything_by_ratio(&mut self, rat: f64) { self.cs = (self.cs * rat).min(10.0); @@ -109,6 +118,9 @@ impl Difficulty { // CS is changed by 1.3 tho diff.cs = old_cs * 1.3; } + + diff.override_stats(&mods.overrides()); + if let Some(ratio) = mods.inner.clock_rate() { if ratio != 1.0 { diff.apply_length_by_ratio(1.0 / ratio as f64); @@ -116,16 +128,6 @@ impl Difficulty { diff.apply_od_by_time_ratio(1.0 / ratio as f64); } } - // if mods.contains(Mods::HT) { - // diff.apply_ar_by_time_ratio(4.0 / 3.0); - // diff.apply_od_by_time_ratio(4.0 / 3.0); - // diff.apply_length_by_ratio(4, 3); - // } - // if mods.contains(Mods::DT) { - // diff.apply_ar_by_time_ratio(2.0 / 3.0); - // diff.apply_od_by_time_ratio(2.0 / 3.0); - // diff.apply_length_by_ratio(2, 3); - // } diff } diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs index 58efa91..eddcfac 100644 --- a/youmubot-osu/src/models/mods.rs +++ b/youmubot-osu/src/models/mods.rs @@ -4,6 +4,7 @@ use rosu_v2::model::mods as rosu; use rosu_v2::prelude::GameModsIntermode; use std::borrow::Cow; use std::fmt; +use std::fmt::Write; use std::str::FromStr; use youmubot_prelude::*; @@ -15,25 +16,16 @@ lazy_static::lazy_static! { // Beatmap(set) hooks static ref MODS: Regex = Regex::new( // r"(?:https?://)?osu\.ppy\.sh/(?Ps|b|beatmaps)/(?P\d+)(?:[\&\?]m=(?P[0123]))?(?:\+(?P[A-Z]+))?" - r"^((\+?)(?P([A-Za-z0-9][A-Za-z])+))?(@(?P\d(\.\d+)?)x)?(v2)?$" + r"^((\+?)(?P([A-Za-z0-9][A-Za-z])+))?(@(?P\d(\.\d+)?)x)?(?P(@(ar|AR|od|OD|cs|CS|hp|HP)\d(\.\d)?)+)?(v2)?$" ).unwrap(); } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct UnparsedMods { mods: Cow<'static, str>, clock: Option, } -impl Default for UnparsedMods { - fn default() -> Self { - Self { - mods: "".into(), - clock: None, - } - } -} - impl FromStr for UnparsedMods { type Err = String; @@ -147,6 +139,30 @@ pub struct Mods { pub inner: GameMods, } +/// Store overrides to the stats. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Stats { + pub ar: Option, + pub od: Option, + pub hp: Option, + pub cs: Option, +} + +impl Stats { + pub fn from_f32(ar: Option, od: Option, hp: Option, cs: Option) -> Self { + Self { + ar: ar.map(|v| v as f64), + od: od.map(|v| v as f64), + hp: hp.map(|v| v as f64), + cs: cs.map(|v| v as f64), + } + } + + pub fn has_any(&self) -> bool { + self.ar.is_some() || self.od.is_some() || self.hp.is_some() || self.cs.is_some() + } +} + impl Mods { pub const NOMOD: &'static Mods = &Mods { inner: GameMods::new(), @@ -172,6 +188,36 @@ impl Mods { } } } + + pub fn overrides(&self) -> Stats { + use rosu_v2::prelude::GameMod::*; + self.inner + .iter() + .find_map(|m| { + Some(match m { + DifficultyAdjustOsu(da) => Stats::from_f32( + da.approach_rate, + da.overall_difficulty, + da.drain_rate, + da.circle_size, + ), + DifficultyAdjustTaiko(da) => { + Stats::from_f32(None, da.overall_difficulty, da.drain_rate, None) + } + DifficultyAdjustCatch(da) => Stats::from_f32( + da.approach_rate, + da.overall_difficulty, + da.drain_rate, + da.circle_size, + ), + DifficultyAdjustMania(da) => { + Stats::from_f32(None, da.overall_difficulty, da.drain_rate, None) + } + _ => return None, + }) + }) + .unwrap_or_default() + } } impl From for Mods { @@ -303,6 +349,28 @@ impl Mods { } Some(s) } + fn fmt_diff_adj( + ar: Option, + od: Option, + hp: Option, + cs: Option, + ) -> Option { + let stats = [("AR", ar), ("OD", od), ("HP", hp), ("CS", cs)]; + let mut output = String::with_capacity(4 * (2 + 3 + 4) + 3 * 2); + for (name, stat) in stats { + if let Some(stat) = stat { + if !output.is_empty() { + write!(output, ", ").unwrap(); + } + write!(output, "{}**{:.1}**", name, stat).unwrap(); + } + } + if output.is_empty() { + None + } else { + Some("**DA**: ".to_owned() + &output) + } + } self.inner .iter() .filter_map(|m| match m { @@ -323,18 +391,27 @@ impl Mods { DaycoreCatch(ht) => fmt_speed_change("DC", &ht.speed_change, &None), DaycoreMania(ht) => fmt_speed_change("DC", &ht.speed_change, &None), + DifficultyAdjustOsu(da) => fmt_diff_adj( + da.approach_rate, + da.overall_difficulty, + da.drain_rate, + da.circle_size, + ), + DifficultyAdjustTaiko(da) => { + fmt_diff_adj(None, da.overall_difficulty, da.drain_rate, None) + } + DifficultyAdjustCatch(da) => fmt_diff_adj( + da.approach_rate, + da.overall_difficulty, + da.drain_rate, + da.circle_size, + ), + DifficultyAdjustMania(da) => { + fmt_diff_adj(None, da.overall_difficulty, da.drain_rate, None) + } _ => None, }) .collect() - // let mut res: Vec = vec![]; - - // for m in &self.inner { - // match m { - // DoubleTimeOsu(dt) => - // } - // } - - // res } }