mirror of
https://github.com/ppy/osu.git
synced 2025-02-16 02:23:10 +08:00
Merge branch 'master' into fix-test-storage-weirdness
This commit is contained in:
commit
d03bfec807
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
|||||||
{
|
{
|
||||||
base.InitialiseDefaults();
|
base.InitialiseDefaults();
|
||||||
|
|
||||||
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1);
|
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
|
||||||
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
new SettingsSlider<double, TimeSlider>
|
new SettingsSlider<double, TimeSlider>
|
||||||
{
|
{
|
||||||
LabelText = "Scroll speed",
|
LabelText = "Scroll speed",
|
||||||
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime)
|
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||||
|
KeyboardStep = 5
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.9811338051242915d, "diffcalc-test")]
|
[TestCase(2.2867022617692685d, "diffcalc-test")]
|
||||||
[TestCase(2.9811338051242915d, "diffcalc-test-strong")]
|
[TestCase(2.2867022617692685d, "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);
|
||||||
|
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Detects special hit object patterns which are easier to hit using special techniques
|
||||||
|
/// than normally assumed in the fully-alternating play style.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This component detects two basic types of patterns, leveraged by the following techniques:
|
||||||
|
/// <list>
|
||||||
|
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
|
||||||
|
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public class StaminaCheeseDetector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
|
||||||
|
/// </summary>
|
||||||
|
private const int roll_min_repetitions = 12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
|
||||||
|
/// </summary>
|
||||||
|
private const int tl_min_repetitions = 16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<TaikoDifficultyHitObject> hitObjects;
|
||||||
|
|
||||||
|
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
this.hitObjects = hitObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
|
||||||
|
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
|
||||||
|
/// </summary>
|
||||||
|
public void FindCheese()
|
||||||
|
{
|
||||||
|
findRolls(3);
|
||||||
|
findRolls(4);
|
||||||
|
|
||||||
|
findTlTap(0, HitType.Rim);
|
||||||
|
findTlTap(1, HitType.Rim);
|
||||||
|
findTlTap(0, HitType.Centre);
|
||||||
|
findTlTap(1, HitType.Centre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all sequences hittable using a roll.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
|
||||||
|
private void findRolls(int patternLength)
|
||||||
|
{
|
||||||
|
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
|
||||||
|
|
||||||
|
// for convenience, we're tracking the index of the item *before* our suspected repeat's start,
|
||||||
|
// as that index can be simply subtracted from the current index to get the number of elements in between
|
||||||
|
// without off-by-one errors
|
||||||
|
int indexBeforeLastRepeat = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
history.Enqueue(hitObjects[i]);
|
||||||
|
if (!history.Full)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!containsPatternRepeat(history, patternLength))
|
||||||
|
{
|
||||||
|
// we're setting this up for the next iteration, hence the +1.
|
||||||
|
// right here this index will point at the queue's front (oldest item),
|
||||||
|
// but that item is about to be popped next loop with an enqueue.
|
||||||
|
indexBeforeLastRepeat = i - history.Count + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int repeatedLength = i - indexBeforeLastRepeat;
|
||||||
|
if (repeatedLength < roll_min_repetitions)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
markObjectsAsCheese(i, repeatedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < patternLength; j++)
|
||||||
|
{
|
||||||
|
if (history[j].HitType != history[j + patternLength].HitType)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all sequences hittable using a TL tap.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
|
||||||
|
/// <param name="type">The type of hit to check for TL taps.</param>
|
||||||
|
private void findTlTap(int parity, HitType type)
|
||||||
|
{
|
||||||
|
int tlLength = -2;
|
||||||
|
|
||||||
|
for (int i = parity; i < hitObjects.Count; i += 2)
|
||||||
|
{
|
||||||
|
if (hitObjects[i].HitType == type)
|
||||||
|
tlLength += 2;
|
||||||
|
else
|
||||||
|
tlLength = -2;
|
||||||
|
|
||||||
|
if (tlLength < tl_min_repetitions)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
markObjectsAsCheese(i, tlLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks <paramref name="count"/> elements counting backwards from <paramref name="end"/> as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void markObjectsAsCheese(int end, int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
hitObjects[end - i].StaminaCheese = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,94 @@
|
|||||||
// 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.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single hit object in taiko difficulty calculation.
|
||||||
|
/// </summary>
|
||||||
public class TaikoDifficultyHitObject : DifficultyHitObject
|
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||||
{
|
{
|
||||||
public readonly bool HasTypeChange;
|
/// <summary>
|
||||||
|
/// The rhythm required to hit this hit object.
|
||||||
|
/// </summary>
|
||||||
|
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
|
||||||
|
|
||||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
/// <summary>
|
||||||
|
/// The hit type of this hit object.
|
||||||
|
/// </summary>
|
||||||
|
public readonly HitType? HitType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the object in the beatmap.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int ObjectIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the object should carry a penalty due to being hittable using special techniques
|
||||||
|
/// making it easier to do so.
|
||||||
|
/// </summary>
|
||||||
|
public bool StaminaCheese;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new difficulty hit object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
|
||||||
|
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
|
||||||
|
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||||
|
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
|
||||||
|
/// <param name="objectIndex">The index of the object in the beatmap.</param>
|
||||||
|
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
|
||||||
: base(hitObject, lastObject, clockRate)
|
: base(hitObject, lastObject, clockRate)
|
||||||
{
|
{
|
||||||
HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type;
|
var currentHit = hitObject as Hit;
|
||||||
|
|
||||||
|
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||||
|
HitType = currentHit?.Type;
|
||||||
|
|
||||||
|
ObjectIndex = objectIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of most common rhythm changes in taiko maps.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The general guidelines for the values are:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||||
|
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
|
||||||
|
{
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
|
||||||
|
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
|
||||||
|
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||||
|
/// <param name="clockRate">The rate of the gameplay clock.</param>
|
||||||
|
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
|
||||||
|
{
|
||||||
|
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
|
||||||
|
double ratio = DeltaTime / prevLength;
|
||||||
|
|
||||||
|
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a rhythm change in a taiko map.
|
||||||
|
/// </summary>
|
||||||
|
public class TaikoDifficultyHitObjectRhythm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The difficulty multiplier associated with this rhythm change.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double Difficulty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||||
|
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||||
|
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double Ratio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an object representing a rhythm change.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
|
||||||
|
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
|
||||||
|
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
|
||||||
|
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
|
||||||
|
{
|
||||||
|
Ratio = numerator / (double)denominator;
|
||||||
|
Difficulty = difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
135
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 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.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the colour coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
public class Colour : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries to keep in <see cref="monoHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int mono_history_max_length = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue with the lengths of the last <see cref="mono_history_max_length"/> most recent mono (single-colour) patterns,
|
||||||
|
/// with the most recent value at the end of the queue.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<int> monoHistory = new LimitedCapacityQueue<int>(mono_history_max_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="HitType"/> of the last object hit before the one being considered.
|
||||||
|
/// </summary>
|
||||||
|
private HitType? previousHitType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Length of the current mono pattern.
|
||||||
|
/// </summary>
|
||||||
|
private int currentMonoLength;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// changing from/to a drum roll or a swell does not constitute a colour change.
|
||||||
|
// hits spaced more than a second apart are also exempt from colour strain.
|
||||||
|
if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
|
||||||
|
{
|
||||||
|
monoHistory.Clear();
|
||||||
|
|
||||||
|
var currentHit = current.BaseObject as Hit;
|
||||||
|
currentMonoLength = currentHit != null ? 1 : 0;
|
||||||
|
previousHitType = currentHit?.Type;
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
double objectStrain = 0.0;
|
||||||
|
|
||||||
|
if (previousHitType != null && taikoCurrent.HitType != previousHitType)
|
||||||
|
{
|
||||||
|
// The colour has changed.
|
||||||
|
objectStrain = 1.0;
|
||||||
|
|
||||||
|
if (monoHistory.Count < 2)
|
||||||
|
{
|
||||||
|
// There needs to be at least two streaks to determine a strain.
|
||||||
|
objectStrain = 0.0;
|
||||||
|
}
|
||||||
|
else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
|
||||||
|
{
|
||||||
|
// The last streak in the history is guaranteed to be a different type to the current streak.
|
||||||
|
// If the total number of notes in the two streaks is even, nullify this object's strain.
|
||||||
|
objectStrain = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectStrain *= repetitionPenalties();
|
||||||
|
currentMonoLength = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentMonoLength += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousHitType = taikoCurrent.HitType;
|
||||||
|
return objectStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The penalty to apply due to the length of repetition in colour streaks.
|
||||||
|
/// </summary>
|
||||||
|
private double repetitionPenalties()
|
||||||
|
{
|
||||||
|
const int most_recent_patterns_to_compare = 2;
|
||||||
|
double penalty = 1.0;
|
||||||
|
|
||||||
|
monoHistory.Enqueue(currentMonoLength);
|
||||||
|
|
||||||
|
for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
|
||||||
|
{
|
||||||
|
if (!isSamePattern(start, most_recent_patterns_to_compare))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int notesSince = 0;
|
||||||
|
for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
|
||||||
|
penalty *= repetitionPenalty(notesSince);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the last <paramref name="mostRecentPatternsToCompare"/> patterns have repeated in the history
|
||||||
|
/// of single-colour note sequences, starting from <paramref name="start"/>.
|
||||||
|
/// </summary>
|
||||||
|
private bool isSamePattern(int start, int mostRecentPatternsToCompare)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||||
|
{
|
||||||
|
if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the strain penalty for a colour pattern repetition.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notesSince">The number of notes since the last repetition of the pattern.</param>
|
||||||
|
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||||
|
}
|
||||||
|
}
|
167
osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
Normal file
167
osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// 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.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the rhythm coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
public class Rhythm : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 10;
|
||||||
|
protected override double StrainDecayBase => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The note-based decay for rhythm strain.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
|
||||||
|
/// </remarks>
|
||||||
|
private const double strain_decay = 0.96;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries in <see cref="rhythmHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int rhythm_history_max_length = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the rolling rhythm strain.
|
||||||
|
/// Used to apply per-note decay.
|
||||||
|
/// </summary>
|
||||||
|
private double currentStrain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of notes since the last rhythm change has taken place.
|
||||||
|
/// </summary>
|
||||||
|
private int notesSinceRhythmChange;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// drum rolls and swells are exempt.
|
||||||
|
if (!(current.BaseObject is Hit))
|
||||||
|
{
|
||||||
|
resetRhythmAndStrain();
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStrain *= strain_decay;
|
||||||
|
|
||||||
|
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||||
|
notesSinceRhythmChange += 1;
|
||||||
|
|
||||||
|
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
|
||||||
|
if (hitObject.Rhythm.Difficulty == 0.0)
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double objectStrain = hitObject.Rhythm.Difficulty;
|
||||||
|
|
||||||
|
objectStrain *= repetitionPenalties(hitObject);
|
||||||
|
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
|
||||||
|
objectStrain *= speedPenalty(hitObject.DeltaTime);
|
||||||
|
|
||||||
|
// careful - needs to be done here since calls above read this value
|
||||||
|
notesSinceRhythmChange = 0;
|
||||||
|
|
||||||
|
currentStrain += objectStrain;
|
||||||
|
return currentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Repetitions of more recent patterns are associated with a higher penalty.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The current hit object being considered.</param>
|
||||||
|
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
|
||||||
|
{
|
||||||
|
double penalty = 1;
|
||||||
|
|
||||||
|
rhythmHistory.Enqueue(hitObject);
|
||||||
|
|
||||||
|
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
|
||||||
|
{
|
||||||
|
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
|
||||||
|
{
|
||||||
|
if (!samePattern(start, mostRecentPatternsToCompare))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
|
||||||
|
penalty *= repetitionPenalty(notesSince);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
|
||||||
|
/// <paramref name="mostRecentPatternsToCompare"/>.
|
||||||
|
/// </summary>
|
||||||
|
private bool samePattern(int start, int mostRecentPatternsToCompare)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||||
|
{
|
||||||
|
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a single rhythm repetition penalty.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
|
||||||
|
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a penalty based on the number of notes since the last rhythm change.
|
||||||
|
/// Both rare and frequent rhythm changes are penalised.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
|
||||||
|
private static double patternLengthPenalty(int patternLength)
|
||||||
|
{
|
||||||
|
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
|
||||||
|
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
|
||||||
|
return Math.Min(shortPatternPenalty, longPatternPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a penalty for objects that do not require alternating hands.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
|
||||||
|
private double speedPenalty(double deltaTime)
|
||||||
|
{
|
||||||
|
if (deltaTime < 80) return 1;
|
||||||
|
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
|
||||||
|
|
||||||
|
resetRhythmAndStrain();
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
|
||||||
|
/// </summary>
|
||||||
|
private void resetRhythmAndStrain()
|
||||||
|
{
|
||||||
|
currentStrain = 0.0;
|
||||||
|
notesSinceRhythmChange = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
113
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the stamina coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
||||||
|
/// </remarks>
|
||||||
|
public class Stamina : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int max_history_length = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
|
||||||
|
/// This naturally translates onto index offsets of the objects in the map.
|
||||||
|
/// </remarks>
|
||||||
|
private readonly int hand;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
|
||||||
|
/// </summary>
|
||||||
|
private double offhandObjectDuration = double.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="Stamina"/> skill.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
|
||||||
|
public Stamina(bool rightHand)
|
||||||
|
{
|
||||||
|
hand = rightHand ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
if (!(current.BaseObject is Hit))
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
if (hitObject.ObjectIndex % 2 == hand)
|
||||||
|
{
|
||||||
|
double objectStrain = 1;
|
||||||
|
|
||||||
|
if (hitObject.ObjectIndex == 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
|
||||||
|
|
||||||
|
double shortestRecentNote = notePairDurationHistory.Min();
|
||||||
|
objectStrain += speedBonus(shortestRecentNote);
|
||||||
|
|
||||||
|
if (hitObject.StaminaCheese)
|
||||||
|
objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
|
||||||
|
|
||||||
|
return objectStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
offhandObjectDuration = hitObject.DeltaTime;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||||
|
private double cheesePenalty(double notePairDuration)
|
||||||
|
{
|
||||||
|
if (notePairDuration > 125) return 1;
|
||||||
|
if (notePairDuration < 100) return 0.6;
|
||||||
|
|
||||||
|
return 0.6 + (notePairDuration - 100) * 0.016;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||||
|
private double speedBonus(double notePairDuration)
|
||||||
|
{
|
||||||
|
if (notePairDuration >= 200) return 0;
|
||||||
|
|
||||||
|
double bonus = 200 - notePairDuration;
|
||||||
|
bonus *= bonus;
|
||||||
|
return bonus / 100000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,95 +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.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.BaseObject.StartTime - current.LastObject.StartTime < 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||||
{
|
{
|
||||||
|
public double StaminaStrain;
|
||||||
|
public double RhythmStrain;
|
||||||
|
public double ColourStrain;
|
||||||
|
public double ApproachRate;
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -19,39 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double star_scaling_factor = 0.04125;
|
private const double rhythm_skill_multiplier = 0.014;
|
||||||
|
private const double colour_skill_multiplier = 0.01;
|
||||||
|
private const double stamina_skill_multiplier = 0.02;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
new Colour(),
|
||||||
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
|
new Rhythm(),
|
||||||
|
new Stamina(true),
|
||||||
HitWindows hitWindows = new TaikoHitWindows();
|
new Stamina(false),
|
||||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
|
||||||
|
|
||||||
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)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
|
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
|
||||||
Skills = skills
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
{
|
{
|
||||||
@ -60,5 +44,124 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
new TaikoModEasy(),
|
new TaikoModEasy(),
|
||||||
new TaikoModHardRock(),
|
new TaikoModHardRock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
List<TaikoDifficultyHitObject> taikoDifficultyHitObjects = new List<TaikoDifficultyHitObject>();
|
||||||
|
|
||||||
|
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
taikoDifficultyHitObjects.Add(
|
||||||
|
new TaikoDifficultyHitObject(
|
||||||
|
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
|
||||||
|
return taikoDifficultyHitObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
if (beatmap.HitObjects.Count == 0)
|
||||||
|
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
|
||||||
|
|
||||||
|
var colour = (Colour)skills[0];
|
||||||
|
var rhythm = (Rhythm)skills[1];
|
||||||
|
var staminaRight = (Stamina)skills[2];
|
||||||
|
var staminaLeft = (Stamina)skills[3];
|
||||||
|
|
||||||
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||||
|
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||||
|
double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
|
||||||
|
|
||||||
|
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
|
||||||
|
staminaRating *= staminaPenalty;
|
||||||
|
|
||||||
|
double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
|
||||||
|
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
|
||||||
|
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
||||||
|
starRating = rescale(starRating);
|
||||||
|
|
||||||
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
|
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
return new TaikoDifficultyAttributes
|
||||||
|
{
|
||||||
|
StarRating = starRating,
|
||||||
|
Mods = mods,
|
||||||
|
StaminaStrain = staminaRating,
|
||||||
|
RhythmStrain = rhythmRating,
|
||||||
|
ColourStrain = colourRating,
|
||||||
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
|
GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
|
Skills = skills
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the penalty for the stamina skill for maps with low colour difficulty.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Some maps (especially converts) can be easy to read despite a high note density.
|
||||||
|
/// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
|
||||||
|
/// </remarks>
|
||||||
|
private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
|
||||||
|
{
|
||||||
|
if (colorDifficulty <= 0) return 0.79 - 0.25;
|
||||||
|
|
||||||
|
return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||||
|
/// <param name="values">The coefficients of the vector.</param>
|
||||||
|
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||||
|
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||||
|
/// </remarks>
|
||||||
|
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
|
||||||
|
{
|
||||||
|
List<double> peaks = new List<double>();
|
||||||
|
|
||||||
|
for (int i = 0; i < colour.StrainPeaks.Count; i++)
|
||||||
|
{
|
||||||
|
double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier;
|
||||||
|
double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier;
|
||||||
|
double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
||||||
|
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
|
||||||
|
}
|
||||||
|
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
foreach (double strain in peaks.OrderByDescending(d => d))
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||||
|
private double rescale(double sr)
|
||||||
|
{
|
||||||
|
if (sr < 0) return sr;
|
||||||
|
|
||||||
|
return 10.43 * Math.Log(sr / 8 + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||||
strainValue *= Math.Pow(0.985, countMiss);
|
strainValue *= Math.Pow(0.985, countMiss);
|
||||||
|
|
||||||
// Combo scaling
|
|
||||||
if (Attributes.MaxCombo > 0)
|
|
||||||
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0);
|
|
||||||
|
|
||||||
if (mods.Any(m => m is ModHidden))
|
if (mods.Any(m => m is ModHidden))
|
||||||
strainValue *= 1.025;
|
strainValue *= 1.025;
|
||||||
|
|
||||||
|
119
osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
Normal file
119
osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LimitedCapacityQueueTest
|
||||||
|
{
|
||||||
|
private const int capacity = 3;
|
||||||
|
|
||||||
|
private LimitedCapacityQueue<int> queue;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
queue = new LimitedCapacityQueue<int>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEmptyQueue()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _ = queue.Dequeue());
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (var _ in queue)
|
||||||
|
count++;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
[TestCase(3)]
|
||||||
|
public void TestBelowCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(count, queue.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
Assert.AreEqual(i, queue[i]);
|
||||||
|
|
||||||
|
int j = 0;
|
||||||
|
foreach (var item in queue)
|
||||||
|
Assert.AreEqual(j++, item);
|
||||||
|
|
||||||
|
for (int i = queue.Count; i < queue.Count + capacity; i++)
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestEnqueueAtFullCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(capacity, queue.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < queue.Count; ++i)
|
||||||
|
Assert.AreEqual(count - capacity + i, queue[i]);
|
||||||
|
|
||||||
|
int j = count - capacity;
|
||||||
|
foreach (var item in queue)
|
||||||
|
Assert.AreEqual(j++, item);
|
||||||
|
|
||||||
|
for (int i = queue.Count; i < queue.Count + capacity; i++)
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestDequeueAtFullCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
for (int i = 0; i < capacity; ++i)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(count - capacity + i, queue.Dequeue());
|
||||||
|
Assert.AreEqual(2 - i, queue.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClearQueue()
|
||||||
|
{
|
||||||
|
queue.Enqueue(3);
|
||||||
|
queue.Enqueue(5);
|
||||||
|
Assert.AreEqual(2, queue.Count);
|
||||||
|
|
||||||
|
queue.Clear();
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
|
||||||
|
|
||||||
|
queue.Enqueue(7);
|
||||||
|
Assert.AreEqual(1, queue.Count);
|
||||||
|
Assert.AreEqual(7, queue[0]);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[1]);
|
||||||
|
|
||||||
|
queue.Enqueue(9);
|
||||||
|
Assert.AreEqual(2, queue.Count);
|
||||||
|
Assert.AreEqual(9, queue[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,12 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
return () => imported;
|
return () => imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
|
||||||
|
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
|
||||||
|
/// </summary>
|
||||||
|
private IScreen lastWaitedScreen;
|
||||||
|
|
||||||
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
||||||
{
|
{
|
||||||
AddStep("present score", () => Game.PresentScore(getImport(), type));
|
AddStep("present score", () => Game.PresentScore(getImport(), type));
|
||||||
@ -140,13 +146,15 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ScorePresentType.Results:
|
case ScorePresentType.Results:
|
||||||
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
|
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||||
|
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
||||||
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
||||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ScorePresentType.Gameplay:
|
case ScorePresentType.Gameplay:
|
||||||
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is ReplayPlayerLoader);
|
AddUntilStep("wait for player loader", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ReplayPlayerLoader);
|
||||||
|
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
||||||
AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
||||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
||||||
break;
|
break;
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Overlays.Mods;
|
|||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Options;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -168,6 +169,29 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBeatmapOptionsInput()
|
||||||
|
{
|
||||||
|
TestSongSelect songSelect = null;
|
||||||
|
|
||||||
|
PushAndConfirm(() => songSelect = new TestSongSelect());
|
||||||
|
|
||||||
|
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
|
||||||
|
|
||||||
|
AddStep("Change ruleset to osu!taiko", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.PressKey(Key.Number2);
|
||||||
|
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
InputManager.ReleaseKey(Key.Number2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.ID == 1);
|
||||||
|
|
||||||
|
AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
|
||||||
private void pushEscape() =>
|
private void pushEscape() =>
|
||||||
AddStep("Press escape", () => pressAndRelease(Key.Escape));
|
AddStep("Press escape", () => pressAndRelease(Key.Escape));
|
||||||
|
|
||||||
@ -193,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
private class TestSongSelect : PlaySongSelect
|
private class TestSongSelect : PlaySongSelect
|
||||||
{
|
{
|
||||||
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
||||||
|
|
||||||
|
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
using osuTK.Graphics;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelect
|
namespace osu.Game.Tests.Visual.SongSelect
|
||||||
{
|
{
|
||||||
@ -16,10 +15,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
var overlay = new BeatmapOptionsOverlay();
|
var overlay = new BeatmapOptionsOverlay();
|
||||||
|
|
||||||
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1);
|
var colours = new OsuColour();
|
||||||
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2);
|
|
||||||
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3);
|
overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null);
|
||||||
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4);
|
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null);
|
||||||
|
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
|
||||||
|
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null);
|
||||||
|
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null);
|
||||||
|
|
||||||
Add(overlay);
|
Add(overlay);
|
||||||
|
|
||||||
|
123
osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
Normal file
123
osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// 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.Difficulty.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An indexed queue with limited capacity.
|
||||||
|
/// Respects first-in-first-out insertion order.
|
||||||
|
/// </summary>
|
||||||
|
public class LimitedCapacityQueue<T> : IEnumerable<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of elements in the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the queue is full (adding any new items will cause removing existing ones).
|
||||||
|
/// </summary>
|
||||||
|
public bool Full => Count == capacity;
|
||||||
|
|
||||||
|
private readonly T[] array;
|
||||||
|
private readonly int capacity;
|
||||||
|
|
||||||
|
// Markers tracking the queue's first and last element.
|
||||||
|
private int start, end;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="LimitedCapacityQueue{T}"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="capacity">The number of items the queue can hold.</param>
|
||||||
|
public LimitedCapacityQueue(int capacity)
|
||||||
|
{
|
||||||
|
if (capacity < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||||
|
|
||||||
|
this.capacity = capacity;
|
||||||
|
array = new T[capacity];
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all elements from the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
end = -1;
|
||||||
|
Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an item from the front of the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The item removed from the front of the queue.</returns>
|
||||||
|
public T Dequeue()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
throw new InvalidOperationException("Queue is empty.");
|
||||||
|
|
||||||
|
var result = array[start];
|
||||||
|
start = (start + 1) % capacity;
|
||||||
|
Count--;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the back of the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// If the queue is holding <see cref="Count"/> elements at the point of addition,
|
||||||
|
/// the item at the front of the queue will be removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to be added to the back of the queue.</param>
|
||||||
|
public void Enqueue(T item)
|
||||||
|
{
|
||||||
|
end = (end + 1) % capacity;
|
||||||
|
if (Count == capacity)
|
||||||
|
start = (start + 1) % capacity;
|
||||||
|
else
|
||||||
|
Count++;
|
||||||
|
array[end] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the item at the given index in the queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">
|
||||||
|
/// The index of the item to retrieve.
|
||||||
|
/// The item with index 0 is at the front of the queue
|
||||||
|
/// (it was added the earliest).
|
||||||
|
/// </param>
|
||||||
|
public T this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
return array[(start + index) % capacity];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the queue from its start to its end.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
yield return array[(start + i) % capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Options
|
namespace osu.Game.Screens.Select.Options
|
||||||
@ -52,8 +51,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
set => secondLine.Text = value;
|
set => secondLine.Text = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Key? HotKey;
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
||||||
@ -75,17 +72,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
return base.OnClick(e);
|
return base.OnClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
|
||||||
{
|
|
||||||
if (!e.Repeat && e.Key == HotKey)
|
|
||||||
{
|
|
||||||
Click();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
public BeatmapOptionsButton()
|
public BeatmapOptionsButton()
|
||||||
|
@ -11,6 +11,8 @@ using osuTK;
|
|||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Options
|
namespace osu.Game.Screens.Select.Options
|
||||||
{
|
{
|
||||||
@ -27,33 +29,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
|
|
||||||
public override bool BlockScreenWideMouse => false;
|
public override bool BlockScreenWideMouse => false;
|
||||||
|
|
||||||
protected override void PopIn()
|
|
||||||
{
|
|
||||||
base.PopIn();
|
|
||||||
|
|
||||||
this.FadeIn(transition_duration, Easing.OutQuint);
|
|
||||||
|
|
||||||
if (buttonsContainer.Position.X == 1 || Alpha == 0)
|
|
||||||
buttonsContainer.MoveToX(x_position - x_movement);
|
|
||||||
|
|
||||||
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
|
|
||||||
|
|
||||||
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
|
|
||||||
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopOut()
|
|
||||||
{
|
|
||||||
base.PopOut();
|
|
||||||
|
|
||||||
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
|
|
||||||
|
|
||||||
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
|
|
||||||
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
|
|
||||||
|
|
||||||
this.FadeOut(transition_duration, Easing.InQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapOptionsOverlay()
|
public BeatmapOptionsOverlay()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
@ -87,9 +62,8 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
/// <param name="secondLine">Text in the second line.</param>
|
/// <param name="secondLine">Text in the second line.</param>
|
||||||
/// <param name="colour">Colour of the button.</param>
|
/// <param name="colour">Colour of the button.</param>
|
||||||
/// <param name="icon">Icon of the button.</param>
|
/// <param name="icon">Icon of the button.</param>
|
||||||
/// <param name="hotkey">Hotkey of the button.</param>
|
|
||||||
/// <param name="action">Binding the button does.</param>
|
/// <param name="action">Binding the button does.</param>
|
||||||
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null)
|
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action)
|
||||||
{
|
{
|
||||||
var button = new BeatmapOptionsButton
|
var button = new BeatmapOptionsButton
|
||||||
{
|
{
|
||||||
@ -102,10 +76,58 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
Hide();
|
Hide();
|
||||||
action?.Invoke();
|
action?.Invoke();
|
||||||
},
|
},
|
||||||
HotKey = hotkey
|
|
||||||
};
|
};
|
||||||
|
|
||||||
buttonsContainer.Add(button);
|
buttonsContainer.Add(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
|
||||||
|
this.FadeIn(transition_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
if (buttonsContainer.Position.X == 1 || Alpha == 0)
|
||||||
|
buttonsContainer.MoveToX(x_position - x_movement);
|
||||||
|
|
||||||
|
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
|
||||||
|
|
||||||
|
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
|
||||||
|
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
|
||||||
|
|
||||||
|
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
|
||||||
|
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
|
||||||
|
|
||||||
|
this.FadeOut(transition_duration, Easing.InQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
|
||||||
|
if (e.ControlPressed) return false;
|
||||||
|
|
||||||
|
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
|
||||||
|
{
|
||||||
|
int requested = e.Key - Key.Number1;
|
||||||
|
|
||||||
|
// go reverse as buttonsContainer is a ReverseChildIDFillFlowContainer
|
||||||
|
BeatmapOptionsButton found = buttonsContainer.Children.ElementAtOrDefault((buttonsContainer.Children.Count - 1) - requested);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
found.Click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
ValidForResume = false;
|
ValidForResume = false;
|
||||||
Edit();
|
Edit();
|
||||||
}, Key.Number4);
|
});
|
||||||
|
|
||||||
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select
|
|||||||
private MusicController music { get; set; }
|
private MusicController music { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections)
|
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog)
|
||||||
{
|
{
|
||||||
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
|
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
|
||||||
transferRulesetValue();
|
transferRulesetValue();
|
||||||
@ -275,9 +275,10 @@ namespace osu.Game.Screens.Select
|
|||||||
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
|
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
|
||||||
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
|
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
|
||||||
|
|
||||||
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null, Key.Number1);
|
BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show());
|
||||||
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2);
|
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo));
|
||||||
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number3);
|
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
|
||||||
|
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogOverlay = dialog;
|
dialogOverlay = dialog;
|
||||||
@ -642,6 +643,8 @@ namespace osu.Game.Screens.Select
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
decoupledRuleset.UnbindAll();
|
decoupledRuleset.UnbindAll();
|
||||||
|
|
||||||
|
if (music != null)
|
||||||
music.TrackChanged -= ensureTrackLooping;
|
music.TrackChanged -= ensureTrackLooping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user