mirror of
https://github.com/ppy/osu.git
synced 2025-02-21 03:53:21 +08:00
implemented everything
This commit is contained in:
parent
8bfb5cedc4
commit
2c5eeefa8d
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@ -8,6 +9,7 @@ using Newtonsoft.Json;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
@ -121,6 +123,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpinnerCount = onlineInfo.SpinnerCount;
|
SpinnerCount = onlineInfo.SpinnerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override SkillValue[] GetSkillValues()
|
||||||
|
{
|
||||||
|
double aimPerformanceWithoutSliders = Math.Pow(OsuStrainSkill.DifficultyToPerformance(AimDifficulty * SliderFactor), 2);
|
||||||
|
double aimPerformanceOnlySliders = Math.Pow(OsuStrainSkill.DifficultyToPerformance(AimDifficulty), 2) - aimPerformanceWithoutSliders;
|
||||||
|
|
||||||
|
double speedPerformance = Math.Pow(OsuStrainSkill.DifficultyToPerformance(SpeedDifficulty), 2);
|
||||||
|
double flashlightPerformance = Math.Pow(Flashlight.DifficultyToPerformance(FlashlightDifficulty), 2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
new SkillValue { Value = aimPerformanceOnlySliders, SkillName = "Slider Aim" },
|
||||||
|
new SkillValue { Value = aimPerformanceWithoutSliders, SkillName = "Aim" },
|
||||||
|
new SkillValue { Value = speedPerformance, SkillName = "Speed" },
|
||||||
|
new SkillValue { Value = flashlightPerformance, SkillName = "Flashlight" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
#region Newtonsoft.Json implicit ShouldSerialize() methods
|
#region Newtonsoft.Json implicit ShouldSerialize() methods
|
||||||
|
|
||||||
// The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases.
|
// The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -59,5 +60,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override SkillValue[] GetSkillValues()
|
||||||
|
{
|
||||||
|
//double aimPerformanceWithoutSliders = OsuStrainSkill.DifficultyToPerformance(AimDifficulty * SliderFactor);
|
||||||
|
//double speedPerformance = OsuStrainSkill.DifficultyToPerformance(SpeedDifficulty);
|
||||||
|
//double flashlightPerformance = Flashlight.DifficultyToPerformance(FlashlightDifficulty);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
new SkillValue { Value = difficultyRescale(ColourDifficulty), SkillName = "Colour" },
|
||||||
|
new SkillValue { Value = difficultyRescale(RhythmDifficulty), SkillName = "Rhythm" },
|
||||||
|
new SkillValue { Value = difficultyRescale(StaminaDifficulty), SkillName = "Stamina" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double difficultyRescale(double difficulty) => 10.43 * Math.Log(difficulty * 1.4 / 8 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
osu.Game.Tests/Visual/Components/TestSceneSkillsBreakdown.cs
Normal file
106
osu.Game.Tests/Visual/Components/TestSceneSkillsBreakdown.cs
Normal file
@ -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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
using osu.Game.Skinning.Components;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using System.Threading;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Components
|
||||||
|
{
|
||||||
|
public partial class TestSceneSkillsBreakdown : OsuTestScene
|
||||||
|
{
|
||||||
|
private TestBeatmapDifficultyCache difficultyCache = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Dependencies.CacheAs<BeatmapDifficultyCache>(difficultyCache = new TestBeatmapDifficultyCache());
|
||||||
|
|
||||||
|
AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
difficultyCache,
|
||||||
|
new SkillsBreakdown
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new osuTK.Vector2(250)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEqualValues()
|
||||||
|
{
|
||||||
|
AddStep("1", () =>
|
||||||
|
{
|
||||||
|
difficultyCache.UseRandomAttributes = false;
|
||||||
|
triggerCacheRecalc();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRandomValues()
|
||||||
|
{
|
||||||
|
AddStep("2", () =>
|
||||||
|
{
|
||||||
|
difficultyCache.UseRandomAttributes = true;
|
||||||
|
triggerCacheRecalc();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerCacheRecalc()
|
||||||
|
{
|
||||||
|
// Invalidate cache to force recalc
|
||||||
|
difficultyCache.Invalidate(Beatmap.Value.BeatmapInfo);
|
||||||
|
|
||||||
|
// Change mods to trigger ValueChanged
|
||||||
|
SelectedMods.Value = SelectedMods.Value.Count >= 0 ? new List<Mod>() : SelectedMods.Value.Append(new OsuModNoFail()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||||
|
{
|
||||||
|
public bool UseRandomAttributes;
|
||||||
|
private Random random = new Random();
|
||||||
|
|
||||||
|
protected override Task<StarDifficulty?> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var attributes = new TestDifficultyAttributes(
|
||||||
|
Enumerable.Range(0, 4)
|
||||||
|
.Select(_ => UseRandomAttributes ? random.NextDouble() : 1.0)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
return Task.FromResult<StarDifficulty?>(new StarDifficulty(attributes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestDifficultyAttributes : DifficultyAttributes
|
||||||
|
{
|
||||||
|
private SkillValue[] values;
|
||||||
|
|
||||||
|
public TestDifficultyAttributes(double[] values)
|
||||||
|
{
|
||||||
|
this.values = new SkillValue[values.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
this.values[i] = new SkillValue { Value = values[i], SkillName = $"Test Skill {i + 1}" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public override SkillValue[] GetSkillValues() => values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
@ -82,5 +83,13 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
{
|
{
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual SkillValue[] GetSkillValues() => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SkillValue
|
||||||
|
{
|
||||||
|
public double Value;
|
||||||
|
public LocalisableString SkillName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
190
osu.Game/Skinning/SkillsBreakdown.cs
Normal file
190
osu.Game/Skinning/SkillsBreakdown.cs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Layout;
|
||||||
|
|
||||||
|
namespace osu.Game.Skinning
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public partial class SkillsBreakdown : CompositeDrawable, ISerialisableDrawable
|
||||||
|
{
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||||
|
private IBindable<StarDifficulty?> starDifficulty = null!;
|
||||||
|
private CancellationTokenSource? cancellationSource;
|
||||||
|
|
||||||
|
private Color4[] colors = null!;
|
||||||
|
private Container<SkillCircle> circles = null!;
|
||||||
|
|
||||||
|
public SkillsBreakdown()
|
||||||
|
{
|
||||||
|
Size = new Vector2(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colorSource)
|
||||||
|
{
|
||||||
|
InternalChild = circles = new Container<SkillCircle>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
colors = new Color4[]
|
||||||
|
{
|
||||||
|
colorSource.Blue3,
|
||||||
|
colorSource.Lime3,
|
||||||
|
colorSource.Red3,
|
||||||
|
colorSource.YellowDark
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var color in colors)
|
||||||
|
circles.Add(new SkillCircle(color, 1, 1));
|
||||||
|
|
||||||
|
circles.First().SetProgress(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
beatmap.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
cancellationSource?.Cancel();
|
||||||
|
starDifficulty = difficultyCache.GetBindableDifficulty(b.NewValue.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
|
||||||
|
starDifficulty.BindValueChanged(d => updateSkillsBreakdown(d.NewValue));
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSkillsBreakdown(StarDifficulty? starDifficulty)
|
||||||
|
{
|
||||||
|
if (starDifficulty == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var skillNameValues = starDifficulty.Value.Attributes.GetSkillValues();
|
||||||
|
|
||||||
|
// Square the values to make visual representation more intuitive
|
||||||
|
double[] skillValues = skillNameValues.Select(x => Math.Pow(x.Value, 1)).ToArray();
|
||||||
|
LocalisableString[] skillNames = skillNameValues.Select(x => x.SkillName).ToArray();
|
||||||
|
|
||||||
|
double sum = skillValues.Sum();
|
||||||
|
if (sum == 0) sum = 1;
|
||||||
|
|
||||||
|
double[] skillValuesNormalized = skillValues.Select(x => x / sum).ToArray();
|
||||||
|
|
||||||
|
double cumulativeValue = 0;
|
||||||
|
for (int i = 0; i < circles.Count; i++)
|
||||||
|
{
|
||||||
|
double nextCumulativeValue = i < skillValuesNormalized.Length ? cumulativeValue + skillValuesNormalized[i] : 1;
|
||||||
|
|
||||||
|
circles[i].SetProgress(Math.Round(cumulativeValue, 5), Math.Round(nextCumulativeValue, 5));
|
||||||
|
circles[i].SkillName = i < skillNames.Length ? skillNames[i] : "";
|
||||||
|
|
||||||
|
cumulativeValue = nextCumulativeValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class SkillCircle : Container
|
||||||
|
{
|
||||||
|
public LocalisableString SkillName { set => innerCircle.SkillName = value; }
|
||||||
|
|
||||||
|
// Same as StarRatingDisplay
|
||||||
|
private const double animation_duration = 700;
|
||||||
|
private const Easing animation_type = Easing.OutQuint;
|
||||||
|
|
||||||
|
private TooltipCircularProgress innerCircle;
|
||||||
|
public SkillCircle(Color4 color, double startProgress, double endProgress)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Child = innerCircle = new TooltipCircularProgress
|
||||||
|
{
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
Colour = color,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
SetProgress(startProgress, endProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProgress(double startProgress, double endProgress)
|
||||||
|
{
|
||||||
|
this.RotateTo((float)startProgress * 360, animation_duration, animation_type);
|
||||||
|
innerCircle.ProgressTo(endProgress - startProgress, animation_duration, animation_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TooltipCircularProgress : CircularProgress, IHasTooltip
|
||||||
|
{
|
||||||
|
public LocalisableString SkillName;
|
||||||
|
LocalisableString IHasTooltip.TooltipText => SkillName;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
precalcInputData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||||
|
{
|
||||||
|
bool result = base.OnInvalidate(invalidation, source);
|
||||||
|
precalcInputData();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void precalcInputData()
|
||||||
|
{
|
||||||
|
center = ScreenSpaceDrawQuad.Centre;
|
||||||
|
|
||||||
|
float radius = ScreenSpaceDrawQuad.Width / 2;
|
||||||
|
innerDistanceSquared = MathF.Pow(radius * InnerRadius, 2);
|
||||||
|
outerDistanceSquared = MathF.Pow(radius, 2);
|
||||||
|
|
||||||
|
startAngle = Parent.Rotation * MathF.PI / 180;
|
||||||
|
endAngle = startAngle + (float)Progress * 2 * MathF.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 center;
|
||||||
|
private double innerDistanceSquared, outerDistanceSquared;
|
||||||
|
private float startAngle, endAngle;
|
||||||
|
private float deltaAngle;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
|
{
|
||||||
|
Vector2 deltaVector = screenSpacePos - center;
|
||||||
|
double distanceSquared = deltaVector.X * deltaVector.X + deltaVector.Y * deltaVector.Y;
|
||||||
|
|
||||||
|
if (distanceSquared > outerDistanceSquared || distanceSquared < innerDistanceSquared)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
deltaAngle = MathF.Atan2(-deltaVector.X, deltaVector.Y) + MathF.PI;
|
||||||
|
|
||||||
|
return deltaAngle > startAngle && deltaAngle < endAngle;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user