add Flashlight skill
This commit is contained in:
@@ -15,6 +15,7 @@ pub struct DifficultyAttributes {
|
|||||||
pub od: f32,
|
pub od: f32,
|
||||||
pub speed_strain: f32,
|
pub speed_strain: f32,
|
||||||
pub aim_strain: f32,
|
pub aim_strain: f32,
|
||||||
|
pub flashlight_strain: f32,
|
||||||
pub max_combo: usize,
|
pub max_combo: usize,
|
||||||
pub n_circles: usize,
|
pub n_circles: usize,
|
||||||
pub n_spinners: 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 aim_value = self.compute_aim_value(total_hits);
|
||||||
let speed_value = self.compute_speed_value(total_hits);
|
let speed_value = self.compute_speed_value(total_hits);
|
||||||
let acc_value = self.compute_accuracy_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))
|
let pp = (aim_value.powf(1.1)
|
||||||
.powf(1.0 / 1.1)
|
+ speed_value.powf(1.1)
|
||||||
|
+ acc_value.powf(1.1)
|
||||||
|
+ flashlight_value.powf(1.1))
|
||||||
|
.powf(1.0 / 1.1)
|
||||||
* multiplier;
|
* multiplier;
|
||||||
|
|
||||||
let attributes = StarResult::Osu(self.attributes.unwrap());
|
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);
|
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FL bonus
|
aim_value *= ar_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);
|
|
||||||
|
|
||||||
// Scale with accuracy
|
// Scale with accuracy
|
||||||
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||||
@@ -454,6 +449,56 @@ impl<'m> OsuPP<'m> {
|
|||||||
acc_value
|
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]
|
#[inline]
|
||||||
fn total_hits(&self) -> usize {
|
fn total_hits(&self) -> usize {
|
||||||
let n_objects = self
|
let n_objects = self
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ impl<'h> DifficultyObject<'h> {
|
|||||||
let strain_time = delta.max(50.0);
|
let strain_time = delta.max(50.0);
|
||||||
|
|
||||||
let pos = base.pos;
|
let pos = base.pos;
|
||||||
let travel_dist = prev.travel_dist.unwrap_or(0.0);
|
let travel_dist = prev.travel_dist();
|
||||||
let prev_cursor_pos = prev.end_pos;
|
let prev_cursor_pos = prev.lazy_end_pos();
|
||||||
|
|
||||||
let jump_dist = if base.is_spinner() {
|
let jump_dist = if base.is_spinner() {
|
||||||
0.0
|
0.0
|
||||||
@@ -34,7 +34,7 @@ impl<'h> DifficultyObject<'h> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let angle = prev_prev.map(|prev_prev| {
|
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 v1 = prev_prev_cursor_pos - prev.pos;
|
||||||
let v2 = pos - prev_cursor_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
|
h
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut aim = Skill::new(SkillKind::Aim);
|
let fl = mods.fl();
|
||||||
let mut speed = Skill::new(SkillKind::Speed);
|
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_prev = None;
|
||||||
let mut prev = hit_objects.next().unwrap();
|
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;
|
current_section_end += SECTION_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
aim.process(&h);
|
for skill in skills.iter_mut() {
|
||||||
speed.process(&h);
|
skill.process(&h);
|
||||||
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
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);
|
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > current_section_end {
|
||||||
aim.save_current_peak();
|
for skill in skills.iter_mut() {
|
||||||
aim.start_new_section_from(current_section_end);
|
skill.save_current_peak();
|
||||||
speed.save_current_peak();
|
skill.start_new_section_from(current_section_end);
|
||||||
speed.start_new_section_from(current_section_end);
|
}
|
||||||
|
|
||||||
current_section_end += SECTION_LEN;
|
current_section_end += SECTION_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
aim.process(&h);
|
for skill in skills.iter_mut() {
|
||||||
speed.process(&h);
|
skill.process(&h);
|
||||||
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
prev = curr;
|
prev = curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
aim.save_current_peak();
|
for skill in skills.iter_mut() {
|
||||||
speed.save_current_peak();
|
skill.save_current_peak();
|
||||||
|
}
|
||||||
|
|
||||||
let aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
let speed_rating = speed.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;
|
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.stars = stars;
|
||||||
diff_attributes.speed_strain = speed_rating;
|
diff_attributes.speed_strain = speed_rating;
|
||||||
diff_attributes.aim_strain = aim_rating;
|
diff_attributes.aim_strain = aim_rating;
|
||||||
|
diff_attributes.flashlight_strain = flashlight_rating;
|
||||||
|
|
||||||
StarResult::Osu(diff_attributes)
|
StarResult::Osu(diff_attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add flashlight strains
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
/// evaluating the final strains, it just returns them as is.
|
/// 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) struct OsuObject {
|
||||||
pub(crate) time: f32,
|
pub(crate) time: f32,
|
||||||
pub(crate) pos: Pos2,
|
pub(crate) pos: Pos2,
|
||||||
pub(crate) end_pos: Pos2,
|
pub(crate) kind: OsuObjectKind,
|
||||||
// circle: Some(0.0) | slider: Some(_) | spinner: None
|
}
|
||||||
pub(crate) travel_dist: Option<f32>,
|
|
||||||
|
pub(crate) enum OsuObjectKind {
|
||||||
|
Circle,
|
||||||
|
Slider {
|
||||||
|
end_pos: Pos2,
|
||||||
|
lazy_end_pos: Pos2,
|
||||||
|
travel_dist: f32,
|
||||||
|
},
|
||||||
|
Spinner,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OsuObject {
|
impl OsuObject {
|
||||||
@@ -33,8 +41,7 @@ impl OsuObject {
|
|||||||
HitObjectKind::Circle => Self {
|
HitObjectKind::Circle => Self {
|
||||||
time: h.start_time,
|
time: h.start_time,
|
||||||
pos: h.pos,
|
pos: h.pos,
|
||||||
end_pos: h.pos,
|
kind: OsuObjectKind::Circle,
|
||||||
travel_dist: Some(0.0),
|
|
||||||
},
|
},
|
||||||
HitObjectKind::Slider {
|
HitObjectKind::Slider {
|
||||||
pixel_len,
|
pixel_len,
|
||||||
@@ -43,7 +50,7 @@ impl OsuObject {
|
|||||||
path_type,
|
path_type,
|
||||||
} => {
|
} => {
|
||||||
// Key values which are computed here
|
// 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;
|
let mut travel_dist = 0.0;
|
||||||
|
|
||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
@@ -82,12 +89,12 @@ impl OsuObject {
|
|||||||
let curr_dist = pixel_len * progress;
|
let curr_dist = pixel_len * progress;
|
||||||
let curr_pos = curve.point_at_distance(curr_dist);
|
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();
|
let mut dist = diff.length();
|
||||||
|
|
||||||
if dist > approx_follow_circle_radius {
|
if dist > approx_follow_circle_radius {
|
||||||
dist -= approx_follow_circle_radius;
|
dist -= approx_follow_circle_radius;
|
||||||
end_pos += diff.normalize() * dist;
|
lazy_end_pos += diff.normalize() * dist;
|
||||||
travel_dist += dist;
|
travel_dist += dist;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -138,20 +145,23 @@ impl OsuObject {
|
|||||||
|
|
||||||
ticks.clear();
|
ticks.clear();
|
||||||
|
|
||||||
|
let end_pos = curve.point_at_distance(*pixel_len);
|
||||||
travel_dist *= scaling_factor;
|
travel_dist *= scaling_factor;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
time: h.start_time,
|
time: h.start_time,
|
||||||
pos: h.pos,
|
pos: h.pos,
|
||||||
end_pos,
|
kind: OsuObjectKind::Slider {
|
||||||
travel_dist: Some(travel_dist),
|
end_pos,
|
||||||
|
lazy_end_pos,
|
||||||
|
travel_dist,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HitObjectKind::Spinner { .. } => Self {
|
HitObjectKind::Spinner { .. } => Self {
|
||||||
time: h.start_time,
|
time: h.start_time,
|
||||||
pos: h.pos,
|
pos: h.pos,
|
||||||
end_pos: h.pos,
|
kind: OsuObjectKind::Spinner,
|
||||||
travel_dist: None,
|
|
||||||
},
|
},
|
||||||
HitObjectKind::Hold { .. } => return None,
|
HitObjectKind::Hold { .. } => return None,
|
||||||
};
|
};
|
||||||
@@ -159,8 +169,32 @@ impl OsuObject {
|
|||||||
Some(obj)
|
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]
|
#[inline]
|
||||||
pub(crate) fn is_spinner(&self) -> bool {
|
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_SKILL_MULTIPLIER: f32 = 1400.0;
|
||||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||||
|
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||||
|
|
||||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
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 {
|
pub(crate) struct Skill {
|
||||||
current_strain: f32,
|
current_strain: f32,
|
||||||
@@ -49,15 +53,18 @@ impl Skill {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
||||||
|
self.kind.pre_process();
|
||||||
self.current_strain *= self.strain_decay(current.delta);
|
self.current_strain *= self.strain_decay(current.delta);
|
||||||
self.current_strain += self.kind.strain_value_of(¤t) * self.skill_multiplier();
|
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.current_section_peak = self.current_section_peak.max(self.current_strain);
|
||||||
self.prev_time.replace(current.base.time);
|
self.prev_time.replace(current.base.time);
|
||||||
|
self.kind.post_process(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.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, difficulty_multiplier) = self.kind.difficulty_values();
|
||||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
let reduced_section_count_f32 = reduced_section_count as f32;
|
||||||
@@ -81,7 +88,7 @@ impl Skill {
|
|||||||
|
|
||||||
for &strain in self.strain_peaks.iter() {
|
for &strain in self.strain_peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= DECAY_WEIGHT;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
difficulty * difficulty_multiplier
|
difficulty * difficulty_multiplier
|
||||||
@@ -91,6 +98,7 @@ impl Skill {
|
|||||||
fn skill_multiplier(&self) -> f32 {
|
fn skill_multiplier(&self) -> f32 {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||||
|
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||||
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
|
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,10 +107,20 @@ impl Skill {
|
|||||||
fn strain_decay_base(&self) -> f32 {
|
fn strain_decay_base(&self) -> f32 {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||||
|
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||||
SkillKind::Speed => SPEED_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]
|
#[inline]
|
||||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||||
self.current_strain * self.strain_decay(delta_time)
|
self.current_strain * self.strain_decay(delta_time)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::parse::Pos2;
|
||||||
|
|
||||||
use super::DifficultyObject;
|
use super::DifficultyObject;
|
||||||
|
|
||||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
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 TIMING_THRESHOLD: f32 = 107.0;
|
||||||
|
|
||||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||||
|
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||||
|
|
||||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||||
|
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
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 {
|
pub(crate) enum SkillKind {
|
||||||
Aim,
|
Aim,
|
||||||
|
Flashlight {
|
||||||
|
history: VecDeque<FlashlightHistoryEntry>,
|
||||||
|
scaling_factor: f32,
|
||||||
|
},
|
||||||
Speed,
|
Speed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SkillKind {
|
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 {
|
match self {
|
||||||
Self::Aim => {
|
Self::Aim => {
|
||||||
if current.base.is_spinner() {
|
if current.base.is_spinner() {
|
||||||
@@ -57,6 +103,46 @@ impl SkillKind {
|
|||||||
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
|
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
|
||||||
.max(dist_exp / current.strain_time)
|
.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 => {
|
Self::Speed => {
|
||||||
if current.base.is_spinner() {
|
if current.base.is_spinner() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -103,6 +189,10 @@ impl SkillKind {
|
|||||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
||||||
match self {
|
match self {
|
||||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
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),
|
Self::Speed => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user