add Flashlight skill
This commit is contained in:
@@ -15,6 +15,7 @@ pub struct DifficultyAttributes {
|
||||
pub od: f32,
|
||||
pub speed_strain: f32,
|
||||
pub aim_strain: f32,
|
||||
pub flashlight_strain: f32,
|
||||
pub max_combo: usize,
|
||||
pub n_circles: usize,
|
||||
pub n_spinners: usize,
|
||||
|
||||
+57
-12
@@ -293,9 +293,13 @@ impl<'m> OsuPP<'m> {
|
||||
let aim_value = self.compute_aim_value(total_hits);
|
||||
let speed_value = self.compute_speed_value(total_hits);
|
||||
let acc_value = self.compute_accuracy_value(total_hits);
|
||||
let flashlight_value = self.compute_flashlight_value(total_hits);
|
||||
|
||||
let pp = (aim_value.powf(1.1) + speed_value.powf(1.1) + acc_value.powf(1.1))
|
||||
.powf(1.0 / 1.1)
|
||||
let pp = (aim_value.powf(1.1)
|
||||
+ speed_value.powf(1.1)
|
||||
+ acc_value.powf(1.1)
|
||||
+ flashlight_value.powf(1.1))
|
||||
.powf(1.0 / 1.1)
|
||||
* multiplier;
|
||||
|
||||
let attributes = StarResult::Osu(self.attributes.unwrap());
|
||||
@@ -350,16 +354,7 @@ impl<'m> OsuPP<'m> {
|
||||
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||
}
|
||||
|
||||
// FL bonus
|
||||
let fl_bonus = if self.mods.fl() {
|
||||
1.0 + 0.35 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f32 * 0.3 * ((total_hits - 200.0) / 300.0).min(1.0)
|
||||
+ (total_hits > 500.0) as u8 as f32 * (total_hits - 500.0) / 1200.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
aim_value *= ar_bonus.max(fl_bonus);
|
||||
aim_value *= ar_bonus;
|
||||
|
||||
// Scale with accuracy
|
||||
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||
@@ -454,6 +449,56 @@ impl<'m> OsuPP<'m> {
|
||||
acc_value
|
||||
}
|
||||
|
||||
fn compute_flashlight_value(&self, total_hits: f32) -> f32 {
|
||||
if self.mods.fl() {
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
// TD penalty
|
||||
let raw_flashlight = if self.mods.td() {
|
||||
attributes.flashlight_strain.powf(0.8)
|
||||
} else {
|
||||
attributes.flashlight_strain
|
||||
};
|
||||
|
||||
let mut flashlight_value = raw_flashlight * raw_flashlight * 25.0;
|
||||
|
||||
// Add an additional bonus for HDFL
|
||||
if self.mods.hd() {
|
||||
flashlight_value *= 1.2;
|
||||
}
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// Default a 3% reduction for any # of misses
|
||||
if self.n_misses > 0 {
|
||||
flashlight_value *= 0.97
|
||||
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||
.powf((self.n_misses as f32).powf(0.875));
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
flashlight_value *=
|
||||
((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
||||
flashlight_value *= 0.5
|
||||
+ 0.15 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f32
|
||||
* (0.35 * ((total_hits - 200.0) / 600.0).min(1.0));
|
||||
|
||||
// Scale the aim value with accuracy _slightly_
|
||||
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||
|
||||
// It is important to also consider accuracy difficulty when doing that
|
||||
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||
|
||||
flashlight_value
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_hits(&self) -> usize {
|
||||
let n_objects = self
|
||||
|
||||
@@ -24,8 +24,8 @@ impl<'h> DifficultyObject<'h> {
|
||||
let strain_time = delta.max(50.0);
|
||||
|
||||
let pos = base.pos;
|
||||
let travel_dist = prev.travel_dist.unwrap_or(0.0);
|
||||
let prev_cursor_pos = prev.end_pos;
|
||||
let travel_dist = prev.travel_dist();
|
||||
let prev_cursor_pos = prev.lazy_end_pos();
|
||||
|
||||
let jump_dist = if base.is_spinner() {
|
||||
0.0
|
||||
@@ -34,7 +34,7 @@ impl<'h> DifficultyObject<'h> {
|
||||
};
|
||||
|
||||
let angle = prev_prev.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev.end_pos;
|
||||
let prev_prev_cursor_pos = prev_prev.lazy_end_pos();
|
||||
|
||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||
let v2 = pos - prev_cursor_pos;
|
||||
|
||||
@@ -84,8 +84,15 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
h
|
||||
});
|
||||
|
||||
let mut aim = Skill::new(SkillKind::Aim);
|
||||
let mut speed = Skill::new(SkillKind::Speed);
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::Speed));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
@@ -102,8 +109,9 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
aim.process(&h);
|
||||
speed.process(&h);
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
@@ -114,27 +122,35 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
aim.save_current_peak();
|
||||
aim.start_new_section_from(current_section_end);
|
||||
speed.save_current_peak();
|
||||
speed.start_new_section_from(current_section_end);
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
aim.process(&h);
|
||||
speed.process(&h);
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
aim.save_current_peak();
|
||||
speed.save_current_peak();
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let flashlight_rating = if let Some(skill) = skills.get_mut(2) {
|
||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
|
||||
|
||||
@@ -143,10 +159,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
diff_attributes.stars = stars;
|
||||
diff_attributes.speed_strain = speed_rating;
|
||||
diff_attributes.aim_strain = aim_rating;
|
||||
diff_attributes.flashlight_strain = flashlight_rating;
|
||||
|
||||
StarResult::Osu(diff_attributes)
|
||||
}
|
||||
|
||||
// TODO: Add flashlight strains
|
||||
/// Essentially the same as the `stars` function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
|
||||
@@ -12,9 +12,17 @@ const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) end_pos: Pos2,
|
||||
// circle: Some(0.0) | slider: Some(_) | spinner: None
|
||||
pub(crate) travel_dist: Option<f32>,
|
||||
pub(crate) kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
pub(crate) enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_pos: Pos2,
|
||||
lazy_end_pos: Pos2,
|
||||
travel_dist: f32,
|
||||
},
|
||||
Spinner,
|
||||
}
|
||||
|
||||
impl OsuObject {
|
||||
@@ -33,8 +41,7 @@ impl OsuObject {
|
||||
HitObjectKind::Circle => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos: h.pos,
|
||||
travel_dist: Some(0.0),
|
||||
kind: OsuObjectKind::Circle,
|
||||
},
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
@@ -43,7 +50,7 @@ impl OsuObject {
|
||||
path_type,
|
||||
} => {
|
||||
// Key values which are computed here
|
||||
let mut end_pos = h.pos;
|
||||
let mut lazy_end_pos = h.pos;
|
||||
let mut travel_dist = 0.0;
|
||||
|
||||
// Responsible for timing point values
|
||||
@@ -82,12 +89,12 @@ impl OsuObject {
|
||||
let curr_dist = pixel_len * progress;
|
||||
let curr_pos = curve.point_at_distance(curr_dist);
|
||||
|
||||
let diff = curr_pos - end_pos;
|
||||
let diff = curr_pos - lazy_end_pos;
|
||||
let mut dist = diff.length();
|
||||
|
||||
if dist > approx_follow_circle_radius {
|
||||
dist -= approx_follow_circle_radius;
|
||||
end_pos += diff.normalize() * dist;
|
||||
lazy_end_pos += diff.normalize() * dist;
|
||||
travel_dist += dist;
|
||||
}
|
||||
};
|
||||
@@ -138,20 +145,23 @@ impl OsuObject {
|
||||
|
||||
ticks.clear();
|
||||
|
||||
let end_pos = curve.point_at_distance(*pixel_len);
|
||||
travel_dist *= scaling_factor;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos,
|
||||
travel_dist: Some(travel_dist),
|
||||
kind: OsuObjectKind::Slider {
|
||||
end_pos,
|
||||
lazy_end_pos,
|
||||
travel_dist,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { .. } => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos: h.pos,
|
||||
travel_dist: None,
|
||||
kind: OsuObjectKind::Spinner,
|
||||
},
|
||||
HitObjectKind::Hold { .. } => return None,
|
||||
};
|
||||
@@ -159,8 +169,32 @@ impl OsuObject {
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { end_pos, .. } => end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => lazy_end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn travel_dist(&self) -> f32 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => 0.0,
|
||||
OsuObjectKind::Slider { travel_dist, .. } => travel_dist,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_spinner(&self) -> bool {
|
||||
self.travel_dist.is_none()
|
||||
matches!(self.kind, OsuObjectKind::Spinner)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ use std::cmp::Ordering;
|
||||
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||
|
||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
||||
|
||||
const DECAY_WEIGHT: f32 = 0.9;
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.065;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
current_strain: f32,
|
||||
@@ -49,15 +53,18 @@ impl Skill {
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
||||
self.kind.pre_process();
|
||||
self.current_strain *= self.strain_decay(current.delta);
|
||||
self.current_strain += self.kind.strain_value_of(¤t) * self.skill_multiplier();
|
||||
self.current_section_peak = self.current_section_peak.max(self.current_strain);
|
||||
self.prev_time.replace(current.base.time);
|
||||
self.kind.post_process(current);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
let decay_weight = self.decay_weight();
|
||||
|
||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
||||
@@ -81,7 +88,7 @@ impl Skill {
|
||||
|
||||
for &strain in self.strain_peaks.iter() {
|
||||
difficulty += strain * weight;
|
||||
weight *= DECAY_WEIGHT;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty * difficulty_multiplier
|
||||
@@ -91,6 +98,7 @@ impl Skill {
|
||||
fn skill_multiplier(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
@@ -99,10 +107,20 @@ impl Skill {
|
||||
fn strain_decay_base(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||
SkillKind::Speed => SPEED_STRAIN_DECAY_BASE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decay_weight(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||
SkillKind::Speed => SPEED_DECAY_WEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
self.current_strain * self.strain_decay(delta_time)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::parse::Pos2;
|
||||
|
||||
use super::DifficultyObject;
|
||||
|
||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
||||
@@ -13,19 +17,61 @@ const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
||||
const TIMING_THRESHOLD: f32 = 107.0;
|
||||
|
||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct FlashlightHistoryEntry {
|
||||
is_spinner: bool,
|
||||
end_pos: Pos2,
|
||||
strain_time: f32,
|
||||
}
|
||||
|
||||
pub(crate) enum SkillKind {
|
||||
Aim,
|
||||
Flashlight {
|
||||
history: VecDeque<FlashlightHistoryEntry>,
|
||||
scaling_factor: f32,
|
||||
},
|
||||
Speed,
|
||||
}
|
||||
|
||||
impl SkillKind {
|
||||
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
|
||||
pub(crate) fn flashlight(scaling_factor: f32) -> Self {
|
||||
Self::Flashlight {
|
||||
history: VecDeque::with_capacity(FLASHLIGHT_REDUCED_SECTION_COUNT),
|
||||
scaling_factor,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pre_process(&mut self) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_REDUCED_SECTION_COUNT),
|
||||
Self::Speed => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn post_process(&mut self, current: &DifficultyObject) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => {
|
||||
let entry = FlashlightHistoryEntry {
|
||||
is_spinner: current.base.is_spinner(),
|
||||
end_pos: current.base.end_pos(),
|
||||
strain_time: current.strain_time,
|
||||
};
|
||||
|
||||
history.push_front(entry);
|
||||
}
|
||||
Self::Speed => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_of(&self, current: &DifficultyObject) -> f32 {
|
||||
match self {
|
||||
Self::Aim => {
|
||||
if current.base.is_spinner() {
|
||||
@@ -57,6 +103,46 @@ impl SkillKind {
|
||||
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / current.strain_time)
|
||||
}
|
||||
Self::Flashlight {
|
||||
history,
|
||||
scaling_factor,
|
||||
} => {
|
||||
if current.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut small_dist_nerf = 1.0;
|
||||
|
||||
let mut result = 0.0;
|
||||
let mut cumulative_strain_time = 0.0;
|
||||
let mut history = history.iter();
|
||||
|
||||
if let Some(entry) = history.next() {
|
||||
if !entry.is_spinner {
|
||||
let jump_dist = (current.base.pos - entry.end_pos).length();
|
||||
cumulative_strain_time += entry.strain_time;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius
|
||||
if jump_dist < 50.0 {
|
||||
small_dist_nerf = jump_dist / 50.0;
|
||||
}
|
||||
|
||||
result += scaling_factor * jump_dist / cumulative_strain_time;
|
||||
}
|
||||
|
||||
for (i, entry) in (1..).zip(history) {
|
||||
if !entry.is_spinner {
|
||||
let jump_dist = (current.base.pos - entry.end_pos).length();
|
||||
cumulative_strain_time += entry.strain_time;
|
||||
|
||||
result += 0.8_f32.powi(i) * scaling_factor * jump_dist
|
||||
/ cumulative_strain_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(small_dist_nerf * result).powf(2.5)
|
||||
}
|
||||
Self::Speed => {
|
||||
if current.base.is_spinner() {
|
||||
return 0.0;
|
||||
@@ -103,6 +189,10 @@ impl SkillKind {
|
||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
||||
match self {
|
||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||
Self::Flashlight { .. } => (
|
||||
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
||||
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
||||
),
|
||||
Self::Speed => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user