diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 7222166535..75e17f6c48 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
- public class Movement : Skill
+ public class Movement : StrainSkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 0761724e83..2ba2ee6b4a 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
- public class Strain : Skill
+ public class Strain : StrainSkill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 90cba13c7c..cb819ec090 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
///
- public class Aim : Skill
+ public class Aim : StrainSkill
{
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 200bc7997d..fbac080fc6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
///
- public class Speed : Skill
+ public class Speed : StrainSkill
{
private const double single_spacing_threshold = 125;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index cc0738e252..769d021362 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the colour coefficient of taiko difficulty.
///
- public class Colour : Skill
+ public class Colour : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index f2b8309ac5..a32f6ebe0d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the rhythm coefficient of taiko difficulty.
///
- public class Rhythm : Skill
+ public class Rhythm : StrainSkill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index c34cce0cd6..4cceadb23f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
///
- public class Stamina : Skill
+ public class Stamina : StrainSkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
index aa187c6afd..b3d7ce3c40 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
@@ -11,140 +9,39 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
///
- /// Used to processes strain values of s, keep track of strain levels caused by the processed objects
- /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
+ /// A bare minimal abstract skill for fully custom skill implementations.
///
public abstract class Skill
{
- ///
- /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
- ///
- protected abstract double SkillMultiplier { get; }
-
- ///
- /// Determines how quickly strain decays for the given skill.
- /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
- ///
- protected abstract double StrainDecayBase { get; }
-
- ///
- /// The weight by which each strain value decays.
- ///
- protected virtual double DecayWeight => 0.9;
-
///
/// s that were processed previously. They can affect the strain values of the following objects.
///
protected readonly LimitedCapacityStack Previous = new LimitedCapacityStack(2); // Contained objects not used yet
- ///
- /// The current strain level.
- ///
- protected double CurrentStrain { get; private set; } = 1;
-
- ///
- /// The length of each strain section.
- ///
- protected virtual int SectionLength => 400;
-
+
///
/// Mods for use in skill calculations.
///
protected IReadOnlyList Mods => mods;
- private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
-
- private double currentSectionEnd;
-
- private readonly List strainPeaks = new List();
-
private readonly Mod[] mods;
-
protected Skill(Mod[] mods)
{
this.mods = mods;
}
///
- /// Process a and update current strain values accordingly.
+ /// Process a .
///
- public void Process(DifficultyHitObject current)
+ /// The to process.
+ public virtual void Process(DifficultyHitObject current)
{
- // The first object doesn't generate a strain, so we begin with an incremented section end
- if (Previous.Count == 0)
- currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
-
- while (current.StartTime > currentSectionEnd)
- {
- saveCurrentPeak();
- startNewSectionFrom(currentSectionEnd);
- currentSectionEnd += SectionLength;
- }
-
- CurrentStrain *= strainDecay(current.DeltaTime);
- CurrentStrain += StrainValueOf(current) * SkillMultiplier;
-
- currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
-
Previous.Push(current);
}
- ///
- /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
- ///
- private void saveCurrentPeak()
- {
- strainPeaks.Add(currentSectionPeak);
- }
-
- ///
- /// Sets the initial strain level for a new section.
- ///
- /// The beginning of the new section in milliseconds.
- private void startNewSectionFrom(double time)
- {
- // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
- // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
- currentSectionPeak = GetPeakStrain(time);
- }
-
- ///
- /// Retrieves the peak strain at a point in time.
- ///
- /// The time to retrieve the peak strain at, adjusted by clockrate.
- /// The peak strain.
- protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
-
- ///
- /// Returns a live enumerable of the peak strains for each section of the beatmap,
- /// including the peak of the current section.
- ///
- public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
-
///
/// Returns the calculated difficulty value representing all s that have been processed up to this point.
///
- public double DifficultyValue()
- {
- double difficulty = 0;
- double weight = 1;
-
- // Difficulty is the weighted sum of the highest strains from every section.
- // We're sorting from highest to lowest strain.
- foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
- {
- difficulty += strain * weight;
- weight *= DecayWeight;
- }
-
- return difficulty;
- }
-
- ///
- /// Calculates the strain value of a . This value is affected by previously processed objects.
- ///
- protected abstract double StrainValueOf(DifficultyHitObject current);
-
- private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
+ public abstract double DifficultyValue();
}
}
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
new file mode 100644
index 0000000000..c324f8e414
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -0,0 +1,137 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Difficulty.Skills
+{
+ ///
+ /// Used to processes strain values of s, keep track of strain levels caused by the processed objects
+ /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
+ ///
+ public abstract class StrainSkill : Skill
+ {
+ ///
+ /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
+ ///
+ protected abstract double SkillMultiplier { get; }
+
+ ///
+ /// Determines how quickly strain decays for the given skill.
+ /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
+ ///
+ protected abstract double StrainDecayBase { get; }
+
+ ///
+ /// The weight by which each strain value decays.
+ ///
+ protected virtual double DecayWeight => 0.9;
+
+ ///
+ /// The current strain level.
+ ///
+ protected double CurrentStrain { get; private set; } = 1;
+
+ ///
+ /// The length of each strain section.
+ ///
+ protected virtual int SectionLength => 400;
+
+ private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
+
+ private double currentSectionEnd;
+
+ private readonly List strainPeaks = new List();
+
+ protected StrainSkill(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ ///
+ /// Process a and update current strain values accordingly.
+ ///
+ public sealed override void Process(DifficultyHitObject current)
+ {
+ // The first object doesn't generate a strain, so we begin with an incremented section end
+ if (Previous.Count == 0)
+ currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
+
+ while (current.StartTime > currentSectionEnd)
+ {
+ saveCurrentPeak();
+ startNewSectionFrom(currentSectionEnd);
+ currentSectionEnd += SectionLength;
+ }
+
+ CurrentStrain *= strainDecay(current.DeltaTime);
+ CurrentStrain += StrainValueOf(current) * SkillMultiplier;
+
+ currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
+
+ base.Process(current);
+ }
+
+ ///
+ /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
+ ///
+ private void saveCurrentPeak()
+ {
+ strainPeaks.Add(currentSectionPeak);
+ }
+
+ ///
+ /// Sets the initial strain level for a new section.
+ ///
+ /// The beginning of the new section in milliseconds.
+ private void startNewSectionFrom(double time)
+ {
+ // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
+ // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
+ currentSectionPeak = GetPeakStrain(time);
+ }
+
+ ///
+ /// Retrieves the peak strain at a point in time.
+ ///
+ /// The time to retrieve the peak strain at.
+ /// The peak strain.
+ protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
+
+ ///
+ /// Returns a live enumerable of the peak strains for each section of the beatmap,
+ /// including the peak of the current section.
+ ///
+ public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
+
+ ///
+ /// Returns the calculated difficulty value representing all s that have been processed up to this point.
+ ///
+ public sealed override double DifficultyValue()
+ {
+ double difficulty = 0;
+ double weight = 1;
+
+ // Difficulty is the weighted sum of the highest strains from every section.
+ // We're sorting from highest to lowest strain.
+ foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
+ {
+ difficulty += strain * weight;
+ weight *= DecayWeight;
+ }
+
+ return difficulty;
+ }
+
+ ///
+ /// Calculates the strain value of a . This value is affected by previously processed objects.
+ ///
+ protected abstract double StrainValueOf(DifficultyHitObject current);
+
+ private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
+ }
+}