implemented taiko pp update
This commit is contained in:
@@ -55,6 +55,8 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
||||
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 span_count = (repeats + 1) as i32;
|
||||
|
||||
@@ -38,7 +38,9 @@ impl Beatmap {
|
||||
|
||||
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 {
|
||||
pos: Default::default(),
|
||||
start_time: j,
|
||||
@@ -109,6 +111,8 @@ impl Beatmap {
|
||||
let timing_point = self.timing_point_at(*start_time);
|
||||
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 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.
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
*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
|
||||
}
|
||||
@@ -134,7 +138,7 @@ impl Beatmap {
|
||||
|
||||
struct SliderParams<'c> {
|
||||
curve: &'c Curve,
|
||||
duration: f64,
|
||||
duration: u32,
|
||||
repeats: usize,
|
||||
start_time: f64,
|
||||
tick_spacing: f64,
|
||||
@@ -146,7 +150,7 @@ impl<'c> SliderParams<'c> {
|
||||
curve,
|
||||
repeats,
|
||||
start_time,
|
||||
duration: 0.0,
|
||||
duration: 0,
|
||||
tick_spacing: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -453,7 +453,7 @@ impl PerformanceAttributes {
|
||||
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
|
||||
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
|
||||
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::{
|
||||
cmp::Ordering,
|
||||
iter::{Cycle, Skip, Take},
|
||||
ops::Index,
|
||||
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 {
|
||||
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>>>>;
|
||||
|
||||
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> {
|
||||
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)]
|
||||
pub(crate) struct DifficultyObject<'o> {
|
||||
pub(crate) idx: usize,
|
||||
pub(crate) base: TaikoObject<'o>,
|
||||
pub(crate) prev: TaikoObject<'o>,
|
||||
pub(crate) delta: f64,
|
||||
pub(crate) rhythm: &'static HitObjectRhythm,
|
||||
pub(crate) start_time: f64,
|
||||
use crate::parse::HitObject;
|
||||
|
||||
use super::{colours::TaikoDifficultyColour, rim::Rim, taiko_object::TaikoObject};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ObjectLists<'o> {
|
||||
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject<'o>>>>,
|
||||
pub(crate) centres: Vec<usize>,
|
||||
pub(crate) rims: Vec<usize>,
|
||||
pub(crate) notes: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'o> DifficultyObject<'o> {
|
||||
#[inline]
|
||||
impl<'o> ObjectLists<'o> {
|
||||
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(
|
||||
idx: usize,
|
||||
base: TaikoObject<'o>,
|
||||
prev: TaikoObject<'o>,
|
||||
prev_prev: TaikoObject<'o>,
|
||||
last: TaikoObject<'o>,
|
||||
last_last: TaikoObject<'o>,
|
||||
clock_rate: f64,
|
||||
lists: &ObjectLists<'o>,
|
||||
idx: usize,
|
||||
) -> Self {
|
||||
let delta = (base.h.start_time - prev.h.start_time) / clock_rate;
|
||||
let rhythm = closest_rhythm(delta, prev.h, prev_prev.h, clock_rate);
|
||||
// * Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
|
||||
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 {
|
||||
idx,
|
||||
base,
|
||||
prev,
|
||||
delta,
|
||||
prev_time: last.h.start_time / clock_rate,
|
||||
colour,
|
||||
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::{
|
||||
cmp::Ordering,
|
||||
iter::{self, Enumerate, Skip, Zip},
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc, vec::IntoIter};
|
||||
|
||||
use crate::{
|
||||
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 crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, Mods};
|
||||
|
||||
use super::{
|
||||
skill::Skills,
|
||||
taiko_object::{IntoTaikoObjectIter, TaikoObjectIter},
|
||||
TaikoDifficultyAttributes,
|
||||
colours::ColourDifficultyPreprocessor,
|
||||
difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject},
|
||||
skills::{Peaks, PeaksDifficultyValues, Skill},
|
||||
taiko_object::IntoTaikoObjectIter,
|
||||
TaikoDifficultyAttributes, DIFFICULTY_MULTIPLIER,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!taiko map.
|
||||
@@ -52,205 +43,122 @@ use super::{
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TaikoGradualDifficultyAttributes<'map> {
|
||||
pub(crate) idx: usize,
|
||||
difficulty_objects: GradualTaikoObjectIter<'map>,
|
||||
cheese: Vec<bool>,
|
||||
skills: Skills,
|
||||
curr_section_end: f64,
|
||||
strain_peak_buf: Vec<f64>,
|
||||
attrs: TaikoDifficultyAttributes,
|
||||
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject<'map>>>>,
|
||||
lists: ObjectLists<'map>,
|
||||
peaks: Peaks,
|
||||
}
|
||||
|
||||
impl<'map> TaikoGradualDifficultyAttributes<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
||||
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
||||
// True if the object at that index is stamina cheese
|
||||
let cheese = map.find_cheese();
|
||||
|
||||
let skills = Skills::new();
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let peaks = Peaks::new();
|
||||
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 {
|
||||
idx: 0,
|
||||
difficulty_objects,
|
||||
cheese,
|
||||
skills,
|
||||
curr_section_end: 0.0,
|
||||
strain_peak_buf: Vec::new(),
|
||||
hit_objects: diff_objects.all.clone().into_iter(),
|
||||
lists: diff_objects,
|
||||
peaks,
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
|
||||
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<'_> {
|
||||
type Item = TaikoDifficultyAttributes;
|
||||
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 curr = curr.borrow();
|
||||
self.peaks.process(&curr, &self.lists);
|
||||
self.attrs.max_combo += curr.base.h.is_circle() as usize;
|
||||
}
|
||||
|
||||
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 {
|
||||
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
} else {
|
||||
while h.start_time > self.curr_section_end {
|
||||
self.skills
|
||||
.save_peak_and_start_new_section(self.curr_section_end);
|
||||
self.curr_section_end += SECTION_LEN;
|
||||
colour_rating *= DIFFICULTY_MULTIPLIER;
|
||||
rhythm_rating *= DIFFICULTY_MULTIPLIER;
|
||||
stamina_rating *= DIFFICULTY_MULTIPLIER;
|
||||
combined_rating *= DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let mut star_rating = rescale(combined_rating * 1.4);
|
||||
|
||||
// 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();
|
||||
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)
|
||||
Some(self.attrs.clone())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -262,112 +170,6 @@ impl Iterator 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]
|
||||
fn len(&self) -> usize {
|
||||
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 gradual_difficulty;
|
||||
mod gradual_performance;
|
||||
mod hitobject_rhythm;
|
||||
mod pp;
|
||||
mod rim;
|
||||
mod skill;
|
||||
mod skill_kind;
|
||||
mod skills;
|
||||
mod stamina_cheese;
|
||||
mod taiko_object;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
pub use gradual_difficulty::*;
|
||||
pub use gradual_performance::*;
|
||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||
pub use pp::*;
|
||||
use rim::Rim;
|
||||
use skill_kind::SkillKind;
|
||||
use stamina_cheese::StaminaCheeseDetector;
|
||||
use taiko_object::IntoTaikoObjectIter;
|
||||
|
||||
use crate::taiko::skill::Skills;
|
||||
use crate::beatmap::BeatmapHitWindows;
|
||||
use crate::{Beatmap, GameMode, Mods, OsuStars};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
use std::f64::consts::PI;
|
||||
use std::{borrow::Cow, cell::RefCell, rc::Rc};
|
||||
|
||||
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 RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
||||
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
||||
const SECTION_LEN: usize = 400;
|
||||
|
||||
const DIFFICULTY_MULTIPLIER: f64 = 1.35;
|
||||
|
||||
/// Difficulty calculator on osu!taiko maps.
|
||||
///
|
||||
@@ -56,6 +51,7 @@ pub struct TaikoStars<'map> {
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
is_convert: bool,
|
||||
}
|
||||
|
||||
impl<'map> TaikoStars<'map> {
|
||||
@@ -67,6 +63,7 @@ impl<'map> TaikoStars<'map> {
|
||||
mods: 0,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
is_convert: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,35 +99,65 @@ impl<'map> TaikoStars<'map> {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn calculate(self) -> TaikoDifficultyAttributes {
|
||||
let (skills, max_combo) = calculate_skills(self);
|
||||
let mut buf = vec![0.0; skills.strain_peaks_len()];
|
||||
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||
|
||||
skills.color.copy_strain_peaks(&mut buf);
|
||||
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
||||
let BeatmapHitWindows { od: hit_window, .. } = self
|
||||
.map
|
||||
.attributes()
|
||||
.mods(self.mods)
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
skills.rhythm.copy_strain_peaks(&mut buf);
|
||||
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
||||
let is_convert = self.is_convert || matches!(self.map, Cow::Owned(_));
|
||||
|
||||
skills.stamina_right.copy_strain_peaks(&mut buf);
|
||||
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
|
||||
let (peaks, max_combo) = calculate_skills(self);
|
||||
|
||||
skills.stamina_left.copy_strain_peaks(&mut buf);
|
||||
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
|
||||
let PeaksDifficultyValues {
|
||||
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);
|
||||
stamina_rating *= stamina_penalty;
|
||||
let mut star_rating = rescale(combined_rating * 1.4);
|
||||
|
||||
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
|
||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
||||
// * 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;
|
||||
|
||||
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.
|
||||
@@ -139,14 +166,19 @@ impl<'map> TaikoStars<'map> {
|
||||
#[inline]
|
||||
pub fn strains(self) -> TaikoStrains {
|
||||
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 {
|
||||
section_len: SECTION_LEN * clock_rate,
|
||||
color: skills.color.strain_peaks,
|
||||
rhythm: skills.rhythm.strain_peaks,
|
||||
stamina_right: skills.stamina_right.strain_peaks,
|
||||
stamina_left: skills.stamina_left.strain_peaks,
|
||||
section_len: SECTION_LEN as f64 * clock_rate,
|
||||
color: colour,
|
||||
rhythm,
|
||||
stamina,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,10 +193,8 @@ pub struct TaikoStrains {
|
||||
pub color: Vec<f64>,
|
||||
/// Strain peaks of the rhythm skill.
|
||||
pub rhythm: Vec<f64>,
|
||||
/// Strain peaks of the left-stamina skill.
|
||||
pub stamina_right: Vec<f64>,
|
||||
/// Strain peaks of the right-stamina skill.
|
||||
pub stamina_left: Vec<f64>,
|
||||
/// Strain peaks of the stamina skill.
|
||||
pub stamina: Vec<f64>,
|
||||
}
|
||||
|
||||
impl TaikoStrains {
|
||||
@@ -176,67 +206,68 @@ impl TaikoStrains {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_skills(params: TaikoStars<'_>) -> (Skills, usize) {
|
||||
fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
|
||||
let TaikoStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
|
||||
// True if the object at that index is stamina cheese
|
||||
let cheese = map.find_cheese();
|
||||
let mut skills = Skills::new();
|
||||
let mut peaks = Peaks::new();
|
||||
let mut max_combo = 0;
|
||||
|
||||
match map.hit_objects.get(0) {
|
||||
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) {
|
||||
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()
|
||||
.take(take)
|
||||
.enumerate()
|
||||
.skip(2)
|
||||
.zip(map.taiko_objects().skip(1))
|
||||
.zip(map.taiko_objects())
|
||||
.inspect(|(((_, base), _), _)| max_combo += base.h.is_circle() as usize)
|
||||
.map(|(((idx, base), prev), prev_prev)| {
|
||||
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
||||
});
|
||||
.enumerate()
|
||||
.inspect(|(_, ((base, _), _))| max_combo += base.h.is_circle() as usize)
|
||||
.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
|
||||
let h = match hit_objects.next() {
|
||||
Some(h) => h,
|
||||
None => return (skills, max_combo),
|
||||
};
|
||||
match &diff_obj.mono_idx {
|
||||
MonoIndex::Centre(_) => lists.centres.push(idx),
|
||||
MonoIndex::Rim(_) => lists.rims.push(idx),
|
||||
MonoIndex::None => {}
|
||||
}
|
||||
|
||||
// No strain for first object
|
||||
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
skills.process(&h, &cheese);
|
||||
if diff_obj.note_idx.is_some() {
|
||||
lists.notes.push(idx);
|
||||
}
|
||||
|
||||
// Handle all other objects
|
||||
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;
|
||||
}
|
||||
lists.all.push(Rc::new(RefCell::new(diff_obj)));
|
||||
|
||||
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();
|
||||
|
||||
(skills, max_combo)
|
||||
(peaks, max_combo)
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
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.
|
||||
pub stars: f64,
|
||||
/// The maximum combo.
|
||||
@@ -322,7 +316,9 @@ pub struct TaikoPerformanceAttributes {
|
||||
/// The accuracy portion of the final pp.
|
||||
pub pp_acc: f64,
|
||||
/// 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 {
|
||||
@@ -367,6 +363,7 @@ impl<'map> From<OsuStars<'map>> for TaikoStars<'map> {
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
is_convert: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+120
-53
@@ -38,7 +38,7 @@ use crate::{
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct TaikoPP<'map> {
|
||||
map: Cow<'map, Beatmap>,
|
||||
pub(crate) map: Cow<'map, Beatmap>,
|
||||
attributes: Option<TaikoDifficultyAttributes>,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
@@ -176,7 +176,9 @@ impl<'map> TaikoPP<'map> {
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||
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 {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
@@ -189,36 +191,65 @@ impl<'map> TaikoPP<'map> {
|
||||
calculator.calculate()
|
||||
});
|
||||
|
||||
if self.n300.or(self.n100).is_some() {
|
||||
let total = self.map.n_circles as usize;
|
||||
let misses = self.n_misses;
|
||||
self.assert_hitresults(attributes).calculate()
|
||||
}
|
||||
|
||||
let mut n300 = self.n300.unwrap_or(0).min(total - misses);
|
||||
let mut n100 = self.n100.unwrap_or(0).min(total - n300 - misses);
|
||||
fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> {
|
||||
let total_result_count = attributes.max_combo();
|
||||
let misses = self.n_misses;
|
||||
|
||||
let given = n300 + n100 + misses;
|
||||
let missing = total - given;
|
||||
let (n300, n100) = match (self.n300, self.n100) {
|
||||
(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) {
|
||||
(Some(_), Some(_)) => n300 += missing,
|
||||
(Some(_), None) => n100 += missing,
|
||||
(None, Some(_)) => n300 += missing,
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
let given = n300 + n100 + misses;
|
||||
let missing = total_result_count - given;
|
||||
|
||||
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 {
|
||||
map: self.map.as_ref(),
|
||||
attributes,
|
||||
mods: self.mods,
|
||||
acc: self.acc,
|
||||
n_misses: self.n_misses,
|
||||
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
|
||||
let n100 = total_result_count
|
||||
.saturating_sub(n300)
|
||||
.saturating_sub(misses);
|
||||
|
||||
(n300, n100)
|
||||
}
|
||||
(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,
|
||||
n_misses: usize,
|
||||
clock_rate: f64,
|
||||
n300: usize,
|
||||
n100: usize,
|
||||
}
|
||||
|
||||
impl<'map> TaikoPPInner<'map> {
|
||||
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() {
|
||||
multiplier *= 0.9;
|
||||
}
|
||||
let effective_miss_count = if total_successful_hits > 0 {
|
||||
(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() {
|
||||
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 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 {
|
||||
difficulty: self.attributes,
|
||||
pp,
|
||||
pp_acc: acc_value,
|
||||
pp_strain: strain_value,
|
||||
pp_difficulty: diff_value,
|
||||
effective_miss_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_strain_value(&self) -> f64 {
|
||||
let attributes = &self.attributes;
|
||||
let exp_base = 5.0 * (attributes.stars / 0.0075).max(1.0) - 4.0;
|
||||
let mut strain = exp_base * exp_base / 100_000.0;
|
||||
fn compute_difficulty_value(&self, effective_miss_count: f64) -> f64 {
|
||||
let attrs = &self.attributes;
|
||||
let exp_base = 5.0 * (attrs.stars / 0.115).max(1.0) - 4.0;
|
||||
let mut diff_value = exp_base.powf(2.25) / 1150.0;
|
||||
|
||||
// Longer maps are worth more
|
||||
let len_bonus = 1.0 + 0.1 * (attributes.max_combo as f64 / 1500.0).min(1.0);
|
||||
strain *= len_bonus;
|
||||
let len_bonus = 1.0 + 0.1 * (attrs.max_combo as f64 / 1500.0).min(1.0);
|
||||
diff_value *= len_bonus;
|
||||
|
||||
// Penalize misses exponentially
|
||||
strain *= 0.985_f64.powi(self.n_misses as i32);
|
||||
diff_value *= 0.986_f64.powf(effective_miss_count);
|
||||
|
||||
if self.mods.ez() {
|
||||
diff_value *= 0.985;
|
||||
}
|
||||
|
||||
// HD bonus
|
||||
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() {
|
||||
strain *= 1.05 * len_bonus;
|
||||
diff_value *= 1.05 * len_bonus;
|
||||
}
|
||||
|
||||
// Scale with accuracy
|
||||
strain * self.acc
|
||||
diff_value * self.acc * self.acc
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -291,12 +338,32 @@ impl<'map> TaikoPPInner<'map> {
|
||||
.clock_rate(self.clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let max_combo = self.attributes.max_combo;
|
||||
if hit_window <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
(150.0 / hit_window).powf(1.1)
|
||||
* self.acc.powi(15)
|
||||
* 22.0
|
||||
* (max_combo as f64 / 1500.0).powf(0.3).min(1.15)
|
||||
let mut acc_value = (60.0 / hit_window).powf(1.1)
|
||||
* self.acc.powi(8)
|
||||
* self.attributes.stars.powf(0.4)
|
||||
* 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 super::rim::Rim;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct TaikoObject<'h> {
|
||||
pub(crate) h: &'h HitObject,
|
||||
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 {
|
||||
fn taiko_objects(&self) -> TaikoObjectIter<'_>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user