mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@ -8,6 +9,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@ -121,6 +123,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
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
|
||||
|
||||
// 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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -59,5 +60,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
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.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -82,5 +83,13 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
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