mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 04:19:53 +08:00
Compare commits
155 Commits
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.220.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.225.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Android
|
||||
[Cached]
|
||||
private readonly OsuGameActivity gameActivity;
|
||||
|
||||
protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||
|
||||
public OsuGameAndroid(OsuGameActivity activity)
|
||||
: base(null)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@@ -35,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
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 = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
StarRating = Math.Sqrt(skills.OfType<Movement>().Single().DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
double approachRate = catchAttributes.ApproachRate;
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
|
||||
|
||||
double approachRateFactor = 1.0;
|
||||
if (approachRate > 9.0)
|
||||
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
const float base_game_width = 1024f;
|
||||
@@ -26,30 +29,49 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = new Container
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame? osuGame)
|
||||
{
|
||||
if (osuGame != null)
|
||||
{
|
||||
// on mobile platforms where the base aspect ratio is wider, the catch playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> which scales its content relative to a target width.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods do not affect the hit window at all in osu-stable.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private const double difficulty_multiplier = 0.018;
|
||||
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20241007;
|
||||
|
||||
@@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@@ -48,11 +46,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||
StarRating = skills.OfType<Strain>().Single().DifficultyValue() * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||
};
|
||||
|
||||
@@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private double getHitWindow300(Mod[] mods)
|
||||
{
|
||||
if (isForCurrentRuleset)
|
||||
{
|
||||
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
|
||||
return applyModAdjustments(34 + 3 * od, mods);
|
||||
}
|
||||
|
||||
if (Math.Round(originalOverallDifficulty) > 4)
|
||||
return applyModAdjustments(34, mods);
|
||||
|
||||
return applyModAdjustments(47, mods);
|
||||
|
||||
static double applyModAdjustments(double value, Mod[] mods)
|
||||
{
|
||||
if (mods.Any(m => m is ManiaModHardRock))
|
||||
value /= 1.4;
|
||||
else if (mods.Any(m => m is ManiaModEasy))
|
||||
value *= 1.4;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 1.95;
|
||||
private const double acute_angle_multiplier = 2.6;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
private const double wiggle_multiplier = 1.02;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of aiming the current object, based on:
|
||||
@@ -64,16 +65,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double acuteAngleBonus = 0;
|
||||
double sliderBonus = 0;
|
||||
double velocityChangeBonus = 0;
|
||||
double wiggleBonus = 0;
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
||||
{
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
double lastLastAngle = osuLastLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
@@ -81,20 +82,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
acuteAngleBonus = 0;
|
||||
else
|
||||
{
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
|
||||
}
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||
|
||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
||||
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
|
||||
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= angleBonus *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
}
|
||||
|
||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||
|
||||
@@ -132,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return aimStrain;
|
||||
}
|
||||
|
||||
private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
||||
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||
|
||||
private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
||||
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
if (!(currentObj.BaseObject is Spinner))
|
||||
{
|
||||
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||
if (i == 0)
|
||||
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
@@ -14,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.94;
|
||||
private const double distance_multiplier = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of tapping the current object, based on:
|
||||
@@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
if (mods.OfType<OsuModAutopilot>().Any())
|
||||
distanceBonus = 0;
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("aim_difficulty")]
|
||||
public double AimDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of <see cref="Slider"/>s weighted by difficulty.
|
||||
/// </summary>
|
||||
[JsonProperty("aim_difficult_slider_count")]
|
||||
public double AimDifficultSliderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the speed skill.
|
||||
/// </summary>
|
||||
@@ -52,24 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficult_strain_count")]
|
||||
public double SpeedDifficultStrainCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("overall_difficulty")]
|
||||
public double OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
@@ -97,8 +86,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightDifficulty())
|
||||
@@ -109,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -117,14 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
||||
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -36,20 +34,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
|
||||
double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier;
|
||||
double aimDifficultyStrainCount = aim.CountTopWeightedStrains();
|
||||
double difficultSliders = aim.GetDifficultSliders();
|
||||
|
||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||
double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier;
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
|
||||
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
|
||||
var speed = skills.OfType<Speed>().Single();
|
||||
double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = speed.RelevantNoteCount();
|
||||
double speedDifficultyStrainCount = speed.CountTopWeightedStrains();
|
||||
|
||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||
double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
@@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedRating = 0.0;
|
||||
flashlightRating *= 0.7;
|
||||
}
|
||||
else if (mods.Any(h => h is OsuModAutopilot))
|
||||
{
|
||||
speedRating *= 0.5;
|
||||
aimRating = 0.0;
|
||||
flashlightRating *= 0.4;
|
||||
}
|
||||
|
||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||
@@ -82,31 +88,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
AimDifficulty = aimRating,
|
||||
AimDifficultSliderCount = difficultSliders,
|
||||
SpeedDifficulty = speedRating,
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double? SpeedDeviation { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
@@ -40,6 +46,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
/// </summary>
|
||||
private double effectiveMissCount;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
private double okHitWindow;
|
||||
private double mehHitWindow;
|
||||
private double overallDifficulty;
|
||||
private double approachRate;
|
||||
|
||||
private double? speedDeviation;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@@ -61,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||
effectiveMissCount = countMiss;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
overallDifficulty = (80 - greatHitWindow) / 6;
|
||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
{
|
||||
if (usingClassicSliderAccuracy)
|
||||
@@ -103,17 +138,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// https://www.desmos.com/calculator/bc9eybdthb
|
||||
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
|
||||
double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
|
||||
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
speedDeviation = calculateSpeedDeviation(osuAttributes);
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(aimValue, 1.1) +
|
||||
@@ -129,13 +167,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
return 0.0;
|
||||
|
||||
double aimDifficulty = attributes.AimDifficulty;
|
||||
|
||||
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
|
||||
aimDifficulty *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@@ -145,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
else if (attributes.ApproachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
else if (approachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
approachRateFactor = 0.0;
|
||||
@@ -160,43 +225,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
|
||||
double estimateDifficultSliders = attributes.SliderCount * 0.15;
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
aimValue *= sliderNerfFactor;
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return aimValue;
|
||||
}
|
||||
|
||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
@@ -209,8 +250,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
@@ -222,21 +266,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||
speedValue *= speedHighDeviationMultiplier;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
@@ -253,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
else
|
||||
betterAccuracyPercentage = 0;
|
||||
|
||||
@@ -263,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||
double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
@@ -300,17 +344,117 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Scale the flashlight value with accuracy _slightly_.
|
||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||
/// Treats all speed notes as hit circles.
|
||||
/// </summary>
|
||||
private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return null;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double speedNoteCount = attributes.SpeedNoteCount;
|
||||
speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
|
||||
|
||||
// Assume worst case: all mistakes were on speed notes
|
||||
double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
|
||||
double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
|
||||
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
||||
|
||||
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
||||
/// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
|
||||
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
|
||||
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||
/// </summary>
|
||||
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
||||
{
|
||||
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||
return null;
|
||||
|
||||
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
||||
|
||||
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
||||
// the number of greats on circles divided by the number of circles, and then add one
|
||||
// to the number of circles as a bias correction.
|
||||
double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = relevantCountGreat / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
|
||||
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
||||
double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
|
||||
/ (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
|
||||
|
||||
deviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// Value deviation approach as greatCount approaches 0
|
||||
double limitValue = okHitWindow / Math.Sqrt(3);
|
||||
|
||||
// If precision is not enough to compute true deviation - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
||||
deviation = limitValue;
|
||||
|
||||
// Then compute the variance for mehs.
|
||||
double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||
|
||||
return deviation;
|
||||
}
|
||||
|
||||
// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
|
||||
// https://www.desmos.com/calculator/dmogdhzofn
|
||||
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (speedDeviation == null)
|
||||
return 0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||
double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
|
||||
|
||||
if (speedValue <= excessSpeedDifficultyCutoff)
|
||||
return 1.0;
|
||||
|
||||
const double scale = 50;
|
||||
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
|
||||
|
||||
// 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
||||
double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0);
|
||||
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
|
||||
|
||||
return adjustedSpeedValue / speedValue;
|
||||
}
|
||||
|
||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@@ -13,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Aim : OsuStrainSkill
|
||||
{
|
||||
public Aim(Mod[] mods, bool withSliders)
|
||||
public readonly bool IncludeSliders;
|
||||
|
||||
public Aim(Mod[] mods, bool includeSliders)
|
||||
: base(mods)
|
||||
{
|
||||
this.withSliders = withSliders;
|
||||
IncludeSliders = includeSliders;
|
||||
}
|
||||
|
||||
private readonly bool withSliders;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 25.18;
|
||||
private double skillMultiplier => 25.6;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private readonly List<double> sliderStrains = new List<double>();
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
@@ -33,9 +38,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||
|
||||
if (current.BaseObject is Slider)
|
||||
{
|
||||
sliderStrains.Add(currentStrain);
|
||||
}
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public double GetDifficultSliders()
|
||||
{
|
||||
if (sliderStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxSliderStrain = sliderStrains.Max();
|
||||
if (maxSliderStrain == 0)
|
||||
return 0;
|
||||
|
||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 1.430;
|
||||
private double skillMultiplier => 1.46;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// 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.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||
var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType<IHasSliderVelocity>().LastOrDefault();
|
||||
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity);
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||
{
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||
new ApproachRateSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new ApproachRateSlider();
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar with more detailed approach rate info for its given value
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test-strong")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||
|
||||
|
||||
@@ -10,45 +10,82 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class ColourEvaluator
|
||||
public static class ColourEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
|
||||
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
|
||||
/// considering the delta time between each colour sequence.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
|
||||
/// <param name="hitObject">The current hitObject to consider.</param>
|
||||
/// <param name="threshold"> The allowable margin of error for determining whether ratios are consistent.</param>
|
||||
/// <param name="maxObjectsToCheck">The maximum objects to check per count of consistent ratio.</param>
|
||||
private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
|
||||
int consistentRatioCount = 0;
|
||||
double totalRatioCount = 0.0;
|
||||
|
||||
TaikoDifficultyHitObject current = hitObject;
|
||||
|
||||
for (int i = 0; i < maxObjectsToCheck; i++)
|
||||
{
|
||||
// Break if there is no valid previous object
|
||||
if (current.Index <= 1)
|
||||
break;
|
||||
|
||||
var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double currentRatio = current.RhythmData.Ratio;
|
||||
double previousRatio = previousHitObject.RhythmData.Ratio;
|
||||
|
||||
// A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
|
||||
if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
|
||||
{
|
||||
consistentRatioCount++;
|
||||
totalRatioCount += currentRatio;
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to the previous object
|
||||
current = previousHitObject;
|
||||
}
|
||||
|
||||
// Ensure no division by zero
|
||||
double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
|
||||
|
||||
return ratioPenalty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
|
||||
/// Evaluate the difficulty of the first hitobject within a colour streak.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
|
||||
{
|
||||
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
||||
{
|
||||
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
||||
var taikoObject = (TaikoDifficultyHitObject)hitObject;
|
||||
TaikoColourData colourData = taikoObject.ColourData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
||||
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
||||
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
||||
if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak);
|
||||
|
||||
if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern);
|
||||
|
||||
if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern);
|
||||
|
||||
double consistencyPenalty = consistentRatioPenalty(taikoObject);
|
||||
difficulty *= consistencyPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;
|
||||
|
||||
private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent);
|
||||
|
||||
private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
|
||||
2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 System;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public static class ReadingEvaluator
|
||||
{
|
||||
private readonly struct VelocityRange
|
||||
{
|
||||
public double Min { get; }
|
||||
public double Max { get; }
|
||||
public double Center => (Max + Min) / 2;
|
||||
public double Range => Max - Min;
|
||||
|
||||
public VelocityRange(double min, double max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the influence of higher slider velocities on hitobject difficulty.
|
||||
/// The bonus is determined based on the EffectiveBPM, shifting within a defined range
|
||||
/// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty.
|
||||
/// </summary>
|
||||
/// <param name="noteObject">The hit object to evaluate.</param>
|
||||
/// <returns>The reading difficulty value for the given hit object.</returns>
|
||||
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
|
||||
{
|
||||
var highVelocity = new VelocityRange(480, 640);
|
||||
var midVelocity = new VelocityRange(360, 480);
|
||||
|
||||
// Apply a cap to prevent outlier values on maps that exceed the editor's parameters.
|
||||
double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM);
|
||||
|
||||
double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
|
||||
|
||||
// Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
|
||||
double expectedDeltaTime = 21000.0 / effectiveBPM;
|
||||
double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime);
|
||||
|
||||
// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
|
||||
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
|
||||
|
||||
double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
|
||||
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
|
||||
|
||||
return midVelocityDifficulty + highVelocityDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class RhythmEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of a hitobject considering its interval change.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
|
||||
{
|
||||
TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
double sameRhythm = 0;
|
||||
double samePattern = 0;
|
||||
double intervalPenalty = 0;
|
||||
|
||||
if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
|
||||
{
|
||||
sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
}
|
||||
|
||||
if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
|
||||
samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio);
|
||||
|
||||
difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow)
|
||||
{
|
||||
double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
|
||||
double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;
|
||||
|
||||
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);
|
||||
|
||||
// If a previous interval exists and there are multiple hit objects in the sequence:
|
||||
if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1)
|
||||
{
|
||||
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count;
|
||||
double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;
|
||||
|
||||
if (durationDifference > 0)
|
||||
{
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
durationDifference / hitWindow,
|
||||
midpointOffset: 0.7,
|
||||
multiplier: 1.0,
|
||||
maxValue: 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Penalise patterns that can be hit within a single hit window.
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
sameRhythmGroupedHitObjects.Duration / hitWindow,
|
||||
midpointOffset: 0.6,
|
||||
multiplier: 1,
|
||||
maxValue: 1);
|
||||
|
||||
return Math.Pow(intervalDifficulty, 0.75);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
|
||||
/// </summary>
|
||||
private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
|
||||
{
|
||||
double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);
|
||||
|
||||
double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6
|
||||
? sameInterval(sameRhythmGroupedHitObjects, 4)
|
||||
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
|
||||
|
||||
// The duration penalty is based on hit object duration relative to hitWindow.
|
||||
double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5);
|
||||
|
||||
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
|
||||
|
||||
double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount)
|
||||
{
|
||||
List<double?> intervals = new List<double?>();
|
||||
var currentObject = startObject;
|
||||
|
||||
for (int i = 0; i < intervalCount && currentObject != null; i++)
|
||||
{
|
||||
intervals.Add(currentObject.HitObjectInterval);
|
||||
currentObject = currentObject.Previous;
|
||||
}
|
||||
|
||||
intervals.RemoveAll(interval => interval == null);
|
||||
|
||||
if (intervals.Count < intervalCount)
|
||||
return 1.0; // No penalty if there aren't enough valid intervals.
|
||||
|
||||
for (int i = 0; i < intervals.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < intervals.Count; j++)
|
||||
{
|
||||
double ratio = intervals[i]!.Value / intervals[j]!.Value;
|
||||
if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty.
|
||||
return 0.80;
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0; // No penalty if all intervals are different.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
|
||||
/// </summary>
|
||||
private static double ratioDifficulty(double ratio, int terms = 8)
|
||||
{
|
||||
double difficulty = 0;
|
||||
|
||||
// Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
|
||||
ratio = double.IsNormal(ratio) ? ratio : 0;
|
||||
|
||||
for (int i = 1; i <= terms; ++i)
|
||||
{
|
||||
difficulty += termPenalty(ratio, i, 4, 1);
|
||||
}
|
||||
|
||||
difficulty += terms / (1 + ratio);
|
||||
|
||||
// Give bonus to near-1 ratios
|
||||
difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
|
||||
|
||||
// Penalize ratios that are VERY near 1
|
||||
difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
|
||||
|
||||
difficulty = Math.Max(difficulty, 0);
|
||||
difficulty /= Math.Sqrt(8);
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for a given denominator term.
|
||||
/// </summary>
|
||||
private static double termPenalty(double ratio, int denominator, double power, double multiplier) =>
|
||||
-multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class StaminaEvaluator
|
||||
public static class StaminaEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 30 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
|
||||
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
|
||||
@@ -59,17 +24,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
|
||||
// available fingers.
|
||||
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
if (keyPrevious == null)
|
||||
{
|
||||
// There is no previous hit object hit by the current finger
|
||||
return 0.0;
|
||||
}
|
||||
TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
|
||||
TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
double objectStrain = 0.5; // Add a base strain to all objects
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
|
||||
if (taikoPrevious == null) return objectStrain;
|
||||
|
||||
if (previousMono != null)
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
|
||||
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 20 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
/// <summary>
|
||||
/// Stores colour compression information for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObjectColour
|
||||
public class TaikoColourData
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="MonoStreak"/> that encodes this note.
|
||||
+5
-5
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
public static class TaikoColourDifficultyPreprocessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoColourData"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoColourData"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
|
||||
{
|
||||
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
|
||||
foreach (var hitObject in monoStreak.HitObjects)
|
||||
{
|
||||
hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.Colour.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.Colour.MonoStreak = monoStreak;
|
||||
hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.ColourData.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.ColourData.MonoStreak = monoStreak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents <see cref="SameRhythmHitObjectGrouping"/> grouped by their <see cref="SameRhythmHitObjectGrouping.StartTime"/>'s interval.
|
||||
/// </summary>
|
||||
public class SamePatternsGroupedHitObjects
|
||||
{
|
||||
public IReadOnlyList<SameRhythmHitObjectGrouping> Groups { get; }
|
||||
|
||||
public SamePatternsGroupedHitObjects? Previous { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SameRhythmHitObjectGrouping.Interval"/> between groups <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// If there is only one group, this will have the value of the first group's <see cref="SameRhythmHitObjectGrouping.Interval"/>.
|
||||
/// </summary>
|
||||
public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="GroupInterval"/> between this and the previous <see cref="SamePatternsGroupedHitObjects"/>. In the
|
||||
/// case where there is no previous <see cref="SamePatternsGroupedHitObjects"/>, this will have a value of 1.
|
||||
/// </summary>
|
||||
public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject;
|
||||
|
||||
public IEnumerable<TaikoDifficultyHitObject> AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects);
|
||||
|
||||
public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List<SameRhythmHitObjectGrouping> groups)
|
||||
{
|
||||
Previous = previous;
|
||||
Groups = groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// 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.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="TaikoDifficultyHitObject"/>s with no rhythm variation.
|
||||
/// </summary>
|
||||
public class SameRhythmHitObjectGrouping : IHasInterval
|
||||
{
|
||||
public readonly List<TaikoDifficultyHitObject> HitObjects;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
|
||||
|
||||
public readonly SameRhythmHitObjectGrouping? Previous;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DifficultyHitObject.StartTime"/> of the first hit object.
|
||||
/// </summary>
|
||||
public double StartTime => HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval between the first and final hit object within this group.
|
||||
/// </summary>
|
||||
public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval in ms of each hit object in this <see cref="SameRhythmHitObjectGrouping"/>. This is only defined if there is
|
||||
/// more than two hit objects in this <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// </summary>
|
||||
public readonly double? HitObjectInterval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="HitObjectInterval"/> between this and the previous <see cref="SameRhythmHitObjectGrouping"/>. In the
|
||||
/// case where one or both of the <see cref="HitObjectInterval"/> is undefined, this will have a value of 1.
|
||||
/// </summary>
|
||||
public readonly double HitObjectIntervalRatio;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double Interval { get; }
|
||||
|
||||
public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
Previous = previous;
|
||||
HitObjects = hitObjects;
|
||||
|
||||
// Calculate the average interval between hitobjects, or null if there are fewer than two
|
||||
HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1);
|
||||
|
||||
// Calculate the ratio between this group's interval and the previous group's interval
|
||||
HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null
|
||||
? HitObjectInterval.Value / Previous.HitObjectInterval.Value
|
||||
: 1;
|
||||
|
||||
// Calculate the interval from the previous group's start time
|
||||
Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +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.
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores rhythm data for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoRhythmData
|
||||
{
|
||||
/// <summary>
|
||||
/// The group of hit objects with consistent rhythm that this object belongs to.
|
||||
/// </summary>
|
||||
public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The larger pattern of rhythm groups that this object is part of.
|
||||
/// </summary>
|
||||
public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of current <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||
/// to previous <see cref="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>
|
||||
/// <remarks>
|
||||
/// This is snapped to the closest matching <see cref="common_ratios"/>.
|
||||
/// </remarks>
|
||||
public readonly double Ratio;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of <see cref="TaikoRhythmData"/>s,
|
||||
/// calculating the closest rhythm change and its associated difficulty for the current hit object.
|
||||
/// </summary>
|
||||
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/> being processed.</param>
|
||||
public TaikoRhythmData(TaikoDifficultyHitObject current)
|
||||
{
|
||||
var previous = current.Previous(0);
|
||||
|
||||
if (previous == null)
|
||||
{
|
||||
Ratio = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
double actualRatio = current.DeltaTime / previous.DeltaTime;
|
||||
double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio));
|
||||
|
||||
Ratio = closestRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
|
||||
/// </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 double[] common_ratios =
|
||||
[
|
||||
1.0 / 1,
|
||||
2.0 / 1,
|
||||
1.0 / 2,
|
||||
3.0 / 1,
|
||||
1.0 / 3,
|
||||
3.0 / 2,
|
||||
2.0 / 3,
|
||||
5.0 / 4,
|
||||
4.0 / 5
|
||||
];
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// 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.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
public static class TaikoRhythmDifficultyPreprocessor
|
||||
{
|
||||
public static void ProcessAndAssign(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects);
|
||||
|
||||
foreach (var rhythmGroup in rhythmGroups)
|
||||
{
|
||||
foreach (var hitObject in rhythmGroup.HitObjects)
|
||||
hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup;
|
||||
}
|
||||
|
||||
var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups);
|
||||
|
||||
foreach (var patternGroup in patternGroups)
|
||||
{
|
||||
foreach (var hitObject in patternGroup.AllHitObjects)
|
||||
hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SameRhythmHitObjectGrouping> createSameRhythmGroupedHitObjects(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = new List<SameRhythmHitObjectGrouping>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects))
|
||||
rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped));
|
||||
|
||||
return rhythmGroups;
|
||||
}
|
||||
|
||||
private static List<SamePatternsGroupedHitObjects> createSamePatternGroupedHitObjects(List<SameRhythmHitObjectGrouping> rhythmGroups)
|
||||
{
|
||||
var patternGroups = new List<SamePatternsGroupedHitObjects>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups))
|
||||
patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped));
|
||||
|
||||
return patternGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
// 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.ControlPoints;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
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, IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of all <see cref="TaikoDifficultyHitObject"/> of the same colour as this <see cref="TaikoDifficultyHitObject"/> in the beatmap.
|
||||
@@ -38,98 +40,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public readonly int NoteIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The rhythm required to hit this hit object.
|
||||
/// Rhythm data used by <see cref="RhythmEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoRhythmDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
|
||||
public readonly TaikoRhythmData RhythmData;
|
||||
|
||||
/// <summary>
|
||||
/// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used
|
||||
/// by other skills in the future.
|
||||
/// Colour data used by <see cref="ColourEvaluator"/> and <see cref="StaminaEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoColourDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectColour Colour;
|
||||
public readonly TaikoColourData ColourData;
|
||||
|
||||
/// <summary>
|
||||
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
|
||||
/// </summary>
|
||||
public double EffectiveBPM;
|
||||
|
||||
/// <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="objects">The list of all <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="centreHitObjects">The list of centre (don) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="rimHitObjects">The list of rim (kat) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="noteObjects">The list of <see cref="DifficultyHitObject"/>s that is a hit (i.e. not a drumroll or swell) in the current beatmap.</param>
|
||||
/// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
|
||||
/// <param name="controlPointInfo">The control point info of the beatmap.</param>
|
||||
/// <param name="globalSliderVelocity">The global slider velocity of the beatmap.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate,
|
||||
List<DifficultyHitObject> objects,
|
||||
List<TaikoDifficultyHitObject> centreHitObjects,
|
||||
List<TaikoDifficultyHitObject> rimHitObjects,
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index)
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index,
|
||||
ControlPointInfo controlPointInfo,
|
||||
double globalSliderVelocity)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
noteDifficultyHitObjects = noteObjects;
|
||||
|
||||
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
|
||||
Colour = new TaikoDifficultyHitObjectColour();
|
||||
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||
ColourData = new TaikoColourData();
|
||||
RhythmData = new TaikoRhythmData(this);
|
||||
|
||||
switch ((hitObject as Hit)?.Type)
|
||||
if (hitObject is Hit hit)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
switch (hit.Type)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hitObject is Hit)
|
||||
{
|
||||
NoteIndex = noteObjects.Count;
|
||||
noteObjects.Add(this);
|
||||
}
|
||||
|
||||
// Using `hitObject.StartTime` causes floating point error differences
|
||||
double normalisedStartTime = StartTime * clockRate;
|
||||
|
||||
// Retrieve the timing point at the note's start time
|
||||
TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime);
|
||||
|
||||
// Calculate the slider velocity at the note's start time.
|
||||
double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate);
|
||||
|
||||
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps.
|
||||
/// Calculates the slider velocity based on control point info and clock rate.
|
||||
/// </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 =
|
||||
private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate)
|
||||
{
|
||||
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();
|
||||
var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
|
||||
return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
|
||||
}
|
||||
|
||||
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
|
||||
@@ -139,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
|
||||
|
||||
public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
|
||||
|
||||
public double Interval => DeltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the reading coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Reading : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
public Reading(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// Drum Rolls and Swells are exempt.
|
||||
if (current.BaseObject is not Hit)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var taikoObject = (TaikoDifficultyHitObject)current;
|
||||
int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
|
||||
|
||||
currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;
|
||||
|
||||
currentStrain *= StrainDecayBase;
|
||||
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// 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.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
@@ -16,158 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Rhythm : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 10;
|
||||
protected override double StrainDecayBase => 0;
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
/// <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;
|
||||
private readonly double greatHitWindow;
|
||||
|
||||
/// <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;
|
||||
|
||||
public Rhythm(Mod[] mods)
|
||||
public Rhythm(Mod[] mods, double greatHitWindow)
|
||||
: base(mods)
|
||||
{
|
||||
this.greatHitWindow = greatHitWindow;
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// drum rolls and swells are exempt.
|
||||
if (!(current.BaseObject is Hit))
|
||||
{
|
||||
resetRhythmAndStrain();
|
||||
return 0.0;
|
||||
}
|
||||
double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow);
|
||||
|
||||
currentStrain *= strain_decay;
|
||||
// To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty.
|
||||
double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain
|
||||
difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0);
|
||||
|
||||
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.Index - rhythmHistory[start].Index;
|
||||
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;
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
@@ -18,7 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
private double skillMultiplier => 1.1;
|
||||
private double strainDecayBase => 0.4;
|
||||
|
||||
private readonly bool singleColourStamina;
|
||||
public readonly bool SingleColourStamina;
|
||||
private readonly bool isConvert;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
@@ -27,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="singleColourStamina">Reads when Stamina is from a single coloured pattern.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina)
|
||||
/// <param name="isConvert">Determines if the currently evaluated beatmap is converted.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert)
|
||||
: base(mods)
|
||||
{
|
||||
this.singleColourStamina = singleColourStamina;
|
||||
SingleColourStamina = singleColourStamina;
|
||||
this.isConvert = isConvert;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@@ -42,14 +46,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
|
||||
// Safely prevents previous strains from shifting as new notes are added.
|
||||
var currentObject = current as TaikoDifficultyHitObject;
|
||||
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
|
||||
if (singleColourStamina)
|
||||
return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0));
|
||||
double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
|
||||
|
||||
return currentStrain;
|
||||
if (SingleColourStamina)
|
||||
return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
|
||||
|
||||
return currentStrain * monolengthBonus;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
[JsonProperty("reading_difficulty")]
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the stamina skill.
|
||||
/// </summary>
|
||||
@@ -22,41 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("mono_stamina_factor")]
|
||||
public double MonoStaminaFactor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
[JsonProperty("rhythm_difficult_strains")]
|
||||
public double RhythmTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
[JsonProperty("colour_difficult_strains")]
|
||||
public double ColourTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the hardest parts of the map.
|
||||
/// </summary>
|
||||
[JsonProperty("peak_difficulty")]
|
||||
public double PeakDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("ok_hit_window")]
|
||||
public double OkHitWindow { get; set; }
|
||||
[JsonProperty("stamina_difficult_strains")]
|
||||
public double StaminaTopStrains { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
@@ -64,8 +55,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||
yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor);
|
||||
}
|
||||
|
||||
@@ -74,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||
MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ 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.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
@@ -21,9 +23,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.084375;
|
||||
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||
private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier;
|
||||
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier;
|
||||
|
||||
private double strainLengthBonus;
|
||||
private double patternMultiplier;
|
||||
|
||||
private bool isConvert;
|
||||
|
||||
public override int Version => 20241007;
|
||||
|
||||
@@ -34,12 +42,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Rhythm(mods),
|
||||
new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate),
|
||||
new Reading(mods),
|
||||
new Colour(mods),
|
||||
new Stamina(mods, false),
|
||||
new Stamina(mods, true)
|
||||
new Stamina(mods, false, isConvert),
|
||||
new Stamina(mods, true, isConvert)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,21 +67,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
var difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
var centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
var rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
var noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
|
||||
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
|
||||
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||
{
|
||||
difficultyHitObjects.Add(
|
||||
new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects,
|
||||
centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count)
|
||||
);
|
||||
difficultyHitObjects.Add(new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i],
|
||||
beatmap.HitObjects[i - 1],
|
||||
clockRate,
|
||||
difficultyHitObjects,
|
||||
centreObjects,
|
||||
rimObjects,
|
||||
noteObjects,
|
||||
difficultyHitObjects.Count,
|
||||
beatmap.ControlPointInfo,
|
||||
beatmap.Difficulty.SliderMultiplier
|
||||
));
|
||||
}
|
||||
|
||||
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
|
||||
TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects);
|
||||
|
||||
return difficultyHitObjects;
|
||||
}
|
||||
@@ -77,60 +100,53 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new TaikoDifficultyAttributes { Mods = mods };
|
||||
|
||||
Colour colour = (Colour)skills.First(x => x is Colour);
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
|
||||
bool isRelax = mods.Any(h => h is TaikoModRelax);
|
||||
|
||||
var rhythm = skills.OfType<Rhythm>().Single();
|
||||
var reading = skills.OfType<Reading>().Single();
|
||||
var colour = skills.OfType<Colour>().Single();
|
||||
var stamina = skills.OfType<Stamina>().Single(s => !s.SingleColourStamina);
|
||||
var singleColourStamina = skills.OfType<Stamina>().Single(s => s.SingleColourStamina);
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||
double colourDifficultStrains = colour.CountTopWeightedStrains();
|
||||
double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
|
||||
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
|
||||
|
||||
// As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
|
||||
patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10);
|
||||
|
||||
strainLengthBonus = 1
|
||||
+ Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15)
|
||||
+ Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
|
||||
double starRating = rescale(combinedRating * 1.4);
|
||||
|
||||
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
|
||||
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
|
||||
{
|
||||
starRating *= 0.925;
|
||||
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
|
||||
if (colourRating < 2 && staminaRating > 8)
|
||||
starRating *= 0.80;
|
||||
}
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ReadingDifficulty = readingRating,
|
||||
ColourDifficulty = colourRating,
|
||||
StaminaDifficulty = staminaRating,
|
||||
MonoStaminaFactor = monoStaminaFactor,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||
RhythmTopStrains = rhythmDifficultStrains,
|
||||
ColourTopStrains = colourDifficultStrains,
|
||||
StaminaTopStrains = staminaDifficultStrains,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||
/// </summary>
|
||||
@@ -138,22 +154,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// 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 combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||
var readingPeaks = reading.GetCurrentStrainPeaks().ToList();
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||
|
||||
for (int i = 0; i < colourPeaks.Count; i++)
|
||||
{
|
||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
|
||||
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
|
||||
double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax.
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
|
||||
staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly.
|
||||
|
||||
double peak = norm(1.5, colourPeak, staminaPeak);
|
||||
peak = norm(2, peak, rhythmPeak);
|
||||
double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
|
||||
|
||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// These sections will not contribute to the difficulty.
|
||||
@@ -174,10 +192,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </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);
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private static double rescale(double sr)
|
||||
{
|
||||
if (sr < 0)
|
||||
return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
@@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int countMiss;
|
||||
private double? estimatedUnstableRate;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
public TaikoPerformanceCalculator()
|
||||
@@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
estimatedUnstableRate = computeDeviationUpperBound() * 10;
|
||||
|
||||
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
||||
if (totalSuccessfulHits > 0)
|
||||
@@ -73,7 +93,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
||||
double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0;
|
||||
double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0);
|
||||
|
||||
difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10);
|
||||
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= lengthBonus;
|
||||
@@ -86,9 +109,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (score.Mods.Any(m => m is ModHidden))
|
||||
difficultyValue *= 1.025;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHardRock))
|
||||
difficultyValue *= 1.10;
|
||||
|
||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
|
||||
|
||||
@@ -97,14 +117,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
|
||||
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3);
|
||||
|
||||
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||
{
|
||||
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
if (greatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
return 0;
|
||||
|
||||
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
|
||||
@@ -123,58 +143,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
|
||||
/// two SS scores on the same map with the same settings will always return the same deviation.
|
||||
/// </summary>
|
||||
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
|
||||
private double? computeDeviationUpperBound()
|
||||
{
|
||||
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
|
||||
if (countGreat == 0 || greatHitWindow <= 0)
|
||||
return null;
|
||||
|
||||
double h300 = attributes.GreatHitWindow;
|
||||
double h100 = attributes.OkHitWindow;
|
||||
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
|
||||
double? calcDeviationGreatWindow()
|
||||
{
|
||||
if (countGreat == 0) return null;
|
||||
double n = totalHits;
|
||||
|
||||
double n = totalHits;
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
|
||||
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
|
||||
double? calcDeviationGoodWindow()
|
||||
{
|
||||
if (totalSuccessfulHits == 0) return null;
|
||||
|
||||
double n = totalHits;
|
||||
|
||||
// Proportion of greats + goods hit.
|
||||
double p = totalSuccessfulHits / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
double? deviationGreatWindow = calcDeviationGreatWindow();
|
||||
double? deviationGoodWindow = calcDeviationGoodWindow();
|
||||
|
||||
if (deviationGreatWindow is null)
|
||||
return deviationGoodWindow;
|
||||
|
||||
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// The interface for objects that provide an interval value.
|
||||
/// </summary>
|
||||
public interface IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The interval – ie delta time – between this object and a known previous object.
|
||||
/// </summary>
|
||||
double Interval { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
|
||||
{
|
||||
public static class IntervalGroupingUtils
|
||||
{
|
||||
public static List<List<T>> GroupByInterval<T>(IReadOnlyList<T> objects) where T : IHasInterval
|
||||
{
|
||||
var groups = new List<List<T>>();
|
||||
|
||||
int i = 0;
|
||||
while (i < objects.Count)
|
||||
groups.Add(createNextGroup(objects, ref i));
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static List<T> createNextGroup<T>(IReadOnlyList<T> objects, ref int i) where T : IHasInterval
|
||||
{
|
||||
const double margin_of_error = 5;
|
||||
|
||||
// This never compares the first two elements in the group.
|
||||
// This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329)
|
||||
var groupedObjects = new List<T> { objects[i] };
|
||||
i++;
|
||||
|
||||
for (; i < objects.Count - 1; i++)
|
||||
{
|
||||
if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error))
|
||||
{
|
||||
// When an interval change occurs, include the object with the differing interval in the case it increased
|
||||
// See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale.
|
||||
if (objects[i + 1].Interval > objects[i].Interval + margin_of_error)
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
|
||||
// No interval change occurred
|
||||
groupedObjects.Add(objects[i]);
|
||||
}
|
||||
|
||||
// Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error.
|
||||
// If true, add the current object to the group and increment the index to process the next object.
|
||||
if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error))
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
@@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms() => this.FadeOut();
|
||||
|
||||
public void TriggerResult(bool hit)
|
||||
{
|
||||
HitObject.StartTime = Time.Current;
|
||||
@@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick),
|
||||
_ => new TickPiece());
|
||||
protected override SkinnableDrawable CreateMainPiece() => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (MainPiece != null)
|
||||
Content.Remove(MainPiece, true);
|
||||
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
MainPiece = CreateMainPiece();
|
||||
|
||||
if (MainPiece != null)
|
||||
Content.Add(MainPiece);
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +59,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 350,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.45f,
|
||||
Y = 20,
|
||||
Masking = true,
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
@@ -19,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? osuGame { get; set; }
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -56,6 +61,18 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
|
||||
// on mobile platforms where the base aspect ratio is wider, the taiko playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
if (RuntimeInfo.IsMobile && osuGame != null)
|
||||
{
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
// this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases.
|
||||
const float magic_scale = 1.25f;
|
||||
Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio);
|
||||
}
|
||||
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
channelList.AddChannel(createRandomPrivateChannel());
|
||||
});
|
||||
|
||||
AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel()));
|
||||
|
||||
AddStep("Add Announce Channels", () =>
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
@@ -189,5 +191,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
|
||||
private Channel createRandomTeamChannel()
|
||||
{
|
||||
int id = TestResources.GetNextTestID();
|
||||
return new Channel
|
||||
{
|
||||
Name = $"Team {id}",
|
||||
Type = ChannelType.Team,
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -39,6 +40,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Cached(typeof(BeatmapStore))]
|
||||
private BeatmapStore store;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private OsuTextFlowContainer stats = null!;
|
||||
|
||||
private int beatmapCount;
|
||||
@@ -94,7 +98,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 500,
|
||||
Width = 800,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
@@ -185,6 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
||||
.OrderBy(p => p.Y)
|
||||
.ElementAt(index)
|
||||
.ChildrenOfType<PanelBase>().Single()
|
||||
.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,42 +28,42 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
}
|
||||
@@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
@@ -29,32 +29,32 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
}
|
||||
@@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
@@ -120,18 +120,18 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
|
||||
ClickVisiblePanel<PanelGroup>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<PanelGroup>);
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
|
||||
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<PanelBeatmap>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<PanelBeatmap>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
@@ -171,23 +171,23 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestInputHandlingWithinGaps()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1)));
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2)));
|
||||
ClickVisiblePanelWithOffset<PanelGroup>(0, new Vector2(0, -(PanelGroup.HEIGHT / 2)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
CheckNoSelection();
|
||||
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,27 +213,27 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(2, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + 1)));
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<PanelBeatmap>().Any());
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
// Panels with higher depth will handle clicks in the gutters for simplicity.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 2);
|
||||
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
ClickVisiblePanelWithOffset<PanelBeatmap>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 3);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
|
||||
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value)));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<PanelBeatmap>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private BeatmapInfo beatmap = null!;
|
||||
|
||||
public TestSceneBeatmapCarouselV2DifficultyPanel()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526)
|
||||
?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected)
|
||||
?? TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
beatmap = beatmapSet.Beatmaps.First();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomBeatmap()
|
||||
{
|
||||
AddStep("random beatmap", () =>
|
||||
{
|
||||
var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next());
|
||||
randomSet ??= TestResources.CreateTestBeatmapSetInfo();
|
||||
beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!;
|
||||
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaRuleset()
|
||||
{
|
||||
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap)
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Selected = { Value = true }
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
KeyboardSelected = { Value = true },
|
||||
Selected = { Value = true }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene
|
||||
{
|
||||
public TestSceneBeatmapCarouselV2GroupPanel()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PanelGroup
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
|
||||
},
|
||||
new PanelGroup
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelGroup
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new PanelGroup
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
KeyboardSelected = { Value = true },
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(1, "1"))
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(3, "3")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(5, "5")),
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(7, "7")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(8, "8")),
|
||||
},
|
||||
new PanelGroupStarDifficulty
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition(9, "9")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
public TestSceneBeatmapCarouselV2SetPanel()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526)
|
||||
?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected)
|
||||
?? TestResources.CreateTestBeatmapSetInfo();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomBeatmap()
|
||||
{
|
||||
AddStep("random beatmap", () =>
|
||||
{
|
||||
var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next());
|
||||
randomSet ??= TestResources.CreateTestBeatmapSetInfo();
|
||||
beatmapSet = randomSet;
|
||||
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PanelBeatmapSet
|
||||
{
|
||||
Item = new CarouselItem(beatmapSet)
|
||||
},
|
||||
new PanelBeatmapSet
|
||||
{
|
||||
Item = new CarouselItem(beatmapSet),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelBeatmapSet
|
||||
{
|
||||
Item = new CarouselItem(beatmapSet),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new PanelBeatmapSet
|
||||
{
|
||||
Item = new CarouselItem(beatmapSet),
|
||||
KeyboardSelected = { Value = true },
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private BeatmapInfo beatmap = null!;
|
||||
|
||||
public TestSceneBeatmapCarouselV2StandalonePanel()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526)
|
||||
?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected)
|
||||
?? TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
beatmap = beatmapSet.Beatmaps.First();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomBeatmap()
|
||||
{
|
||||
AddStep("random beatmap", () =>
|
||||
{
|
||||
var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next());
|
||||
randomSet ??= TestResources.CreateTestBeatmapSetInfo();
|
||||
beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!;
|
||||
|
||||
CreateThemedContent(OverlayColourScheme.Aquamarine);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaRuleset()
|
||||
{
|
||||
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap)
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Selected = { Value = true }
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
KeyboardSelected = { Value = true },
|
||||
Selected = { Value = true }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,27 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.SelectV2.Footer;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -30,6 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Cached]
|
||||
private readonly OsuLogo logo;
|
||||
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
public TestSceneSongSelect()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
@@ -49,6 +65,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, IAPIProvider onlineAPI)
|
||||
{
|
||||
BeatmapStore beatmapStore;
|
||||
BeatmapUpdater beatmapUpdater;
|
||||
BeatmapDifficultyCache difficultyCache;
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache));
|
||||
Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage));
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
|
||||
beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope);
|
||||
|
||||
MusicController music;
|
||||
Dependencies.Cache(music = new MusicController());
|
||||
|
||||
// required to get bindables attached
|
||||
Add(difficultyCache);
|
||||
Add(music);
|
||||
Add(beatmapStore);
|
||||
|
||||
Dependencies.Cache(new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -64,6 +109,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2()));
|
||||
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded);
|
||||
AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesets()
|
||||
{
|
||||
AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||
AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
|
||||
AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
#region Footer
|
||||
@@ -80,8 +135,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddStep("modified", () => SelectedMods.Value = new List<Mod> { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + one", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + two", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + three", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + four", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + three",
|
||||
() => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
AddStep("modified + four",
|
||||
() => SelectedMods.Value = new List<Mod>
|
||||
{ new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddWaitStep("wait", 3);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene
|
||||
{
|
||||
private UpdateBeatmapSetButton button = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = button = new UpdateBeatmapSetButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmap()
|
||||
{
|
||||
AddStep("null beatmap", () => button.BeatmapSet = null);
|
||||
AddAssert("button invisible", () => button.Alpha == 0f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUpdatedBeatmap()
|
||||
{
|
||||
AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Beatmaps = { new BeatmapInfo() }
|
||||
});
|
||||
AddAssert("button invisible", () => button.Alpha == 0f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonUpdatedBeatmap()
|
||||
{
|
||||
AddStep("non-updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Beatmaps =
|
||||
{
|
||||
new BeatmapInfo
|
||||
{
|
||||
MD5Hash = "test",
|
||||
OnlineMD5Hash = "online",
|
||||
LastOnlineUpdate = DateTimeOffset.Now,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("button visible", () => button.Alpha == 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@@ -220,6 +221,29 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestResetToDefaultViaDoubleClickingNub()
|
||||
{
|
||||
setBeatmapWithDifficultyParameters(5);
|
||||
|
||||
setSliderValue("Circle Size", 3);
|
||||
setExtendedLimits(true);
|
||||
|
||||
checkSliderAtValue("Circle Size", 3);
|
||||
checkBindableAtValue("Circle Size", 3);
|
||||
|
||||
AddStep("double click circle size nub", () =>
|
||||
{
|
||||
var nub = this.ChildrenOfType<RoundedSliderBar<float>.SliderNub>().First();
|
||||
InputManager.MoveMouseTo(nub);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
checkSliderAtValue("Circle Size", 5);
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSettingChangeTracker()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Tournament.Models;
|
||||
@@ -35,12 +36,20 @@ namespace osu.Game.Tournament.Components
|
||||
Size = new Vector2(75, 54);
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
Child = flagSprite = new Sprite
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.FromHex("333"),
|
||||
},
|
||||
flagSprite = new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fit
|
||||
},
|
||||
};
|
||||
|
||||
(flag = team.FlagName.GetBoundCopy()).BindValueChanged(_ => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true);
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
dotSize = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateDotDimensions();
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,27 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
dotSpacing = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateDotDimensions();
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
private IBeatmapSetInfo? beatmapSet;
|
||||
|
||||
public IBeatmapSetInfo? BeatmapSet
|
||||
{
|
||||
get => beatmapSet;
|
||||
set
|
||||
{
|
||||
beatmapSet = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly FillFlowContainer<RulesetDifficultyGroup> flow;
|
||||
|
||||
public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet)
|
||||
public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
@@ -59,25 +73,31 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Direction = FillDirection.Horizontal,
|
||||
};
|
||||
|
||||
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
|
||||
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
|
||||
|
||||
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
|
||||
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed));
|
||||
BeatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateDotDimensions();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
private void updateDotDimensions()
|
||||
private void updateDisplay()
|
||||
{
|
||||
foreach (var group in flow)
|
||||
flow.Clear();
|
||||
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
|
||||
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
|
||||
|
||||
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
|
||||
{
|
||||
group.DotSize = DotSize;
|
||||
group.DotSpacing = DotSpacing;
|
||||
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize)
|
||||
{
|
||||
Spacing = new Vector2(DotSpacing, 0f),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,26 +106,14 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
private readonly int rulesetId;
|
||||
private readonly IEnumerable<IBeatmapInfo> beatmapInfos;
|
||||
private readonly bool collapsed;
|
||||
private readonly Vector2 dotSize;
|
||||
|
||||
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed)
|
||||
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed, Vector2 dotSize)
|
||||
{
|
||||
this.rulesetId = rulesetId;
|
||||
this.beatmapInfos = beatmapInfos;
|
||||
this.collapsed = collapsed;
|
||||
}
|
||||
|
||||
public Vector2 DotSize
|
||||
{
|
||||
set
|
||||
{
|
||||
foreach (var dot in Children.OfType<DifficultyDot>())
|
||||
dot.Size = value;
|
||||
}
|
||||
}
|
||||
|
||||
public float DotSpacing
|
||||
{
|
||||
set => Spacing = new Vector2(value, 0);
|
||||
this.dotSize = dotSize;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -125,7 +133,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
if (!collapsed)
|
||||
{
|
||||
foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating))
|
||||
Add(new DifficultyDot(beatmapInfo.StarRating));
|
||||
Add(new DifficultyDot(beatmapInfo.StarRating, dotSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -145,9 +153,10 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
private readonly double starDifficulty;
|
||||
|
||||
public DifficultyDot(double starDifficulty)
|
||||
public DifficultyDot(double starDifficulty, Vector2 dotSize)
|
||||
{
|
||||
this.starDifficulty = starDifficulty;
|
||||
Size = dotSize;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
@@ -52,10 +53,21 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The action to use to reset the value of <see cref="SliderBar{T}.Current"/> to the default.
|
||||
/// Triggered on double click.
|
||||
/// </summary>
|
||||
public Action ResetToDefault { get; internal set; }
|
||||
|
||||
public RoundedSliderBar()
|
||||
{
|
||||
Height = Nub.HEIGHT;
|
||||
RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2;
|
||||
ResetToDefault = () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.SetDefault();
|
||||
};
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@@ -102,11 +114,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
OnDoubleClicked = () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.SetDefault();
|
||||
},
|
||||
OnDoubleClicked = () => ResetToDefault.Invoke(),
|
||||
},
|
||||
},
|
||||
hoverClickSounds = new HoverClickSounds()
|
||||
|
||||
@@ -25,9 +25,9 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse");
|
||||
|
||||
/// <summary>
|
||||
/// "Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as "Raw Input"."
|
||||
/// "Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"."
|
||||
/// </summary>
|
||||
public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as ""Raw Input"".");
|
||||
public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input"".");
|
||||
|
||||
/// <summary>
|
||||
/// "Confine mouse cursor to window"
|
||||
|
||||
@@ -14,5 +14,6 @@ namespace osu.Game.Online.Chat
|
||||
Group,
|
||||
System,
|
||||
Announce,
|
||||
Team,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +271,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(-10, 0),
|
||||
Direction = FillDirection.Horizontal,
|
||||
ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) })
|
||||
},
|
||||
@@ -394,7 +395,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = statistic.Value,
|
||||
Spacing = new Vector2(-1, 0),
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true)
|
||||
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold, fixedWidth: true)
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
public DateLabel(DateTimeOffset date)
|
||||
: base(date)
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true);
|
||||
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold);
|
||||
}
|
||||
|
||||
protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30));
|
||||
|
||||
@@ -51,6 +51,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserKicked;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the room's host is changed.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser?>? HostChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new item is added to the playlist.
|
||||
/// </summary>
|
||||
@@ -531,6 +536,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Host = user;
|
||||
APIRoom.Host = user?.User;
|
||||
|
||||
HostChanged?.Invoke(user);
|
||||
RoomUpdated?.Invoke();
|
||||
}, false);
|
||||
|
||||
|
||||
+1
-1
@@ -818,7 +818,7 @@ namespace osu.Game
|
||||
/// Adjust the globally applied <see cref="DrawSizePreservingFillContainer.TargetDrawSize"/> in every <see cref="ScalingContainer"/>.
|
||||
/// Useful for changing how the game handles different aspect ratios.
|
||||
/// </summary>
|
||||
protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768);
|
||||
public virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768);
|
||||
|
||||
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
@@ -39,6 +40,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
|
||||
public ChannelGroup AnnounceChannelGroup { get; private set; } = null!;
|
||||
public ChannelGroup PublicChannelGroup { get; private set; } = null!;
|
||||
public ChannelGroup TeamChannelGroup { get; private set; } = null!;
|
||||
public ChannelGroup PrivateChannelGroup { get; private set; } = null!;
|
||||
|
||||
private OsuScrollContainer scroll = null!;
|
||||
@@ -79,10 +81,12 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
},
|
||||
AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
|
||||
PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
|
||||
// cross-reference for icons: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19
|
||||
AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false),
|
||||
PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false),
|
||||
selector = new ChannelListItem(ChannelListingChannel),
|
||||
PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
|
||||
TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available
|
||||
PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,6 +106,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
};
|
||||
|
||||
selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public void AddChannel(Channel channel)
|
||||
@@ -109,9 +114,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
if (channelMap.ContainsKey(channel))
|
||||
return;
|
||||
|
||||
ChannelListItem item = new ChannelListItem(channel);
|
||||
ChannelListItem item = new ChannelListItem(channel)
|
||||
{
|
||||
CanLeave = channel.Type != ChannelType.Team
|
||||
};
|
||||
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
|
||||
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
|
||||
if (item.CanLeave)
|
||||
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
|
||||
|
||||
ChannelGroup group = getGroupFromChannel(channel);
|
||||
channelMap.Add(channel, item);
|
||||
@@ -156,6 +165,9 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
case ChannelType.Announce:
|
||||
return AnnounceChannelGroup;
|
||||
|
||||
case ChannelType.Team:
|
||||
return TeamChannelGroup;
|
||||
|
||||
default:
|
||||
return PublicChannelGroup;
|
||||
}
|
||||
@@ -163,10 +175,8 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
|
||||
private void updateVisibility()
|
||||
{
|
||||
if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
|
||||
AnnounceChannelGroup.Hide();
|
||||
else
|
||||
AnnounceChannelGroup.Show();
|
||||
AnnounceChannelGroup.Alpha = AnnounceChannelGroup.ItemFlow.Any() ? 1 : 0;
|
||||
TeamChannelGroup.Alpha = TeamChannelGroup.ItemFlow.Any() ? 1 : 0;
|
||||
}
|
||||
|
||||
public partial class ChannelGroup : FillFlowContainer
|
||||
@@ -174,7 +184,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
private readonly bool sortByRecent;
|
||||
public readonly ChannelListItemFlow ItemFlow;
|
||||
|
||||
public ChannelGroup(LocalisableString label, bool sortByRecent)
|
||||
public ChannelGroup(LocalisableString label, IconUsage icon, bool sortByRecent)
|
||||
{
|
||||
this.sortByRecent = sortByRecent;
|
||||
Direction = FillDirection.Vertical;
|
||||
@@ -184,11 +194,26 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new FillFlowContainer
|
||||
{
|
||||
Text = label,
|
||||
Margin = new MarginPadding { Left = 18, Bottom = 5 },
|
||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = label,
|
||||
Margin = new MarginPadding { Left = 18, Bottom = 5 },
|
||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = icon,
|
||||
Size = new Vector2(12),
|
||||
},
|
||||
}
|
||||
},
|
||||
ItemFlow = new ChannelListItemFlow(sortByRecent)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
public partial class ChannelListItem : OsuClickableContainer, IFilterable
|
||||
{
|
||||
public event Action<Channel>? OnRequestSelect;
|
||||
|
||||
public bool CanLeave { get; init; } = true;
|
||||
public event Action<Channel>? OnRequestLeave;
|
||||
|
||||
public readonly Channel Channel;
|
||||
@@ -160,7 +162,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
|
||||
private ChannelListItemCloseButton? createCloseButton()
|
||||
{
|
||||
if (isSelector)
|
||||
if (isSelector || !CanLeave)
|
||||
return null;
|
||||
|
||||
return new ChannelListItemCloseButton
|
||||
|
||||
@@ -228,7 +228,8 @@ namespace osu.Game.Overlays
|
||||
return true;
|
||||
|
||||
case PlatformAction.DocumentClose:
|
||||
channelManager.LeaveChannel(currentChannel.Value);
|
||||
if (currentChannel.Value?.Type != ChannelType.Team)
|
||||
channelManager.LeaveChannel(currentChannel.Value);
|
||||
return true;
|
||||
|
||||
case PlatformAction.TabRestore:
|
||||
|
||||
@@ -17,19 +17,15 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
protected const int ATTRIB_ID_AIM = 1;
|
||||
protected const int ATTRIB_ID_SPEED = 3;
|
||||
protected const int ATTRIB_ID_OVERALL_DIFFICULTY = 5;
|
||||
protected const int ATTRIB_ID_APPROACH_RATE = 7;
|
||||
protected const int ATTRIB_ID_MAX_COMBO = 9;
|
||||
protected const int ATTRIB_ID_DIFFICULTY = 11;
|
||||
protected const int ATTRIB_ID_GREAT_HIT_WINDOW = 13;
|
||||
protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15;
|
||||
protected const int ATTRIB_ID_FLASHLIGHT = 17;
|
||||
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
||||
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
|
||||
protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23;
|
||||
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
|
||||
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
|
||||
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
|
||||
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
|
||||
|
||||
/// <summary>
|
||||
/// The mods which were applied to the beatmap.
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
{
|
||||
public static class DifficultyCalculationUtils
|
||||
public static partial class DifficultyCalculationUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts BPM value into milliseconds
|
||||
@@ -46,5 +47,60 @@ namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
/// <param name="exponent">Exponent</param>
|
||||
/// <returns>The output of logistic function</returns>
|
||||
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics))
|
||||
/// </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>
|
||||
/// <returns>The <i>p</i>-norm of the vector.</returns>
|
||||
public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function)
|
||||
/// </summary>
|
||||
/// <param name="x">Value to calculate the function for</param>
|
||||
/// <param name="mean">The mean (center) of the bell curve</param>
|
||||
/// <param name="width">The width (spread) of the curve</param>
|
||||
/// <param name="multiplier">Multiplier to adjust the curve's height</param>
|
||||
/// <returns>The output of the bell curve function of <paramref name="x"/></returns>
|
||||
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
|
||||
|
||||
/// <summary>
|
||||
/// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep)
|
||||
/// </summary>
|
||||
/// <param name="x">Value to calculate the function for</param>
|
||||
/// <param name="start">Value at which function returns 0</param>
|
||||
/// <param name="end">Value at which function returns 1</param>
|
||||
public static double Smoothstep(double x, double start, double end)
|
||||
{
|
||||
x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
|
||||
|
||||
return x * x * (3.0 - 2.0 * x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
|
||||
/// </summary>
|
||||
/// <param name="x">Value to calculate the function for</param>
|
||||
/// <param name="start">Value at which function returns 0</param>
|
||||
/// <param name="end">Value at which function returns 1</param>
|
||||
public static double Smootherstep(double x, double start, double end)
|
||||
{
|
||||
x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
|
||||
|
||||
return x * x * x * (x * (6.0 * x - 15.0) + 10.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverse linear interpolation function (https://en.wikipedia.org/wiki/Linear_interpolation)
|
||||
/// </summary>
|
||||
/// <param name="x">Value to calculate the function for</param>
|
||||
/// <param name="start">Value at which function returns 0</param>
|
||||
/// <param name="end">Value at which function returns 1</param>
|
||||
public static double ReverseLerp(double x, double start, double end)
|
||||
{
|
||||
return Math.Clamp((x - start) / (end - start), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -3,7 +3,6 @@
|
||||
|
||||
// All code is referenced from the following:
|
||||
// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs
|
||||
// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs
|
||||
|
||||
/*
|
||||
Copyright (c) 2002-2022 Math.NET
|
||||
@@ -14,12 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Utils
|
||||
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
{
|
||||
public class SpecialFunctions
|
||||
public partial class DifficultyCalculationUtils
|
||||
{
|
||||
private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d;
|
||||
|
||||
/// <summary>
|
||||
/// **************************************
|
||||
/// COEFFICIENTS FOR METHOD ErfImp *
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
|
||||
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
});
|
||||
|
||||
DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing;
|
||||
DistanceSpacingMultiplier.Value = EditorBeatmap.DistanceSpacing;
|
||||
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
|
||||
{
|
||||
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
|
||||
@@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (multiplier.NewValue != multiplier.OldValue)
|
||||
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
|
||||
|
||||
editorBeatmap.DistanceSpacing = multiplier.NewValue;
|
||||
EditorBeatmap.DistanceSpacing = multiplier.NewValue;
|
||||
}, true);
|
||||
|
||||
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);
|
||||
@@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null)
|
||||
{
|
||||
return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
|
||||
return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
|
||||
/ beatSnapProvider.BeatDivisor);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider);
|
||||
|
||||
protected virtual RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new RoundedSliderBar<float>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
protected virtual RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new RoundedSliderBar<float>();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against beatmap values displayed on slider bars being transferred to user override.
|
||||
@@ -111,7 +106,21 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
createSlider(currentNumber)
|
||||
createSlider(currentNumber).With(slider =>
|
||||
{
|
||||
slider.RelativeSizeAxes = Axes.X;
|
||||
slider.Current = currentNumber;
|
||||
slider.KeyboardStep = 0.1f;
|
||||
// this looks redundant, but isn't because of the various games this component plays
|
||||
// (`Current` is nullable and represents the underlying setting value,
|
||||
// `currentNumber` is not nullable and represents what is getting displayed,
|
||||
// therefore without this, double-clicking the slider would reset `currentNumber` to its bogus default of 0).
|
||||
slider.ResetToDefault = () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.SetDefault();
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
@@ -247,7 +247,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
// It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked
|
||||
// location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead
|
||||
// of this can be quite crippling.
|
||||
entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value;
|
||||
//
|
||||
// However, additionally do not attempt to alter lifetime of judged entries.
|
||||
// This is to prevent freak accidents like objects suddenly becoming alive because of this estimate assigning a later lifetime
|
||||
// than the object itself decided it should have when it underwent judgement.
|
||||
if (!entry.Judged)
|
||||
entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value;
|
||||
}
|
||||
|
||||
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
// in case 2, we want *flooring* to occur, to prevent a possible off-by-one
|
||||
// because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error.
|
||||
// the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found.
|
||||
if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005))
|
||||
if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01))
|
||||
beatIndex = (int)Math.Floor(fractionalBeatIndex);
|
||||
|
||||
var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);
|
||||
|
||||
@@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localHash != onlineHash)
|
||||
if (!localHash.Equals(onlineHash, StringComparison.OrdinalIgnoreCase))
|
||||
filesToUpdate.Add(filename);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@@ -20,7 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private Sample? userJoinedSample;
|
||||
private Sample? userLeftSample;
|
||||
private Sample? userKickedSample;
|
||||
private MultiplayerRoomUser? host;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
@@ -35,25 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.UserJoined += onUserJoined;
|
||||
client.UserLeft += onUserLeft;
|
||||
client.UserKicked += onUserKicked;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (EqualityComparer<MultiplayerRoomUser>.Default.Equals(host, client.Room?.Host))
|
||||
return;
|
||||
|
||||
// only play sound when the host changes from an already-existing host.
|
||||
if (host != null)
|
||||
Scheduler.AddOnce(() => hostChangedSample?.Play());
|
||||
|
||||
host = client.Room?.Host;
|
||||
client.HostChanged += onHostChanged;
|
||||
}
|
||||
|
||||
private void onUserJoined(MultiplayerRoomUser user)
|
||||
@@ -65,16 +48,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private void onUserKicked(MultiplayerRoomUser user)
|
||||
=> Scheduler.AddOnce(() => userKickedSample?.Play());
|
||||
|
||||
private void onHostChanged(MultiplayerRoomUser? host)
|
||||
{
|
||||
if (host != null)
|
||||
Scheduler.AddOnce(() => hostChangedSample?.Play());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.UserJoined -= onUserJoined;
|
||||
client.UserLeft -= onUserLeft;
|
||||
client.UserKicked -= onUserKicked;
|
||||
client.HostChanged -= onHostChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -128,8 +129,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
get => mute;
|
||||
set
|
||||
{
|
||||
if (mute == value)
|
||||
return;
|
||||
|
||||
mute = value;
|
||||
volumeAdjustment.Value = value ? 0 : 1;
|
||||
Logger.Log($"{(mute ? "muting" : "unmuting")} player {UserId}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +154,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private partial class PlayerIsolationContainer : Container
|
||||
{
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<WorkingBeatmap>))]
|
||||
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
|
||||
private readonly Bindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>();
|
||||
|
||||
public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList<Mod> mods)
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Skinning;
|
||||
@@ -40,10 +42,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(UserLookupCache userLookupCache)
|
||||
{
|
||||
if (gameplayState != null)
|
||||
flag.Team = gameplayState.Score.ScoreInfo.User.Team;
|
||||
{
|
||||
if (gameplayState.Score.ScoreInfo.User.Team != null)
|
||||
flag.Team = gameplayState.Score.ScoreInfo.User.Team;
|
||||
else
|
||||
{
|
||||
// We only store very basic information about a user to realm, so there's a high chance we don't have the team information.
|
||||
userLookupCache.GetUserAsync(gameplayState.Score.ScoreInfo.User.Id)
|
||||
.ContinueWith(task => Schedule(() => flag.Team = task.GetResultSafely()?.Team));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
apiUser = api.LocalUser.GetBoundCopy();
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -55,7 +54,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
if (EffectPoint.KiaiMode && !isTriggered)
|
||||
{
|
||||
Logger.Log("shooting");
|
||||
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500;
|
||||
if (isNearEffectPoint)
|
||||
Shoot();
|
||||
|
||||
@@ -317,6 +317,8 @@ namespace osu.Game.Screens.Ranking
|
||||
if (!this.IsCurrentScreen() || s != rankApplauseSound)
|
||||
return;
|
||||
|
||||
AddInternal(rankApplauseSound);
|
||||
|
||||
rankApplauseSound.VolumeTo(applause_volume);
|
||||
rankApplauseSound.Play();
|
||||
});
|
||||
|
||||
@@ -267,9 +267,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#region Drawable pooling
|
||||
|
||||
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
|
||||
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
|
||||
private readonly DrawablePool<GroupPanel> groupPanelPool = new DrawablePool<GroupPanel>(100);
|
||||
private readonly DrawablePool<PanelBeatmap> beatmapPanelPool = new DrawablePool<PanelBeatmap>(100);
|
||||
private readonly DrawablePool<PanelBeatmapSet> setPanelPool = new DrawablePool<PanelBeatmapSet>(100);
|
||||
private readonly DrawablePool<PanelGroup> groupPanelPool = new DrawablePool<PanelGroup>(100);
|
||||
|
||||
private void setupPools()
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
addItem(new CarouselItem(newGroup)
|
||||
{
|
||||
DrawHeight = GroupPanel.HEIGHT,
|
||||
DrawHeight = PanelGroup.HEIGHT,
|
||||
DepthLayer = -2,
|
||||
});
|
||||
}
|
||||
@@ -85,7 +85,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = BeatmapSetPanel.HEIGHT,
|
||||
DrawHeight = PanelBeatmapSet.HEIGHT,
|
||||
DepthLayer = -1
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,119 +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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private Box activationFlash = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = DrawRectangle;
|
||||
|
||||
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
|
||||
//
|
||||
// Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
|
||||
// larger hit target.
|
||||
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING });
|
||||
|
||||
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT);
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Aqua.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
activationFlash = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
};
|
||||
|
||||
Selected.BindValueChanged(value =>
|
||||
{
|
||||
activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
{
|
||||
BorderThickness = 5;
|
||||
BorderColour = Color4.Pink;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)";
|
||||
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
carousel.Activate(Item!);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,104 +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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(500, HEIGHT);
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.Yellow.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
};
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
{
|
||||
BorderThickness = 5;
|
||||
BorderColour = Color4.Pink;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var beatmapSetInfo = (BeatmapSetInfo)Item.Model;
|
||||
|
||||
text.Text = $"{beatmapSetInfo.Metadata}";
|
||||
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
carousel.Activate(Item!);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapSetPanelBackground : ModelBackedDrawable<WorkingBeatmap>
|
||||
{
|
||||
protected override bool TransformImmediately => true;
|
||||
|
||||
public WorkingBeatmap? Beatmap
|
||||
{
|
||||
get => Model;
|
||||
set => Model = value;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model);
|
||||
|
||||
private partial class BackgroundSprite : CompositeDrawable
|
||||
{
|
||||
private readonly WorkingBeatmap? working;
|
||||
|
||||
public BackgroundSprite(WorkingBeatmap? working)
|
||||
{
|
||||
this.working = working;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
var texture = working?.GetPanelBackground();
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
Texture = texture,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Depth = -1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
|
||||
Shear = new Vector2(0.8f, 0),
|
||||
Alpha = 0.5f,
|
||||
Children = new[]
|
||||
{
|
||||
// The left half with no gradient applied
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Width = 0.4f,
|
||||
},
|
||||
// Piecewise-linear gradient with 3 segments to make it appear smoother
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
|
||||
Width = 0.05f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
|
||||
Width = 0.2f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
|
||||
Width = 0.05f,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalChild = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background6,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public sealed class CarouselItem : IComparable<CarouselItem>
|
||||
{
|
||||
public const float DEFAULT_HEIGHT = 40;
|
||||
public const float DEFAULT_HEIGHT = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The model this item is representing.
|
||||
|
||||
@@ -1,117 +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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class GroupPanel : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private Box activationFlash = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(500, HEIGHT);
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
activationFlash = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
};
|
||||
|
||||
Selected.BindValueChanged(value =>
|
||||
{
|
||||
activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
{
|
||||
BorderThickness = 5;
|
||||
BorderColour = Color4.Pink;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
text.Text = group.Title;
|
||||
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
carousel.Activate(Item!);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
private const float corner_radius = 10;
|
||||
|
||||
private const float left_edge_x_offset = 20f;
|
||||
private const float keyboard_active_x_offset = 25f;
|
||||
private const float active_x_offset = 50f;
|
||||
|
||||
private const float duration = 500;
|
||||
|
||||
protected float PanelXOffset { get; init; }
|
||||
|
||||
private Box backgroundBorder = null!;
|
||||
private Box backgroundGradient = null!;
|
||||
private Box backgroundAccentGradient = null!;
|
||||
private Container backgroundLayer = null!;
|
||||
private Container backgroundLayerHorizontalPadding = null!;
|
||||
private Container backgroundContainer = null!;
|
||||
private Container iconContainer = null!;
|
||||
private Box activationFlash = null!;
|
||||
private Box hoverLayer = null!;
|
||||
|
||||
public Container TopLevelContent { get; private set; } = null!;
|
||||
|
||||
protected Container Content { get; private set; } = null!;
|
||||
|
||||
public Drawable Background { set => backgroundContainer.Child = value; }
|
||||
|
||||
public Drawable Icon { set => iconContainer.Child = value; }
|
||||
|
||||
private Color4? accentColour;
|
||||
|
||||
public Color4? AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
Anchor = Anchor.TopRight;
|
||||
Origin = Anchor.TopRight;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = CarouselItem.DEFAULT_HEIGHT;
|
||||
|
||||
InternalChild = TopLevelContent = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
X = corner_radius,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(1f),
|
||||
Radius = 10,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new BufferedContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
backgroundBorder = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
},
|
||||
backgroundLayerHorizontalPadding = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = backgroundLayer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
backgroundGradient = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
backgroundAccentGradient = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
backgroundContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
iconContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = PanelXOffset + corner_radius },
|
||||
},
|
||||
hoverLayer = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
activationFlash = new Box
|
||||
{
|
||||
Colour = Color4.White.Opacity(0.4f),
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new HoverSounds(),
|
||||
}
|
||||
};
|
||||
|
||||
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
|
||||
backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(_ => updateDisplay());
|
||||
KeyboardSelected.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
this.FadeInFromZero(duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel? carousel { get; set; }
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
activationFlash.FadeOutFromOne(500, Easing.OutQuint);
|
||||
carousel?.Activate(Item!);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint);
|
||||
|
||||
var backgroundColour = accentColour ?? Color4.White;
|
||||
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
|
||||
|
||||
backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint);
|
||||
backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint);
|
||||
|
||||
TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint);
|
||||
|
||||
updateXOffset();
|
||||
updateHover();
|
||||
}
|
||||
|
||||
private void updateXOffset()
|
||||
{
|
||||
float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset;
|
||||
|
||||
if (Expanded.Value)
|
||||
x -= active_x_offset;
|
||||
|
||||
if (KeyboardSelected.Value)
|
||||
x -= keyboard_active_x_offset;
|
||||
|
||||
this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateHover()
|
||||
{
|
||||
bool hovered = IsHovered || KeyboardSelected.Value;
|
||||
|
||||
if (hovered)
|
||||
hoverLayer.FadeIn(100, Easing.OutQuint);
|
||||
else
|
||||
hoverLayer.FadeOut(1000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateDisplay();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth };
|
||||
backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth };
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public virtual void Activated()
|
||||
{
|
||||
activationFlash.FadeOutFromOne(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmap : PanelBase
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT;
|
||||
|
||||
private StarCounter starCounter = null!;
|
||||
private ConstrainedIconContainer difficultyIcon = null!;
|
||||
private OsuSpriteText keyCountText = null!;
|
||||
private StarRatingDisplay starRatingDisplay = null!;
|
||||
private TopLocalRank difficultyRank = null!;
|
||||
private OsuSpriteText difficultyText = null!;
|
||||
private OsuSpriteText authorText = null!;
|
||||
|
||||
private IBindable<StarDifficulty?>? starDifficultyBindable;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = DrawRectangle;
|
||||
|
||||
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
|
||||
//
|
||||
// Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
|
||||
// larger hit target.
|
||||
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING });
|
||||
|
||||
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Height = HEIGHT;
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
|
||||
Content.Children = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding { Left = 10f },
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(3, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
difficultyRank = new TopLocalRank
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.75f)
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.4f)
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 8f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ =>
|
||||
{
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
});
|
||||
|
||||
mods.BindValueChanged(_ =>
|
||||
{
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
}, true);
|
||||
|
||||
Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon();
|
||||
|
||||
difficultyRank.Beatmap = beatmap;
|
||||
difficultyText.Text = beatmap.DifficultyName;
|
||||
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
|
||||
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
difficultyRank.Beatmap = null;
|
||||
starDifficultyBindable = null;
|
||||
}
|
||||
|
||||
private void computeStarRating()
|
||||
{
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token);
|
||||
starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
{
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
|
||||
|
||||
keyCountText.Alpha = 1;
|
||||
keyCountText.Text = $"[{keyCount}K] ";
|
||||
}
|
||||
else
|
||||
keyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
var starDifficulty = starDifficultyBindable?.Value ?? default;
|
||||
|
||||
starRatingDisplay.Current.Value = starDifficulty;
|
||||
starCounter.Current = (float)starDifficulty.Stars;
|
||||
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
|
||||
var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
|
||||
AccentColour = starRatingColour;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapSet : PanelBase
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
|
||||
private BeatmapSetPanelBackground background = null!;
|
||||
|
||||
private OsuSpriteText titleText = null!;
|
||||
private OsuSpriteText artistText = null!;
|
||||
private Drawable chevronIcon = null!;
|
||||
private UpdateBeatmapSetButton updateButton = null!;
|
||||
private BeatmapSetOnlineStatusPill statusPill = null!;
|
||||
private DifficultySpectrumDisplay difficultiesDisplay = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
public PanelBeatmapSet()
|
||||
{
|
||||
PanelXOffset = 20f;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Height = HEIGHT;
|
||||
|
||||
Icon = chevronIcon = new Container
|
||||
{
|
||||
Size = new Vector2(22),
|
||||
Child = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(12),
|
||||
X = 1f,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
};
|
||||
|
||||
Background = background = new BeatmapSetPanelBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Content.Children = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new UpdateBeatmapSetButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Top = -2f },
|
||||
},
|
||||
statusPill = new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultiesDisplay = new DifficultySpectrumDisplay
|
||||
{
|
||||
DotSize = new Vector2(5, 10),
|
||||
DotSpacing = 2,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(_ => onExpanded(), true);
|
||||
KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true);
|
||||
}
|
||||
|
||||
private void onExpanded()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint);
|
||||
chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var beatmapSet = (BeatmapSetInfo)Item.Model;
|
||||
|
||||
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
|
||||
background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID));
|
||||
|
||||
titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title);
|
||||
artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist);
|
||||
updateButton.BeatmapSet = beatmapSet;
|
||||
statusPill.Status = beatmapSet.Status;
|
||||
difficultiesDisplay.BeatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
background.Beatmap = null;
|
||||
updateButton.BeatmapSet = null;
|
||||
difficultiesDisplay.BeatmapSet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapStandalone : PanelBase
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||
|
||||
private IBindable<StarDifficulty?>? starDifficultyBindable;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
private BeatmapSetPanelBackground background = null!;
|
||||
|
||||
private OsuSpriteText titleText = null!;
|
||||
private OsuSpriteText artistText = null!;
|
||||
private UpdateBeatmapSetButton updateButton = null!;
|
||||
private BeatmapSetOnlineStatusPill statusPill = null!;
|
||||
|
||||
private ConstrainedIconContainer difficultyIcon = null!;
|
||||
private FillFlowContainer difficultyLine = null!;
|
||||
private StarRatingDisplay difficultyStarRating = null!;
|
||||
private TopLocalRank difficultyRank = null!;
|
||||
private OsuSpriteText difficultyKeyCountText = null!;
|
||||
private OsuSpriteText difficultyName = null!;
|
||||
private OsuSpriteText difficultyAuthor = null!;
|
||||
|
||||
public PanelBeatmapStandalone()
|
||||
{
|
||||
PanelXOffset = 20;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Height = HEIGHT;
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
|
||||
Background = background = new BeatmapSetPanelBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Content.Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Shadow = true,
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Shadow = true,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new UpdateBeatmapSetButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Top = -2f },
|
||||
},
|
||||
statusPill = new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyLine = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(8f / 9f),
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyRank = new TopLocalRank
|
||||
{
|
||||
Scale = new Vector2(8f / 11),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyKeyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Bottom = 2f },
|
||||
},
|
||||
difficultyName = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
},
|
||||
difficultyAuthor = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ =>
|
||||
{
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
});
|
||||
|
||||
mods.BindValueChanged(_ =>
|
||||
{
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
}, true);
|
||||
|
||||
Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
var beatmapSet = beatmap.BeatmapSet!;
|
||||
|
||||
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
|
||||
background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID));
|
||||
|
||||
titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title);
|
||||
artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist);
|
||||
updateButton.BeatmapSet = beatmapSet;
|
||||
statusPill.Status = beatmapSet.Status;
|
||||
|
||||
difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon();
|
||||
difficultyIcon.Show();
|
||||
|
||||
difficultyRank.Beatmap = beatmap;
|
||||
difficultyName.Text = beatmap.DifficultyName;
|
||||
difficultyAuthor.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
|
||||
difficultyLine.Show();
|
||||
|
||||
computeStarRating();
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
background.Beatmap = null;
|
||||
updateButton.BeatmapSet = null;
|
||||
difficultyRank.Beatmap = null;
|
||||
starDifficultyBindable = null;
|
||||
}
|
||||
|
||||
private void computeStarRating()
|
||||
{
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token);
|
||||
starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
{
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
|
||||
|
||||
difficultyKeyCountText.Alpha = 1;
|
||||
difficultyKeyCountText.Text = $"[{keyCount}K] ";
|
||||
}
|
||||
else
|
||||
difficultyKeyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
var starDifficulty = starDifficultyBindable?.Value ?? default;
|
||||
|
||||
AccentColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
difficultyStarRating.Current.Value = starDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelGroup : PanelBase
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT;
|
||||
|
||||
private Drawable chevronIcon = null!;
|
||||
private OsuSpriteText titleText = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Height = HEIGHT;
|
||||
|
||||
Icon = chevronIcon = new SpriteIcon
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Icon = FontAwesome.Solid.ChevronDown,
|
||||
Size = new Vector2(12),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
X = 2f,
|
||||
Colour = colourProvider.Background3,
|
||||
};
|
||||
Background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Dark1,
|
||||
};
|
||||
AccentColour = colourProvider.Highlight1;
|
||||
Content.Children = new Drawable[]
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
X = 10f,
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(50f, 14f),
|
||||
Margin = new MarginPadding { Right = 20f },
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
|
||||
// TODO: requires Carousel/CarouselItem-side implementation
|
||||
Text = "43",
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(_ => onExpanded(), true);
|
||||
}
|
||||
|
||||
private void onExpanded()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint);
|
||||
chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
titleText.Text = group.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelGroupStarDifficulty : PanelBase
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
private Drawable chevronIcon = null!;
|
||||
private Box contentBackground = null!;
|
||||
private StarRatingDisplay starRatingDisplay = null!;
|
||||
private StarCounter starCounter = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Height = PanelGroup.HEIGHT;
|
||||
|
||||
Icon = chevronIcon = new SpriteIcon
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Icon = FontAwesome.Solid.ChevronDown,
|
||||
Size = new Vector2(12),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
X = 2f,
|
||||
};
|
||||
Background = contentBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Dark1,
|
||||
};
|
||||
AccentColour = colourProvider.Highlight1;
|
||||
Content.Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(10f, 0f),
|
||||
Margin = new MarginPadding { Left = 10f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(8f / 20f),
|
||||
},
|
||||
}
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(50f, 14f),
|
||||
Margin = new MarginPadding { Right = 20f },
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
|
||||
// TODO: requires Carousel/CarouselItem-side implementation
|
||||
Text = "43",
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(_ => onExpanded(), true);
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
int starNumber = (int)((GroupDefinition)Item.Model).Data;
|
||||
|
||||
Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber);
|
||||
Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5;
|
||||
|
||||
AccentColour = colour;
|
||||
contentBackground.Colour = colour.Darken(0.3f);
|
||||
|
||||
starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0);
|
||||
starCounter.Current = starNumber;
|
||||
|
||||
chevronIcon.Colour = contentColour;
|
||||
starCounter.Colour = contentColour;
|
||||
}
|
||||
|
||||
private void onExpanded()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint);
|
||||
chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,18 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
|
||||
Child = new BeatmapCarousel
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.6f,
|
||||
},
|
||||
},
|
||||
modSelectOverlay,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class TopLocalRank : CompositeDrawable
|
||||
{
|
||||
private BeatmapInfo? beatmap;
|
||||
|
||||
public BeatmapInfo? Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
set
|
||||
{
|
||||
beatmap = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private IDisposable? scoreSubscription;
|
||||
|
||||
private readonly UpdateableRank updateable;
|
||||
|
||||
public TopLocalRank(BeatmapInfo? beatmap = null)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = updateable = new UpdateableRank
|
||||
{
|
||||
Size = new Vector2(40, 20),
|
||||
Alpha = 0,
|
||||
};
|
||||
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateSubscription(), true);
|
||||
}
|
||||
|
||||
private void updateSubscription()
|
||||
{
|
||||
scoreSubscription?.Dispose();
|
||||
|
||||
if (beatmap == null)
|
||||
return;
|
||||
|
||||
scoreSubscription = realm.RegisterForNotifications(r =>
|
||||
r.All<ScoreInfo>()
|
||||
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
|
||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
|
||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
|
||||
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
|
||||
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName),
|
||||
localScoresChanged);
|
||||
}
|
||||
|
||||
private void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
|
||||
{
|
||||
// This subscription may fire from changes to linked beatmaps, which we don't care about.
|
||||
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
|
||||
if (changes?.HasCollectionChanges() == false)
|
||||
return;
|
||||
|
||||
ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks));
|
||||
updateable.Rank = topScore?.Rank;
|
||||
updateable.Alpha = topScore != null ? 1 : 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
scoreSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class UpdateBeatmapSetButton : OsuAnimatedButton
|
||||
{
|
||||
private BeatmapSetInfo? beatmapSet;
|
||||
|
||||
public BeatmapSetInfo? BeatmapSet
|
||||
{
|
||||
get => beatmapSet;
|
||||
set
|
||||
{
|
||||
beatmapSet = value;
|
||||
|
||||
if (IsLoaded)
|
||||
beatmapChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private SpriteIcon icon = null!;
|
||||
private Box progressFill = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LoginOverlay? loginOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
public UpdateBeatmapSetButton()
|
||||
{
|
||||
Size = new Vector2(75f, 22f);
|
||||
}
|
||||
|
||||
private Bindable<bool> preferNoVideo = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
const float icon_size = 14;
|
||||
|
||||
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
|
||||
|
||||
Content.Anchor = Anchor.Centre;
|
||||
Content.Origin = Anchor.Centre;
|
||||
Content.Shear = new Vector2(OsuGame.SHEAR, 0);
|
||||
|
||||
Content.AddRange(new Drawable[]
|
||||
{
|
||||
progressFill = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Alpha = 0.2f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = 5, Vertical = 3 },
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(4),
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Size = new Vector2(icon_size),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.SyncAlt,
|
||||
Size = new Vector2(icon_size),
|
||||
},
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Text = "Update",
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Action = performUpdate;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
beatmapChanged();
|
||||
}
|
||||
|
||||
private void beatmapChanged()
|
||||
{
|
||||
Alpha = beatmapSet?.AllBeatmapsUpToDate == false ? 1 : 0;
|
||||
icon.Spin(4000, RotationDirection.Clockwise);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
icon.Spin(400, RotationDirection.Clockwise);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
icon.Spin(4000, RotationDirection.Clockwise);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private bool updateConfirmed;
|
||||
|
||||
private void performUpdate()
|
||||
{
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
loginOverlay?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogOverlay != null && beatmapSet.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed)
|
||||
{
|
||||
dialogOverlay.Push(new UpdateLocalConfirmationDialog(() =>
|
||||
{
|
||||
updateConfirmed = true;
|
||||
performUpdate();
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfirmed = false;
|
||||
|
||||
beatmapDownloader.DownloadAsUpdate(beatmapSet, preferNoVideo.Value);
|
||||
attachExistingDownload();
|
||||
}
|
||||
|
||||
private void attachExistingDownload()
|
||||
{
|
||||
Debug.Assert(beatmapSet != null);
|
||||
var download = beatmapDownloader.GetExistingDownload(beatmapSet);
|
||||
|
||||
if (download != null)
|
||||
{
|
||||
Enabled.Value = false;
|
||||
TooltipText = string.Empty;
|
||||
|
||||
download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint);
|
||||
download.Failure += _ => attachExistingDownload();
|
||||
}
|
||||
else
|
||||
{
|
||||
Enabled.Value = true;
|
||||
TooltipText = "Update beatmap with online changes";
|
||||
|
||||
progressFill.ResizeWidthTo(0, 100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -75,10 +76,18 @@ namespace osu.Game.Users.Drawables
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new HoverClickSounds(),
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.FromHex("333"),
|
||||
},
|
||||
new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = textures.Get(team.FlagUrl)
|
||||
Texture = textures.Get(team.FlagUrl),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fit,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user