mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 06:03:08 +08:00
Merge remote-tracking branch 'upstream/master' into isactive-changes
This commit is contained in:
commit
a4768a75e2
@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
||||||
|
|
||||||
[TestCase(3.8664391043534758, "diffcalc-test")]
|
[TestCase(4.2038001515546597d, "diffcalc-test")]
|
||||||
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 CatchLegacyDifficultyCalculator(new CatchRuleset(), beatmap);
|
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
|
||||||
|
|
||||||
protected override Ruleset CreateRuleset() => new CatchRuleset();
|
protected override Ruleset CreateRuleset() => new CatchRuleset();
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
|
||||||
|
|
||||||
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(this, beatmap);
|
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override int? LegacyID => 2;
|
public override int? LegacyID => 2;
|
||||||
|
|
||||||
|
@ -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.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
@ -10,10 +9,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
public double ApproachRate;
|
public double ApproachRate;
|
||||||
public int MaxCombo;
|
public int MaxCombo;
|
||||||
|
|
||||||
public CatchDifficultyAttributes(Mod[] mods, double starRating)
|
|
||||||
: base(mods, starRating)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
// 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 System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Catch.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
|
{
|
||||||
|
public class CatchDifficultyCalculator : DifficultyCalculator
|
||||||
|
{
|
||||||
|
private const double star_scaling_factor = 0.145;
|
||||||
|
|
||||||
|
protected override int SectionLength => 750;
|
||||||
|
|
||||||
|
private readonly float halfCatchWidth;
|
||||||
|
|
||||||
|
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
|
: base(ruleset, beatmap)
|
||||||
|
{
|
||||||
|
var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty);
|
||||||
|
halfCatchWidth = catcher.CatchWidth * 0.5f;
|
||||||
|
|
||||||
|
// We're only using 80% of the catcher's width to simulate imperfect gameplay.
|
||||||
|
halfCatchWidth *= 0.8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
if (beatmap.HitObjects.Count == 0)
|
||||||
|
return new CatchDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
|
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||||
|
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|
||||||
|
return new CatchDifficultyAttributes
|
||||||
|
{
|
||||||
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||||
|
Mods = mods,
|
||||||
|
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||||
|
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
CatchHitObject lastObject = null;
|
||||||
|
|
||||||
|
foreach (var hitObject in beatmap.HitObjects.OfType<CatchHitObject>())
|
||||||
|
{
|
||||||
|
if (lastObject == null)
|
||||||
|
{
|
||||||
|
lastObject = hitObject;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
|
||||||
|
case Fruit fruit:
|
||||||
|
yield return new CatchDifficultyHitObject(fruit, lastObject, clockRate, halfCatchWidth);
|
||||||
|
lastObject = hitObject;
|
||||||
|
break;
|
||||||
|
case JuiceStream _:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>().Where(o => !(o is TinyDroplet)))
|
||||||
|
{
|
||||||
|
yield return new CatchDifficultyHitObject(nested, lastObject, clockRate, halfCatchWidth);
|
||||||
|
lastObject = nested;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
|
{
|
||||||
|
new Movement(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,130 +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 osu.Game.Rulesets.Catch.Objects;
|
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
|
||||||
{
|
|
||||||
public class CatchDifficultyHitObject
|
|
||||||
{
|
|
||||||
internal static readonly double DECAY_BASE = 0.20;
|
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
|
||||||
private const float absolute_player_positioning_error = 16f;
|
|
||||||
private readonly float playerPositioningError;
|
|
||||||
|
|
||||||
internal CatchHitObject BaseHitObject;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures jump difficulty. CtB doesn't have something like button pressing speed or accuracy
|
|
||||||
/// </summary>
|
|
||||||
internal double Strain = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This is required to keep track of lazy player movement (always moving only as far as necessary)
|
|
||||||
/// Without this quick repeat sliders / weirdly shaped streams might become ridiculously overrated
|
|
||||||
/// </summary>
|
|
||||||
internal float PlayerPositionOffset;
|
|
||||||
internal float LastMovement;
|
|
||||||
|
|
||||||
internal float NormalizedPosition;
|
|
||||||
internal float ActualNormalizedPosition => NormalizedPosition + PlayerPositionOffset;
|
|
||||||
|
|
||||||
internal CatchDifficultyHitObject(CatchHitObject baseHitObject, float catcherWidthHalf)
|
|
||||||
{
|
|
||||||
BaseHitObject = baseHitObject;
|
|
||||||
|
|
||||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
||||||
float scalingFactor = normalized_hitobject_radius / catcherWidthHalf;
|
|
||||||
|
|
||||||
playerPositioningError = absolute_player_positioning_error; // * scalingFactor;
|
|
||||||
NormalizedPosition = baseHitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const double direction_change_bonus = 12.5;
|
|
||||||
internal void CalculateStrains(CatchDifficultyHitObject previousHitObject, double timeRate)
|
|
||||||
{
|
|
||||||
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
|
|
||||||
// See Taiko feedback thread.
|
|
||||||
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
|
||||||
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
|
|
||||||
|
|
||||||
// Update new position with lazy movement.
|
|
||||||
PlayerPositionOffset =
|
|
||||||
MathHelper.Clamp(
|
|
||||||
previousHitObject.ActualNormalizedPosition,
|
|
||||||
NormalizedPosition - (normalized_hitobject_radius - playerPositioningError),
|
|
||||||
NormalizedPosition + (normalized_hitobject_radius - playerPositioningError)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player.
|
|
||||||
- NormalizedPosition; // Subtract HitObject position to obtain offset
|
|
||||||
|
|
||||||
LastMovement = DistanceTo(previousHitObject);
|
|
||||||
double addition = spacingWeight(LastMovement);
|
|
||||||
|
|
||||||
if (NormalizedPosition < previousHitObject.NormalizedPosition)
|
|
||||||
{
|
|
||||||
LastMovement = -LastMovement;
|
|
||||||
}
|
|
||||||
|
|
||||||
CatchHitObject previousHitCircle = previousHitObject.BaseHitObject;
|
|
||||||
|
|
||||||
double additionBonus = 0;
|
|
||||||
double sqrtTime = Math.Sqrt(Math.Max(timeElapsed, 25));
|
|
||||||
|
|
||||||
// Direction changes give an extra point!
|
|
||||||
if (Math.Abs(LastMovement) > 0.1)
|
|
||||||
{
|
|
||||||
if (Math.Abs(previousHitObject.LastMovement) > 0.1 && Math.Sign(LastMovement) != Math.Sign(previousHitObject.LastMovement))
|
|
||||||
{
|
|
||||||
double bonus = direction_change_bonus / sqrtTime;
|
|
||||||
|
|
||||||
// Weight bonus by how
|
|
||||||
double bonusFactor = Math.Min(playerPositioningError, Math.Abs(LastMovement)) / playerPositioningError;
|
|
||||||
|
|
||||||
// We want time to play a role twice here!
|
|
||||||
addition += bonus * bonusFactor;
|
|
||||||
|
|
||||||
// Bonus for tougher direction switches and "almost" hyperdashes at this point
|
|
||||||
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
|
|
||||||
{
|
|
||||||
additionBonus += 0.3 * bonusFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base bonus for every movement, giving some weight to streams.
|
|
||||||
addition += 7.5 * Math.Min(Math.Abs(LastMovement), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bonus for "almost" hyperdashes at corner points
|
|
||||||
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
|
|
||||||
{
|
|
||||||
if (!previousHitCircle.HyperDash)
|
|
||||||
{
|
|
||||||
additionBonus += 1.0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// After a hyperdash we ARE in the correct position. Always!
|
|
||||||
PlayerPositionOffset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
addition *= 1.0 + additionBonus * ((10 - previousHitCircle.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
addition *= 850.0 / Math.Max(timeElapsed, 25);
|
|
||||||
|
|
||||||
Strain = previousHitObject.Strain * decay + addition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double spacingWeight(float distance)
|
|
||||||
{
|
|
||||||
return Math.Pow(distance, 1.3) / 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal float DistanceTo(CatchDifficultyHitObject other)
|
|
||||||
{
|
|
||||||
return Math.Abs(ActualNormalizedPosition - other.ActualNormalizedPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,148 +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 System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
|
||||||
{
|
|
||||||
public class CatchLegacyDifficultyCalculator : LegacyDifficultyCalculator
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
|
|
||||||
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
|
|
||||||
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
|
|
||||||
/// </summary>
|
|
||||||
private const double strain_step = 750;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The weighting of each strain value decays to this number * it's previous value
|
|
||||||
/// </summary>
|
|
||||||
private const double decay_weight = 0.94;
|
|
||||||
|
|
||||||
private const double star_scaling_factor = 0.145;
|
|
||||||
|
|
||||||
public CatchLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
|
||||||
: base(ruleset, beatmap)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
|
||||||
{
|
|
||||||
if (!beatmap.HitObjects.Any())
|
|
||||||
return new CatchDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty);
|
|
||||||
float halfCatchWidth = catcher.CatchWidth * 0.5f;
|
|
||||||
|
|
||||||
var difficultyHitObjects = new List<CatchDifficultyHitObject>();
|
|
||||||
|
|
||||||
foreach (var hitObject in beatmap.HitObjects)
|
|
||||||
{
|
|
||||||
switch (hitObject)
|
|
||||||
{
|
|
||||||
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
|
|
||||||
case Fruit fruit:
|
|
||||||
difficultyHitObjects.Add(new CatchDifficultyHitObject(fruit, halfCatchWidth));
|
|
||||||
break;
|
|
||||||
case JuiceStream _:
|
|
||||||
difficultyHitObjects.AddRange(hitObject.NestedHitObjects.OfType<CatchHitObject>().Where(o => !(o is TinyDroplet)).Select(o => new CatchDifficultyHitObject(o, halfCatchWidth)));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
|
|
||||||
|
|
||||||
if (!calculateStrainValues(difficultyHitObjects, clockRate))
|
|
||||||
return new CatchDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
|
||||||
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
|
||||||
double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, clockRate)) * star_scaling_factor;
|
|
||||||
|
|
||||||
return new CatchDifficultyAttributes(mods, starRating)
|
|
||||||
{
|
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
|
||||||
MaxCombo = difficultyHitObjects.Count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool calculateStrainValues(List<CatchDifficultyHitObject> objects, double timeRate)
|
|
||||||
{
|
|
||||||
CatchDifficultyHitObject lastObject = null;
|
|
||||||
|
|
||||||
if (!objects.Any()) return false;
|
|
||||||
|
|
||||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
|
||||||
foreach (var currentObject in objects)
|
|
||||||
{
|
|
||||||
if (lastObject != null)
|
|
||||||
currentObject.CalculateStrains(lastObject, timeRate);
|
|
||||||
|
|
||||||
lastObject = currentObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateDifficulty(List<CatchDifficultyHitObject> objects, double timeRate)
|
|
||||||
{
|
|
||||||
// The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods
|
|
||||||
double actualStrainStep = strain_step * timeRate;
|
|
||||||
|
|
||||||
// Find the highest strain value within each strain step
|
|
||||||
var highestStrains = new List<double>();
|
|
||||||
double intervalEndTime = actualStrainStep;
|
|
||||||
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
|
|
||||||
|
|
||||||
CatchDifficultyHitObject previousHitObject = null;
|
|
||||||
foreach (CatchDifficultyHitObject hitObject in objects)
|
|
||||||
{
|
|
||||||
// While we are beyond the current interval push the currently available maximum to our strain list
|
|
||||||
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
|
|
||||||
{
|
|
||||||
highestStrains.Add(maximumStrain);
|
|
||||||
|
|
||||||
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
|
|
||||||
// until the beginning of the next interval.
|
|
||||||
if (previousHitObject == null)
|
|
||||||
{
|
|
||||||
maximumStrain = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double decay = Math.Pow(CatchDifficultyHitObject.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
|
||||||
maximumStrain = previousHitObject.Strain * decay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to the next time interval
|
|
||||||
intervalEndTime += actualStrainStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain maximum strain
|
|
||||||
maximumStrain = Math.Max(hitObject.Strain, maximumStrain);
|
|
||||||
|
|
||||||
previousHitObject = hitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the weighted sum over the highest strains for each interval
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
foreach (double strain in highestStrains)
|
|
||||||
{
|
|
||||||
difficulty += weight * strain;
|
|
||||||
weight *= decay_weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||||
|
{
|
||||||
|
private const float normalized_hitobject_radius = 41.0f;
|
||||||
|
|
||||||
|
public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject;
|
||||||
|
|
||||||
|
public new CatchHitObject LastObject => (CatchHitObject)base.LastObject;
|
||||||
|
|
||||||
|
public readonly float NormalizedPosition;
|
||||||
|
public readonly float LastNormalizedPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 25ms.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double StrainTime;
|
||||||
|
|
||||||
|
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
|
||||||
|
: base(hitObject, lastObject, clockRate)
|
||||||
|
{
|
||||||
|
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
|
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||||
|
|
||||||
|
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
|
||||||
|
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
|
||||||
|
|
||||||
|
// Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure
|
||||||
|
StrainTime = Math.Max(25, DeltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
Normal file
85
osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Movement : Skill
|
||||||
|
{
|
||||||
|
private const float absolute_player_positioning_error = 16f;
|
||||||
|
private const float normalized_hitobject_radius = 41.0f;
|
||||||
|
private const double direction_change_bonus = 12.5;
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 850;
|
||||||
|
protected override double StrainDecayBase => 0.2;
|
||||||
|
|
||||||
|
protected override double DecayWeight => 0.94;
|
||||||
|
|
||||||
|
private float? lastPlayerPosition;
|
||||||
|
private float lastDistanceMoved;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||||
|
|
||||||
|
if (lastPlayerPosition == null)
|
||||||
|
lastPlayerPosition = catchCurrent.LastNormalizedPosition;
|
||||||
|
|
||||||
|
float playerPosition = MathHelper.Clamp(
|
||||||
|
lastPlayerPosition.Value,
|
||||||
|
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
||||||
|
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
||||||
|
);
|
||||||
|
|
||||||
|
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||||
|
|
||||||
|
double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500;
|
||||||
|
double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
|
||||||
|
|
||||||
|
double bonus = 0;
|
||||||
|
|
||||||
|
// Direction changes give an extra point!
|
||||||
|
if (Math.Abs(distanceMoved) > 0.1)
|
||||||
|
{
|
||||||
|
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
||||||
|
{
|
||||||
|
double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error;
|
||||||
|
|
||||||
|
distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor;
|
||||||
|
|
||||||
|
// Bonus for tougher direction switches and "almost" hyperdashes at this point
|
||||||
|
if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
|
||||||
|
bonus = 0.3 * bonusFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base bonus for every movement, giving some weight to streams.
|
||||||
|
distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for "almost" hyperdashes at corner points
|
||||||
|
if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
|
||||||
|
{
|
||||||
|
if (!catchCurrent.LastObject.HyperDash)
|
||||||
|
bonus += 1.0;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// After a hyperdash we ARE in the correct position. Always!
|
||||||
|
playerPosition = catchCurrent.NormalizedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPlayerPosition = playerPosition;
|
||||||
|
lastDistanceMoved = distanceMoved;
|
||||||
|
|
||||||
|
return distanceAddition / catchCurrent.StrainTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
public class CatcherArea : Container
|
public class CatcherArea : Container
|
||||||
{
|
{
|
||||||
public const float CATCHER_SIZE = 100;
|
public const float CATCHER_SIZE = 106.75f;
|
||||||
|
|
||||||
protected internal readonly Catcher MovableCatcher;
|
protected internal readonly Catcher MovableCatcher;
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||||
|
|
||||||
[TestCase(2.2676066895468976, "diffcalc-test")]
|
[TestCase(2.3683365342338796d, "diffcalc-test")]
|
||||||
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 ManiaLegacyDifficultyCalculator(new ManiaRuleset(), beatmap);
|
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
|
||||||
|
|
||||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,11 @@
|
|||||||
// 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.Mania.Difficulty
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
{
|
{
|
||||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||||
{
|
{
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
|
|
||||||
public ManiaDifficultyAttributes(Mod[] mods, double starRating)
|
|
||||||
: base(mods, starRating)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
125
osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
Normal file
125
osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// 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.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
|
{
|
||||||
|
public class ManiaDifficultyCalculator : DifficultyCalculator
|
||||||
|
{
|
||||||
|
private const double star_scaling_factor = 0.018;
|
||||||
|
|
||||||
|
private readonly bool isForCurrentRuleset;
|
||||||
|
|
||||||
|
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
|
: base(ruleset, beatmap)
|
||||||
|
{
|
||||||
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
if (beatmap.HitObjects.Count == 0)
|
||||||
|
return new ManiaDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
|
return new ManiaDifficultyAttributes
|
||||||
|
{
|
||||||
|
StarRating = difficultyValue(skills) * star_scaling_factor,
|
||||||
|
Mods = mods,
|
||||||
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
|
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private double difficultyValue(Skill[] skills)
|
||||||
|
{
|
||||||
|
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
|
||||||
|
var overall = skills.OfType<Overall>().Single();
|
||||||
|
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
|
||||||
|
|
||||||
|
foreach (var individual in skills.OfType<Individual>())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < individual.StrainPeaks.Count; i++)
|
||||||
|
{
|
||||||
|
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
|
||||||
|
|
||||||
|
if (aggregate > aggregatePeaks[i])
|
||||||
|
aggregatePeaks[i] = aggregate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregatePeaks.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 aggregatePeaks)
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
|
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||||
|
|
||||||
|
var skills = new List<Skill> { new Overall(columnCount) };
|
||||||
|
|
||||||
|
for (int i = 0; i < columnCount; i++)
|
||||||
|
skills.Add(new Individual(i, columnCount));
|
||||||
|
|
||||||
|
return skills.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Mod[] DifficultyAdjustmentMods
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var mods = new Mod[]
|
||||||
|
{
|
||||||
|
new ManiaModDoubleTime(),
|
||||||
|
new ManiaModHalfTime(),
|
||||||
|
new ManiaModEasy(),
|
||||||
|
new ManiaModHardRock(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isForCurrentRuleset)
|
||||||
|
return mods;
|
||||||
|
|
||||||
|
// if we are a convert, we can be played in any key mod.
|
||||||
|
return mods.Concat(new Mod[]
|
||||||
|
{
|
||||||
|
new ManiaModKey1(),
|
||||||
|
new ManiaModKey2(),
|
||||||
|
new ManiaModKey3(),
|
||||||
|
new ManiaModKey4(),
|
||||||
|
new ManiaModKey5(),
|
||||||
|
new ManiaModKey6(),
|
||||||
|
new ManiaModKey7(),
|
||||||
|
new ManiaModKey8(),
|
||||||
|
new ManiaModKey9(),
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,173 +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 System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Mania.Mods;
|
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
|
||||||
{
|
|
||||||
internal class ManiaLegacyDifficultyCalculator : LegacyDifficultyCalculator
|
|
||||||
{
|
|
||||||
private const double star_scaling_factor = 0.018;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size strain_step.
|
|
||||||
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
|
|
||||||
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
|
|
||||||
/// </summary>
|
|
||||||
private const double strain_step = 400;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The weighting of each strain value decays to this number * it's previous value
|
|
||||||
/// </summary>
|
|
||||||
private const double decay_weight = 0.9;
|
|
||||||
|
|
||||||
private readonly bool isForCurrentRuleset;
|
|
||||||
|
|
||||||
public ManiaLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
|
||||||
: base(ruleset, beatmap)
|
|
||||||
{
|
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
|
||||||
{
|
|
||||||
if (!beatmap.HitObjects.Any())
|
|
||||||
return new ManiaDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
var difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
|
|
||||||
|
|
||||||
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
|
|
||||||
|
|
||||||
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
|
|
||||||
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
|
|
||||||
difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
|
|
||||||
|
|
||||||
if (!calculateStrainValues(difficultyHitObjects, clockRate))
|
|
||||||
return new ManiaDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
double starRating = calculateDifficulty(difficultyHitObjects, clockRate) * star_scaling_factor;
|
|
||||||
|
|
||||||
return new ManiaDifficultyAttributes(mods, starRating)
|
|
||||||
{
|
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
|
|
||||||
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool calculateStrainValues(List<ManiaHitObjectDifficulty> objects, double timeRate)
|
|
||||||
{
|
|
||||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
|
||||||
using (var hitObjectsEnumerator = objects.GetEnumerator())
|
|
||||||
{
|
|
||||||
if (!hitObjectsEnumerator.MoveNext())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ManiaHitObjectDifficulty current = hitObjectsEnumerator.Current;
|
|
||||||
|
|
||||||
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
|
|
||||||
while (hitObjectsEnumerator.MoveNext())
|
|
||||||
{
|
|
||||||
var next = hitObjectsEnumerator.Current;
|
|
||||||
next?.CalculateStrains(current, timeRate);
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateDifficulty(List<ManiaHitObjectDifficulty> objects, double timeRate)
|
|
||||||
{
|
|
||||||
double actualStrainStep = strain_step * timeRate;
|
|
||||||
|
|
||||||
// Find the highest strain value within each strain step
|
|
||||||
List<double> highestStrains = new List<double>();
|
|
||||||
double intervalEndTime = actualStrainStep;
|
|
||||||
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
|
|
||||||
|
|
||||||
ManiaHitObjectDifficulty previousHitObject = null;
|
|
||||||
foreach (var hitObject in objects)
|
|
||||||
{
|
|
||||||
// While we are beyond the current interval push the currently available maximum to our strain list
|
|
||||||
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
|
|
||||||
{
|
|
||||||
highestStrains.Add(maximumStrain);
|
|
||||||
|
|
||||||
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
|
|
||||||
// until the beginning of the next interval.
|
|
||||||
if (previousHitObject == null)
|
|
||||||
{
|
|
||||||
maximumStrain = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double individualDecay = Math.Pow(ManiaHitObjectDifficulty.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
|
||||||
double overallDecay = Math.Pow(ManiaHitObjectDifficulty.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
|
||||||
maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to the next time interval
|
|
||||||
intervalEndTime += actualStrainStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain maximum strain
|
|
||||||
double strain = hitObject.IndividualStrain + hitObject.OverallStrain;
|
|
||||||
maximumStrain = Math.Max(strain, maximumStrain);
|
|
||||||
|
|
||||||
previousHitObject = hitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the weighted sum over the highest strains for each interval
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
foreach (double strain in highestStrains)
|
|
||||||
{
|
|
||||||
difficulty += weight * strain;
|
|
||||||
weight *= decay_weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var mods = new Mod[]
|
|
||||||
{
|
|
||||||
new ManiaModDoubleTime(),
|
|
||||||
new ManiaModHalfTime(),
|
|
||||||
new ManiaModEasy(),
|
|
||||||
new ManiaModHardRock(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isForCurrentRuleset)
|
|
||||||
return mods;
|
|
||||||
|
|
||||||
// if we are a convert, we can be played in any key mod.
|
|
||||||
return mods.Concat(new Mod[]
|
|
||||||
{
|
|
||||||
new ManiaModKey1(),
|
|
||||||
new ManiaModKey2(),
|
|
||||||
new ManiaModKey3(),
|
|
||||||
new ManiaModKey4(),
|
|
||||||
new ManiaModKey5(),
|
|
||||||
new ManiaModKey6(),
|
|
||||||
new ManiaModKey7(),
|
|
||||||
new ManiaModKey8(),
|
|
||||||
new ManiaModKey9(),
|
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,19 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
public class ManiaDifficultyHitObject : DifficultyHitObject
|
||||||
|
{
|
||||||
|
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||||
|
|
||||||
|
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
||||||
|
: base(hitObject, lastObject, clockRate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
Normal file
47
osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Individual : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.125;
|
||||||
|
|
||||||
|
private readonly double[] holdEndTimes;
|
||||||
|
|
||||||
|
private readonly int column;
|
||||||
|
|
||||||
|
public Individual(int column, int columnCount)
|
||||||
|
{
|
||||||
|
this.column = column;
|
||||||
|
|
||||||
|
holdEndTimes = new double[columnCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (maniaCurrent.BaseObject.Column != column)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// We give a slight bonus if something is held meanwhile
|
||||||
|
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
Normal file
56
osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Overall : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.3;
|
||||||
|
|
||||||
|
private readonly double[] holdEndTimes;
|
||||||
|
|
||||||
|
private readonly int columnCount;
|
||||||
|
|
||||||
|
public Overall(int columnCount)
|
||||||
|
{
|
||||||
|
this.columnCount = columnCount;
|
||||||
|
|
||||||
|
holdEndTimes = new double[columnCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
|
||||||
|
|
||||||
|
double holdFactor = 1.0; // Factor in case something else is held
|
||||||
|
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||||
|
|
||||||
|
for (int i = 0; i < columnCount; i++)
|
||||||
|
{
|
||||||
|
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
||||||
|
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
|
||||||
|
holdAddition = 1.0;
|
||||||
|
|
||||||
|
// ... this addition only is valid if there is _no_ other note with the same ending.
|
||||||
|
// Releasing multiple notes at the same time is just as easy as releasing one
|
||||||
|
if (endTime == holdEndTimes[i])
|
||||||
|
holdAddition = 0;
|
||||||
|
|
||||||
|
// We give a slight bonus if something is held meanwhile
|
||||||
|
if (holdEndTimes[i] > endTime)
|
||||||
|
holdFactor = 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
||||||
|
|
||||||
|
return (1 + holdAddition) * holdFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
|
||||||
|
|
||||||
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(this, beatmap);
|
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override int? LegacyID => 3;
|
public override int? LegacyID => 3;
|
||||||
|
|
||||||
|
@ -1,112 +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 osu.Game.Rulesets.Objects.Types;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects
|
|
||||||
{
|
|
||||||
internal class ManiaHitObjectDifficulty
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Factor by how much individual / overall strain decays per second.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// These values are results of tweaking a lot and taking into account general feedback.
|
|
||||||
/// </remarks>
|
|
||||||
internal const double INDIVIDUAL_DECAY_BASE = 0.125;
|
|
||||||
internal const double OVERALL_DECAY_BASE = 0.30;
|
|
||||||
|
|
||||||
internal ManiaHitObject BaseHitObject;
|
|
||||||
|
|
||||||
private readonly int beatmapColumnCount;
|
|
||||||
|
|
||||||
private readonly double endTime;
|
|
||||||
private readonly double[] heldUntil;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures jacks or more generally: repeated presses of the same button
|
|
||||||
/// </summary>
|
|
||||||
private readonly double[] individualStrains;
|
|
||||||
|
|
||||||
internal double IndividualStrain
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return individualStrains[BaseHitObject.Column];
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
individualStrains[BaseHitObject.Column] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures note density in a way
|
|
||||||
/// </summary>
|
|
||||||
internal double OverallStrain = 1;
|
|
||||||
|
|
||||||
public ManiaHitObjectDifficulty(ManiaHitObject baseHitObject, int columnCount)
|
|
||||||
{
|
|
||||||
BaseHitObject = baseHitObject;
|
|
||||||
|
|
||||||
endTime = (baseHitObject as IHasEndTime)?.EndTime ?? baseHitObject.StartTime;
|
|
||||||
|
|
||||||
beatmapColumnCount = columnCount;
|
|
||||||
heldUntil = new double[beatmapColumnCount];
|
|
||||||
individualStrains = new double[beatmapColumnCount];
|
|
||||||
|
|
||||||
for (int i = 0; i < beatmapColumnCount; ++i)
|
|
||||||
{
|
|
||||||
individualStrains[i] = 0;
|
|
||||||
heldUntil[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate)
|
|
||||||
{
|
|
||||||
// TODO: Factor in holds
|
|
||||||
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
|
||||||
double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000);
|
|
||||||
double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000);
|
|
||||||
|
|
||||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
|
||||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
|
||||||
|
|
||||||
// Fill up the heldUntil array
|
|
||||||
for (int i = 0; i < beatmapColumnCount; ++i)
|
|
||||||
{
|
|
||||||
heldUntil[i] = previousHitObject.heldUntil[i];
|
|
||||||
|
|
||||||
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
|
||||||
if (BaseHitObject.StartTime < heldUntil[i] && endTime > heldUntil[i])
|
|
||||||
{
|
|
||||||
holdAddition = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
|
|
||||||
if (endTime == heldUntil[i])
|
|
||||||
{
|
|
||||||
holdAddition = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We give a slight bonus to everything if something is held meanwhile
|
|
||||||
if (heldUntil[i] > endTime)
|
|
||||||
{
|
|
||||||
holdFactor = 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decay individual strains
|
|
||||||
individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay;
|
|
||||||
}
|
|
||||||
|
|
||||||
heldUntil[BaseHitObject.Column] = endTime;
|
|
||||||
|
|
||||||
// Increase individual strain in own column
|
|
||||||
IndividualStrain += 2.0 * holdFactor;
|
|
||||||
|
|
||||||
OverallStrain = previousHitObject.OverallStrain * overallDecay + (1.0 + holdAddition) * holdFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.9811336589467095, "diffcalc-test")]
|
[TestCase(2.9811338051242915d, "diffcalc-test")]
|
||||||
[TestCase(2.9811336589467095, "diffcalc-test-strong")]
|
[TestCase(2.9811338051242915d, "diffcalc-test-strong")]
|
||||||
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 TaikoLegacyDifficultyCalculator(new TaikoRuleset(), beatmap);
|
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
|
||||||
|
|
||||||
protected override Ruleset CreateRuleset() => new TaikoRuleset();
|
protected override Ruleset CreateRuleset() => new TaikoRuleset();
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
Ruleset = new TaikoRuleset().RulesetInfo
|
Ruleset = new TaikoRuleset().RulesetInfo
|
||||||
},
|
},
|
||||||
ControlPointInfo = controlPointInfo
|
ControlPointInfo = controlPointInfo
|
||||||
});
|
}, Clock);
|
||||||
|
|
||||||
Add(playfieldContainer = new Container
|
Add(playfieldContainer = new Container
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||||
|
{
|
||||||
|
public readonly bool HasTypeChange;
|
||||||
|
|
||||||
|
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
||||||
|
: base(hitObject, lastObject, clockRate)
|
||||||
|
{
|
||||||
|
HasTypeChange = lastObject is RimHit != hitObject is RimHit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
Normal file
95
osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Strain : Skill
|
||||||
|
{
|
||||||
|
private const double rhythm_change_base_threshold = 0.2;
|
||||||
|
private const double rhythm_change_base = 2.0;
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.3;
|
||||||
|
|
||||||
|
private ColourSwitch lastColourSwitch = ColourSwitch.None;
|
||||||
|
|
||||||
|
private int sameColourCount = 1;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
double addition = 1;
|
||||||
|
|
||||||
|
// We get an extra addition if we are not a slider or spinner
|
||||||
|
if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)
|
||||||
|
{
|
||||||
|
if (hasColourChange(current))
|
||||||
|
addition += 0.75;
|
||||||
|
|
||||||
|
if (hasRhythmChange(current))
|
||||||
|
addition += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastColourSwitch = ColourSwitch.None;
|
||||||
|
sameColourCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double additionFactor = 1;
|
||||||
|
|
||||||
|
// Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50
|
||||||
|
if (current.DeltaTime < 50)
|
||||||
|
additionFactor = 0.4 + 0.6 * current.DeltaTime / 50;
|
||||||
|
|
||||||
|
return additionFactor * addition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool hasRhythmChange(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// We don't want a division by zero if some random mapper decides to put two HitObjects at the same time.
|
||||||
|
if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime);
|
||||||
|
|
||||||
|
if (timeElapsedRatio >= 8)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
||||||
|
|
||||||
|
return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool hasColourChange(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
if (!taikoCurrent.HasTypeChange)
|
||||||
|
{
|
||||||
|
sameColourCount++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldColourSwitch = lastColourSwitch;
|
||||||
|
var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd;
|
||||||
|
|
||||||
|
lastColourSwitch = newColourSwitch;
|
||||||
|
sameColourCount = 1;
|
||||||
|
|
||||||
|
// We only want a bonus if the parity of the color switch changes
|
||||||
|
return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ColourSwitch
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Even,
|
||||||
|
Odd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
@ -10,10 +9,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
public int MaxCombo;
|
public int MaxCombo;
|
||||||
|
|
||||||
public TaikoDifficultyAttributes(Mod[] mods, double starRating)
|
|
||||||
: base(mods, starRating)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
// 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.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
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.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
|
{
|
||||||
|
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||||
|
{
|
||||||
|
private const double star_scaling_factor = 0.04125;
|
||||||
|
|
||||||
|
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
|
: base(ruleset, beatmap)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
if (beatmap.HitObjects.Count == 0)
|
||||||
|
return new TaikoDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
|
return new TaikoDifficultyAttributes
|
||||||
|
{
|
||||||
|
StarRating = skills.Single().DifficultyValue() * star_scaling_factor,
|
||||||
|
Mods = mods,
|
||||||
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
|
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
|
||||||
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
|
yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() };
|
||||||
|
|
||||||
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
|
{
|
||||||
|
new TaikoModDoubleTime(),
|
||||||
|
new TaikoModHalfTime(),
|
||||||
|
new TaikoModEasy(),
|
||||||
|
new TaikoModHardRock(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,144 +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 System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
|
||||||
{
|
|
||||||
internal class TaikoLegacyDifficultyCalculator : LegacyDifficultyCalculator
|
|
||||||
{
|
|
||||||
private const double star_scaling_factor = 0.04125;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
|
|
||||||
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
|
|
||||||
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
|
|
||||||
/// </summary>
|
|
||||||
private const double strain_step = 400;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The weighting of each strain value decays to this number * it's previous value
|
|
||||||
/// </summary>
|
|
||||||
private const double decay_weight = 0.9;
|
|
||||||
|
|
||||||
public TaikoLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
|
||||||
: base(ruleset, beatmap)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
|
||||||
{
|
|
||||||
if (!beatmap.HitObjects.Any())
|
|
||||||
return new TaikoDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
var difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
|
|
||||||
|
|
||||||
foreach (var hitObject in beatmap.HitObjects)
|
|
||||||
difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject));
|
|
||||||
|
|
||||||
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
|
|
||||||
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
|
|
||||||
|
|
||||||
if (!calculateStrainValues(difficultyHitObjects, clockRate))
|
|
||||||
return new TaikoDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
double starRating = calculateDifficulty(difficultyHitObjects, clockRate) * star_scaling_factor;
|
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes(mods, starRating)
|
|
||||||
{
|
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
|
|
||||||
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
|
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool calculateStrainValues(List<TaikoHitObjectDifficulty> objects, double timeRate)
|
|
||||||
{
|
|
||||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
|
||||||
using (var hitObjectsEnumerator = objects.GetEnumerator())
|
|
||||||
{
|
|
||||||
if (!hitObjectsEnumerator.MoveNext()) return false;
|
|
||||||
|
|
||||||
TaikoHitObjectDifficulty current = hitObjectsEnumerator.Current;
|
|
||||||
|
|
||||||
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
|
|
||||||
while (hitObjectsEnumerator.MoveNext())
|
|
||||||
{
|
|
||||||
var next = hitObjectsEnumerator.Current;
|
|
||||||
next?.CalculateStrains(current, timeRate);
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateDifficulty(List<TaikoHitObjectDifficulty> objects, double timeRate)
|
|
||||||
{
|
|
||||||
double actualStrainStep = strain_step * timeRate;
|
|
||||||
|
|
||||||
// Find the highest strain value within each strain step
|
|
||||||
List<double> highestStrains = new List<double>();
|
|
||||||
double intervalEndTime = actualStrainStep;
|
|
||||||
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
|
|
||||||
|
|
||||||
TaikoHitObjectDifficulty previousHitObject = null;
|
|
||||||
foreach (var hitObject in objects)
|
|
||||||
{
|
|
||||||
// While we are beyond the current interval push the currently available maximum to our strain list
|
|
||||||
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
|
|
||||||
{
|
|
||||||
highestStrains.Add(maximumStrain);
|
|
||||||
|
|
||||||
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
|
|
||||||
// until the beginning of the next interval.
|
|
||||||
if (previousHitObject == null)
|
|
||||||
{
|
|
||||||
maximumStrain = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double decay = Math.Pow(TaikoHitObjectDifficulty.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
|
||||||
maximumStrain = previousHitObject.Strain * decay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to the next time interval
|
|
||||||
intervalEndTime += actualStrainStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain maximum strain
|
|
||||||
maximumStrain = Math.Max(hitObject.Strain, maximumStrain);
|
|
||||||
|
|
||||||
previousHitObject = hitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the weighted sum over the highest strains for each interval
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
foreach (double strain in highestStrains)
|
|
||||||
{
|
|
||||||
difficulty += weight * strain;
|
|
||||||
weight *= decay_weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
|
||||||
{
|
|
||||||
new TaikoModDoubleTime(),
|
|
||||||
new TaikoModHalfTime(),
|
|
||||||
new TaikoModEasy(),
|
|
||||||
new TaikoModHardRock(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,127 +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;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects
|
|
||||||
{
|
|
||||||
internal class TaikoHitObjectDifficulty
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Factor by how much individual / overall strain decays per second.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// These values are results of tweaking a lot and taking into account general feedback.
|
|
||||||
/// </remarks>
|
|
||||||
internal const double DECAY_BASE = 0.30;
|
|
||||||
|
|
||||||
private const double type_change_bonus = 0.75;
|
|
||||||
private const double rhythm_change_bonus = 1.0;
|
|
||||||
private const double rhythm_change_base_threshold = 0.2;
|
|
||||||
private const double rhythm_change_base = 2.0;
|
|
||||||
|
|
||||||
internal TaikoHitObject BaseHitObject;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures note density in a way
|
|
||||||
/// </summary>
|
|
||||||
internal double Strain = 1;
|
|
||||||
|
|
||||||
private double timeElapsed;
|
|
||||||
private int sameTypeSince = 1;
|
|
||||||
|
|
||||||
private bool isRim => BaseHitObject is RimHit;
|
|
||||||
|
|
||||||
public TaikoHitObjectDifficulty(TaikoHitObject baseHitObject)
|
|
||||||
{
|
|
||||||
BaseHitObject = baseHitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CalculateStrains(TaikoHitObjectDifficulty previousHitObject, double timeRate)
|
|
||||||
{
|
|
||||||
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
|
|
||||||
// See Taiko feedback thread.
|
|
||||||
timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
|
||||||
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
|
|
||||||
|
|
||||||
double addition = 1;
|
|
||||||
|
|
||||||
// Only if we are no slider or spinner we get an extra addition
|
|
||||||
if (previousHitObject.BaseHitObject is Hit && BaseHitObject is Hit
|
|
||||||
&& BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime < 1000) // And we only want to check out hitobjects which aren't so far in the past
|
|
||||||
{
|
|
||||||
addition += typeChangeAddition(previousHitObject);
|
|
||||||
addition += rhythmChangeAddition(previousHitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
double additionFactor = 1.0;
|
|
||||||
// Scale AdditionFactor linearly from 0.4 to 1 for TimeElapsed from 0 to 50
|
|
||||||
if (timeElapsed < 50.0)
|
|
||||||
additionFactor = 0.4 + 0.6 * timeElapsed / 50.0;
|
|
||||||
|
|
||||||
Strain = previousHitObject.Strain * decay + addition * additionFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeSwitch lastTypeSwitchEven = TypeSwitch.None;
|
|
||||||
private double typeChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
|
||||||
{
|
|
||||||
// If we don't have the same hit type, trigger a type change!
|
|
||||||
if (previousHitObject.isRim != isRim)
|
|
||||||
{
|
|
||||||
lastTypeSwitchEven = previousHitObject.sameTypeSince % 2 == 0 ? TypeSwitch.Even : TypeSwitch.Odd;
|
|
||||||
|
|
||||||
// We only want a bonus if the parity of the type switch changes!
|
|
||||||
switch (previousHitObject.lastTypeSwitchEven)
|
|
||||||
{
|
|
||||||
case TypeSwitch.Even:
|
|
||||||
if (lastTypeSwitchEven == TypeSwitch.Odd)
|
|
||||||
return type_change_bonus;
|
|
||||||
break;
|
|
||||||
case TypeSwitch.Odd:
|
|
||||||
if (lastTypeSwitchEven == TypeSwitch.Even)
|
|
||||||
return type_change_bonus;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No type change? Increment counter and keep track of last type switch
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lastTypeSwitchEven = previousHitObject.lastTypeSwitchEven;
|
|
||||||
sameTypeSince = previousHitObject.sameTypeSince + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double rhythmChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
|
||||||
{
|
|
||||||
// We don't want a division by zero if some random mapper decides to put 2 HitObjects at the same time.
|
|
||||||
if (timeElapsed == 0 || previousHitObject.timeElapsed == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double timeElapsedRatio = Math.Max(previousHitObject.timeElapsed / timeElapsed, timeElapsed / previousHitObject.timeElapsed);
|
|
||||||
|
|
||||||
if (timeElapsedRatio >= 8)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
|
||||||
|
|
||||||
if (isWithinChangeThreshold(difference))
|
|
||||||
return rhythm_change_bonus;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool isWithinChangeThreshold(double value)
|
|
||||||
{
|
|
||||||
return value > rhythm_change_base_threshold && value < 1 - rhythm_change_base_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum TypeSwitch
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Even,
|
|
||||||
Odd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
|
||||||
|
|
||||||
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoLegacyDifficultyCalculator(this, beatmap);
|
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);
|
||||||
|
|
||||||
|
@ -10,4 +10,4 @@
|
|||||||
<ItemGroup Label="Project References">
|
<ItemGroup Label="Project References">
|
||||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, Clock);
|
||||||
Child = new ComposeScreen();
|
Child = new ComposeScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Beatmap.Value = new TestWorkingBeatmap(testBeatmap);
|
Beatmap.Value = new TestWorkingBeatmap(testBeatmap, Clock);
|
||||||
|
|
||||||
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
|
||||||
|
|
||||||
Add(new SummaryTimeline
|
Add(new SummaryTimeline
|
||||||
{
|
{
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
Size = new Vector2(200,100)
|
Size = new Vector2(200,100)
|
||||||
};
|
};
|
||||||
|
|
||||||
Beatmap.Value = new TestWorkingBeatmap(new Beatmap());
|
Beatmap.Value = new TestWorkingBeatmap(new Beatmap(), Clock);
|
||||||
|
|
||||||
Child = playback;
|
Child = playback;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> FilterTerms => new[] { Text };
|
public virtual IEnumerable<string> FilterTerms => new[] { Text };
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
new SettingsButton
|
new SettingsButton
|
||||||
{
|
{
|
||||||
Text = "Key configuration",
|
Text = "Key configuration",
|
||||||
|
TooltipText = "Change global shortcut keys and gameplay bindings",
|
||||||
Action = keyConfig.ToggleVisibility
|
Action = keyConfig.ToggleVisibility
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,33 @@
|
|||||||
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
namespace osu.Game.Overlays.Settings
|
||||||
{
|
{
|
||||||
public class SettingsButton : TriangleButton
|
public class SettingsButton : TriangleButton, IHasTooltip
|
||||||
{
|
{
|
||||||
public SettingsButton()
|
public SettingsButton()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS };
|
Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string TooltipText { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<string> FilterTerms
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (TooltipText != null)
|
||||||
|
return base.FilterTerms.Append(TooltipText);
|
||||||
|
|
||||||
|
return base.FilterTerms;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,14 +25,12 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
{
|
{
|
||||||
var attributes = CreateDifficultyAttributes();
|
var skills = CreateSkills(beatmap);
|
||||||
attributes.Mods = mods;
|
|
||||||
|
|
||||||
if (!beatmap.HitObjects.Any())
|
if (!beatmap.HitObjects.Any())
|
||||||
return attributes;
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||||
|
|
||||||
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
|
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
|
||||||
var skills = CreateSkills();
|
|
||||||
|
|
||||||
double sectionLength = SectionLength * clockRate;
|
double sectionLength = SectionLength * clockRate;
|
||||||
|
|
||||||
@ -60,9 +58,7 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
foreach (Skill s in skills)
|
foreach (Skill s in skills)
|
||||||
s.SaveCurrentPeak();
|
s.SaveCurrentPeak();
|
||||||
|
|
||||||
PopulateAttributes(attributes, beatmap, skills, clockRate);
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -108,13 +104,13 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populates <see cref="DifficultyAttributes"/> after difficulty has been processed.
|
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="attributes">The <see cref="DifficultyAttributes"/> to populate with information about the difficulty of <paramref name="beatmap"/>.</param>
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.</param>
|
||||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was processed.</param>
|
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
|
||||||
/// <param name="skills">The skills which processed the difficulty.</param>
|
/// <param name="skills">The skills which processed the beatmap.</param>
|
||||||
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
||||||
protected abstract void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double clockRate);
|
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
|
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
|
||||||
@ -125,15 +121,10 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
|
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the <see cref="Skill"/>s to calculate the difficulty of <see cref="DifficultyHitObject"/>s.
|
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param
|
||||||
/// <returns>The <see cref="Skill"/>s.</returns>
|
/// <returns>The <see cref="Skill"/>s.</returns>
|
||||||
protected abstract Skill[] CreateSkills();
|
protected abstract Skill[] CreateSkills(IBeatmap beatmap);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an empty <see cref="DifficultyAttributes"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The empty <see cref="DifficultyAttributes"/>.</returns>
|
|
||||||
protected abstract DifficultyAttributes CreateDifficultyAttributes();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Skill
|
public abstract class Skill
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The peak strain for each <see cref="DifficultyCalculator.SectionLength"/> section of the beatmap.
|
||||||
|
/// </summary>
|
||||||
|
public IList<double> StrainPeaks => strainPeaks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
|
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -37,6 +42,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
|
|
||||||
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
|
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 double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
||||||
|
|
||||||
private readonly List<double> strainPeaks = new List<double>();
|
private readonly List<double> strainPeaks = new List<double>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,29 +1,133 @@
|
|||||||
// 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 osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Beatmaps
|
namespace osu.Game.Tests.Beatmaps
|
||||||
{
|
{
|
||||||
public class TestWorkingBeatmap : WorkingBeatmap
|
public class TestWorkingBeatmap : WorkingBeatmap
|
||||||
{
|
{
|
||||||
public TestWorkingBeatmap(RulesetInfo ruleset)
|
private readonly TrackVirtualManual track;
|
||||||
: this(new TestBeatmap(ruleset))
|
private readonly IBeatmap beatmap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an instance which creates a <see cref="TestBeatmap"/> for the provided ruleset when requested.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ruleset">The target ruleset.</param>
|
||||||
|
/// <param name="referenceClock">A clock which should be used instead of a stopwatch for virtual time progression.</param>
|
||||||
|
public TestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock)
|
||||||
|
: this(new TestBeatmap(ruleset), referenceClock)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestWorkingBeatmap(IBeatmap beatmap)
|
/// <summary>
|
||||||
|
/// Create an instance which provides the <see cref="IBeatmap"/> when requested.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmap">The beatmap</param>
|
||||||
|
/// <param name="referenceClock">An optional clock which should be used instead of a stopwatch for virtual time progression.</param>
|
||||||
|
public TestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock = null)
|
||||||
: base(beatmap.BeatmapInfo)
|
: base(beatmap.BeatmapInfo)
|
||||||
{
|
{
|
||||||
this.beatmap = beatmap;
|
this.beatmap = beatmap;
|
||||||
|
|
||||||
|
if (referenceClock != null)
|
||||||
|
track = new TrackVirtualManual(referenceClock);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IBeatmap beatmap;
|
|
||||||
protected override IBeatmap GetBeatmap() => beatmap;
|
protected override IBeatmap GetBeatmap() => beatmap;
|
||||||
protected override Texture GetBackground() => null;
|
protected override Texture GetBackground() => null;
|
||||||
protected override Track GetTrack() => null;
|
protected override Track GetTrack() => track;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A virtual track which tracks a reference clock.
|
||||||
|
/// </summary>
|
||||||
|
public class TrackVirtualManual : Track
|
||||||
|
{
|
||||||
|
private readonly IFrameBasedClock referenceClock;
|
||||||
|
|
||||||
|
private readonly ManualClock clock = new ManualClock();
|
||||||
|
|
||||||
|
private bool running;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local offset added to the reference clock to resolve correct time.
|
||||||
|
/// </summary>
|
||||||
|
private double offset;
|
||||||
|
|
||||||
|
public TrackVirtualManual(IFrameBasedClock referenceClock)
|
||||||
|
{
|
||||||
|
this.referenceClock = referenceClock;
|
||||||
|
Length = double.PositiveInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Seek(double seek)
|
||||||
|
{
|
||||||
|
offset = MathHelper.Clamp(seek, 0, Length);
|
||||||
|
lastReferenceTime = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Start()
|
||||||
|
{
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
Seek(0);
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Stop()
|
||||||
|
{
|
||||||
|
if (running)
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
// on stopping, the current value should be transferred out of the clock, as we can no longer rely on
|
||||||
|
// the referenceClock (which will still be counting time).
|
||||||
|
offset = clock.CurrentTime;
|
||||||
|
lastReferenceTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsRunning => running;
|
||||||
|
|
||||||
|
private double? lastReferenceTime;
|
||||||
|
|
||||||
|
public override double CurrentTime => clock.CurrentTime;
|
||||||
|
|
||||||
|
protected override void UpdateState()
|
||||||
|
{
|
||||||
|
base.UpdateState();
|
||||||
|
|
||||||
|
if (running)
|
||||||
|
{
|
||||||
|
double refTime = referenceClock.CurrentTime;
|
||||||
|
|
||||||
|
if (!lastReferenceTime.HasValue)
|
||||||
|
{
|
||||||
|
// if the clock just started running, the current value should be transferred to the offset
|
||||||
|
// (to zero the progression of time).
|
||||||
|
offset -= refTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReferenceTime = refTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length);
|
||||||
|
|
||||||
|
if (CurrentTime >= Length)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
RaiseCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo);
|
Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo, null);
|
||||||
|
|
||||||
LoadComponentAsync(new Editor(), LoadScreen);
|
LoadComponentAsync(new Editor(), LoadScreen);
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
private Player loadPlayerFor(Ruleset r)
|
private Player loadPlayerFor(Ruleset r)
|
||||||
{
|
{
|
||||||
var beatmap = CreateBeatmap(r);
|
var beatmap = CreateBeatmap(r);
|
||||||
var working = new TestWorkingBeatmap(beatmap);
|
var working = new TestWorkingBeatmap(beatmap, Clock);
|
||||||
|
|
||||||
workingWeakReferences.Add(working);
|
workingWeakReferences.Add(working);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user