mirror of
https://github.com/ppy/osu.git
synced 2025-03-05 15:03:16 +08:00
Merge branch 'master' into spectator-state-rework
This commit is contained in:
commit
45b3f472ab
@ -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.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
|
||||||
@ -16,5 +17,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
[JsonProperty("scaled_score")]
|
[JsonProperty("scaled_score")]
|
||||||
public double ScaledScore { get; set; }
|
public double ScaledScore { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
|
{
|
||||||
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
yield return attribute;
|
||||||
|
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,6 +366,17 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||||
{
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
|
||||||
@ -22,5 +23,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
[JsonProperty("effective_miss_count")]
|
[JsonProperty("effective_miss_count")]
|
||||||
public double EffectiveMissCount { get; set; }
|
public double EffectiveMissCount { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
|
{
|
||||||
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
yield return attribute;
|
||||||
|
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Aim), "Aim", Aim);
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +275,17 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
|
||||||
@ -13,5 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
[JsonProperty("accuracy")]
|
[JsonProperty("accuracy")]
|
||||||
public double Accuracy { get; set; }
|
public double Accuracy { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
|
{
|
||||||
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
yield return attribute;
|
||||||
|
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,6 +209,17 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
|
@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
AddStep("click to right of panel", () =>
|
AddStep("click to right of panel", () =>
|
||||||
{
|
{
|
||||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||||
InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0));
|
InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(50, 0));
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
115
osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs
Normal file
115
osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public class TestSceneSafeAreaHandling : OsuGameTestScene
|
||||||
|
{
|
||||||
|
private SafeAreaDefiningContainer safeAreaContainer;
|
||||||
|
|
||||||
|
private static BindableSafeArea safeArea;
|
||||||
|
|
||||||
|
private readonly Bindable<float> safeAreaPaddingTop = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
|
private readonly Bindable<float> safeAreaPaddingBottom = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
|
private readonly Bindable<float> safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
|
private readonly Bindable<float> safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 };
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// Usually this would be placed between the host and the game, but that's a bit of a pain to do with the test scene hierarchy.
|
||||||
|
|
||||||
|
// Add is required for the container to get a size (and give out correct metrics to the usages in SafeAreaContainer).
|
||||||
|
Add(safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache is required for the test game to see the safe area.
|
||||||
|
Dependencies.CacheAs<ISafeArea>(safeAreaContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("Add adjust controls", () =>
|
||||||
|
{
|
||||||
|
Add(new Container
|
||||||
|
{
|
||||||
|
Depth = float.MinValue,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = Color4.Black,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0.8f,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Width = 400,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new SettingsSlider<float>
|
||||||
|
{
|
||||||
|
Current = safeAreaPaddingTop,
|
||||||
|
LabelText = "Top"
|
||||||
|
},
|
||||||
|
new SettingsSlider<float>
|
||||||
|
{
|
||||||
|
Current = safeAreaPaddingBottom,
|
||||||
|
LabelText = "Bottom"
|
||||||
|
},
|
||||||
|
new SettingsSlider<float>
|
||||||
|
{
|
||||||
|
Current = safeAreaPaddingLeft,
|
||||||
|
LabelText = "Left"
|
||||||
|
},
|
||||||
|
new SettingsSlider<float>
|
||||||
|
{
|
||||||
|
Current = safeAreaPaddingRight,
|
||||||
|
LabelText = "Right"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
safeAreaPaddingTop.BindValueChanged(_ => updateSafeArea());
|
||||||
|
safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea());
|
||||||
|
safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea());
|
||||||
|
safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea());
|
||||||
|
});
|
||||||
|
|
||||||
|
base.SetUpSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSafeArea()
|
||||||
|
{
|
||||||
|
safeArea.Value = new MarginPadding
|
||||||
|
{
|
||||||
|
Top = safeAreaPaddingTop.Value,
|
||||||
|
Bottom = safeAreaPaddingBottom.Value,
|
||||||
|
Left = safeAreaPaddingLeft.Value,
|
||||||
|
Right = safeAreaPaddingRight.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSafeArea()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
|
|||||||
private Bindable<float> posX;
|
private Bindable<float> posX;
|
||||||
private Bindable<float> posY;
|
private Bindable<float> posY;
|
||||||
|
|
||||||
|
private Bindable<MarginPadding> safeAreaPadding;
|
||||||
|
|
||||||
private readonly ScalingMode? targetMode;
|
private readonly ScalingMode? targetMode;
|
||||||
|
|
||||||
private Bindable<ScalingMode> scalingMode;
|
private Bindable<ScalingMode> scalingMode;
|
||||||
@ -50,7 +52,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
allowScaling = value;
|
allowScaling = value;
|
||||||
if (IsLoaded) updateSize();
|
if (IsLoaded) Scheduler.AddOnce(updateSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,22 +104,25 @@ namespace osu.Game.Graphics.Containers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config, ISafeArea safeArea)
|
||||||
{
|
{
|
||||||
scalingMode = config.GetBindable<ScalingMode>(OsuSetting.Scaling);
|
scalingMode = config.GetBindable<ScalingMode>(OsuSetting.Scaling);
|
||||||
scalingMode.ValueChanged += _ => updateSize();
|
scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
sizeX = config.GetBindable<float>(OsuSetting.ScalingSizeX);
|
sizeX = config.GetBindable<float>(OsuSetting.ScalingSizeX);
|
||||||
sizeX.ValueChanged += _ => updateSize();
|
sizeX.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
sizeY = config.GetBindable<float>(OsuSetting.ScalingSizeY);
|
sizeY = config.GetBindable<float>(OsuSetting.ScalingSizeY);
|
||||||
sizeY.ValueChanged += _ => updateSize();
|
sizeY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
posX = config.GetBindable<float>(OsuSetting.ScalingPositionX);
|
posX = config.GetBindable<float>(OsuSetting.ScalingPositionX);
|
||||||
posX.ValueChanged += _ => updateSize();
|
posX.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
|
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
|
||||||
posY.ValueChanged += _ => updateSize();
|
posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
|
||||||
|
|
||||||
|
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
|
||||||
|
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -161,7 +166,10 @@ namespace osu.Game.Graphics.Containers
|
|||||||
|
|
||||||
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
|
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
|
||||||
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;
|
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;
|
||||||
bool requiresMasking = scaling && targetSize != Vector2.One;
|
bool requiresMasking = (scaling && targetSize != Vector2.One)
|
||||||
|
// For the top level scaling container, for now we apply masking if safe areas are in use.
|
||||||
|
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|
||||||
|
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
|
||||||
|
|
||||||
if (requiresMasking)
|
if (requiresMasking)
|
||||||
sizableContainer.Masking = true;
|
sizableContainer.Masking = true;
|
||||||
|
@ -89,6 +89,12 @@ namespace osu.Game
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Edges"/> that the game should be drawn over at a top level.
|
||||||
|
/// Defaults to <see cref="Edges.None"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual Edges SafeAreaOverrideEdges => Edges.None;
|
||||||
|
|
||||||
protected OsuConfigManager LocalConfig { get; private set; }
|
protected OsuConfigManager LocalConfig { get; private set; }
|
||||||
|
|
||||||
protected SessionStatics SessionStatics { get; private set; }
|
protected SessionStatics SessionStatics { get; private set; }
|
||||||
@ -299,16 +305,23 @@ namespace osu.Game
|
|||||||
|
|
||||||
GlobalActionContainer globalBindings;
|
GlobalActionContainer globalBindings;
|
||||||
|
|
||||||
var mainContent = new Drawable[]
|
base.Content.Add(new SafeAreaContainer
|
||||||
{
|
{
|
||||||
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both },
|
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
|
||||||
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
|
RelativeSizeAxes = Axes.Both,
|
||||||
globalBindings = new GlobalActionContainer(this)
|
Child = CreateScalingContainer().WithChildren(new Drawable[]
|
||||||
};
|
{
|
||||||
|
(MenuCursorContainer = new MenuCursorContainer
|
||||||
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
}).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}),
|
||||||
|
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
|
||||||
|
globalBindings = new GlobalActionContainer(this)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
|
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
|
||||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty
|
namespace osu.Game.Rulesets.Difficulty
|
||||||
@ -12,5 +13,15 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("pp")]
|
[JsonProperty("pp")]
|
||||||
public double Total { get; set; }
|
public double Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a <see cref="PerformanceDisplayAttribute"/> for each attribute so that a performance breakdown can be displayed.
|
||||||
|
/// Some attributes may be omitted if they are not meant for display.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
|
{
|
||||||
|
yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs
Normal file
21
osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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.Difficulty
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data for generating a performance breakdown by comparing performance to a perfect play.
|
||||||
|
/// </summary>
|
||||||
|
public class PerformanceBreakdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Actual gameplay performance.
|
||||||
|
/// </summary>
|
||||||
|
public PerformanceAttributes Performance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performance of a perfect play for comparison.
|
||||||
|
/// </summary>
|
||||||
|
public PerformanceAttributes PerfectPerformance { get; set; }
|
||||||
|
}
|
||||||
|
}
|
105
osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
Normal file
105
osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
Normal file
@ -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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Difficulty
|
||||||
|
{
|
||||||
|
public class PerformanceBreakdownCalculator
|
||||||
|
{
|
||||||
|
private readonly IBeatmap playableBeatmap;
|
||||||
|
private readonly BeatmapDifficultyCache difficultyCache;
|
||||||
|
private readonly ScorePerformanceCache performanceCache;
|
||||||
|
|
||||||
|
public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache)
|
||||||
|
{
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
this.difficultyCache = difficultyCache;
|
||||||
|
this.performanceCache = performanceCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ItemCanBeNull]
|
||||||
|
public async Task<PerformanceBreakdown> CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
PerformanceAttributes[] performanceArray = await Task.WhenAll(
|
||||||
|
// compute actual performance
|
||||||
|
performanceCache.CalculatePerformanceAsync(score, cancellationToken),
|
||||||
|
// compute performance for perfect play
|
||||||
|
getPerfectPerformance(score, cancellationToken)
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
[ItemCanBeNull]
|
||||||
|
private Task<PerformanceAttributes> getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.Run(async () =>
|
||||||
|
{
|
||||||
|
Ruleset ruleset = score.Ruleset.CreateInstance();
|
||||||
|
ScoreInfo perfectPlay = score.DeepClone();
|
||||||
|
perfectPlay.Accuracy = 1;
|
||||||
|
perfectPlay.Passed = true;
|
||||||
|
|
||||||
|
// calculate max combo
|
||||||
|
// todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores
|
||||||
|
perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap);
|
||||||
|
|
||||||
|
// create statistics assuming all hit objects have perfect hit result
|
||||||
|
var statistics = playableBeatmap.HitObjects
|
||||||
|
.SelectMany(getPerfectHitResults)
|
||||||
|
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
|
||||||
|
.ToDictionary(pair => pair.hitResult, pair => pair.count);
|
||||||
|
perfectPlay.Statistics = statistics;
|
||||||
|
|
||||||
|
// calculate total score
|
||||||
|
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
|
scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo;
|
||||||
|
scoreProcessor.Mods.Value = perfectPlay.Mods;
|
||||||
|
perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics);
|
||||||
|
|
||||||
|
// compute rank achieved
|
||||||
|
// default to SS, then adjust the rank with mods
|
||||||
|
perfectPlay.Rank = ScoreRank.X;
|
||||||
|
|
||||||
|
foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType<IApplicableToScoreProcessor>())
|
||||||
|
{
|
||||||
|
perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate performance for this perfect score
|
||||||
|
var difficulty = await difficultyCache.GetDifficultyAsync(
|
||||||
|
playableBeatmap.BeatmapInfo,
|
||||||
|
score.Ruleset,
|
||||||
|
score.Mods,
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
|
||||||
|
return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate();
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateMaxCombo(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<HitResult> getPerfectHitResults(HitObject hitObject)
|
||||||
|
{
|
||||||
|
foreach (HitObject nested in hitObject.NestedHitObjects)
|
||||||
|
yield return nested.CreateJudgement().MaxResult;
|
||||||
|
|
||||||
|
yield return hitObject.CreateJudgement().MaxResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs
Normal file
33
osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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.Difficulty
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data for displaying a performance attribute to user. Includes a display name for clarity.
|
||||||
|
/// </summary>
|
||||||
|
public class PerformanceDisplayAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the attribute property in <see cref="PerformanceAttributes"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A custom display name for the attribute.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The associated attribute value.
|
||||||
|
/// </summary>
|
||||||
|
public double Value { get; }
|
||||||
|
|
||||||
|
public PerformanceDisplayAttribute(string propertyName, string displayName, double value)
|
||||||
|
{
|
||||||
|
PropertyName = propertyName;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
{
|
{
|
||||||
@ -15,7 +16,7 @@ namespace osu.Game.Scoring
|
|||||||
/// A component which performs and acts as a central cache for performance calculations of locally databased scores.
|
/// A component which performs and acts as a central cache for performance calculations of locally databased scores.
|
||||||
/// Currently not persisted between game sessions.
|
/// Currently not persisted between game sessions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, double?>
|
public class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, PerformanceAttributes>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
@ -27,10 +28,10 @@ namespace osu.Game.Scoring
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to do the calculation on. </param>
|
/// <param name="score">The score to do the calculation on. </param>
|
||||||
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
|
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
|
||||||
public Task<double?> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) =>
|
public Task<PerformanceAttributes> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) =>
|
||||||
GetAsync(new PerformanceCacheLookup(score), token);
|
GetAsync(new PerformanceCacheLookup(score), token);
|
||||||
|
|
||||||
protected override async Task<double?> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
|
protected override async Task<PerformanceAttributes> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var score = lookup.ScoreInfo;
|
var score = lookup.ScoreInfo;
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score);
|
var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score);
|
||||||
|
|
||||||
return calculator?.Calculate().Total;
|
return calculator?.Calculate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct PerformanceCacheLookup
|
public readonly struct PerformanceCacheLookup
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -231,7 +230,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
|
|
||||||
public override bool OnBackButton()
|
public override bool OnBackButton()
|
||||||
{
|
{
|
||||||
Debug.Assert(multiplayerClient.Room != null);
|
if (multiplayerClient.Room == null)
|
||||||
|
return base.OnBackButton();
|
||||||
|
|
||||||
// On a manual exit, set the player back to idle unless gameplay has finished.
|
// On a manual exit, set the player back to idle unless gameplay has finished.
|
||||||
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
|
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token)
|
performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token)
|
||||||
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token);
|
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
247
osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
Normal file
247
osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
// 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 System.Threading;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Ranking.Statistics
|
||||||
|
{
|
||||||
|
public class PerformanceBreakdownChart : Container
|
||||||
|
{
|
||||||
|
private readonly ScoreInfo score;
|
||||||
|
private readonly IBeatmap playableBeatmap;
|
||||||
|
|
||||||
|
private Drawable spinner;
|
||||||
|
private Drawable content;
|
||||||
|
private GridContainer chart;
|
||||||
|
private OsuSpriteText achievedPerformance;
|
||||||
|
private OsuSpriteText maximumPerformance;
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScorePerformanceCache performanceCache { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
|
|
||||||
|
public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
this.score = score;
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
spinner = new LoadingSpinner(true)
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre
|
||||||
|
},
|
||||||
|
content = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Width = 0.6f,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Spacing = new Vector2(15, 15),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.8f,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
|
||||||
|
Text = "Achieved PP",
|
||||||
|
Colour = Color4Extensions.FromHex("#66FFCC")
|
||||||
|
},
|
||||||
|
achievedPerformance = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18),
|
||||||
|
Colour = Color4Extensions.FromHex("#66FFCC")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
|
||||||
|
Text = "Maximum",
|
||||||
|
Colour = OsuColour.Gray(0.7f)
|
||||||
|
},
|
||||||
|
maximumPerformance = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
|
||||||
|
Colour = OsuColour.Gray(0.7f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chart = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spinner.Show();
|
||||||
|
|
||||||
|
new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache)
|
||||||
|
.CalculateAsync(score, cancellationTokenSource.Token)
|
||||||
|
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPerformanceValue(PerformanceBreakdown breakdown)
|
||||||
|
{
|
||||||
|
spinner.Hide();
|
||||||
|
content.FadeIn(200);
|
||||||
|
|
||||||
|
var displayAttributes = breakdown.Performance.GetAttributesForDisplay();
|
||||||
|
var perfectDisplayAttributes = breakdown.PerfectPerformance.GetAttributesForDisplay();
|
||||||
|
|
||||||
|
setTotalValues(
|
||||||
|
displayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)),
|
||||||
|
perfectDisplayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total))
|
||||||
|
);
|
||||||
|
|
||||||
|
var rowDimensions = new List<Dimension>();
|
||||||
|
var rows = new List<Drawable[]>();
|
||||||
|
|
||||||
|
foreach (PerformanceDisplayAttribute attr in displayAttributes)
|
||||||
|
{
|
||||||
|
if (attr.PropertyName == nameof(PerformanceAttributes.Total)) continue;
|
||||||
|
|
||||||
|
var row = createAttributeRow(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName));
|
||||||
|
|
||||||
|
if (row != null)
|
||||||
|
{
|
||||||
|
rows.Add(row);
|
||||||
|
rowDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.RowDimensions = rowDimensions.ToArray();
|
||||||
|
chart.Content = rows.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTotalValues(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute)
|
||||||
|
{
|
||||||
|
achievedPerformance.Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString();
|
||||||
|
maximumPerformance.Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute)
|
||||||
|
{
|
||||||
|
// Don't display the attribute if its maximum is 0
|
||||||
|
// For example, flashlight bonus would be zero if flashlight mod isn't on
|
||||||
|
if (Precision.AlmostEquals(perfectAttribute.Value, 0f))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
float percentage = (float)(attribute.Value / perfectAttribute.Value);
|
||||||
|
|
||||||
|
return new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular),
|
||||||
|
Text = attribute.DisplayName,
|
||||||
|
Colour = Colour4.White
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Left = 10, Right = 10 },
|
||||||
|
Child = new Bar
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
CornerRadius = 2.5f,
|
||||||
|
Masking = true,
|
||||||
|
Height = 5,
|
||||||
|
BackgroundColour = Color4.White.Opacity(0.5f),
|
||||||
|
AccentColour = Color4Extensions.FromHex("#66FFCC"),
|
||||||
|
Length = percentage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||||
|
Text = percentage.ToLocalisableString("0%"),
|
||||||
|
Colour = Colour4.White
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -925,8 +925,10 @@ namespace osu.Game.Screens.Select
|
|||||||
// child items (difficulties) are still visible.
|
// child items (difficulties) are still visible.
|
||||||
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
|
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
|
||||||
|
|
||||||
// We are applying alpha to the header here such that we can layer alpha transformations on top.
|
// We are applying a multiplicative alpha (which is internally done by nesting an
|
||||||
item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1);
|
// additional container and setting that container's alpha) such that we can
|
||||||
|
// layer alpha transformations on top.
|
||||||
|
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum PendingScrollOperation
|
private enum PendingScrollOperation
|
||||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
public class CarouselHeader : Container
|
public class CarouselHeader : Container
|
||||||
{
|
{
|
||||||
|
public Container BorderContainer;
|
||||||
|
|
||||||
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||||
|
|
||||||
private readonly HoverLayer hoverLayer;
|
private readonly HoverLayer hoverLayer;
|
||||||
@ -35,14 +37,17 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = DrawableCarouselItem.MAX_HEIGHT;
|
Height = DrawableCarouselItem.MAX_HEIGHT;
|
||||||
|
|
||||||
Masking = true;
|
InternalChild = BorderContainer = new Container
|
||||||
CornerRadius = corner_radius;
|
|
||||||
BorderColour = new Color4(221, 255, 255, 255);
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
{
|
||||||
Content,
|
RelativeSizeAxes = Axes.Both,
|
||||||
hoverLayer = new HoverLayer()
|
Masking = true,
|
||||||
|
CornerRadius = corner_radius,
|
||||||
|
BorderColour = new Color4(221, 255, 255, 255),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
Content,
|
||||||
|
hoverLayer = new HoverLayer()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,21 +66,21 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
case CarouselItemState.NotSelected:
|
case CarouselItemState.NotSelected:
|
||||||
hoverLayer.InsetForBorder = false;
|
hoverLayer.InsetForBorder = false;
|
||||||
|
|
||||||
BorderThickness = 0;
|
BorderContainer.BorderThickness = 0;
|
||||||
EdgeEffect = new EdgeEffectParameters
|
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||||
{
|
{
|
||||||
Type = EdgeEffectType.Shadow,
|
Type = EdgeEffectType.Shadow,
|
||||||
Offset = new Vector2(1),
|
Offset = new Vector2(1),
|
||||||
Radius = 10,
|
Radius = 10,
|
||||||
Colour = Color4.Black.Opacity(0.5f),
|
Colour = Color4.Black.Opacity(100),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CarouselItemState.Selected:
|
case CarouselItemState.Selected:
|
||||||
hoverLayer.InsetForBorder = true;
|
hoverLayer.InsetForBorder = true;
|
||||||
|
|
||||||
BorderThickness = border_thickness;
|
BorderContainer.BorderThickness = border_thickness;
|
||||||
EdgeEffect = new EdgeEffectParameters
|
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||||
{
|
{
|
||||||
Type = EdgeEffectType.Glow,
|
Type = EdgeEffectType.Glow,
|
||||||
Colour = new Color4(130, 204, 255, 150),
|
Colour = new Color4(130, 204, 255, 150),
|
||||||
|
@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The height of a carousel beatmap, including vertical spacing.
|
/// The height of a carousel beatmap, including vertical spacing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float HEIGHT = header_height + CAROUSEL_BEATMAP_SPACING;
|
public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING;
|
||||||
|
|
||||||
private const float header_height = MAX_HEIGHT * 0.6f;
|
private const float height = MAX_HEIGHT * 0.6f;
|
||||||
|
|
||||||
private readonly BeatmapInfo beatmapInfo;
|
private readonly BeatmapInfo beatmapInfo;
|
||||||
|
|
||||||
@ -67,18 +67,16 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
private CancellationTokenSource starDifficultyCancellationSource;
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
||||||
: base(header_height)
|
|
||||||
{
|
{
|
||||||
beatmapInfo = panel.BeatmapInfo;
|
beatmapInfo = panel.BeatmapInfo;
|
||||||
Item = panel;
|
Item = panel;
|
||||||
|
|
||||||
// Difficulty panels should start hidden for a better initial effect.
|
|
||||||
Hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(BeatmapManager manager, SongSelect songSelect)
|
private void load(BeatmapManager manager, SongSelect songSelect)
|
||||||
{
|
{
|
||||||
|
Header.Height = height;
|
||||||
|
|
||||||
if (songSelect != null)
|
if (songSelect != null)
|
||||||
{
|
{
|
||||||
startRequested = b => songSelect.FinaliseSelection(b);
|
startRequested = b => songSelect.FinaliseSelection(b);
|
||||||
|
@ -122,10 +122,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint);
|
background.DelayedLoadComplete += fadeContentIn;
|
||||||
mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint);
|
mainFlow.DelayedLoadComplete += fadeContentIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint);
|
||||||
|
|
||||||
protected override void Deselected()
|
protected override void Deselected()
|
||||||
{
|
{
|
||||||
base.Deselected();
|
base.Deselected();
|
||||||
|
@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT)
|
protected DrawableCarouselItem()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
|
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
MovementContainer = new Container
|
MovementContainer = new Container
|
||||||
@ -71,20 +73,18 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Header = new CarouselHeader
|
Header = new CarouselHeader(),
|
||||||
{
|
|
||||||
Height = headerHeight,
|
|
||||||
},
|
|
||||||
Content = new Container
|
Content = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Y = headerHeight,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -92,6 +92,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
UpdateItem();
|
UpdateItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
Content.Y = Header.Height;
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void UpdateItem()
|
protected virtual void UpdateItem()
|
||||||
{
|
{
|
||||||
if (item == null)
|
if (item == null)
|
||||||
@ -115,56 +121,29 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
|
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
|
||||||
|
|
||||||
private CarouselItemState? lastAppliedState;
|
|
||||||
|
|
||||||
protected virtual void ApplyState()
|
protected virtual void ApplyState()
|
||||||
{
|
{
|
||||||
|
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
|
||||||
|
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
|
||||||
|
Height = Item.TotalHeight;
|
||||||
|
|
||||||
Debug.Assert(Item != null);
|
Debug.Assert(Item != null);
|
||||||
|
|
||||||
if (lastAppliedState != Item.State.Value)
|
switch (Item.State.Value)
|
||||||
{
|
{
|
||||||
lastAppliedState = Item.State.Value;
|
case CarouselItemState.NotSelected:
|
||||||
|
Deselected();
|
||||||
|
break;
|
||||||
|
|
||||||
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
|
case CarouselItemState.Selected:
|
||||||
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
|
Selected();
|
||||||
Height = Item.TotalHeight;
|
break;
|
||||||
|
|
||||||
switch (lastAppliedState)
|
|
||||||
{
|
|
||||||
case CarouselItemState.NotSelected:
|
|
||||||
Deselected();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CarouselItemState.Selected:
|
|
||||||
Selected();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Item.Visible)
|
if (!Item.Visible)
|
||||||
Hide();
|
this.FadeOut(300, Easing.OutQuint);
|
||||||
else
|
else
|
||||||
Show();
|
this.FadeIn(250);
|
||||||
}
|
|
||||||
|
|
||||||
private bool isVisible = true;
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
if (isVisible)
|
|
||||||
return;
|
|
||||||
|
|
||||||
isVisible = true;
|
|
||||||
this.FadeIn(250);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Hide()
|
|
||||||
{
|
|
||||||
if (!isVisible)
|
|
||||||
return;
|
|
||||||
|
|
||||||
isVisible = false;
|
|
||||||
this.FadeOut(300, Easing.OutQuint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Selected()
|
protected virtual void Selected()
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@ -18,6 +19,11 @@ namespace osu.iOS
|
|||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
|
||||||
|
|
||||||
|
protected override Edges SafeAreaOverrideEdges =>
|
||||||
|
// iOS shows a home indicator at the bottom, and adds a safe area to account for this.
|
||||||
|
// Because we have the home indicator (mostly) hidden we don't really care about drawing in this region.
|
||||||
|
Edges.Bottom;
|
||||||
|
|
||||||
private class IOSBatteryInfo : BatteryInfo
|
private class IOSBatteryInfo : BatteryInfo
|
||||||
{
|
{
|
||||||
public override double ChargeLevel => Battery.ChargeLevel;
|
public override double ChargeLevel => Battery.ChargeLevel;
|
||||||
|
Loading…
Reference in New Issue
Block a user