implemented taiko pp update
This commit is contained in:
@@ -55,6 +55,8 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
|||||||
PatternType::LOW_PROBABILITY
|
PatternType::LOW_PROBABILITY
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ! BUG: Since `LegacyDifficultyControlPoint` are not considered while parsing,
|
||||||
|
// ! this value can be slightly off due to float arithmetics.
|
||||||
let beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
let beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
||||||
|
|
||||||
let span_count = (repeats + 1) as i32;
|
let span_count = (repeats + 1) as i32;
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ impl Beatmap {
|
|||||||
|
|
||||||
let edge_sound_count = edge_sounds.len().max(1);
|
let edge_sound_count = edge_sounds.len().max(1);
|
||||||
|
|
||||||
while j <= obj.start_time + params.duration + params.tick_spacing / 8.0 {
|
while j
|
||||||
|
<= obj.start_time + params.duration as f64 + params.tick_spacing / 8.0
|
||||||
|
{
|
||||||
let h = HitObject {
|
let h = HitObject {
|
||||||
pos: Default::default(),
|
pos: Default::default(),
|
||||||
start_time: j,
|
start_time: j,
|
||||||
@@ -109,6 +111,8 @@ impl Beatmap {
|
|||||||
let timing_point = self.timing_point_at(*start_time);
|
let timing_point = self.timing_point_at(*start_time);
|
||||||
let difficulty_point = self.difficulty_point_at(*start_time).unwrap_or_default();
|
let difficulty_point = self.difficulty_point_at(*start_time).unwrap_or_default();
|
||||||
|
|
||||||
|
// ! BUG: Since `LegacyDifficultyControlPoint` are not considered while parsing,
|
||||||
|
// ! this value can be slightly off due to float arithmetics.
|
||||||
let mut beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
let mut beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
||||||
|
|
||||||
let slider_scoring_point_dist =
|
let slider_scoring_point_dist =
|
||||||
@@ -116,7 +120,7 @@ impl Beatmap {
|
|||||||
|
|
||||||
// * The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
|
// * The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
|
||||||
let taiko_vel = slider_scoring_point_dist * self.tick_rate;
|
let taiko_vel = slider_scoring_point_dist * self.tick_rate;
|
||||||
*duration = (dist / taiko_vel * beat_len).floor();
|
*duration = (dist / taiko_vel * beat_len) as u32;
|
||||||
|
|
||||||
let osu_vel = taiko_vel * (1000.0_f32 as f64 / beat_len);
|
let osu_vel = taiko_vel * (1000.0_f32 as f64 / beat_len);
|
||||||
|
|
||||||
@@ -126,7 +130,7 @@ impl Beatmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
|
// * If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
|
||||||
*tick_spacing = (beat_len / self.tick_rate).min(*duration / spans);
|
*tick_spacing = (beat_len / self.tick_rate).min(*duration as f64 / spans);
|
||||||
|
|
||||||
*tick_spacing > 0.0 && dist / osu_vel * 1000.0 < 2.0 * beat_len
|
*tick_spacing > 0.0 && dist / osu_vel * 1000.0 < 2.0 * beat_len
|
||||||
}
|
}
|
||||||
@@ -134,7 +138,7 @@ impl Beatmap {
|
|||||||
|
|
||||||
struct SliderParams<'c> {
|
struct SliderParams<'c> {
|
||||||
curve: &'c Curve,
|
curve: &'c Curve,
|
||||||
duration: f64,
|
duration: u32,
|
||||||
repeats: usize,
|
repeats: usize,
|
||||||
start_time: f64,
|
start_time: f64,
|
||||||
tick_spacing: f64,
|
tick_spacing: f64,
|
||||||
@@ -146,7 +150,7 @@ impl<'c> SliderParams<'c> {
|
|||||||
curve,
|
curve,
|
||||||
repeats,
|
repeats,
|
||||||
start_time,
|
start_time,
|
||||||
duration: 0.0,
|
duration: 0,
|
||||||
tick_spacing: 0.0,
|
tick_spacing: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -453,7 +453,7 @@ impl PerformanceAttributes {
|
|||||||
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
|
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
|
||||||
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
|
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
|
||||||
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()),
|
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()),
|
||||||
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty),
|
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
|
||||||
iter::{Cycle, Skip, Take},
|
iter::{Cycle, Skip, Take},
|
||||||
ops::Index,
|
ops::Index,
|
||||||
slice::Iter,
|
slice::Iter,
|
||||||
@@ -71,11 +70,6 @@ impl<T, const N: usize> LimitedQueue<T, N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clear(&mut self) {
|
|
||||||
self.end = N - 1;
|
|
||||||
self.len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn full(&self) -> bool {
|
pub(crate) fn full(&self) -> bool {
|
||||||
self.len == N
|
self.len == N
|
||||||
}
|
}
|
||||||
@@ -91,20 +85,6 @@ impl<T, const N: usize> LimitedQueue<T, N> {
|
|||||||
|
|
||||||
pub(crate) type LimitedQueueIter<'a, T> = Take<Skip<Cycle<Iter<'a, T>>>>;
|
pub(crate) type LimitedQueueIter<'a, T> = Take<Skip<Cycle<Iter<'a, T>>>>;
|
||||||
|
|
||||||
impl<T: PartialOrd, const N: usize> LimitedQueue<T, N> {
|
|
||||||
pub(crate) fn min(&self) -> Option<&T> {
|
|
||||||
self.queue
|
|
||||||
.iter()
|
|
||||||
.take(self.len)
|
|
||||||
.reduce(|min, next| match min.partial_cmp(next) {
|
|
||||||
Some(Ordering::Less) => min,
|
|
||||||
Some(Ordering::Equal) => min,
|
|
||||||
Some(Ordering::Greater) => next,
|
|
||||||
None => min,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, const N: usize> Index<usize> for LimitedQueue<T, N> {
|
impl<T, const N: usize> Index<usize> for LimitedQueue<T, N> {
|
||||||
type Output = T;
|
type Output = T;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::taiko::difficulty_object::TaikoDifficultyObject;
|
||||||
|
|
||||||
|
use super::{mono_streak::MonoStreak, repeating_hit_patterns::RepeatingHitPatterns};
|
||||||
|
|
||||||
|
pub(crate) struct AlternatingMonoPattern<'o> {
|
||||||
|
pub(crate) mono_streaks: Vec<Rc<RefCell<MonoStreak<'o>>>>,
|
||||||
|
pub(crate) parent: Option<Weak<RefCell<RepeatingHitPatterns<'o>>>>,
|
||||||
|
pub(crate) idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for AlternatingMonoPattern<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"(idx={}, mono_len={}, has_parent={})",
|
||||||
|
self.idx,
|
||||||
|
self.mono_streaks.len(),
|
||||||
|
self.parent.is_some()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'o> AlternatingMonoPattern<'o> {
|
||||||
|
pub(crate) fn new() -> Rc<RefCell<Self>> {
|
||||||
|
let this = Self {
|
||||||
|
mono_streaks: Vec::new(),
|
||||||
|
parent: None,
|
||||||
|
idx: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Rc::new(RefCell::new(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
self.mono_streaks
|
||||||
|
.first()
|
||||||
|
.and_then(|streak| streak.borrow().first_hit_object())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_repetition_of(&self, other: &Self) -> bool {
|
||||||
|
self.has_identical_mono_len(other)
|
||||||
|
&& other.mono_streaks.len() == self.mono_streaks.len()
|
||||||
|
&& other
|
||||||
|
.mono_streaks
|
||||||
|
.first()
|
||||||
|
.map(|streak| streak.borrow().hit_kind())
|
||||||
|
== self
|
||||||
|
.mono_streaks
|
||||||
|
.first()
|
||||||
|
.map(|streak| streak.borrow().hit_kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_identical_mono_len(&self, other: &Self) -> bool {
|
||||||
|
let other_len = other
|
||||||
|
.mono_streaks
|
||||||
|
.first()
|
||||||
|
.map(|streak| streak.borrow().run_len());
|
||||||
|
|
||||||
|
let self_len = self
|
||||||
|
.mono_streaks
|
||||||
|
.first()
|
||||||
|
.map(|streak| streak.borrow().run_len());
|
||||||
|
|
||||||
|
other_len == self_len
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) use self::{
|
||||||
|
alternating_mono_pattern::AlternatingMonoPattern, mono_streak::MonoStreak,
|
||||||
|
preprocessor::ColourDifficultyPreprocessor, repeating_hit_patterns::RepeatingHitPatterns,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod alternating_mono_pattern;
|
||||||
|
mod mono_streak;
|
||||||
|
mod preprocessor;
|
||||||
|
mod repeating_hit_patterns;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(crate) struct TaikoDifficultyColour<'o> {
|
||||||
|
pub(crate) mono_streak: Option<Weak<RefCell<MonoStreak<'o>>>>,
|
||||||
|
pub(crate) alternating_mono_pattern: Option<Weak<RefCell<AlternatingMonoPattern<'o>>>>,
|
||||||
|
pub(crate) repeating_hit_patterns: Option<Rc<RefCell<RepeatingHitPatterns<'o>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub(crate) enum HitKind {
|
||||||
|
Centre,
|
||||||
|
Rim,
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::taiko::difficulty_object::{MonoIndex, TaikoDifficultyObject};
|
||||||
|
|
||||||
|
use super::{alternating_mono_pattern::AlternatingMonoPattern, HitKind};
|
||||||
|
|
||||||
|
pub(crate) struct MonoStreak<'o> {
|
||||||
|
pub(crate) hit_objects: Vec<Weak<RefCell<TaikoDifficultyObject<'o>>>>,
|
||||||
|
pub(crate) parent: Option<Weak<RefCell<AlternatingMonoPattern<'o>>>>,
|
||||||
|
pub(crate) idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MonoStreak<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"(idx={}, obj_len={}, has_parent={})",
|
||||||
|
self.idx,
|
||||||
|
self.hit_objects.len(),
|
||||||
|
self.parent.is_some()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'o> MonoStreak<'o> {
|
||||||
|
pub(crate) fn new() -> Rc<RefCell<Self>> {
|
||||||
|
let this = Self {
|
||||||
|
hit_objects: Vec::new(),
|
||||||
|
parent: None,
|
||||||
|
idx: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Rc::new(RefCell::new(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
self.hit_objects.first().map(Weak::clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hit_kind(&self) -> Option<HitKind> {
|
||||||
|
self.hit_objects
|
||||||
|
.first()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
.and_then(|obj| match obj.borrow().mono_idx {
|
||||||
|
MonoIndex::Centre(_) => Some(HitKind::Centre),
|
||||||
|
MonoIndex::Rim(_) => Some(HitKind::Rim),
|
||||||
|
MonoIndex::None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_len(&self) -> usize {
|
||||||
|
self.hit_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::VecDeque,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::taiko::difficulty_object::ObjectLists;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
alternating_mono_pattern::AlternatingMonoPattern, mono_streak::MonoStreak,
|
||||||
|
repeating_hit_patterns::RepeatingHitPatterns,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct ColourDifficultyPreprocessor {}
|
||||||
|
|
||||||
|
impl<'o> ColourDifficultyPreprocessor {
|
||||||
|
pub(crate) fn process_and_assign(lists: &mut ObjectLists<'o>) {
|
||||||
|
// * Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
|
||||||
|
// * assigned with the relevant encodings.
|
||||||
|
for repeating_hit_pattern in Self::encode(lists) {
|
||||||
|
if let Some(obj) = repeating_hit_pattern
|
||||||
|
.borrow()
|
||||||
|
.first_hit_object()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
{
|
||||||
|
obj.borrow_mut().colour.repeating_hit_patterns =
|
||||||
|
Some(Rc::clone(&repeating_hit_pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
|
||||||
|
// * keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
|
||||||
|
// * documentation.
|
||||||
|
for i in 0..repeating_hit_pattern
|
||||||
|
.borrow()
|
||||||
|
.alternating_mono_patterns
|
||||||
|
.len()
|
||||||
|
{
|
||||||
|
let borrowed_repeating_hit_pattern = repeating_hit_pattern.borrow();
|
||||||
|
let mono_pattern = &borrowed_repeating_hit_pattern.alternating_mono_patterns[i];
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut borrowed = mono_pattern.borrow_mut();
|
||||||
|
borrowed.parent = Some(Rc::downgrade(&repeating_hit_pattern));
|
||||||
|
borrowed.idx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(obj) = mono_pattern
|
||||||
|
.borrow()
|
||||||
|
.first_hit_object()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
{
|
||||||
|
obj.borrow_mut().colour.alternating_mono_pattern =
|
||||||
|
Some(Rc::downgrade(mono_pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
for j in 0..mono_pattern.borrow().mono_streaks.len() {
|
||||||
|
let borrowed_mono_pattern = mono_pattern.borrow();
|
||||||
|
let mono_streak = &borrowed_mono_pattern.mono_streaks[j];
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut borrowed = mono_streak.borrow_mut();
|
||||||
|
borrowed.parent = Some(Rc::downgrade(mono_pattern));
|
||||||
|
borrowed.idx = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(obj) = mono_streak
|
||||||
|
.borrow()
|
||||||
|
.first_hit_object()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
{
|
||||||
|
obj.borrow_mut().colour.mono_streak = Some(Rc::downgrade(mono_streak));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(data: &mut ObjectLists<'o>) -> Vec<Rc<RefCell<RepeatingHitPatterns<'o>>>> {
|
||||||
|
let mono_streaks = Self::encode_mono_streak(data);
|
||||||
|
let alternating_mono_patterns = Self::encode_alternating_mono_pattern(mono_streaks);
|
||||||
|
|
||||||
|
Self::encode_repeating_hit_pattern(alternating_mono_patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_mono_streak(data: &mut ObjectLists<'o>) -> Vec<Rc<RefCell<MonoStreak<'o>>>> {
|
||||||
|
let mut mono_streaks = vec![MonoStreak::new()];
|
||||||
|
let mut curr_mono_streak = mono_streaks.last_mut();
|
||||||
|
let mut data_iter = data.all.iter();
|
||||||
|
|
||||||
|
if let (Some(curr), Some(taiko_obj)) = (&curr_mono_streak, data_iter.next()) {
|
||||||
|
curr.borrow_mut().hit_objects.push(Rc::downgrade(taiko_obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
for taiko_obj in data_iter {
|
||||||
|
// * This ignores all non-note objects, which may or may not be the desired behaviour
|
||||||
|
let prev = data.prev_note(taiko_obj.borrow().idx, 0);
|
||||||
|
|
||||||
|
// * If this is the first object in the list or the colour changed, create a new mono streak
|
||||||
|
let condition = prev.filter(|prev| {
|
||||||
|
!(taiko_obj.borrow().base.is_hit()
|
||||||
|
&& prev.borrow().base.is_hit()
|
||||||
|
&& (taiko_obj.borrow().base.is_rim() != prev.borrow().base.is_rim()))
|
||||||
|
});
|
||||||
|
|
||||||
|
if condition.is_none() {
|
||||||
|
mono_streaks.push(MonoStreak::new());
|
||||||
|
curr_mono_streak = mono_streaks.last_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Add the current object to the encoded payload.
|
||||||
|
if let Some(ref curr) = curr_mono_streak {
|
||||||
|
curr.borrow_mut().hit_objects.push(Rc::downgrade(taiko_obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mono_streaks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_alternating_mono_pattern(
|
||||||
|
data: Vec<Rc<RefCell<MonoStreak<'o>>>>,
|
||||||
|
) -> VecDeque<Rc<RefCell<AlternatingMonoPattern<'o>>>> {
|
||||||
|
let mut mono_patterns = VecDeque::new();
|
||||||
|
mono_patterns.push_back(AlternatingMonoPattern::new());
|
||||||
|
let mut curr_mono_pattern = mono_patterns.back_mut();
|
||||||
|
|
||||||
|
if let (Some(curr), Some(mono)) = (&curr_mono_pattern, data.first()) {
|
||||||
|
curr.borrow_mut().mono_streaks.push(Rc::clone(mono));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (prev, curr) in data.iter().zip(data.iter().skip(1)) {
|
||||||
|
// * Start a new AlternatingMonoPattern if the previous MonoStreak has a different mono length,
|
||||||
|
// * or if this is the first MonoStreak in the list.
|
||||||
|
if curr.borrow().run_len() != prev.borrow().run_len() {
|
||||||
|
mono_patterns.push_back(AlternatingMonoPattern::new());
|
||||||
|
curr_mono_pattern = mono_patterns.back_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Add the current MonoStreak to the encoded payload.
|
||||||
|
if let Some(ref curr_mono_pattern) = curr_mono_pattern {
|
||||||
|
curr_mono_pattern
|
||||||
|
.borrow_mut()
|
||||||
|
.mono_streaks
|
||||||
|
.push(Rc::clone(curr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mono_patterns
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_repeating_hit_pattern(
|
||||||
|
mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern<'o>>>>,
|
||||||
|
) -> Vec<Rc<RefCell<RepeatingHitPatterns<'o>>>> {
|
||||||
|
let mut hit_patterns = Vec::new();
|
||||||
|
let mut curr_hit_pattern: Option<Rc<std::cell::RefCell<_>>> = None;
|
||||||
|
|
||||||
|
while !data.is_empty() {
|
||||||
|
let old = curr_hit_pattern.as_ref().map(Rc::downgrade);
|
||||||
|
let curr_hit_pattern = curr_hit_pattern.insert(RepeatingHitPatterns::new(old));
|
||||||
|
|
||||||
|
let mut is_coupled = data.get(2).map_or(false, |other| {
|
||||||
|
data[0].borrow().is_repetition_of(&other.borrow())
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_coupled {
|
||||||
|
// * If so, add the current AlternatingMonoPattern to the encoded payload and start repeatedly checking if the
|
||||||
|
// * subsequent AlternatingMonoPatterns should be grouped by increasing i and doing the appropriate isCoupled check.
|
||||||
|
while is_coupled {
|
||||||
|
curr_hit_pattern
|
||||||
|
.borrow_mut()
|
||||||
|
.alternating_mono_patterns
|
||||||
|
.push(data.pop_front().unwrap());
|
||||||
|
|
||||||
|
is_coupled = data.get(2).map_or(false, |other| {
|
||||||
|
data[0].borrow().is_repetition_of(&other.borrow())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Skip over viewed data and add the rest to the payload
|
||||||
|
for front in data.drain(..2) {
|
||||||
|
curr_hit_pattern
|
||||||
|
.borrow_mut()
|
||||||
|
.alternating_mono_patterns
|
||||||
|
.push(front);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// * If not, add the current AlternatingMonoPattern to the encoded payload and continue.
|
||||||
|
curr_hit_pattern
|
||||||
|
.borrow_mut()
|
||||||
|
.alternating_mono_patterns
|
||||||
|
.push(data.pop_front().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
hit_patterns.push(Rc::clone(&*curr_hit_pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
hit_patterns
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|pattern| pattern.borrow_mut().find_repetition_interval());
|
||||||
|
|
||||||
|
hit_patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::taiko::difficulty_object::TaikoDifficultyObject;
|
||||||
|
|
||||||
|
use super::alternating_mono_pattern::AlternatingMonoPattern;
|
||||||
|
|
||||||
|
pub(crate) struct RepeatingHitPatterns<'o> {
|
||||||
|
pub(crate) alternating_mono_patterns: Vec<Rc<RefCell<AlternatingMonoPattern<'o>>>>,
|
||||||
|
pub(crate) prev: Option<Weak<RefCell<Self>>>,
|
||||||
|
pub(crate) repetition_interval: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for RepeatingHitPatterns<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"(interval={}, alt_len={}, has_prev={})",
|
||||||
|
self.repetition_interval,
|
||||||
|
self.alternating_mono_patterns.len(),
|
||||||
|
self.prev.is_some()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'o> RepeatingHitPatterns<'o> {
|
||||||
|
const MAX_REPETITION_INTERVAL: usize = 16;
|
||||||
|
|
||||||
|
pub(crate) fn new(prev: Option<Weak<RefCell<Self>>>) -> Rc<RefCell<Self>> {
|
||||||
|
let this = Self {
|
||||||
|
alternating_mono_patterns: Vec::new(),
|
||||||
|
prev,
|
||||||
|
repetition_interval: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Rc::new(RefCell::new(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
self.alternating_mono_patterns
|
||||||
|
.first()
|
||||||
|
.and_then(|pattern| pattern.borrow().first_hit_object())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_repetition_of(&self, other: &Self) -> bool {
|
||||||
|
if self.alternating_mono_patterns.len() != other.alternating_mono_patterns.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.alternating_mono_patterns
|
||||||
|
.iter()
|
||||||
|
.zip(other.alternating_mono_patterns.iter())
|
||||||
|
.take(2)
|
||||||
|
.all(|(self_pat, other_pat)| {
|
||||||
|
self_pat
|
||||||
|
.borrow()
|
||||||
|
.has_identical_mono_len(&other_pat.borrow())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_repetition_interval(&mut self) {
|
||||||
|
let mut other = match self.prev.as_ref().and_then(Weak::upgrade) {
|
||||||
|
Some(prev) => prev,
|
||||||
|
None => return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut interval = 1;
|
||||||
|
|
||||||
|
while interval < Self::MAX_REPETITION_INTERVAL {
|
||||||
|
if self.is_repetition_of(&other.borrow()) {
|
||||||
|
return self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = match other.borrow().prev.as_ref().and_then(Weak::upgrade) {
|
||||||
|
Some(prev) => prev,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
// gotta love NLL...
|
||||||
|
other = next;
|
||||||
|
|
||||||
|
interval += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+178
-20
@@ -1,34 +1,192 @@
|
|||||||
use super::{closest_rhythm, taiko_object::TaikoObject, HitObjectRhythm};
|
use std::{cell::RefCell, cmp::Ordering, rc::Rc};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
use crate::parse::HitObject;
|
||||||
pub(crate) struct DifficultyObject<'o> {
|
|
||||||
pub(crate) idx: usize,
|
use super::{colours::TaikoDifficultyColour, rim::Rim, taiko_object::TaikoObject};
|
||||||
pub(crate) base: TaikoObject<'o>,
|
|
||||||
pub(crate) prev: TaikoObject<'o>,
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) delta: f64,
|
pub(crate) struct ObjectLists<'o> {
|
||||||
pub(crate) rhythm: &'static HitObjectRhythm,
|
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject<'o>>>>,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) centres: Vec<usize>,
|
||||||
|
pub(crate) rims: Vec<usize>,
|
||||||
|
pub(crate) notes: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'o> DifficultyObject<'o> {
|
impl<'o> ObjectLists<'o> {
|
||||||
#[inline]
|
pub(crate) fn prev_mono(
|
||||||
|
&self,
|
||||||
|
curr: usize,
|
||||||
|
backwards_idx: usize,
|
||||||
|
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
let curr = &self.all[curr];
|
||||||
|
|
||||||
|
let prev = match curr.borrow().mono_idx {
|
||||||
|
MonoIndex::Centre(idx) => idx
|
||||||
|
.checked_sub(backwards_idx + 1)
|
||||||
|
.and_then(|idx| self.centres.get(idx))?,
|
||||||
|
MonoIndex::Rim(idx) => idx
|
||||||
|
.checked_sub(backwards_idx + 1)
|
||||||
|
.and_then(|idx| self.rims.get(idx))?,
|
||||||
|
MonoIndex::None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.all.get(*prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn next_mono(
|
||||||
|
&self,
|
||||||
|
curr: usize,
|
||||||
|
forwards_idx: usize,
|
||||||
|
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
let curr = &self.all[curr];
|
||||||
|
|
||||||
|
let next = match curr.borrow().mono_idx {
|
||||||
|
MonoIndex::Centre(idx) => self.centres.get(idx + (forwards_idx + 1))?,
|
||||||
|
MonoIndex::Rim(idx) => self.rims.get(idx + (forwards_idx + 1))?,
|
||||||
|
MonoIndex::None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.all.get(*next)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prev_note(
|
||||||
|
&self,
|
||||||
|
curr: usize,
|
||||||
|
backwards_idx: usize,
|
||||||
|
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
let curr = &self.all[curr];
|
||||||
|
let note_idx = curr.borrow().note_idx?;
|
||||||
|
let idx = note_idx.checked_sub(backwards_idx + 1)?;
|
||||||
|
let prev = self.notes.get(idx)?;
|
||||||
|
|
||||||
|
self.all.get(*prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn next_note(
|
||||||
|
&self,
|
||||||
|
curr: usize,
|
||||||
|
forwards_idx: usize,
|
||||||
|
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
|
||||||
|
let curr = &self.all[curr];
|
||||||
|
let note_idx = curr.borrow().note_idx?;
|
||||||
|
let idx = note_idx + (forwards_idx + 1);
|
||||||
|
let prev = self.notes.get(idx)?;
|
||||||
|
|
||||||
|
self.all.get(*prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) enum MonoIndex {
|
||||||
|
Centre(usize),
|
||||||
|
Rim(usize),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct TaikoDifficultyObject<'o> {
|
||||||
|
pub(crate) base: TaikoObject<'o>,
|
||||||
|
pub(crate) prev_time: f64,
|
||||||
|
pub(crate) colour: TaikoDifficultyColour<'o>,
|
||||||
|
pub(crate) rhythm: &'static HitObjectRhythm,
|
||||||
|
pub(crate) mono_idx: MonoIndex,
|
||||||
|
pub(crate) note_idx: Option<usize>,
|
||||||
|
pub(crate) idx: usize,
|
||||||
|
pub(crate) delta: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'o> TaikoDifficultyObject<'o> {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
idx: usize,
|
|
||||||
base: TaikoObject<'o>,
|
base: TaikoObject<'o>,
|
||||||
prev: TaikoObject<'o>,
|
last: TaikoObject<'o>,
|
||||||
prev_prev: TaikoObject<'o>,
|
last_last: TaikoObject<'o>,
|
||||||
clock_rate: f64,
|
clock_rate: f64,
|
||||||
|
lists: &ObjectLists<'o>,
|
||||||
|
idx: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta = (base.h.start_time - prev.h.start_time) / clock_rate;
|
// * Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
|
||||||
let rhythm = closest_rhythm(delta, prev.h, prev_prev.h, clock_rate);
|
let colour = TaikoDifficultyColour::default();
|
||||||
|
|
||||||
|
let delta = (base.h.start_time - last.h.start_time) / clock_rate;
|
||||||
|
let rhythm = closest_rhythm(delta, last.h, last_last.h, clock_rate);
|
||||||
|
|
||||||
|
let mono_idx = if !base.is_hit() {
|
||||||
|
MonoIndex::None
|
||||||
|
} else if base.sound.is_rim() {
|
||||||
|
MonoIndex::Rim(lists.rims.len())
|
||||||
|
} else {
|
||||||
|
MonoIndex::Centre(lists.centres.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
let note_idx = base.is_hit().then_some(lists.notes.len());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx,
|
|
||||||
base,
|
base,
|
||||||
prev,
|
prev_time: last.h.start_time / clock_rate,
|
||||||
delta,
|
colour,
|
||||||
rhythm,
|
rhythm,
|
||||||
start_time: base.h.start_time / clock_rate,
|
mono_idx,
|
||||||
|
note_idx,
|
||||||
|
idx,
|
||||||
|
delta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(crate) static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
|
||||||
|
HitObjectRhythm { id: 0, ratio: 1.0, difficulty: 0.0 },
|
||||||
|
HitObjectRhythm { id: 1, ratio: 2.0 / 1.0, difficulty: 0.3 },
|
||||||
|
HitObjectRhythm { id: 2, ratio: 1.0 / 2.0, difficulty: 0.5 },
|
||||||
|
HitObjectRhythm { id: 3, ratio: 3.0 / 1.0, difficulty: 0.3 },
|
||||||
|
HitObjectRhythm { id: 4, ratio: 1.0 / 3.0, difficulty: 0.35 },
|
||||||
|
// * purposefully higher (requires hand switch in full alternating gameplay style)
|
||||||
|
HitObjectRhythm { id: 5, ratio: 3.0 / 2.0, difficulty: 0.6 },
|
||||||
|
HitObjectRhythm { id: 6, ratio: 2.0 / 3.0, difficulty: 0.4 },
|
||||||
|
HitObjectRhythm { id: 7, ratio: 5.0 / 4.0, difficulty: 0.5 },
|
||||||
|
HitObjectRhythm { id: 8, ratio: 4.0 / 5.0, difficulty: 0.7 },
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) struct HitObjectRhythm {
|
||||||
|
id: u8,
|
||||||
|
pub(crate) ratio: f64,
|
||||||
|
pub(crate) difficulty: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HitObjectRhythm {
|
||||||
|
pub(crate) fn static_ref() -> &'static Self {
|
||||||
|
&COMMON_RHYTHMS[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for HitObjectRhythm {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for HitObjectRhythm {}
|
||||||
|
|
||||||
|
fn closest_rhythm(
|
||||||
|
delta_time: f64,
|
||||||
|
last: &HitObject,
|
||||||
|
last_last: &HitObject,
|
||||||
|
clock_rate: f64,
|
||||||
|
) -> &'static HitObjectRhythm {
|
||||||
|
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
|
||||||
|
let ratio = delta_time / prev_len;
|
||||||
|
|
||||||
|
COMMON_RHYTHMS
|
||||||
|
.iter()
|
||||||
|
.min_by(|r1, r2| {
|
||||||
|
(r1.ratio - ratio)
|
||||||
|
.abs()
|
||||||
|
.partial_cmp(&(r2.ratio - ratio).abs())
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|||||||
+99
-297
@@ -1,22 +1,13 @@
|
|||||||
use std::{
|
use std::{cell::RefCell, rc::Rc, vec::IntoIter};
|
||||||
cmp::Ordering,
|
|
||||||
iter::{self, Enumerate, Skip, Zip},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, Mods};
|
||||||
parse::{HitObject, HitObjectKind},
|
|
||||||
taiko::{
|
|
||||||
difficulty_object::DifficultyObject, norm, rescale, simple_color_penalty,
|
|
||||||
stamina_cheese::StaminaCheeseDetector, COLOR_SKILL_MULTIPLIER, RHYTHM_SKILL_MULTIPLIER,
|
|
||||||
SECTION_LEN, STAMINA_SKILL_MULTIPLIER,
|
|
||||||
},
|
|
||||||
Beatmap, Mods,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
skill::Skills,
|
colours::ColourDifficultyPreprocessor,
|
||||||
taiko_object::{IntoTaikoObjectIter, TaikoObjectIter},
|
difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject},
|
||||||
TaikoDifficultyAttributes,
|
skills::{Peaks, PeaksDifficultyValues, Skill},
|
||||||
|
taiko_object::IntoTaikoObjectIter,
|
||||||
|
TaikoDifficultyAttributes, DIFFICULTY_MULTIPLIER,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gradually calculate the difficulty attributes of an osu!taiko map.
|
/// Gradually calculate the difficulty attributes of an osu!taiko map.
|
||||||
@@ -52,205 +43,122 @@ use super::{
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TaikoGradualDifficultyAttributes<'map> {
|
pub struct TaikoGradualDifficultyAttributes<'map> {
|
||||||
pub(crate) idx: usize,
|
pub(crate) idx: usize,
|
||||||
difficulty_objects: GradualTaikoObjectIter<'map>,
|
attrs: TaikoDifficultyAttributes,
|
||||||
cheese: Vec<bool>,
|
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject<'map>>>>,
|
||||||
skills: Skills,
|
lists: ObjectLists<'map>,
|
||||||
curr_section_end: f64,
|
peaks: Peaks,
|
||||||
strain_peak_buf: Vec<f64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> TaikoGradualDifficultyAttributes<'map> {
|
impl<'map> TaikoGradualDifficultyAttributes<'map> {
|
||||||
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
||||||
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||||
// True if the object at that index is stamina cheese
|
let peaks = Peaks::new();
|
||||||
let cheese = map.find_cheese();
|
|
||||||
|
|
||||||
let skills = Skills::new();
|
|
||||||
let clock_rate = mods.clock_rate();
|
let clock_rate = mods.clock_rate();
|
||||||
let difficulty_objects = GradualTaikoObjectIter::new(map, clock_rate);
|
|
||||||
|
let BeatmapHitWindows { od: hit_window, .. } = map
|
||||||
|
.attributes()
|
||||||
|
.mods(mods)
|
||||||
|
.clock_rate(clock_rate)
|
||||||
|
.hit_windows();
|
||||||
|
|
||||||
|
let attrs = TaikoDifficultyAttributes {
|
||||||
|
stamina: 0.0,
|
||||||
|
rhythm: 0.0,
|
||||||
|
colour: 0.0,
|
||||||
|
peak: 0.0,
|
||||||
|
hit_window,
|
||||||
|
stars: 0.0,
|
||||||
|
max_combo: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diff_objects = map
|
||||||
|
.taiko_objects()
|
||||||
|
.enumerate()
|
||||||
|
.skip(2)
|
||||||
|
.zip(map.taiko_objects().skip(1))
|
||||||
|
.zip(map.taiko_objects())
|
||||||
|
.fold(
|
||||||
|
ObjectLists::default(),
|
||||||
|
|mut lists, (((idx, base), last), last_last)| {
|
||||||
|
let diff_obj =
|
||||||
|
TaikoDifficultyObject::new(base, last, last_last, clock_rate, &lists, idx);
|
||||||
|
|
||||||
|
match &diff_obj.mono_idx {
|
||||||
|
MonoIndex::Centre(_) => lists.centres.push(idx),
|
||||||
|
MonoIndex::Rim(_) => lists.rims.push(idx),
|
||||||
|
MonoIndex::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff_obj.note_idx.is_some() {
|
||||||
|
lists.notes.push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
lists.all.push(Rc::new(RefCell::new(diff_obj)));
|
||||||
|
|
||||||
|
lists
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty_objects,
|
hit_objects: diff_objects.all.clone().into_iter(),
|
||||||
cheese,
|
lists: diff_objects,
|
||||||
skills,
|
peaks,
|
||||||
curr_section_end: 0.0,
|
attrs,
|
||||||
strain_peak_buf: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locally_combined_difficulty(&mut self, stamina_penalty: f64) -> f64 {
|
|
||||||
let iter = self
|
|
||||||
.skills
|
|
||||||
.color
|
|
||||||
.strain_peaks
|
|
||||||
.iter()
|
|
||||||
.zip(self.skills.rhythm.strain_peaks.iter())
|
|
||||||
.zip(self.skills.stamina_right.strain_peaks.iter())
|
|
||||||
.zip(self.skills.stamina_left.strain_peaks.iter())
|
|
||||||
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
|
||||||
norm(
|
|
||||||
2.0,
|
|
||||||
color * COLOR_SKILL_MULTIPLIER,
|
|
||||||
rhythm * RHYTHM_SKILL_MULTIPLIER,
|
|
||||||
(stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER * stamina_penalty,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.strain_peak_buf.clear();
|
|
||||||
self.strain_peak_buf.extend(iter);
|
|
||||||
|
|
||||||
let last = norm(
|
|
||||||
2.0,
|
|
||||||
self.skills.color.curr_section_peak * COLOR_SKILL_MULTIPLIER,
|
|
||||||
self.skills.rhythm.curr_section_peak * RHYTHM_SKILL_MULTIPLIER,
|
|
||||||
(self.skills.stamina_right.curr_section_peak
|
|
||||||
+ self.skills.stamina_left.curr_section_peak)
|
|
||||||
* STAMINA_SKILL_MULTIPLIER
|
|
||||||
* stamina_penalty,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.strain_peak_buf.push(last);
|
|
||||||
|
|
||||||
self.strain_peak_buf
|
|
||||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
let mut difficulty = 0.0;
|
|
||||||
let mut weight = 1.0;
|
|
||||||
|
|
||||||
for strain in &self.strain_peak_buf {
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
difficulty
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for TaikoGradualDifficultyAttributes<'_> {
|
impl Iterator for TaikoGradualDifficultyAttributes<'_> {
|
||||||
type Item = TaikoDifficultyAttributes;
|
type Item = TaikoDifficultyAttributes;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.idx = self.idx.saturating_add(1);
|
let curr = self.hit_objects.next()?;
|
||||||
|
|
||||||
if self.idx == 1 {
|
{
|
||||||
if self.difficulty_objects.first_object.is_empty() {
|
let curr = curr.borrow();
|
||||||
return None;
|
self.peaks.process(&curr, &self.lists);
|
||||||
}
|
self.attrs.max_combo += curr.base.h.is_circle() as usize;
|
||||||
|
|
||||||
self.difficulty_objects.max_combo +=
|
|
||||||
self.difficulty_objects.first_object.is_circle() as usize;
|
|
||||||
|
|
||||||
let attributes = TaikoDifficultyAttributes {
|
|
||||||
stars: 0.0,
|
|
||||||
max_combo: self.difficulty_objects.max_combo,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(attributes);
|
|
||||||
} else if self.idx == 2 {
|
|
||||||
if self.difficulty_objects.second_object.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.difficulty_objects.max_combo +=
|
|
||||||
self.difficulty_objects.second_object.is_circle() as usize;
|
|
||||||
|
|
||||||
let attributes = TaikoDifficultyAttributes {
|
|
||||||
stars: 0.0,
|
|
||||||
max_combo: self.difficulty_objects.max_combo,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(attributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let h = self.difficulty_objects.next()?;
|
let PeaksDifficultyValues {
|
||||||
|
mut colour_rating,
|
||||||
|
mut rhythm_rating,
|
||||||
|
mut stamina_rating,
|
||||||
|
mut combined_rating,
|
||||||
|
} = self.peaks.clone().difficulty_values();
|
||||||
|
|
||||||
if self.idx == 3 {
|
colour_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
rhythm_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
} else {
|
stamina_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
while h.start_time > self.curr_section_end {
|
combined_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
self.skills
|
|
||||||
.save_peak_and_start_new_section(self.curr_section_end);
|
let mut star_rating = rescale(combined_rating * 1.4);
|
||||||
self.curr_section_end += SECTION_LEN;
|
|
||||||
|
// TODO: adjust once converts are available for gradual processing
|
||||||
|
let is_convert = false;
|
||||||
|
|
||||||
|
// * TODO: This is temporary measure as we don't detect abuse of multiple-input
|
||||||
|
// * playstyles of converts within the current system.
|
||||||
|
if is_convert {
|
||||||
|
star_rating *= 0.925;
|
||||||
|
|
||||||
|
// * For maps with low colour variance and high stamina requirement,
|
||||||
|
// * multiple inputs are more likely to be abused.
|
||||||
|
if colour_rating < 2.0 && stamina_rating > 8.0 {
|
||||||
|
star_rating *= 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.skills.process(&h, &self.cheese);
|
self.attrs.stamina = stamina_rating;
|
||||||
|
self.attrs.colour = colour_rating;
|
||||||
|
self.attrs.rhythm = rhythm_rating;
|
||||||
|
self.attrs.peak = combined_rating;
|
||||||
|
self.attrs.stars = star_rating;
|
||||||
|
|
||||||
let len = self.skills.strain_peaks_len();
|
Some(self.attrs.clone())
|
||||||
let missing = len + 1 - self.strain_peak_buf.len();
|
|
||||||
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
|
||||||
|
|
||||||
self.skills
|
|
||||||
.color
|
|
||||||
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
|
||||||
|
|
||||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
|
||||||
*last = self.skills.color.curr_section_peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
let color_rating = self
|
|
||||||
.skills
|
|
||||||
.color
|
|
||||||
.difficulty_value(&mut self.strain_peak_buf)
|
|
||||||
* COLOR_SKILL_MULTIPLIER;
|
|
||||||
|
|
||||||
self.skills
|
|
||||||
.rhythm
|
|
||||||
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
|
||||||
|
|
||||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
|
||||||
*last = self.skills.rhythm.curr_section_peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rhythm_rating = self
|
|
||||||
.skills
|
|
||||||
.rhythm
|
|
||||||
.difficulty_value(&mut self.strain_peak_buf)
|
|
||||||
* RHYTHM_SKILL_MULTIPLIER;
|
|
||||||
|
|
||||||
self.skills
|
|
||||||
.stamina_right
|
|
||||||
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
|
||||||
|
|
||||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
|
||||||
*last = self.skills.stamina_right.curr_section_peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stamina_right = self
|
|
||||||
.skills
|
|
||||||
.stamina_right
|
|
||||||
.difficulty_value(&mut self.strain_peak_buf);
|
|
||||||
|
|
||||||
self.skills
|
|
||||||
.stamina_left
|
|
||||||
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
|
||||||
|
|
||||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
|
||||||
*last = self.skills.stamina_left.curr_section_peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stamina_left = self
|
|
||||||
.skills
|
|
||||||
.stamina_left
|
|
||||||
.difficulty_value(&mut self.strain_peak_buf);
|
|
||||||
|
|
||||||
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
|
|
||||||
|
|
||||||
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
|
||||||
stamina_rating *= stamina_penalty;
|
|
||||||
|
|
||||||
let combined_rating = self.locally_combined_difficulty(stamina_penalty);
|
|
||||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
|
||||||
|
|
||||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
|
||||||
|
|
||||||
let attributes = TaikoDifficultyAttributes {
|
|
||||||
stars,
|
|
||||||
max_combo: self.difficulty_objects.max_combo,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(attributes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -262,112 +170,6 @@ impl Iterator for TaikoGradualDifficultyAttributes<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExactSizeIterator for TaikoGradualDifficultyAttributes<'_> {
|
impl ExactSizeIterator for TaikoGradualDifficultyAttributes<'_> {
|
||||||
#[inline]
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
let mut len = self.difficulty_objects.len();
|
|
||||||
|
|
||||||
if self.idx == 0 && !self.difficulty_objects.first_object.is_empty() {
|
|
||||||
len += 1 + !self.difficulty_objects.second_object.is_empty() as usize;
|
|
||||||
} else if self.idx == 1 && !self.difficulty_objects.second_object.is_empty() {
|
|
||||||
len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type InnerIter<'map> = Zip<
|
|
||||||
Zip<Skip<Enumerate<TaikoObjectIter<'map>>>, Skip<TaikoObjectIter<'map>>>,
|
|
||||||
TaikoObjectIter<'map>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
enum SimpleObject {
|
|
||||||
Circle,
|
|
||||||
Empty,
|
|
||||||
NonCircle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&HitObject> for SimpleObject {
|
|
||||||
fn from(h: &HitObject) -> Self {
|
|
||||||
match h.kind {
|
|
||||||
HitObjectKind::Circle => Self::Circle,
|
|
||||||
_ => Self::NonCircle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleObject {
|
|
||||||
fn is_empty(self) -> bool {
|
|
||||||
self == Self::Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_circle(self) -> bool {
|
|
||||||
self == Self::Circle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct GradualTaikoObjectIter<'map> {
|
|
||||||
hit_objects: InnerIter<'map>,
|
|
||||||
max_combo: usize,
|
|
||||||
clock_rate: f64,
|
|
||||||
first_object: SimpleObject,
|
|
||||||
second_object: SimpleObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> GradualTaikoObjectIter<'map> {
|
|
||||||
fn new(map: &'map Beatmap, clock_rate: f64) -> Self {
|
|
||||||
let first_object = map
|
|
||||||
.hit_objects
|
|
||||||
.get(0)
|
|
||||||
.map_or(SimpleObject::Empty, From::from);
|
|
||||||
let second_object = map
|
|
||||||
.hit_objects
|
|
||||||
.get(1)
|
|
||||||
.map_or(SimpleObject::Empty, From::from);
|
|
||||||
|
|
||||||
let hit_objects = map
|
|
||||||
.taiko_objects()
|
|
||||||
.enumerate()
|
|
||||||
.skip(2)
|
|
||||||
.zip(map.taiko_objects().skip(1))
|
|
||||||
.zip(map.taiko_objects());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
hit_objects,
|
|
||||||
max_combo: 0,
|
|
||||||
clock_rate,
|
|
||||||
first_object,
|
|
||||||
second_object,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> Iterator for GradualTaikoObjectIter<'map> {
|
|
||||||
type Item = DifficultyObject<'map>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let (((idx, base), prev), prev_prev) = self.hit_objects.next()?;
|
|
||||||
self.max_combo += base.h.is_circle() as usize;
|
|
||||||
|
|
||||||
Some(DifficultyObject::new(
|
|
||||||
idx,
|
|
||||||
base,
|
|
||||||
prev,
|
|
||||||
prev_prev,
|
|
||||||
self.clock_rate,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
||||||
self.hit_objects.size_hint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExactSizeIterator for GradualTaikoObjectIter<'_> {
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.hit_objects.len()
|
self.hit_objects.len()
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
use crate::parse::HitObject;
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 0,
|
|
||||||
ratio: 1.0,
|
|
||||||
difficulty: 0.0,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 1,
|
|
||||||
ratio: 2.0 / 1.0,
|
|
||||||
difficulty: 0.3,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 2,
|
|
||||||
ratio: 1.0 / 2.0,
|
|
||||||
difficulty: 0.5,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 3,
|
|
||||||
ratio: 3.0 / 1.0,
|
|
||||||
difficulty: 0.3,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 4,
|
|
||||||
ratio: 1.0 / 3.0,
|
|
||||||
difficulty: 0.35,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 5,
|
|
||||||
ratio: 3.0 / 2.0,
|
|
||||||
difficulty: 0.6,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 6,
|
|
||||||
ratio: 2.0 / 3.0,
|
|
||||||
difficulty: 0.4,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 7,
|
|
||||||
ratio: 5.0 / 4.0,
|
|
||||||
difficulty: 0.5,
|
|
||||||
},
|
|
||||||
HitObjectRhythm {
|
|
||||||
id: 8,
|
|
||||||
ratio: 4.0 / 5.0,
|
|
||||||
difficulty: 0.7,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub(crate) struct HitObjectRhythm {
|
|
||||||
id: u8,
|
|
||||||
ratio: f64,
|
|
||||||
pub(crate) difficulty: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HitObjectRhythm {
|
|
||||||
pub(crate) fn static_ref() -> &'static Self {
|
|
||||||
&COMMON_RHYTHMS[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for HitObjectRhythm {
|
|
||||||
#[inline]
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for HitObjectRhythm {}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn closest_rhythm(
|
|
||||||
delta_time: f64,
|
|
||||||
last: &HitObject,
|
|
||||||
last_last: &HitObject,
|
|
||||||
clock_rate: f64,
|
|
||||||
) -> &'static HitObjectRhythm {
|
|
||||||
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
|
|
||||||
let ratio = delta_time / prev_len;
|
|
||||||
|
|
||||||
COMMON_RHYTHMS
|
|
||||||
.iter()
|
|
||||||
.min_by(|r1, r2| {
|
|
||||||
(r1.ratio - ratio)
|
|
||||||
.abs()
|
|
||||||
.partial_cmp(&(r2.ratio - ratio).abs())
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
+118
-121
@@ -1,36 +1,31 @@
|
|||||||
|
mod colours;
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod gradual_difficulty;
|
mod gradual_difficulty;
|
||||||
mod gradual_performance;
|
mod gradual_performance;
|
||||||
mod hitobject_rhythm;
|
|
||||||
mod pp;
|
mod pp;
|
||||||
mod rim;
|
mod rim;
|
||||||
mod skill;
|
mod skills;
|
||||||
mod skill_kind;
|
|
||||||
mod stamina_cheese;
|
mod stamina_cheese;
|
||||||
mod taiko_object;
|
mod taiko_object;
|
||||||
|
|
||||||
use difficulty_object::DifficultyObject;
|
|
||||||
pub use gradual_difficulty::*;
|
pub use gradual_difficulty::*;
|
||||||
pub use gradual_performance::*;
|
pub use gradual_performance::*;
|
||||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use rim::Rim;
|
use rim::Rim;
|
||||||
use skill_kind::SkillKind;
|
|
||||||
use stamina_cheese::StaminaCheeseDetector;
|
|
||||||
use taiko_object::IntoTaikoObjectIter;
|
use taiko_object::IntoTaikoObjectIter;
|
||||||
|
|
||||||
use crate::taiko::skill::Skills;
|
use crate::beatmap::BeatmapHitWindows;
|
||||||
use crate::{Beatmap, GameMode, Mods, OsuStars};
|
use crate::{Beatmap, GameMode, Mods, OsuStars};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, cell::RefCell, rc::Rc};
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::f64::consts::PI;
|
|
||||||
|
|
||||||
const SECTION_LEN: f64 = 400.0;
|
use self::colours::ColourDifficultyPreprocessor;
|
||||||
|
use self::difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject};
|
||||||
|
use self::skills::{Peaks, PeaksDifficultyValues, PeaksRaw, Skill};
|
||||||
|
|
||||||
const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
|
const SECTION_LEN: usize = 400;
|
||||||
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
|
||||||
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
const DIFFICULTY_MULTIPLIER: f64 = 1.35;
|
||||||
|
|
||||||
/// Difficulty calculator on osu!taiko maps.
|
/// Difficulty calculator on osu!taiko maps.
|
||||||
///
|
///
|
||||||
@@ -56,6 +51,7 @@ pub struct TaikoStars<'map> {
|
|||||||
mods: u32,
|
mods: u32,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
clock_rate: Option<f64>,
|
||||||
|
is_convert: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> TaikoStars<'map> {
|
impl<'map> TaikoStars<'map> {
|
||||||
@@ -67,6 +63,7 @@ impl<'map> TaikoStars<'map> {
|
|||||||
mods: 0,
|
mods: 0,
|
||||||
passed_objects: None,
|
passed_objects: None,
|
||||||
clock_rate: None,
|
clock_rate: None,
|
||||||
|
is_convert: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,35 +99,65 @@ impl<'map> TaikoStars<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify whether the map is a convert i.e. an osu!standard map.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_convert(mut self, is_convert: bool) -> Self {
|
||||||
|
self.is_convert = is_convert;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate all difficulty related values, including stars.
|
/// Calculate all difficulty related values, including stars.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn calculate(self) -> TaikoDifficultyAttributes {
|
pub fn calculate(self) -> TaikoDifficultyAttributes {
|
||||||
let (skills, max_combo) = calculate_skills(self);
|
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||||
let mut buf = vec![0.0; skills.strain_peaks_len()];
|
|
||||||
|
|
||||||
skills.color.copy_strain_peaks(&mut buf);
|
let BeatmapHitWindows { od: hit_window, .. } = self
|
||||||
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
.map
|
||||||
|
.attributes()
|
||||||
|
.mods(self.mods)
|
||||||
|
.clock_rate(clock_rate)
|
||||||
|
.hit_windows();
|
||||||
|
|
||||||
skills.rhythm.copy_strain_peaks(&mut buf);
|
let is_convert = self.is_convert || matches!(self.map, Cow::Owned(_));
|
||||||
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
|
||||||
|
|
||||||
skills.stamina_right.copy_strain_peaks(&mut buf);
|
let (peaks, max_combo) = calculate_skills(self);
|
||||||
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
|
|
||||||
|
|
||||||
skills.stamina_left.copy_strain_peaks(&mut buf);
|
let PeaksDifficultyValues {
|
||||||
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
|
mut colour_rating,
|
||||||
|
mut rhythm_rating,
|
||||||
|
mut stamina_rating,
|
||||||
|
mut combined_rating,
|
||||||
|
} = peaks.difficulty_values();
|
||||||
|
|
||||||
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
|
colour_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
|
rhythm_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
|
stamina_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
|
combined_rating *= DIFFICULTY_MULTIPLIER;
|
||||||
|
|
||||||
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
let mut star_rating = rescale(combined_rating * 1.4);
|
||||||
stamina_rating *= stamina_penalty;
|
|
||||||
|
|
||||||
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
|
// * TODO: This is temporary measure as we don't detect abuse of multiple-input
|
||||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
// * playstyles of converts within the current system.
|
||||||
|
if is_convert {
|
||||||
|
star_rating *= 0.925;
|
||||||
|
|
||||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
// * For maps with low colour variance and high stamina requirement,
|
||||||
|
// * multiple inputs are more likely to be abused.
|
||||||
|
if colour_rating < 2.0 && stamina_rating > 8.0 {
|
||||||
|
star_rating *= 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TaikoDifficultyAttributes { stars, max_combo }
|
TaikoDifficultyAttributes {
|
||||||
|
stamina: stamina_rating,
|
||||||
|
rhythm: rhythm_rating,
|
||||||
|
colour: colour_rating,
|
||||||
|
peak: combined_rating,
|
||||||
|
hit_window,
|
||||||
|
stars: star_rating,
|
||||||
|
max_combo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the skill strains.
|
/// Calculate the skill strains.
|
||||||
@@ -139,14 +166,19 @@ impl<'map> TaikoStars<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn strains(self) -> TaikoStrains {
|
pub fn strains(self) -> TaikoStrains {
|
||||||
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||||
let (skills, _) = calculate_skills(self);
|
let (peaks, _) = calculate_skills(self);
|
||||||
|
|
||||||
|
let PeaksRaw {
|
||||||
|
colour,
|
||||||
|
rhythm,
|
||||||
|
stamina,
|
||||||
|
} = peaks.into_raw();
|
||||||
|
|
||||||
TaikoStrains {
|
TaikoStrains {
|
||||||
section_len: SECTION_LEN * clock_rate,
|
section_len: SECTION_LEN as f64 * clock_rate,
|
||||||
color: skills.color.strain_peaks,
|
color: colour,
|
||||||
rhythm: skills.rhythm.strain_peaks,
|
rhythm,
|
||||||
stamina_right: skills.stamina_right.strain_peaks,
|
stamina,
|
||||||
stamina_left: skills.stamina_left.strain_peaks,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +193,8 @@ pub struct TaikoStrains {
|
|||||||
pub color: Vec<f64>,
|
pub color: Vec<f64>,
|
||||||
/// Strain peaks of the rhythm skill.
|
/// Strain peaks of the rhythm skill.
|
||||||
pub rhythm: Vec<f64>,
|
pub rhythm: Vec<f64>,
|
||||||
/// Strain peaks of the left-stamina skill.
|
/// Strain peaks of the stamina skill.
|
||||||
pub stamina_right: Vec<f64>,
|
pub stamina: Vec<f64>,
|
||||||
/// Strain peaks of the right-stamina skill.
|
|
||||||
pub stamina_left: Vec<f64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoStrains {
|
impl TaikoStrains {
|
||||||
@@ -176,67 +206,68 @@ impl TaikoStrains {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_skills(params: TaikoStars<'_>) -> (Skills, usize) {
|
fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
|
||||||
let TaikoStars {
|
let TaikoStars {
|
||||||
map,
|
map,
|
||||||
mods,
|
mods,
|
||||||
passed_objects,
|
passed_objects,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
|
..
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
|
|
||||||
// True if the object at that index is stamina cheese
|
let mut peaks = Peaks::new();
|
||||||
let cheese = map.find_cheese();
|
|
||||||
let mut skills = Skills::new();
|
|
||||||
let mut max_combo = 0;
|
let mut max_combo = 0;
|
||||||
|
|
||||||
match map.hit_objects.get(0) {
|
match map.hit_objects.get(0) {
|
||||||
Some(h) => max_combo += h.is_circle() as usize,
|
Some(h) => max_combo += h.is_circle() as usize,
|
||||||
None => return (skills, max_combo),
|
None => return (peaks, max_combo),
|
||||||
}
|
}
|
||||||
|
|
||||||
match map.hit_objects.get(1) {
|
match map.hit_objects.get(1) {
|
||||||
Some(h) => max_combo += h.is_circle() as usize,
|
Some(h) => max_combo += h.is_circle() as usize,
|
||||||
None => return (skills, max_combo),
|
None => return (peaks, max_combo),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hit_objects = map
|
let mut diff_objects = map
|
||||||
.taiko_objects()
|
.taiko_objects()
|
||||||
.take(take)
|
.take(take)
|
||||||
.enumerate()
|
|
||||||
.skip(2)
|
.skip(2)
|
||||||
.zip(map.taiko_objects().skip(1))
|
.zip(map.taiko_objects().skip(1))
|
||||||
.zip(map.taiko_objects())
|
.zip(map.taiko_objects())
|
||||||
.inspect(|(((_, base), _), _)| max_combo += base.h.is_circle() as usize)
|
.enumerate()
|
||||||
.map(|(((idx, base), prev), prev_prev)| {
|
.inspect(|(_, ((base, _), _))| max_combo += base.h.is_circle() as usize)
|
||||||
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
.fold(
|
||||||
});
|
ObjectLists::default(),
|
||||||
|
|mut lists, (idx, ((base, last), last_last))| {
|
||||||
|
let diff_obj =
|
||||||
|
TaikoDifficultyObject::new(base, last, last_last, clock_rate, &lists, idx);
|
||||||
|
|
||||||
// Handle first element distinctly
|
match &diff_obj.mono_idx {
|
||||||
let h = match hit_objects.next() {
|
MonoIndex::Centre(_) => lists.centres.push(idx),
|
||||||
Some(h) => h,
|
MonoIndex::Rim(_) => lists.rims.push(idx),
|
||||||
None => return (skills, max_combo),
|
MonoIndex::None => {}
|
||||||
};
|
}
|
||||||
|
|
||||||
// No strain for first object
|
if diff_obj.note_idx.is_some() {
|
||||||
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
lists.notes.push(idx);
|
||||||
skills.process(&h, &cheese);
|
}
|
||||||
|
|
||||||
// Handle all other objects
|
lists.all.push(Rc::new(RefCell::new(diff_obj)));
|
||||||
for h in hit_objects {
|
|
||||||
while h.start_time > curr_section_end {
|
|
||||||
skills.save_peak_and_start_new_section(curr_section_end);
|
|
||||||
curr_section_end += SECTION_LEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
skills.process(&h, &cheese);
|
lists
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
|
||||||
|
|
||||||
|
for hit_object in diff_objects.all.iter() {
|
||||||
|
peaks.process(&hit_object.borrow(), &diff_objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
skills.save_current_peak();
|
(peaks, max_combo)
|
||||||
|
|
||||||
(skills, max_combo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -248,56 +279,19 @@ fn rescale(stars: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
|
|
||||||
if color <= 0.0 {
|
|
||||||
0.79 - 0.25
|
|
||||||
} else {
|
|
||||||
0.79 - (stamina / color - 12.0).atan() / PI / 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn locally_combined_difficulty(peaks: &mut Vec<f64>, skills: &Skills, stamina_penalty: f64) -> f64 {
|
|
||||||
peaks.clear();
|
|
||||||
|
|
||||||
let iter = skills
|
|
||||||
.color
|
|
||||||
.strain_peaks
|
|
||||||
.iter()
|
|
||||||
.zip(skills.rhythm.strain_peaks.iter())
|
|
||||||
.zip(skills.stamina_right.strain_peaks.iter())
|
|
||||||
.zip(skills.stamina_left.strain_peaks.iter())
|
|
||||||
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
|
||||||
norm(
|
|
||||||
2.0,
|
|
||||||
color * COLOR_SKILL_MULTIPLIER,
|
|
||||||
rhythm * RHYTHM_SKILL_MULTIPLIER,
|
|
||||||
(stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER * stamina_penalty,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
peaks.extend(iter);
|
|
||||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
let mut difficulty = 0.0;
|
|
||||||
let mut weight = 1.0;
|
|
||||||
|
|
||||||
for strain in peaks {
|
|
||||||
difficulty += *strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
difficulty
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn norm(p: f64, a: f64, b: f64, c: f64) -> f64 {
|
|
||||||
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!taiko map.
|
/// The result of a difficulty calculation on an osu!taiko map.
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct TaikoDifficultyAttributes {
|
pub struct TaikoDifficultyAttributes {
|
||||||
|
/// The difficulty corresponding to the stamina skill.
|
||||||
|
pub stamina: f64,
|
||||||
|
/// The difficulty corresponding to the rhythm skill.
|
||||||
|
pub rhythm: f64,
|
||||||
|
/// The difficulty corresponding to the colour skill.
|
||||||
|
pub colour: f64,
|
||||||
|
/// The difficulty corresponding to the hardest parts of the map.
|
||||||
|
pub peak: f64,
|
||||||
|
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc)
|
||||||
|
pub hit_window: f64,
|
||||||
/// The final star rating.
|
/// The final star rating.
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
/// The maximum combo.
|
/// The maximum combo.
|
||||||
@@ -322,7 +316,9 @@ pub struct TaikoPerformanceAttributes {
|
|||||||
/// The accuracy portion of the final pp.
|
/// The accuracy portion of the final pp.
|
||||||
pub pp_acc: f64,
|
pub pp_acc: f64,
|
||||||
/// The strain portion of the final pp.
|
/// The strain portion of the final pp.
|
||||||
pub pp_strain: f64,
|
pub pp_difficulty: f64,
|
||||||
|
/// Scaled miss count based on total hits.
|
||||||
|
pub effective_miss_count: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoPerformanceAttributes {
|
impl TaikoPerformanceAttributes {
|
||||||
@@ -367,6 +363,7 @@ impl<'map> From<OsuStars<'map>> for TaikoStars<'map> {
|
|||||||
mods,
|
mods,
|
||||||
passed_objects,
|
passed_objects,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
|
is_convert: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-53
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct TaikoPP<'map> {
|
pub struct TaikoPP<'map> {
|
||||||
map: Cow<'map, Beatmap>,
|
pub(crate) map: Cow<'map, Beatmap>,
|
||||||
attributes: Option<TaikoDifficultyAttributes>,
|
attributes: Option<TaikoDifficultyAttributes>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
@@ -176,7 +176,9 @@ impl<'map> TaikoPP<'map> {
|
|||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||||
let mut calculator = TaikoStars::new(self.map.as_ref()).mods(self.mods);
|
let mut calculator = TaikoStars::new(self.map.as_ref())
|
||||||
|
.mods(self.mods)
|
||||||
|
.is_convert(matches!(self.map, Cow::Owned(_)));
|
||||||
|
|
||||||
if let Some(passed_objects) = self.passed_objects {
|
if let Some(passed_objects) = self.passed_objects {
|
||||||
calculator = calculator.passed_objects(passed_objects);
|
calculator = calculator.passed_objects(passed_objects);
|
||||||
@@ -189,36 +191,65 @@ impl<'map> TaikoPP<'map> {
|
|||||||
calculator.calculate()
|
calculator.calculate()
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.n300.or(self.n100).is_some() {
|
self.assert_hitresults(attributes).calculate()
|
||||||
let total = self.map.n_circles as usize;
|
}
|
||||||
let misses = self.n_misses;
|
|
||||||
|
|
||||||
let mut n300 = self.n300.unwrap_or(0).min(total - misses);
|
fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> {
|
||||||
let mut n100 = self.n100.unwrap_or(0).min(total - n300 - misses);
|
let total_result_count = attributes.max_combo();
|
||||||
|
let misses = self.n_misses;
|
||||||
|
|
||||||
let given = n300 + n100 + misses;
|
let (n300, n100) = match (self.n300, self.n100) {
|
||||||
let missing = total - given;
|
(Some(n300), Some(n100)) => {
|
||||||
|
let n300 = n300.min(total_result_count - misses);
|
||||||
|
let n100 = n100.min(total_result_count - n300 - misses);
|
||||||
|
|
||||||
match (self.n300, self.n100) {
|
let given = n300 + n100 + misses;
|
||||||
(Some(_), Some(_)) => n300 += missing,
|
let missing = total_result_count - given;
|
||||||
(Some(_), None) => n100 += missing,
|
|
||||||
(None, Some(_)) => n300 += missing,
|
|
||||||
(None, None) => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
|
(n300 + missing, n100)
|
||||||
}
|
}
|
||||||
|
(Some(n300), None) => {
|
||||||
|
let n300 = n300.min(total_result_count - misses);
|
||||||
|
|
||||||
let inner = TaikoPPInner {
|
let n100 = total_result_count
|
||||||
map: self.map.as_ref(),
|
.saturating_sub(n300)
|
||||||
attributes,
|
.saturating_sub(misses);
|
||||||
mods: self.mods,
|
|
||||||
acc: self.acc,
|
(n300, n100)
|
||||||
n_misses: self.n_misses,
|
}
|
||||||
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
|
(None, Some(n100)) => {
|
||||||
|
let n100 = n100.min(total_result_count - misses);
|
||||||
|
|
||||||
|
let n300 = total_result_count
|
||||||
|
.saturating_sub(n100)
|
||||||
|
.saturating_sub(misses);
|
||||||
|
|
||||||
|
(n300, n100)
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
let target_total = (self.acc * (total_result_count * 2) as f64) as usize;
|
||||||
|
|
||||||
|
let n300 = target_total - total_result_count.saturating_sub(misses);
|
||||||
|
let n100 = total_result_count
|
||||||
|
.saturating_sub(n300)
|
||||||
|
.saturating_sub(misses);
|
||||||
|
|
||||||
|
(n300, n100)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
let acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
|
||||||
|
|
||||||
|
TaikoPPInner {
|
||||||
|
map: self.map.as_ref(),
|
||||||
|
attributes,
|
||||||
|
acc,
|
||||||
|
n300,
|
||||||
|
n100,
|
||||||
|
mods: self.mods,
|
||||||
|
n_misses: misses,
|
||||||
|
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,57 +260,73 @@ struct TaikoPPInner<'map> {
|
|||||||
acc: f64,
|
acc: f64,
|
||||||
n_misses: usize,
|
n_misses: usize,
|
||||||
clock_rate: f64,
|
clock_rate: f64,
|
||||||
|
n300: usize,
|
||||||
|
n100: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> TaikoPPInner<'map> {
|
impl<'map> TaikoPPInner<'map> {
|
||||||
fn calculate(self) -> TaikoPerformanceAttributes {
|
fn calculate(self) -> TaikoPerformanceAttributes {
|
||||||
let mut multiplier = 1.1;
|
// * The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits
|
||||||
|
// * and increasing the miss penalty for shorter object counts lower than 1000.
|
||||||
|
let total_successful_hits = self.total_successful_hits();
|
||||||
|
|
||||||
if self.mods.nf() {
|
let effective_miss_count = if total_successful_hits > 0 {
|
||||||
multiplier *= 0.9;
|
(1000.0 / (total_successful_hits as f64)).max(1.0) * self.n_misses as f64
|
||||||
}
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut multiplier = 1.13;
|
||||||
|
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
multiplier *= 1.1;
|
multiplier *= 1.075;
|
||||||
}
|
}
|
||||||
|
|
||||||
let strain_value = self.compute_strain_value();
|
if self.mods.ez() {
|
||||||
|
multiplier *= 0.975;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_value = self.compute_difficulty_value(effective_miss_count);
|
||||||
let acc_value = self.compute_accuracy_value();
|
let acc_value = self.compute_accuracy_value();
|
||||||
|
|
||||||
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
let pp = (diff_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
||||||
|
|
||||||
TaikoPerformanceAttributes {
|
TaikoPerformanceAttributes {
|
||||||
difficulty: self.attributes,
|
difficulty: self.attributes,
|
||||||
pp,
|
pp,
|
||||||
pp_acc: acc_value,
|
pp_acc: acc_value,
|
||||||
pp_strain: strain_value,
|
pp_difficulty: diff_value,
|
||||||
|
effective_miss_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_strain_value(&self) -> f64 {
|
fn compute_difficulty_value(&self, effective_miss_count: f64) -> f64 {
|
||||||
let attributes = &self.attributes;
|
let attrs = &self.attributes;
|
||||||
let exp_base = 5.0 * (attributes.stars / 0.0075).max(1.0) - 4.0;
|
let exp_base = 5.0 * (attrs.stars / 0.115).max(1.0) - 4.0;
|
||||||
let mut strain = exp_base * exp_base / 100_000.0;
|
let mut diff_value = exp_base.powf(2.25) / 1150.0;
|
||||||
|
|
||||||
// Longer maps are worth more
|
let len_bonus = 1.0 + 0.1 * (attrs.max_combo as f64 / 1500.0).min(1.0);
|
||||||
let len_bonus = 1.0 + 0.1 * (attributes.max_combo as f64 / 1500.0).min(1.0);
|
diff_value *= len_bonus;
|
||||||
strain *= len_bonus;
|
|
||||||
|
|
||||||
// Penalize misses exponentially
|
diff_value *= 0.986_f64.powf(effective_miss_count);
|
||||||
strain *= 0.985_f64.powi(self.n_misses as i32);
|
|
||||||
|
if self.mods.ez() {
|
||||||
|
diff_value *= 0.985;
|
||||||
|
}
|
||||||
|
|
||||||
// HD bonus
|
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
strain *= 1.025;
|
diff_value *= 1.025;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mods.hr() {
|
||||||
|
diff_value *= 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FL bonus
|
|
||||||
if self.mods.fl() {
|
if self.mods.fl() {
|
||||||
strain *= 1.05 * len_bonus;
|
diff_value *= 1.05 * len_bonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale with accuracy
|
diff_value * self.acc * self.acc
|
||||||
strain * self.acc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -291,12 +338,32 @@ impl<'map> TaikoPPInner<'map> {
|
|||||||
.clock_rate(self.clock_rate)
|
.clock_rate(self.clock_rate)
|
||||||
.hit_windows();
|
.hit_windows();
|
||||||
|
|
||||||
let max_combo = self.attributes.max_combo;
|
if hit_window <= 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
(150.0 / hit_window).powf(1.1)
|
let mut acc_value = (60.0 / hit_window).powf(1.1)
|
||||||
* self.acc.powi(15)
|
* self.acc.powi(8)
|
||||||
* 22.0
|
* self.attributes.stars.powf(0.4)
|
||||||
* (max_combo as f64 / 1500.0).powf(0.3).min(1.15)
|
* 27.0;
|
||||||
|
|
||||||
|
let len_bonus = (self.total_hits() as f64 / 1500.0).powf(0.3).min(1.15);
|
||||||
|
acc_value *= len_bonus;
|
||||||
|
|
||||||
|
// * Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
|
||||||
|
if self.mods.hd() && self.mods.fl() {
|
||||||
|
acc_value *= (1.075 * len_bonus).max(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_hits(&self) -> usize {
|
||||||
|
self.n300 + self.n100 + self.n_misses
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_successful_hits(&self) -> usize {
|
||||||
|
self.n300 + self.n100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
use super::{DifficultyObject, SkillKind};
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
const DECAY_WEIGHT: f64 = 0.9;
|
|
||||||
|
|
||||||
const COLOR_SKILL_MULTIPLIER: f64 = 1.0;
|
|
||||||
const COLOR_STRAIN_DECAY_BASE: f64 = 0.4;
|
|
||||||
|
|
||||||
const RHYTHM_SKILL_MULTIPLIER: f64 = 10.0;
|
|
||||||
const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
|
|
||||||
|
|
||||||
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
|
|
||||||
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Skills {
|
|
||||||
pub(crate) color: Skill,
|
|
||||||
pub(crate) rhythm: Skill,
|
|
||||||
pub(crate) stamina_right: Skill,
|
|
||||||
pub(crate) stamina_left: Skill,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Skills {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
color: Skill::new(SkillKind::color()),
|
|
||||||
rhythm: Skill::new(SkillKind::rhythm()),
|
|
||||||
stamina_right: Skill::new(SkillKind::stamina(true)),
|
|
||||||
stamina_left: Skill::new(SkillKind::stamina(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn save_peak_and_start_new_section(&mut self, time: f64) {
|
|
||||||
self.color.save_current_peak();
|
|
||||||
self.color.start_new_section_from(time);
|
|
||||||
self.rhythm.save_current_peak();
|
|
||||||
self.rhythm.start_new_section_from(time);
|
|
||||||
self.stamina_right.save_current_peak();
|
|
||||||
self.stamina_right.start_new_section_from(time);
|
|
||||||
self.stamina_left.save_current_peak();
|
|
||||||
self.stamina_left.start_new_section_from(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
|
||||||
self.color.save_current_peak();
|
|
||||||
self.rhythm.save_current_peak();
|
|
||||||
self.stamina_right.save_current_peak();
|
|
||||||
self.stamina_left.save_current_peak();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
|
|
||||||
self.color.process(curr, cheese);
|
|
||||||
self.rhythm.process(curr, cheese);
|
|
||||||
self.stamina_right.process(curr, cheese);
|
|
||||||
self.stamina_left.process(curr, cheese);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn strain_peaks_len(&self) -> usize {
|
|
||||||
self.color.strain_peaks.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Skill {
|
|
||||||
pub(crate) current_strain: f64,
|
|
||||||
pub(crate) curr_section_peak: f64,
|
|
||||||
|
|
||||||
kind: SkillKind,
|
|
||||||
pub(crate) strain_peaks: Vec<f64>,
|
|
||||||
|
|
||||||
prev_time: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Skill {
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
|
||||||
Self {
|
|
||||||
current_strain: 1.0,
|
|
||||||
curr_section_peak: 1.0,
|
|
||||||
|
|
||||||
kind,
|
|
||||||
strain_peaks: Vec::with_capacity(128),
|
|
||||||
|
|
||||||
prev_time: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
|
||||||
self.strain_peaks.push(self.curr_section_peak);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
|
||||||
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
|
|
||||||
self.current_strain *= self.strain_decay(curr.delta);
|
|
||||||
self.current_strain += self.kind.strain_value_of(curr, cheese) * self.skill_multiplier();
|
|
||||||
self.curr_section_peak = self.curr_section_peak.max(self.current_strain);
|
|
||||||
self.prev_time.replace(curr.start_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn copy_strain_peaks(&self, buf: &mut [f64]) {
|
|
||||||
buf.copy_from_slice(&self.strain_peaks);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn difficulty_value(&self, peaks: &mut [f64]) -> f64 {
|
|
||||||
let mut difficulty = 0.0;
|
|
||||||
let mut weight = 1.0;
|
|
||||||
|
|
||||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
for &strain in peaks.iter() {
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= DECAY_WEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
difficulty
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn skill_multiplier(&self) -> f64 {
|
|
||||||
match self.kind {
|
|
||||||
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
|
|
||||||
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
|
|
||||||
SkillKind::Stamina { .. } => STAMINA_SKILL_MULTIPLIER,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn strain_decay_base(&self) -> f64 {
|
|
||||||
match self.kind {
|
|
||||||
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
|
|
||||||
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
|
|
||||||
SkillKind::Stamina { .. } => STAMINA_STRAIN_DECAY_BASE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn peak_strain(&self, delta_time: f64) -> f64 {
|
|
||||||
self.current_strain * self.strain_decay(delta_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn strain_decay(&self, ms: f64) -> f64 {
|
|
||||||
self.strain_decay_base().powf(ms / 1000.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
use crate::limited_queue::LimitedQueue;
|
|
||||||
|
|
||||||
use super::{DifficultyObject, HitObjectRhythm, Rim};
|
|
||||||
|
|
||||||
const RHYTHM_STRAIN_DECAY: f64 = 0.96;
|
|
||||||
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
|
|
||||||
|
|
||||||
const MONO_HISTORY_MAX_LEN: usize = 5;
|
|
||||||
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
|
|
||||||
const STAMINA_HISTORY_MAX_LEN: usize = 2;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub(crate) struct RhythmHistoryElement {
|
|
||||||
idx: usize,
|
|
||||||
rhythm: &'static HitObjectRhythm,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RhythmHistoryElement {
|
|
||||||
fn new(difficulty_object: &DifficultyObject<'_>) -> Self {
|
|
||||||
Self {
|
|
||||||
idx: difficulty_object.idx,
|
|
||||||
rhythm: difficulty_object.rhythm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RhythmHistoryElement {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
idx: 0,
|
|
||||||
rhythm: HitObjectRhythm::static_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum SkillKind {
|
|
||||||
Color {
|
|
||||||
mono_history: LimitedQueue<usize, MONO_HISTORY_MAX_LEN>,
|
|
||||||
prev_is_rim: Option<bool>,
|
|
||||||
current_mono_len: usize,
|
|
||||||
},
|
|
||||||
Rhythm {
|
|
||||||
rhythm_history: LimitedQueue<RhythmHistoryElement, RHYTHM_HISTORY_MAX_LEN>,
|
|
||||||
notes_since_rhythm_change: usize,
|
|
||||||
current_strain: f64,
|
|
||||||
},
|
|
||||||
Stamina {
|
|
||||||
note_pair_duration_history: LimitedQueue<f64, STAMINA_HISTORY_MAX_LEN>,
|
|
||||||
hand: u8,
|
|
||||||
off_hand_object_duration: f64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SkillKind {
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn color() -> Self {
|
|
||||||
Self::Color {
|
|
||||||
mono_history: LimitedQueue::new(),
|
|
||||||
prev_is_rim: None,
|
|
||||||
current_mono_len: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn rhythm() -> Self {
|
|
||||||
Self::Rhythm {
|
|
||||||
rhythm_history: LimitedQueue::new(),
|
|
||||||
notes_since_rhythm_change: 0,
|
|
||||||
current_strain: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn stamina(right_hand: bool) -> Self {
|
|
||||||
Self::Stamina {
|
|
||||||
note_pair_duration_history: LimitedQueue::new(),
|
|
||||||
hand: right_hand as u8,
|
|
||||||
off_hand_object_duration: f64::MAX,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn strain_value_of(
|
|
||||||
&mut self,
|
|
||||||
current: &DifficultyObject<'_>,
|
|
||||||
cheese: &[bool],
|
|
||||||
) -> f64 {
|
|
||||||
match self {
|
|
||||||
Self::Color {
|
|
||||||
mono_history,
|
|
||||||
prev_is_rim,
|
|
||||||
current_mono_len,
|
|
||||||
} => {
|
|
||||||
let prev_is_circle = current.prev.h.is_circle();
|
|
||||||
let base_is_circle = current.base.h.is_circle();
|
|
||||||
let curr_is_rim = current.base.sound.is_rim();
|
|
||||||
|
|
||||||
if !(current.delta < 1000.0 && prev_is_circle && base_is_circle) {
|
|
||||||
mono_history.clear();
|
|
||||||
*current_mono_len = base_is_circle as usize;
|
|
||||||
|
|
||||||
*prev_is_rim = if base_is_circle {
|
|
||||||
Some(curr_is_rim)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut strain = 0.0;
|
|
||||||
|
|
||||||
if prev_is_rim
|
|
||||||
.filter(|&is_rim| is_rim != curr_is_rim)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
strain = if mono_history.len() < 2
|
|
||||||
|| (*mono_history.last().unwrap() + *current_mono_len) % 2 == 0
|
|
||||||
{
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut reps_penalty = 1.0;
|
|
||||||
|
|
||||||
mono_history.push(*current_mono_len);
|
|
||||||
|
|
||||||
let iter = (0..mono_history
|
|
||||||
.len()
|
|
||||||
.saturating_sub(MOST_RECENT_PATTERNS_TO_COMPARE))
|
|
||||||
.rev();
|
|
||||||
|
|
||||||
for start in iter {
|
|
||||||
let different_pattern = (0..MOST_RECENT_PATTERNS_TO_COMPARE).any(|i| {
|
|
||||||
let to_compare =
|
|
||||||
mono_history.len() + i - MOST_RECENT_PATTERNS_TO_COMPARE;
|
|
||||||
|
|
||||||
mono_history[start + i] != mono_history[to_compare]
|
|
||||||
});
|
|
||||||
|
|
||||||
if different_pattern {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut notes_since = 0;
|
|
||||||
|
|
||||||
for i in start..mono_history.len() {
|
|
||||||
notes_since += mono_history[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
reps_penalty *= repetition_penalty(notes_since);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
strain *= reps_penalty;
|
|
||||||
|
|
||||||
*current_mono_len = 1;
|
|
||||||
} else {
|
|
||||||
*current_mono_len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*prev_is_rim = Some(curr_is_rim);
|
|
||||||
|
|
||||||
strain
|
|
||||||
}
|
|
||||||
Self::Rhythm {
|
|
||||||
rhythm_history,
|
|
||||||
notes_since_rhythm_change,
|
|
||||||
current_strain,
|
|
||||||
} => {
|
|
||||||
let base_is_circle = current.base.h.is_circle();
|
|
||||||
|
|
||||||
if !base_is_circle {
|
|
||||||
*current_strain = 0.0;
|
|
||||||
*notes_since_rhythm_change = 0;
|
|
||||||
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*current_strain *= RHYTHM_STRAIN_DECAY;
|
|
||||||
*notes_since_rhythm_change += 1;
|
|
||||||
|
|
||||||
if current.rhythm.difficulty.abs() <= f64::EPSILON {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut strain = current.rhythm.difficulty;
|
|
||||||
|
|
||||||
rhythm_history.push(RhythmHistoryElement::new(current));
|
|
||||||
|
|
||||||
let mut reps_penalty = 1.0;
|
|
||||||
|
|
||||||
for most_recent_patterns_to_compare in 2..=RHYTHM_HISTORY_MAX_LEN / 2 {
|
|
||||||
let iter = (0..rhythm_history
|
|
||||||
.len()
|
|
||||||
.saturating_sub(most_recent_patterns_to_compare))
|
|
||||||
.rev();
|
|
||||||
|
|
||||||
for start in iter {
|
|
||||||
let different_pattern = (0..most_recent_patterns_to_compare).any(|i| {
|
|
||||||
let to_compare =
|
|
||||||
rhythm_history.len() + i - most_recent_patterns_to_compare;
|
|
||||||
|
|
||||||
rhythm_history[start + i].rhythm != rhythm_history[to_compare].rhythm
|
|
||||||
});
|
|
||||||
|
|
||||||
if different_pattern {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
reps_penalty *= repetition_penalty(current.idx - rhythm_history[start].idx);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let speed_penalty = if current.delta < 80.0 {
|
|
||||||
1.0
|
|
||||||
} else if current.delta < 210.0 {
|
|
||||||
(1.4 - 0.005 * current.delta).max(0.0)
|
|
||||||
} else {
|
|
||||||
*current_strain = 0.0;
|
|
||||||
*notes_since_rhythm_change = 0;
|
|
||||||
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
strain *= reps_penalty;
|
|
||||||
strain *= pattern_len_penalty(*notes_since_rhythm_change);
|
|
||||||
strain *= speed_penalty;
|
|
||||||
|
|
||||||
*notes_since_rhythm_change = 0;
|
|
||||||
*current_strain += strain;
|
|
||||||
|
|
||||||
*current_strain
|
|
||||||
}
|
|
||||||
Self::Stamina {
|
|
||||||
hand,
|
|
||||||
note_pair_duration_history,
|
|
||||||
off_hand_object_duration,
|
|
||||||
} => {
|
|
||||||
let base_is_circle = current.base.h.is_circle();
|
|
||||||
|
|
||||||
if !base_is_circle {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current.idx % 2 == *hand as usize {
|
|
||||||
if current.idx == 1 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut strain = 1.0;
|
|
||||||
note_pair_duration_history.push(current.delta + *off_hand_object_duration);
|
|
||||||
let shortest_recent_note = *note_pair_duration_history.min().unwrap();
|
|
||||||
strain += speed_bonus(shortest_recent_note);
|
|
||||||
|
|
||||||
if cheese[current.idx] {
|
|
||||||
let p = cheese_penalty(current.delta + *off_hand_object_duration);
|
|
||||||
strain *= p;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strain;
|
|
||||||
}
|
|
||||||
|
|
||||||
*off_hand_object_duration = current.delta;
|
|
||||||
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn pattern_len_penalty(pattern_len: usize) -> f64 {
|
|
||||||
let pattern_len = pattern_len as f64;
|
|
||||||
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
|
|
||||||
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).max(0.0).min(1.0);
|
|
||||||
|
|
||||||
short_pattern_penalty.min(long_pattern_penalty)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn cheese_penalty(note_pair_duration: f64) -> f64 {
|
|
||||||
if note_pair_duration > 125.0 {
|
|
||||||
1.0
|
|
||||||
} else if note_pair_duration < 100.0 {
|
|
||||||
0.6
|
|
||||||
} else {
|
|
||||||
0.6 + (note_pair_duration - 100.0) * 0.016
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn speed_bonus(note_pair_duration: f64) -> f64 {
|
|
||||||
if note_pair_duration > 200.0 {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut bonus = 200.0 - note_pair_duration;
|
|
||||||
bonus *= bonus;
|
|
||||||
|
|
||||||
bonus / 100_000.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn repetition_penalty(notes_since: usize) -> f64 {
|
|
||||||
(0.032 * notes_since as f64).min(1.0)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::taiko::{
|
||||||
|
colours::{AlternatingMonoPattern, MonoStreak, RepeatingHitPatterns},
|
||||||
|
difficulty_object::{ObjectLists, TaikoDifficultyObject},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Skill, StrainDecaySkill, StrainSkill};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Colour {
|
||||||
|
curr_strain: f64,
|
||||||
|
curr_section_peak: f64,
|
||||||
|
curr_section_end: f64,
|
||||||
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Colour {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
curr_strain: 0.0,
|
||||||
|
curr_section_peak: 0.0,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peaks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skill for Colour {
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
|
||||||
|
<Self as StrainSkill>::process(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difficulty_value(self) -> f64 {
|
||||||
|
<Self as StrainSkill>::difficulty_value(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainSkill for Colour {
|
||||||
|
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
|
||||||
|
&mut self.strain_peaks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_peak(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_peak
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_end(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_at(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainDecaySkill for Colour {
|
||||||
|
const SKILL_MULTIPLIER: f64 = 0.12;
|
||||||
|
const STRAIN_DECAY_BASE: f64 = 0.8;
|
||||||
|
|
||||||
|
fn curr_strain(&self) -> f64 {
|
||||||
|
self.curr_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_strain_mut(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject<'_>, _: &ObjectLists<'_>) -> f64 {
|
||||||
|
ColourEvaluator::evaluate_diff_of(curr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ColourEvaluator;
|
||||||
|
|
||||||
|
impl ColourEvaluator {
|
||||||
|
fn sigmoid(val: f64, center: f64, width: f64, middle: f64, height: f64) -> f64 {
|
||||||
|
let sigmoid = (std::f64::consts::E * -(val - center) / width).tanh();
|
||||||
|
|
||||||
|
sigmoid * (height / 2.0) + middle
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_diff_of_mono_streak(mono_streak: Rc<RefCell<MonoStreak<'_>>>) -> f64 {
|
||||||
|
let mono_streak = mono_streak.borrow();
|
||||||
|
|
||||||
|
let parent_eval = mono_streak
|
||||||
|
.parent
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
.map_or(1.0, Self::evaluate_diff_of_alternating_mono_pattern);
|
||||||
|
|
||||||
|
Self::sigmoid(mono_streak.idx as f64, 2.0, 2.0, 0.5, 1.0) * parent_eval * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_diff_of_alternating_mono_pattern(
|
||||||
|
alternating_mono_pattern: Rc<RefCell<AlternatingMonoPattern<'_>>>,
|
||||||
|
) -> f64 {
|
||||||
|
let alternating_mono_pattern = alternating_mono_pattern.borrow();
|
||||||
|
|
||||||
|
let parent_eval = alternating_mono_pattern
|
||||||
|
.parent
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
.map_or(1.0, Self::evaluate_diff_of_repeating_hit_patterns);
|
||||||
|
|
||||||
|
Self::sigmoid(alternating_mono_pattern.idx as f64, 2.0, 2.0, 0.5, 1.0) * parent_eval
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_diff_of_repeating_hit_patterns(
|
||||||
|
repeating_hit_patterns: Rc<RefCell<RepeatingHitPatterns<'_>>>,
|
||||||
|
) -> f64 {
|
||||||
|
let repetition_interval = repeating_hit_patterns.borrow().repetition_interval as f64;
|
||||||
|
|
||||||
|
2.0 * (1.0 - Self::sigmoid(repetition_interval, 2.0, 2.0, 0.5, 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_diff_of(hit_object: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
let colour = &hit_object.colour;
|
||||||
|
let mut difficulty = 0.0;
|
||||||
|
|
||||||
|
// * Difficulty for MonoStreak
|
||||||
|
if let Some(mono_streak) = colour.mono_streak.as_ref().and_then(Weak::upgrade) {
|
||||||
|
difficulty += Self::evaluate_diff_of_mono_streak(mono_streak);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Difficulty for AlternatingMonoPattern
|
||||||
|
if let Some(alternating_mono_pattern) = colour
|
||||||
|
.alternating_mono_pattern
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Weak::upgrade)
|
||||||
|
{
|
||||||
|
difficulty += Self::evaluate_diff_of_alternating_mono_pattern(alternating_mono_pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Difficulty for RepeatingHitPattern
|
||||||
|
if let Some(repeating_hit_patterns) = colour.repeating_hit_patterns.as_ref().map(Rc::clone)
|
||||||
|
{
|
||||||
|
difficulty += Self::evaluate_diff_of_repeating_hit_patterns(repeating_hit_patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
mod colour;
|
||||||
|
mod peaks;
|
||||||
|
mod rhythm;
|
||||||
|
mod stamina;
|
||||||
|
|
||||||
|
use std::{cmp::Ordering, mem};
|
||||||
|
|
||||||
|
pub(crate) use self::peaks::{Peaks, PeaksDifficultyValues, PeaksRaw};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
difficulty_object::{ObjectLists, TaikoDifficultyObject},
|
||||||
|
SECTION_LEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) trait Skill: Sized {
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>);
|
||||||
|
fn difficulty_value(self) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait StrainSkill: Skill {
|
||||||
|
const DECAY_WEIGHT: f64 = 0.9;
|
||||||
|
|
||||||
|
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
|
||||||
|
fn curr_section_peak(&mut self) -> &mut f64;
|
||||||
|
fn curr_section_end(&mut self) -> &mut f64;
|
||||||
|
|
||||||
|
fn strain_value_at(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64;
|
||||||
|
|
||||||
|
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64;
|
||||||
|
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
|
||||||
|
// * The first object doesn't generate a strain, so we begin with an incremented section end
|
||||||
|
if curr.idx == 0 {
|
||||||
|
let section_len = SECTION_LEN as f64;
|
||||||
|
*self.curr_section_end() = (curr.base.h.start_time / section_len).ceil() * section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
while curr.base.h.start_time > *self.curr_section_end() {
|
||||||
|
self.save_curr_peak();
|
||||||
|
|
||||||
|
{
|
||||||
|
let section_end = *self.curr_section_end();
|
||||||
|
self.start_new_section_from(section_end, curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.curr_section_end() += SECTION_LEN as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.curr_section_peak() = self
|
||||||
|
.strain_value_at(curr, hit_objects)
|
||||||
|
.max(*self.curr_section_peak());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_curr_peak(&mut self) {
|
||||||
|
let peak = *self.curr_section_peak();
|
||||||
|
self.strain_peaks_mut().push(peak);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_new_section_from(&mut self, time: f64, curr: &TaikoDifficultyObject<'_>) {
|
||||||
|
// * The maximum strain of the new section is not zero by default
|
||||||
|
// * This means we need to capture the strain level at the beginning of the new section,
|
||||||
|
// * and use that as the initial peak level.
|
||||||
|
*self.curr_section_peak() = self.calculate_initial_strain(time, curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difficulty_value(self) -> f64 {
|
||||||
|
let mut difficulty = 0.0;
|
||||||
|
let mut weight = 1.0;
|
||||||
|
|
||||||
|
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// * These sections will not contribute to the difficulty.
|
||||||
|
let mut peaks = self.get_curr_strain_peaks();
|
||||||
|
|
||||||
|
peaks.retain(|&peak| peak > 0.0);
|
||||||
|
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
// * Difficulty is the weighted sum of the highest strains from every section.
|
||||||
|
// * We're sorting from highest to lowest strain.
|
||||||
|
for strain in peaks {
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= Self::DECAY_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
|
||||||
|
let curr_peak = *self.curr_section_peak();
|
||||||
|
let mut strain_peaks = mem::take(self.strain_peaks_mut());
|
||||||
|
strain_peaks.push(curr_peak);
|
||||||
|
|
||||||
|
strain_peaks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait StrainDecaySkill: StrainSkill {
|
||||||
|
const SKILL_MULTIPLIER: f64;
|
||||||
|
const STRAIN_DECAY_BASE: f64;
|
||||||
|
|
||||||
|
fn curr_strain(&self) -> f64;
|
||||||
|
fn curr_strain_mut(&mut self) -> &mut f64;
|
||||||
|
|
||||||
|
fn strain_value_of(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64;
|
||||||
|
|
||||||
|
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
self.curr_strain() * self.strain_decay(time - curr.prev_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_at(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
*self.curr_strain_mut() *= self.strain_decay(curr.delta);
|
||||||
|
*self.curr_strain_mut() += self.strain_value_of(curr, hit_objects) * Self::SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
self.curr_strain()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_decay(&self, ms: f64) -> f64 {
|
||||||
|
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use crate::taiko::difficulty_object::{ObjectLists, TaikoDifficultyObject};
|
||||||
|
|
||||||
|
use super::{colour::Colour, rhythm::Rhythm, stamina::Stamina, Skill, StrainSkill};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Peaks {
|
||||||
|
colour: Colour,
|
||||||
|
rhythm: Rhythm,
|
||||||
|
stamina: Stamina,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Peaks {
|
||||||
|
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.2 * Self::FINAL_MULTIPLIER;
|
||||||
|
const COLOUR_SKILL_MULTIPLIER: f64 = 0.375 * Self::FINAL_MULTIPLIER;
|
||||||
|
const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * Self::FINAL_MULTIPLIER;
|
||||||
|
|
||||||
|
const FINAL_MULTIPLIER: f64 = 0.0625;
|
||||||
|
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
colour: Colour::new(),
|
||||||
|
rhythm: Rhythm::new(),
|
||||||
|
stamina: Stamina::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues {
|
||||||
|
let colour_rating =
|
||||||
|
StrainSkill::difficulty_value(self.colour) * Self::COLOUR_SKILL_MULTIPLIER;
|
||||||
|
// let rhythm_rating =
|
||||||
|
// StrainSkill::difficulty_value(self.rhythm.clone()) * Self::RHYTHM_SKILL_MULTIPLIER;
|
||||||
|
// let stamina_rating =
|
||||||
|
// StrainSkill::difficulty_value(self.stamina.clone()) * Self::STAMINA_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
PeaksDifficultyValues {
|
||||||
|
colour_rating,
|
||||||
|
rhythm_rating: 0.0,
|
||||||
|
stamina_rating: 0.0,
|
||||||
|
// combined_rating: self.difficulty_value(),
|
||||||
|
combined_rating: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_raw(self) -> PeaksRaw {
|
||||||
|
PeaksRaw {
|
||||||
|
colour: self.colour.strain_peaks,
|
||||||
|
rhythm: self.rhythm.strain_peaks,
|
||||||
|
stamina: self.stamina.strain_peaks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn norm(p: f64, values: impl IntoIterator<Item = f64>) -> f64 {
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.fold(0.0, |sum, x| sum + x.powf(p))
|
||||||
|
.powf(p.recip())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skill for Peaks {
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
|
||||||
|
StrainSkill::process(&mut self.colour, curr, hit_objects);
|
||||||
|
StrainSkill::process(&mut self.rhythm, curr, hit_objects);
|
||||||
|
StrainSkill::process(&mut self.stamina, curr, hit_objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difficulty_value(self) -> f64 {
|
||||||
|
let mut peaks = Vec::new();
|
||||||
|
|
||||||
|
let colour_peaks = self.colour.get_curr_strain_peaks();
|
||||||
|
let rhythm_peaks = self.rhythm.get_curr_strain_peaks();
|
||||||
|
let stamina_peaks = self.stamina.get_curr_strain_peaks();
|
||||||
|
|
||||||
|
let zip = colour_peaks
|
||||||
|
.into_iter()
|
||||||
|
.zip(rhythm_peaks)
|
||||||
|
.zip(stamina_peaks);
|
||||||
|
|
||||||
|
for ((mut colour_peak, mut rhythm_peak), mut stamina_peak) in zip {
|
||||||
|
colour_peak *= Self::COLOUR_SKILL_MULTIPLIER;
|
||||||
|
rhythm_peak *= Self::RHYTHM_SKILL_MULTIPLIER;
|
||||||
|
stamina_peak *= Self::STAMINA_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
let mut peak = Self::norm(1.5, [colour_peak, stamina_peak]);
|
||||||
|
peak = Self::norm(2.0, [peak, rhythm_peak]);
|
||||||
|
|
||||||
|
// * Sections with 0 strain are excluded to avoid worst-case
|
||||||
|
// * time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// * These sections will not contribute to the difficulty.
|
||||||
|
if peak > 0.0 {
|
||||||
|
peaks.push(peak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut difficulty = 0.0;
|
||||||
|
let mut weight = 1.0;
|
||||||
|
|
||||||
|
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
for strain in peaks {
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PeaksDifficultyValues {
|
||||||
|
pub(crate) colour_rating: f64,
|
||||||
|
pub(crate) rhythm_rating: f64,
|
||||||
|
pub(crate) stamina_rating: f64,
|
||||||
|
pub(crate) combined_rating: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PeaksRaw {
|
||||||
|
pub(crate) colour: Vec<f64>,
|
||||||
|
pub(crate) rhythm: Vec<f64>,
|
||||||
|
pub(crate) stamina: Vec<f64>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use crate::{
|
||||||
|
limited_queue::LimitedQueue,
|
||||||
|
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Skill, StrainDecaySkill, StrainSkill};
|
||||||
|
|
||||||
|
const HISTORY_MAX_LEN: usize = 8;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Rhythm {
|
||||||
|
// Equal to osu's CurrentStrain from abstract class StrainDecaySkill
|
||||||
|
curr_decay_strain: f64,
|
||||||
|
// Equal to osu's currentStrain from class Rhythm
|
||||||
|
curr_strain: f64,
|
||||||
|
notes_since_rhythm_change: usize,
|
||||||
|
history: LimitedQueue<HistoryElement, HISTORY_MAX_LEN>,
|
||||||
|
curr_section_peak: f64,
|
||||||
|
curr_section_end: f64,
|
||||||
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rhythm {
|
||||||
|
const STRAIN_DECAY: f64 = 0.96;
|
||||||
|
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
curr_decay_strain: 0.0,
|
||||||
|
curr_strain: 0.0,
|
||||||
|
notes_since_rhythm_change: 0,
|
||||||
|
history: LimitedQueue::new(),
|
||||||
|
curr_section_peak: 0.0,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peaks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_rhythm_and_strain(&mut self) {
|
||||||
|
self.curr_strain = 0.0;
|
||||||
|
self.notes_since_rhythm_change = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repetition_penalties(&mut self, hit_object: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
let mut penalty = 1.0;
|
||||||
|
|
||||||
|
self.history.push(HistoryElement::new(hit_object));
|
||||||
|
|
||||||
|
for most_recent_patterns_to_compare in 2..=(HISTORY_MAX_LEN / 2).min(self.history.len()) {
|
||||||
|
for start in (0..self.history.len() - most_recent_patterns_to_compare).rev() {
|
||||||
|
if !self.same_pattern(start, most_recent_patterns_to_compare) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notes_since = hit_object.idx - self.history[start].idx;
|
||||||
|
penalty *= Self::repetition_penalty(notes_since);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
penalty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_pattern(&self, start: usize, most_recent_patterns_to_compare: usize) -> bool {
|
||||||
|
let start = self.history.iter().skip(start);
|
||||||
|
|
||||||
|
let most_recent_patterns_to_compare = self
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.skip(self.history.len() - most_recent_patterns_to_compare);
|
||||||
|
|
||||||
|
start
|
||||||
|
.zip(most_recent_patterns_to_compare)
|
||||||
|
.all(|(a, b)| a.rhythm == b.rhythm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repetition_penalty(notes_since: usize) -> f64 {
|
||||||
|
(0.032 * notes_since as f64).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pattern_len_penalty(pattern_len: usize) -> f64 {
|
||||||
|
let pattern_len = pattern_len as f64;
|
||||||
|
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
|
||||||
|
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
short_pattern_penalty.min(long_pattern_penalty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn speed_penalty(&mut self, delta: f64) -> f64 {
|
||||||
|
if delta < 80.0 {
|
||||||
|
return 1.0;
|
||||||
|
} else if delta < 210.0 {
|
||||||
|
return (1.4 - 0.005 * delta).max(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset_rhythm_and_strain();
|
||||||
|
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skill for Rhythm {
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
|
||||||
|
<Self as StrainSkill>::process(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difficulty_value(self) -> f64 {
|
||||||
|
<Self as StrainSkill>::difficulty_value(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainSkill for Rhythm {
|
||||||
|
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
|
||||||
|
&mut self.strain_peaks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_peak(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_peak
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_end(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_at(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainDecaySkill for Rhythm {
|
||||||
|
const SKILL_MULTIPLIER: f64 = 10.0;
|
||||||
|
const STRAIN_DECAY_BASE: f64 = 0.0;
|
||||||
|
|
||||||
|
fn curr_strain(&self) -> f64 {
|
||||||
|
self.curr_decay_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_strain_mut(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_decay_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject<'_>, _: &ObjectLists<'_>) -> f64 {
|
||||||
|
let base_is_circle = curr.base.h.is_circle();
|
||||||
|
|
||||||
|
// * drum rolls and swells are exempt.
|
||||||
|
if !base_is_circle {
|
||||||
|
self.reset_rhythm_and_strain();
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.curr_strain *= Self::STRAIN_DECAY;
|
||||||
|
self.notes_since_rhythm_change += 1;
|
||||||
|
|
||||||
|
// * rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
|
||||||
|
if curr.rhythm.difficulty.abs() <= f64::EPSILON {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut obj_strain = curr.rhythm.difficulty;
|
||||||
|
|
||||||
|
obj_strain *= self.repetition_penalties(curr);
|
||||||
|
obj_strain *= Self::pattern_len_penalty(self.notes_since_rhythm_change);
|
||||||
|
obj_strain *= self.speed_penalty(curr.delta);
|
||||||
|
|
||||||
|
// * careful - needs to be done here since calls above read this value
|
||||||
|
self.notes_since_rhythm_change = 0;
|
||||||
|
|
||||||
|
self.curr_strain += obj_strain;
|
||||||
|
|
||||||
|
self.curr_strain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) struct HistoryElement {
|
||||||
|
idx: usize,
|
||||||
|
rhythm: &'static HitObjectRhythm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HistoryElement {
|
||||||
|
fn new(difficulty_object: &TaikoDifficultyObject<'_>) -> Self {
|
||||||
|
Self {
|
||||||
|
idx: difficulty_object.idx,
|
||||||
|
rhythm: difficulty_object.rhythm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HistoryElement {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
idx: 0,
|
||||||
|
rhythm: HitObjectRhythm::static_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use crate::taiko::difficulty_object::{ObjectLists, TaikoDifficultyObject};
|
||||||
|
|
||||||
|
use super::{Skill, StrainDecaySkill, StrainSkill};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Stamina {
|
||||||
|
curr_strain: f64,
|
||||||
|
curr_section_peak: f64,
|
||||||
|
curr_section_end: f64,
|
||||||
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stamina {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
curr_strain: 0.0,
|
||||||
|
curr_section_peak: 0.0,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peaks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skill for Stamina {
|
||||||
|
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
|
||||||
|
<Self as StrainSkill>::process(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difficulty_value(self) -> f64 {
|
||||||
|
<Self as StrainSkill>::difficulty_value(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainSkill for Stamina {
|
||||||
|
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
|
||||||
|
&mut self.strain_peaks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_peak(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_peak
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_section_end(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_section_end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_at(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
|
||||||
|
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainDecaySkill for Stamina {
|
||||||
|
const SKILL_MULTIPLIER: f64 = 1.1;
|
||||||
|
const STRAIN_DECAY_BASE: f64 = 0.4;
|
||||||
|
|
||||||
|
fn curr_strain(&self) -> f64 {
|
||||||
|
self.curr_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curr_strain_mut(&mut self) -> &mut f64 {
|
||||||
|
&mut self.curr_strain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strain_value_of(
|
||||||
|
&mut self,
|
||||||
|
curr: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
StaminaEvaluator::evaluate_diff_of(curr, hit_objects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StaminaEvaluator;
|
||||||
|
|
||||||
|
impl StaminaEvaluator {
|
||||||
|
fn speed_bonus(mut interval: f64) -> f64 {
|
||||||
|
// * Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
|
||||||
|
// * Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
|
||||||
|
// * TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
|
||||||
|
interval = interval.max(50.0);
|
||||||
|
|
||||||
|
30.0 / interval
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_diff_of(
|
||||||
|
hit_object: &TaikoDifficultyObject<'_>,
|
||||||
|
hit_objects: &ObjectLists<'_>,
|
||||||
|
) -> f64 {
|
||||||
|
if !hit_object.base.is_hit() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Find the previous hit object hit by the current key, which is two notes of the same colour prior.
|
||||||
|
let curr = hit_object;
|
||||||
|
let key_prev = hit_objects.prev_mono(curr.idx, 1);
|
||||||
|
|
||||||
|
if let Some(key_prev) = key_prev {
|
||||||
|
// * Add a base strain to all objects
|
||||||
|
0.5 + Self::speed_bonus(curr.base.h.start_time - key_prev.borrow().base.h.start_time)
|
||||||
|
} else {
|
||||||
|
// * There is no previous hit object hit by the current key
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,24 @@ use std::slice::Iter;
|
|||||||
|
|
||||||
use crate::{parse::HitObject, Beatmap};
|
use crate::{parse::HitObject, Beatmap};
|
||||||
|
|
||||||
|
use super::rim::Rim;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) struct TaikoObject<'h> {
|
pub(crate) struct TaikoObject<'h> {
|
||||||
pub(crate) h: &'h HitObject,
|
pub(crate) h: &'h HitObject,
|
||||||
pub(crate) sound: u8,
|
pub(crate) sound: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TaikoObject<'_> {
|
||||||
|
pub(crate) fn is_rim(&self) -> bool {
|
||||||
|
self.sound.is_rim()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_hit(&self) -> bool {
|
||||||
|
self.h.is_circle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) trait IntoTaikoObjectIter {
|
pub(crate) trait IntoTaikoObjectIter {
|
||||||
fn taiko_objects(&self) -> TaikoObjectIter<'_>;
|
fn taiko_objects(&self) -> TaikoObjectIter<'_>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user