add Flashlight skill

This commit is contained in:
MaxOhn
2021-10-28 16:13:46 +02:00
parent a6f35e7985
commit a4ff08826b
7 changed files with 252 additions and 46 deletions
+1
View File
@@ -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
View File
@@ -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;
+32 -14
View File
@@ -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.
///
+47 -13
View File
@@ -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)
}
}
+20 -2
View File
@@ -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(&current) * 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)
+92 -2
View File
@@ -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),
}
}