mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 13:37:25 +08:00
Merge branch 'master' into new-diffcalc-taiko
This commit is contained in:
commit
6bf0363db5
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(new OsuRuleset(), beatmap);
|
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
|
||||||
|
|
||||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
@ -13,10 +12,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
public double ApproachRate;
|
public double ApproachRate;
|
||||||
public double OverallDifficulty;
|
public double OverallDifficulty;
|
||||||
public int MaxCombo;
|
public int MaxCombo;
|
||||||
|
|
||||||
public OsuDifficultyAttributes(Mod[] mods, double starRating)
|
|
||||||
: base(mods, starRating)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
@ -13,53 +16,19 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
public class OsuLegacyDifficultyCalculator : LegacyDifficultyCalculator
|
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const int section_length = 400;
|
|
||||||
private const double difficulty_multiplier = 0.0675;
|
private const double difficulty_multiplier = 0.0675;
|
||||||
|
|
||||||
public OsuLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
if (!beatmap.HitObjects.Any())
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new OsuDifficultyAttributes(mods, 0);
|
return new OsuDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), clockRate);
|
|
||||||
Skill[] skills =
|
|
||||||
{
|
|
||||||
new Aim(),
|
|
||||||
new Speed()
|
|
||||||
};
|
|
||||||
|
|
||||||
double sectionLength = section_length * clockRate;
|
|
||||||
|
|
||||||
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
||||||
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
|
|
||||||
|
|
||||||
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
|
|
||||||
{
|
|
||||||
while (h.BaseObject.StartTime > currentSectionEnd)
|
|
||||||
{
|
|
||||||
foreach (Skill s in skills)
|
|
||||||
{
|
|
||||||
s.SaveCurrentPeak();
|
|
||||||
s.StartNewSectionFrom(currentSectionEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSectionEnd += sectionLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Skill s in skills)
|
|
||||||
s.Process(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The peak strain will not be saved for the last section in the above loop
|
|
||||||
foreach (Skill s in skills)
|
|
||||||
s.SaveCurrentPeak();
|
|
||||||
|
|
||||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||||
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||||
@ -73,8 +42,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
||||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
||||||
|
|
||||||
return new OsuDifficultyAttributes(mods, starRating)
|
return new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
|
StarRating = starRating,
|
||||||
|
Mods = mods,
|
||||||
AimStrain = aimRating,
|
AimStrain = aimRating,
|
||||||
SpeedStrain = speedRating,
|
SpeedStrain = speedRating,
|
||||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||||
@ -83,6 +54,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
// The first jump is formed by the first two hitobjects of the map.
|
||||||
|
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
||||||
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null;
|
||||||
|
var last = beatmap.HitObjects[i - 1];
|
||||||
|
var current = beatmap.HitObjects[i];
|
||||||
|
|
||||||
|
yield return new OsuDifficultyHitObject(current, lastLast, last, clockRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
|
{
|
||||||
|
new Aim(),
|
||||||
|
new Speed()
|
||||||
|
};
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
{
|
{
|
||||||
new OsuModDoubleTime(),
|
new OsuModDoubleTime(),
|
@ -1,50 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
|
|
||||||
/// which contains extra data required for difficulty calculation.
|
|
||||||
/// </summary>
|
|
||||||
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
|
|
||||||
{
|
|
||||||
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
|
|
||||||
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
|
|
||||||
/// </summary>
|
|
||||||
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
|
|
||||||
{
|
|
||||||
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
|
|
||||||
// This should probably happen before the objects reach the difficulty calculator.
|
|
||||||
difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerator<OsuDifficultyHitObject> GetEnumerator() => difficultyObjects;
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
||||||
|
|
||||||
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
|
|
||||||
{
|
|
||||||
// The first jump is formed by the first two hitobjects of the map.
|
|
||||||
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
|
||||||
for (int i = 1; i < objects.Count; i++)
|
|
||||||
{
|
|
||||||
var lastLast = i > 1 ? objects[i - 2] : null;
|
|
||||||
var last = objects[i - 1];
|
|
||||||
var current = objects[i];
|
|
||||||
|
|
||||||
yield return new OsuDifficultyHitObject(lastLast, last, current, timeRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +1,20 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class OsuDifficultyHitObject : DifficultyHitObject
|
||||||
/// A wrapper around <see cref="OsuHitObject"/> extending it with additional data required for difficulty calculation.
|
|
||||||
/// </summary>
|
|
||||||
public class OsuDifficultyHitObject
|
|
||||||
{
|
{
|
||||||
private const int normalized_radius = 52;
|
private const int normalized_radius = 52;
|
||||||
|
|
||||||
/// <summary>
|
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||||
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
|
|
||||||
/// </summary>
|
|
||||||
public OsuHitObject BaseObject { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||||
@ -30,40 +26,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double TravelDistance { get; private set; }
|
public double TravelDistance { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
|
|
||||||
/// </summary>
|
|
||||||
public double DeltaTime { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
|
|
||||||
/// </summary>
|
|
||||||
public double StrainTime { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
|
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// Calculated as the angle between the circles (current-2, current-1, current).
|
/// Calculated as the angle between the circles (current-2, current-1, current).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double? Angle { get; private set; }
|
public double? Angle { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double StrainTime;
|
||||||
|
|
||||||
private readonly OsuHitObject lastLastObject;
|
private readonly OsuHitObject lastLastObject;
|
||||||
private readonly OsuHitObject lastObject;
|
private readonly OsuHitObject lastObject;
|
||||||
private readonly double timeRate;
|
|
||||||
|
|
||||||
/// <summary>
|
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, HitObject lastObject, double clockRate)
|
||||||
/// Initializes the object calculating extra data required for difficulty calculation.
|
: base(hitObject, lastObject, clockRate)
|
||||||
/// </summary>
|
|
||||||
public OsuDifficultyHitObject(OsuHitObject lastLastObject, OsuHitObject lastObject, OsuHitObject currentObject, double timeRate)
|
|
||||||
{
|
{
|
||||||
this.lastLastObject = lastLastObject;
|
this.lastLastObject = (OsuHitObject)lastLastObject;
|
||||||
this.lastObject = lastObject;
|
this.lastObject = (OsuHitObject)lastObject;
|
||||||
this.timeRate = timeRate;
|
|
||||||
|
|
||||||
BaseObject = currentObject;
|
|
||||||
|
|
||||||
setDistances();
|
setDistances();
|
||||||
setTimingValues();
|
|
||||||
// Calculate angle here
|
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||||
|
StrainTime = Math.Max(50, DeltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDistances()
|
private void setDistances()
|
||||||
@ -102,14 +88,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTimingValues()
|
|
||||||
{
|
|
||||||
DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
|
|
||||||
|
|
||||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
|
||||||
StrainTime = Math.Max(50, DeltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void computeSliderCursorPosition(Slider slider)
|
private void computeSliderCursorPosition(Slider slider)
|
||||||
{
|
{
|
||||||
if (slider.LazyEndPosition != null)
|
if (slider.LazyEndPosition != null)
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -17,33 +20,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
protected override double SkillMultiplier => 26.25;
|
protected override double SkillMultiplier => 26.25;
|
||||||
protected override double StrainDecayBase => 0.15;
|
protected override double StrainDecayBase => 0.15;
|
||||||
|
|
||||||
protected override double StrainValueOf(OsuDifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||||
|
|
||||||
double result = 0;
|
double result = 0;
|
||||||
|
|
||||||
const double scale = 90;
|
|
||||||
|
|
||||||
double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
|
|
||||||
|
|
||||||
if (Previous.Count > 0)
|
if (Previous.Count > 0)
|
||||||
{
|
{
|
||||||
if (current.Angle != null && current.Angle.Value > angle_bonus_begin)
|
var osuPrevious = (OsuDifficultyHitObject)Previous[0];
|
||||||
|
|
||||||
|
if (osuCurrent.Angle != null && osuCurrent.Angle.Value > angle_bonus_begin)
|
||||||
{
|
{
|
||||||
|
const double scale = 90;
|
||||||
|
|
||||||
var angleBonus = Math.Sqrt(
|
var angleBonus = Math.Sqrt(
|
||||||
Math.Max(Previous[0].JumpDistance - scale, 0)
|
Math.Max(osuPrevious.JumpDistance - scale, 0)
|
||||||
* Math.Pow(Math.Sin(current.Angle.Value - angle_bonus_begin), 2)
|
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
|
||||||
* Math.Max(current.JumpDistance - scale, 0));
|
* Math.Max(osuCurrent.JumpDistance - scale, 0));
|
||||||
result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, Previous[0].StrainTime);
|
result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double jumpDistanceExp = applyDiminishingExp(current.JumpDistance);
|
double jumpDistanceExp = applyDiminishingExp(osuCurrent.JumpDistance);
|
||||||
double travelDistanceExp = applyDiminishingExp(current.TravelDistance);
|
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
|
||||||
|
|
||||||
return Math.Max(
|
return Math.Max(
|
||||||
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(current.StrainTime, timing_threshold),
|
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
|
||||||
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / current.StrainTime
|
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to processes strain values of <see cref="OsuDifficultyHitObject"/>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.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class Skill
|
|
||||||
{
|
|
||||||
protected const double SINGLE_SPACING_THRESHOLD = 125;
|
|
||||||
protected const double STREAM_SPACING_THRESHOLD = 110;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double SkillMultiplier { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double StrainDecayBase { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <see cref="OsuDifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
|
||||||
/// </summary>
|
|
||||||
protected readonly History<OsuDifficultyHitObject> Previous = new History<OsuDifficultyHitObject>(2); // Contained objects not used yet
|
|
||||||
|
|
||||||
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
|
|
||||||
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
|
||||||
private readonly List<double> strainPeaks = new List<double>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process an <see cref="OsuDifficultyHitObject"/> and update current strain values accordingly.
|
|
||||||
/// </summary>
|
|
||||||
public void Process(OsuDifficultyHitObject current)
|
|
||||||
{
|
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
|
||||||
if (!(current.BaseObject is Spinner))
|
|
||||||
currentStrain += StrainValueOf(current) * SkillMultiplier;
|
|
||||||
|
|
||||||
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
|
|
||||||
|
|
||||||
Previous.Push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
|
||||||
/// </summary>
|
|
||||||
public void SaveCurrentPeak()
|
|
||||||
{
|
|
||||||
if (Previous.Count > 0)
|
|
||||||
strainPeaks.Add(currentSectionPeak);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the initial strain level for a new section.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="offset">The beginning of the new section in milliseconds</param>
|
|
||||||
public void StartNewSectionFrom(double offset)
|
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
if (Previous.Count > 0)
|
|
||||||
currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the calculated difficulty value representing all processed <see cref="OsuDifficultyHitObject"/>s.
|
|
||||||
/// </summary>
|
|
||||||
public double DifficultyValue()
|
|
||||||
{
|
|
||||||
strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
|
|
||||||
// Difficulty is the weighted sum of the highest strains from every section.
|
|
||||||
foreach (double strain in strainPeaks)
|
|
||||||
{
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the strain value of an <see cref="OsuDifficultyHitObject"/>. This value is affected by previously processed objects.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double StrainValueOf(OsuDifficultyHitObject current);
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,10 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -11,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : Skill
|
public class Speed : Skill
|
||||||
{
|
{
|
||||||
|
private const double single_spacing_threshold = 125;
|
||||||
|
|
||||||
private const double angle_bonus_begin = 5 * Math.PI / 6;
|
private const double angle_bonus_begin = 5 * Math.PI / 6;
|
||||||
private const double pi_over_4 = Math.PI / 4;
|
private const double pi_over_4 = Math.PI / 4;
|
||||||
private const double pi_over_2 = Math.PI / 2;
|
private const double pi_over_2 = Math.PI / 2;
|
||||||
@ -22,9 +27,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
private const double max_speed_bonus = 45; // ~330BPM
|
private const double max_speed_bonus = 45; // ~330BPM
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
|
|
||||||
protected override double StrainValueOf(OsuDifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
double distance = Math.Min(SINGLE_SPACING_THRESHOLD, current.TravelDistance + current.JumpDistance);
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||||
|
|
||||||
|
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
|
||||||
double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
|
double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
|
||||||
|
|
||||||
double speedBonus = 1.0;
|
double speedBonus = 1.0;
|
||||||
@ -32,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
|
speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
double angleBonus = 1.0;
|
double angleBonus = 1.0;
|
||||||
if (current.Angle != null && current.Angle.Value < angle_bonus_begin)
|
if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
|
||||||
{
|
{
|
||||||
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - current.Angle.Value)), 2) / 3.57;
|
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
|
||||||
if (current.Angle.Value < pi_over_2)
|
if (osuCurrent.Angle.Value < pi_over_2)
|
||||||
{
|
{
|
||||||
angleBonus = 1.28;
|
angleBonus = 1.28;
|
||||||
if (distance < 90 && current.Angle.Value < pi_over_4)
|
if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
|
||||||
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
|
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
|
||||||
else if (distance < 90)
|
else if (distance < 90)
|
||||||
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - current.Angle.Value) / pi_over_4);
|
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / SINGLE_SPACING_THRESHOLD, 3.5)) / current.StrainTime;
|
return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.
|
|
||||||
/// Indexing starts at the top of the stack.
|
|
||||||
/// </summary>
|
|
||||||
public class History<T> : IEnumerable<T>
|
|
||||||
{
|
|
||||||
public int Count { get; private set; }
|
|
||||||
|
|
||||||
private readonly T[] array;
|
|
||||||
private readonly int capacity;
|
|
||||||
private int marker; // Marks the position of the most recently added item.
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the History class that is empty and has the specified capacity.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="capacity">The number of items the History can hold.</param>
|
|
||||||
public History(int capacity)
|
|
||||||
{
|
|
||||||
if (capacity < 0)
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
|
|
||||||
this.capacity = capacity;
|
|
||||||
array = new T[capacity];
|
|
||||||
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The most recently added item is returned at index 0.
|
|
||||||
/// </summary>
|
|
||||||
public T this[int i]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (i < 0 || i > Count - 1)
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
|
|
||||||
i += marker;
|
|
||||||
if (i > capacity - 1)
|
|
||||||
i -= capacity;
|
|
||||||
|
|
||||||
return array[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds the item as the most recent one in the history.
|
|
||||||
/// The oldest item is disposed if the history is full.
|
|
||||||
/// </summary>
|
|
||||||
public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition.
|
|
||||||
{
|
|
||||||
if (marker == 0)
|
|
||||||
marker = capacity - 1;
|
|
||||||
else
|
|
||||||
--marker;
|
|
||||||
|
|
||||||
array[marker] = item;
|
|
||||||
|
|
||||||
if (Count < capacity)
|
|
||||||
++Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerator<T> GetEnumerator()
|
|
||||||
{
|
|
||||||
for (int i = marker; i < capacity; ++i)
|
|
||||||
yield return array[i];
|
|
||||||
|
|
||||||
if (Count == capacity)
|
|
||||||
for (int i = 0; i < marker; ++i)
|
|
||||||
yield return array[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
|
@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
|
||||||
|
|
||||||
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(this, beatmap);
|
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user