mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 22:00:51 +08:00
Merge branch 'master' into use-global-leaderboard-on-results
This commit is contained in:
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// 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);
|
||||
scaleContainer.Scale = new Vector2(Math.Min(1, base_aspect_ratio / aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
@@ -20,6 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
ReadCurrentFromDifficulty = _ => 1,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -77,33 +77,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerScore()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
var player2Score = new BindableLong(1234567);
|
||||
var player3Score = new BindableLong(1111111);
|
||||
|
||||
AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" }));
|
||||
AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" }));
|
||||
|
||||
AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
|
||||
AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
|
||||
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500);
|
||||
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2));
|
||||
AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456);
|
||||
AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2));
|
||||
AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomScores()
|
||||
{
|
||||
@@ -183,30 +156,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
() => Does.Contain("#FF549A"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackedScorePosition([Values] bool partial)
|
||||
{
|
||||
createLeaderboard(partial);
|
||||
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
for (int i = 0; i < 49; i++)
|
||||
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
||||
|
||||
// Add player at end to force an animation down the whole list.
|
||||
playerScore.Value = 0;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
|
||||
if (partial)
|
||||
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
|
||||
else
|
||||
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
|
||||
|
||||
AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000);
|
||||
AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null));
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
@@ -216,12 +165,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
private void createLeaderboard(bool partial = false)
|
||||
private void createLeaderboard()
|
||||
{
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
leaderboardProvider.Scores.Clear();
|
||||
leaderboardProvider.IsPartial = partial;
|
||||
Child = leaderboard = new TestDrawableGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -243,24 +191,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public float Spacing => Flow.Spacing.Y;
|
||||
|
||||
public bool CheckPositionByUsername(string username, int? expectedPosition)
|
||||
{
|
||||
var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
|
||||
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> GetAllScoresForUsername(string username)
|
||||
=> Flow.Where(i => i.User?.Username == username);
|
||||
|
||||
public IEnumerable<DrawableGameplayLeaderboardScore> AllScores => Flow;
|
||||
}
|
||||
|
||||
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
|
||||
{
|
||||
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
|
||||
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
|
||||
public bool IsPartial { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneSoloGameplayLeaderboardProvider : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestLocalLeaderboardHasPositionsAutofilled()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Local, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 100).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 10_000 * (100 - i),
|
||||
Position = i,
|
||||
}).ToArray(),
|
||||
null
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows #101", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(101));
|
||||
AddUntilStep("tracked score ordered #101", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(101));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFullGlobalLeaderboard()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 40).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 600_000 + 10_000 * (40 - i),
|
||||
Position = i,
|
||||
}).ToArray(),
|
||||
null
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows #41", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(41));
|
||||
AddUntilStep("tracked score ordered #41", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(41));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPartialGlobalLeaderboard()
|
||||
{
|
||||
SoloGameplayLeaderboardProvider provider = null!;
|
||||
|
||||
var leaderboardManager = new LeaderboardManager();
|
||||
LoadComponent(leaderboardManager);
|
||||
var gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null)));
|
||||
AddStep("set scores", () =>
|
||||
{
|
||||
// this is dodgy but anything less dodgy is a lot of work
|
||||
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(
|
||||
Enumerable.Range(1, 50).Select(i => new ScoreInfo
|
||||
{
|
||||
TotalScore = 500_000 + 10_000 * (50 - i),
|
||||
Position = i
|
||||
}).ToArray(),
|
||||
new ScoreInfo { TotalScore = 200_000 }
|
||||
);
|
||||
});
|
||||
AddStep("create content", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LeaderboardManager), leaderboardManager),
|
||||
(typeof(GameplayState), gameplayState)
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leaderboardManager,
|
||||
provider = new SoloGameplayLeaderboardProvider()
|
||||
}
|
||||
});
|
||||
AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null);
|
||||
AddUntilStep("tracked score ordered #52", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(52));
|
||||
AddStep("move score above user best", () => gameplayState.ScoreProcessor.TotalScore.Value = 202_000);
|
||||
AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null);
|
||||
AddUntilStep("tracked score ordered #51", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(51));
|
||||
AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000);
|
||||
AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20));
|
||||
AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20));
|
||||
AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000);
|
||||
AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1));
|
||||
AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
};
|
||||
|
||||
private Container? resizeContainer;
|
||||
private float relativeWidth;
|
||||
|
||||
protected virtual Anchor ComponentAnchor => Anchor.TopLeft;
|
||||
protected virtual float InitialRelativeWidth => 0.5f;
|
||||
@@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Origin = ComponentAnchor,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = relativeWidth,
|
||||
Width = InitialRelativeWidth,
|
||||
Child = Content
|
||||
}
|
||||
};
|
||||
@@ -49,8 +48,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
if (resizeContainer != null)
|
||||
resizeContainer.Width = v;
|
||||
|
||||
relativeWidth = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+50
-33
@@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@@ -28,7 +29,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene
|
||||
public partial class TestSceneBeatmapLeaderboardScore : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
@@ -44,18 +45,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
@@ -78,22 +84,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
{
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
|
||||
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false)
|
||||
{
|
||||
Rank = scoreInfo.Position,
|
||||
IsPersonalBest = scoreInfo.User.Id == 2,
|
||||
@@ -112,18 +123,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
var scoreInfo = new ScoreInfo
|
||||
@@ -260,9 +276,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
scores[2].TotalScore = RNG.Next(120_000, 400_000);
|
||||
scores[2].MaximumStatistics[HitResult.Great] = 3000;
|
||||
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() };
|
||||
scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() };
|
||||
scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() };
|
||||
scores[3].Mods = new Mod[]
|
||||
{ new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust { CircleSize = { Value = 3.2f } } };
|
||||
scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray();
|
||||
|
||||
return scores;
|
||||
@@ -0,0 +1,370 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapLeaderboardWedge : SongSelectComponentsTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private TestBeatmapLeaderboardWedge leaderboard = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
private RulesetStore rulesetStore = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private OsuContextMenuContainer contentContainer = null!;
|
||||
private DialogOverlay dialogOverlay = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager = null!;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
|
||||
dependencies.Cache(leaderboardManager = new LeaderboardManager());
|
||||
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LoadComponent(dialogOverlay = new DialogOverlay
|
||||
{
|
||||
Depth = -1
|
||||
});
|
||||
|
||||
LoadComponent(leaderboardManager);
|
||||
|
||||
Child = contentContainer = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 500,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dialogOverlay,
|
||||
}
|
||||
};
|
||||
|
||||
AddSliderStep("change relative height", 0f, 1f, 0.65f, v => Schedule(() =>
|
||||
{
|
||||
contentContainer.Height = v * DrawHeight;
|
||||
}));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
if (leaderboard.IsNotNull())
|
||||
contentContainer.Remove(leaderboard, false);
|
||||
|
||||
contentContainer.Add(leaderboard = new TestBeatmapLeaderboardWedge
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
});
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBest()
|
||||
{
|
||||
AddStep(@"Show personal best", showPersonalBest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalScoresDisplay()
|
||||
{
|
||||
setScope(BeatmapLeaderboardScope.Global);
|
||||
|
||||
AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo())));
|
||||
AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s =>
|
||||
{
|
||||
s.User.Team = new APITeam();
|
||||
return s;
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPersonalBestWithNullPosition()
|
||||
{
|
||||
AddStep("null personal best position", showPersonalBestWithNullPosition);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderStates()
|
||||
{
|
||||
AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty<ScoreInfo>()));
|
||||
|
||||
AddStep(@"Retrieving", () => leaderboard.SetState(LeaderboardState.Retrieving));
|
||||
AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure));
|
||||
AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam));
|
||||
AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter));
|
||||
AddStep(@"Not logged in", () => leaderboard.SetState(LeaderboardState.NotLoggedIn));
|
||||
AddStep(@"Ruleset unavailable", () => leaderboard.SetState(LeaderboardState.RulesetUnavailable));
|
||||
AddStep(@"Beatmap unavailable", () => leaderboard.SetState(LeaderboardState.BeatmapUnavailable));
|
||||
AddStep(@"None selected", () => leaderboard.SetState(LeaderboardState.NoneSelected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseTheseModsDoesNotCopySystemMods()
|
||||
{
|
||||
AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
}));
|
||||
AddUntilStep("wait for scores", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.GreaterThan(0));
|
||||
AddStep("right click panel", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().Last());
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddStep("click use these mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableOsuMenuItem>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("received HD", () => this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is OsuModHidden));
|
||||
AddAssert("did not receive SV2", () => !this.ChildrenOfType<BeatmapLeaderboardScore>().Last().SelectedMods.Value.Any(m => m is ModScoreV2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplay()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Set beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayWorksWhenStartingOffline()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
AddStep("Log out", () => API.Logout());
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayOnBeatmapEdit()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
string originalHash = string.Empty;
|
||||
|
||||
setScope(BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
AddStep(@"Perform initial save to guarantee stable hash", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
|
||||
originalHash = beatmapInfo.Hash;
|
||||
});
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(10);
|
||||
|
||||
AddStep(@"Save with changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 12;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
checkStoredCount(30);
|
||||
|
||||
AddStep(@"Revert changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 8;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(30);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(0);
|
||||
}
|
||||
|
||||
private void showPersonalBestWithNullPosition()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void showPersonalBest()
|
||||
{
|
||||
leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo
|
||||
{
|
||||
OnlineID = 1337,
|
||||
Position = 999,
|
||||
Rank = ScoreRank.XH,
|
||||
Accuracy = 1,
|
||||
MaxCombo = 244,
|
||||
TotalScore = 1707827,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
Username = @"waaiiru",
|
||||
CountryCode = CountryCode.ES,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setScope(BeatmapLeaderboardScope scope)
|
||||
{
|
||||
AddStep(@"Set scope", () => ((Bindable<BeatmapLeaderboardScope>)leaderboard.Scope).Value = scope);
|
||||
}
|
||||
|
||||
private void importMoreScores(Func<BeatmapInfo> beatmapInfo)
|
||||
{
|
||||
AddStep(@"Import new scores", () =>
|
||||
{
|
||||
foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo()))
|
||||
scoreManager.Import(score);
|
||||
});
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
AddStep("Clear all scores", () => scoreManager.Delete());
|
||||
}
|
||||
|
||||
private void checkDisplayedCount(int expected) =>
|
||||
AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().Count(), () => Is.EqualTo(expected));
|
||||
|
||||
private void checkStoredCount(int expected) =>
|
||||
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
|
||||
|
||||
private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge
|
||||
{
|
||||
public new void SetState(LeaderboardState state) => base.SetState(state);
|
||||
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge visible", () => wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible);
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible);
|
||||
AddUntilStep("fail time wedge hidden", () => !wedge.FailRetryVisible);
|
||||
AddStep("local beatmap", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible);
|
||||
AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible);
|
||||
}
|
||||
|
||||
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
|
||||
{
|
||||
var working = CreateWorkingBeatmap(Ruleset.Value);
|
||||
|
||||
@@ -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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -10,6 +11,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -26,6 +30,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
private BeatmapTitleWedge titleWedge = null!;
|
||||
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
|
||||
|
||||
private APIBeatmapSet? currentOnlineSet;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
@@ -36,6 +42,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@@ -115,11 +139,45 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddAssert("check visibility", () => titleWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "10,000");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.ResetOnlineInfo();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddStep("local beatmapset", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = null;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "-");
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
@@ -155,5 +213,29 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
return label.Text == target;
|
||||
});
|
||||
}
|
||||
|
||||
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
|
||||
{
|
||||
var working = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var onlineSet = new APIBeatmapSet
|
||||
{
|
||||
OnlineID = working.BeatmapSetInfo.OnlineID,
|
||||
FavouriteCount = 2345,
|
||||
Beatmaps = new[]
|
||||
{
|
||||
new APIBeatmap
|
||||
{
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
UserPlayCount = 123,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
|
||||
return (working, onlineSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,81 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneModIcon : OsuTestScene
|
||||
{
|
||||
private FillFlowContainer spreadOutFlow = null!;
|
||||
private ModDisplay modDisplay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create flows", () =>
|
||||
{
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
spreadOutFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void addRange(IEnumerable<IMod> mods)
|
||||
{
|
||||
spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m)));
|
||||
modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType<Mod>()).ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowAllMods()
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
|
||||
};
|
||||
if (m is OsuModFlashlight fl)
|
||||
fl.FollowDelay.Value = 1245;
|
||||
|
||||
if (m is OsuModDaycore dc)
|
||||
dc.SpeedChange.Value = 0.74f;
|
||||
|
||||
if (m is OsuModDifficultyAdjust da)
|
||||
da.CircleSize.Value = 8.2f;
|
||||
|
||||
if (m is ModAdaptiveSpeed ad)
|
||||
ad.AdjustPitch.Value = false;
|
||||
|
||||
return m;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("toggle selected", () =>
|
||||
@@ -42,26 +101,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
var rateAdjustMods = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>();
|
||||
|
||||
addRange(rateAdjustMods.SelectMany(m =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>()
|
||||
.SelectMany(m =>
|
||||
{
|
||||
List<ModIcon> icons = new List<ModIcon> { new ModIcon(m) };
|
||||
List<Mod> mods = new List<Mod> { m };
|
||||
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
icons.Add(new ModIcon(m));
|
||||
}
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
mods.Add(m);
|
||||
}
|
||||
|
||||
return icons;
|
||||
}),
|
||||
};
|
||||
return mods;
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("adjust rates", () =>
|
||||
@@ -81,21 +136,50 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestChangeModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
|
||||
AddStep("change mod", () => icon.Mod = new OsuModEasy());
|
||||
AddStep("create mod icon", () => addRange([new OsuModDoubleTime()]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = new OsuModEasy();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInterfaceModType()
|
||||
{
|
||||
ModIcon icon = null!;
|
||||
|
||||
var ruleset = new OsuRuleset();
|
||||
|
||||
AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT")));
|
||||
AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"));
|
||||
AddStep("create mod icon", () => addRange([ruleset.AllMods.First(m => m.Acronym == "DT")]));
|
||||
AddStep("change mod", () =>
|
||||
{
|
||||
foreach (var modIcon in this.ChildrenOfType<ModIcon>())
|
||||
modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyAdjust()
|
||||
{
|
||||
AddStep("create icons", () =>
|
||||
{
|
||||
addRange([
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 5.5f }
|
||||
},
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
CircleSize = { Value = 8 },
|
||||
ApproachRate = { Value = 8 },
|
||||
OverallDifficulty = { Value = 8 },
|
||||
DrainRate = { Value = 8 },
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@@ -183,32 +182,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(2.5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearedButton(120)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding(),
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 40)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = -1f },
|
||||
Height = 30,
|
||||
},
|
||||
new ShearedButton(120, 70)
|
||||
new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = "Test",
|
||||
Text = "Button",
|
||||
Action = () => { },
|
||||
Padding = new MarginPadding { Left = 3f },
|
||||
Height = 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,50 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene
|
||||
public partial class TestSceneShearedSliderBar : ThemeComparisonTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
private TestSliderBar slider = null!;
|
||||
|
||||
private ShearedSliderBar<double> slider = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
protected override Drawable CreateContent() => slider = new TestSliderBar
|
||||
{
|
||||
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = new BindableDouble(5)
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestNubDisplay()
|
||||
{
|
||||
AddSliderStep("nub width", 20, 80, 50, v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
{
|
||||
Precision = 0.1,
|
||||
MinValue = 0,
|
||||
MaxValue = 15
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f
|
||||
slider.Nub.Width = v;
|
||||
slider.RangePadding = v / 2f;
|
||||
}
|
||||
});
|
||||
AddToggleStep("nub shadow", v =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
slider.NubShadowColour = v ? Color4.Black.Opacity(0.2f) : Color4.Black.Opacity(0f);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,6 +81,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
|
||||
AddStep("enable slider", () => slider.Current.Disabled = false);
|
||||
}
|
||||
|
||||
public partial class TestSliderBar : ShearedSliderBar<double>
|
||||
{
|
||||
public new ShearedNub Nub => base.Nub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Extensions
|
||||
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
|
||||
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
|
||||
/// <returns>The formatted output.</returns>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
double floatValue = double.CreateTruncating(value);
|
||||
|
||||
|
||||
@@ -88,12 +88,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
|
||||
{
|
||||
Height = height;
|
||||
Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height };
|
||||
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Shear = OsuGame.SHEAR;
|
||||
Content.Masking = true;
|
||||
Shear = OsuGame.SHEAR;
|
||||
|
||||
Content.Anchor = Content.Origin = Anchor.Centre;
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
||||
@@ -21,37 +21,54 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public Action? OnDoubleClicked { get; init; }
|
||||
|
||||
protected const float BORDER_WIDTH = 3;
|
||||
|
||||
public const int HEIGHT = 30;
|
||||
public const float EXPANDED_SIZE = 50;
|
||||
public const float CORNER_RADIUS = 5;
|
||||
|
||||
private readonly Box fill;
|
||||
private readonly Container main;
|
||||
private readonly Container shadow;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the shape for the nub, allowing for any type of container to be used.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ShearedNub()
|
||||
{
|
||||
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
|
||||
InternalChild = main = new Container
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = BORDER_WIDTH,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
shadow = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 20f,
|
||||
},
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
main = new Container
|
||||
{
|
||||
Shear = OsuGame.SHEAR,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = 8f,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = fill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +93,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(onCurrentValueChanged, true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private bool glowing;
|
||||
@@ -89,22 +107,22 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return;
|
||||
|
||||
glowing = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
private Color4 shadowColour = Color4.Black.Opacity(0f);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
public Color4 ShadowColour
|
||||
{
|
||||
get => shadowColour;
|
||||
set
|
||||
{
|
||||
if (shadowColour == value)
|
||||
return;
|
||||
|
||||
shadowColour = value;
|
||||
shadow.FadeEdgeEffectTo(value, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +148,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
if (!Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +160,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowingAccentColour = value;
|
||||
if (Glowing)
|
||||
main.Colour = value;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set
|
||||
{
|
||||
glowColour = value;
|
||||
|
||||
var effect = main.EdgeEffect;
|
||||
effect.Colour = Glowing ? value : value.Opacity(0);
|
||||
main.EdgeEffect = effect;
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +190,26 @@ namespace osu.Game.Graphics.UserInterface
|
||||
else
|
||||
{
|
||||
main.ResizeWidthTo(0.75f, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
|
||||
main.TransformTo(nameof(BorderThickness), 8f, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Glowing)
|
||||
{
|
||||
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
|
||||
|
||||
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
|
||||
main.FadeColour(AccentColour, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using static osu.Game.Graphics.UserInterface.ShearedNub;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@@ -29,6 +28,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private readonly Container mainContent;
|
||||
|
||||
protected virtual bool FocusIndicator => true;
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
@@ -56,11 +57,17 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public Color4 NubShadowColour
|
||||
{
|
||||
get => Nub.ShadowColour;
|
||||
set => Nub.ShadowColour = value;
|
||||
}
|
||||
|
||||
public ShearedSliderBar()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Height = HEIGHT;
|
||||
RangePadding = EXPANDED_SIZE / 2;
|
||||
Height = ShearedNub.HEIGHT;
|
||||
RangePadding = ShearedNub.EXPANDED_SIZE / 2;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
@@ -102,7 +109,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Nub = new ShearedNub
|
||||
{
|
||||
X = -OsuGame.SHEAR.X * HEIGHT / 2f,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Current = { Value = true },
|
||||
@@ -146,13 +152,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
if (FocusIndicator)
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
mainContent.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = AccentColour.Darken(1),
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
@@ -191,8 +200,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1);
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Height = ModSelectPanel.HEIGHT;
|
||||
|
||||
// shear will be applied at a higher level in `ModPresetColumn`.
|
||||
Content.Shear = Vector2.Zero;
|
||||
Shear = Vector2.Zero;
|
||||
Padding = new MarginPadding();
|
||||
|
||||
Text = "+";
|
||||
|
||||
@@ -40,11 +40,13 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public AddPresetPopover(AddPresetButton addPresetButton)
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
button = addPresetButton;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
@@ -63,12 +65,24 @@ namespace osu.Game.Overlays.Mods
|
||||
Label = CommonStrings.Description,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
createButton = new ShearedButton
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
createButton = new ShearedButton(content_width)
|
||||
{
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = createPreset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,9 +52,11 @@ namespace osu.Game.Overlays.Mods
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float content_width = 300;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
Width = content_width,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
@@ -107,25 +109,27 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
useCurrentModsButton = new ShearedButton
|
||||
useCurrentModsButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = ModSelectOverlayStrings.UseCurrentMods,
|
||||
DarkerColour = colours.Blue1,
|
||||
LighterColour = colours.Blue0,
|
||||
TextColour = colourProvider.Background6,
|
||||
Action = useCurrentMods,
|
||||
},
|
||||
saveButton = new ShearedButton
|
||||
saveButton = new ShearedButton(content_width)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = Resources.Localisation.Web.CommonStrings.ButtonsSave,
|
||||
DarkerColour = colours.Orange1,
|
||||
LighterColour = colours.Orange0,
|
||||
|
||||
@@ -243,11 +243,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Padding = new MarginPadding { Right = OsuGame.SCREEN_EDGE_MARGIN };
|
||||
|
||||
InternalChild = NextButton = new ShearedButton(0)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 12f },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = FirstRunSetupOverlayStrings.GetStarted,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -81,5 +83,33 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// Create a fresh <see cref="Mod"/> instance based on this mod.
|
||||
/// </summary>
|
||||
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any user adjustable setting attached to this mod has a non-default value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This returns the instantaneous state of this mod. It may change over time.
|
||||
/// For tracking changes on a dynamic display, make sure to setup a <see cref="ModSettingChangeTracker"/>.
|
||||
/// </remarks>
|
||||
bool HasNonDefaultSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
bool hasAdjustments = false;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
{
|
||||
hasAdjustments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasAdjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -67,6 +68,22 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UserAdjustedSettingsCount != 1)
|
||||
return string.Empty;
|
||||
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
@@ -94,5 +111,26 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
|
||||
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of settings on this mod instance which have been adjusted by the user from their default values.
|
||||
/// </summary>
|
||||
protected int UserAdjustedSettingsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = (IBindable)property.GetValue(this)!;
|
||||
|
||||
if (!bindable.IsDefault)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
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.Utils;
|
||||
@@ -81,6 +82,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private Container extendedContent = null!;
|
||||
|
||||
private Drawable adjustmentMarker = null!;
|
||||
|
||||
private Circle cogBackground = null!;
|
||||
private SpriteIcon cog = null!;
|
||||
|
||||
private ModSettingChangeTracker? modSettingsChangeTracker;
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +145,7 @@ namespace osu.Game.Rulesets.UI
|
||||
Origin = Anchor.CentreLeft,
|
||||
Name = "main content",
|
||||
Size = MOD_ICON_SIZE,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
background = new Sprite
|
||||
{
|
||||
@@ -165,6 +171,29 @@ namespace osu.Game.Rulesets.UI
|
||||
Size = new Vector2(45),
|
||||
Icon = FontAwesome.Solid.Question
|
||||
},
|
||||
adjustmentMarker = new Container
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(64, 14),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
cogBackground = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
cog = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Cog,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.6f),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -216,11 +245,18 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
extendedContent.Alpha = showExtended ? 1 : 0;
|
||||
extendedText.Text = mod.ExtendedIconInformation;
|
||||
|
||||
if (mod.HasNonDefaultSettings)
|
||||
adjustmentMarker.Show();
|
||||
else
|
||||
adjustmentMarker.Hide();
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cogBackground.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
cog.Colour = backgroundColour;
|
||||
|
||||
extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
|
||||
extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Screens.Footer
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Padding = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
@@ -89,7 +89,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(7, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Screens.Footer
|
||||
footerContentContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = -15f,
|
||||
Y = -OsuGame.SCREEN_EDGE_MARGIN,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Screens.Footer
|
||||
hiddenButtonsContainer = new Container<ScreenFooterButton>
|
||||
{
|
||||
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Y = 10f,
|
||||
Y = ScreenFooterButton.Y_OFFSET,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected const int CORNER_RADIUS = 10;
|
||||
public const int Y_OFFSET = 10;
|
||||
|
||||
protected const int BUTTON_HEIGHT = 75;
|
||||
protected const int BUTTON_WIDTH = 116;
|
||||
|
||||
@@ -87,7 +88,7 @@ namespace osu.Game.Screens.Footer
|
||||
},
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -134,7 +135,7 @@ namespace osu.Game.Screens.Footer
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -CORNER_RADIUS,
|
||||
Y = -Y_OFFSET,
|
||||
Size = new Vector2(100, 5),
|
||||
Masking = true,
|
||||
CornerRadius = 3,
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@@ -20,8 +19,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DrawableGameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
|
||||
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
|
||||
@@ -87,7 +84,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}, true);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true);
|
||||
}
|
||||
|
||||
@@ -109,8 +105,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
drawable.Expanded.BindTo(Expanded);
|
||||
|
||||
Flow.Add(drawable);
|
||||
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort));
|
||||
drawable.DisplayOrder.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
@@ -179,22 +175,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = Flow
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.DisplayOrder.Value)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
Flow.SetLayoutPosition(score, i);
|
||||
score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
foreach (var score in Flow.ToArray())
|
||||
Flow.SetLayoutPosition(score, score.DisplayOrder.Value);
|
||||
}
|
||||
|
||||
private partial class InputDisabledScrollContainer : OsuScrollContainer
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||
public BindableInt Combo { get; } = new BindableInt();
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
public Bindable<int?> ScorePosition { get; } = new Bindable<int?>();
|
||||
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
|
||||
|
||||
private Func<ScoringMode, long>? getDisplayScoreFunction;
|
||||
@@ -69,28 +70,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
public Color4? TextColour { get; set; }
|
||||
|
||||
private int? scorePosition;
|
||||
|
||||
private bool scorePositionIsSet;
|
||||
|
||||
public int? ScorePosition
|
||||
{
|
||||
get => scorePosition;
|
||||
set
|
||||
{
|
||||
// We always want to run once, as the incoming value may be null and require a visual update to "-".
|
||||
if (value == scorePosition && scorePositionIsSet)
|
||||
return;
|
||||
|
||||
scorePosition = value;
|
||||
|
||||
positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-";
|
||||
scorePositionIsSet = true;
|
||||
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
public IUser? User { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -123,6 +102,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Accuracy.BindTo(score.Accuracy);
|
||||
Combo.BindTo(score.Combo);
|
||||
HasQuit.BindTo(score.HasQuit);
|
||||
ScorePosition.BindTo(score.Position);
|
||||
DisplayOrder.BindTo(score.DisplayOrder);
|
||||
GetDisplayScore = score.GetDisplayScore;
|
||||
|
||||
@@ -334,6 +314,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
updateState();
|
||||
Expanded.BindValueChanged(changeExpandedState, true);
|
||||
ScorePosition.BindValueChanged(_ => updateState(), true);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
@@ -392,7 +373,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
return;
|
||||
}
|
||||
|
||||
if (scorePosition == 1)
|
||||
positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-";
|
||||
|
||||
if (ScorePosition.Value == 1)
|
||||
{
|
||||
widthExtension = true;
|
||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
public long TotalScoreTiebreaker { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
|
||||
@@ -68,6 +68,25 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// </summary>
|
||||
public Colour4? TeamColour { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial position of the score on the leaderboard.
|
||||
/// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores).
|
||||
/// </summary>
|
||||
public int? InitialPosition { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The displayed rank of the score on the leaderboard.
|
||||
/// </summary>
|
||||
public Bindable<int?> Position { get; } = new Bindable<int?>();
|
||||
|
||||
/// <summary>
|
||||
/// The index of the score on the leaderboard.
|
||||
/// This differs from <see cref="Position"/> in that it is required (must always be known)
|
||||
/// and that it doesn't represent the score's position on global leaderboards.
|
||||
/// It's a property completely local to and relative to all scores provided by the managing <see cref="IGameplayLeaderboardProvider"/>.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
@@ -95,8 +114,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
TotalScore.Value = scoreInfo.TotalScore;
|
||||
Accuracy.Value = scoreInfo.Accuracy;
|
||||
Combo.Value = scoreInfo.Combo;
|
||||
DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
GetDisplayScore = scoreInfo.GetDisplayScore;
|
||||
InitialPosition = scoreInfo.Position;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
|
||||
@@ -14,14 +14,5 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
/// List of all scores to display on the leaderboard.
|
||||
/// </summary>
|
||||
public IBindableList<GameplayLeaderboardScore> Scores { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores),
|
||||
/// or is a full leaderboard (contains all scores that there will ever be).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is <see langword="true"/> and a tracked score is last on the leaderboard, it will show an "unknown" score position.
|
||||
/// </remarks>
|
||||
bool IsPartial { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -55,6 +56,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
{
|
||||
this.users = users;
|
||||
@@ -101,6 +104,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
HasQuit = { BindTarget = trackedUser.UserQuit },
|
||||
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
|
||||
};
|
||||
leaderboardScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
|
||||
leaderboardScore.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
scores.Add(leaderboardScore);
|
||||
}
|
||||
});
|
||||
@@ -124,6 +129,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
// new players are not supported.
|
||||
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
}
|
||||
|
||||
private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -174,6 +181,26 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
}
|
||||
}
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = scores
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.TotalScoreTiebreaker)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < orderedByScore.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
score.DisplayOrder.Value = i;
|
||||
score.Position.Value = i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
@@ -12,8 +14,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider
|
||||
{
|
||||
public bool IsPartial { get; private set; }
|
||||
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
@@ -23,13 +23,16 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private GameplayState? gameplayState { get; set; }
|
||||
|
||||
private readonly Cached sorting = new Cached();
|
||||
private bool isPartial;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var globalScores = leaderboardManager?.Scores.Value;
|
||||
|
||||
IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
|
||||
if (globalScores != null)
|
||||
{
|
||||
@@ -39,12 +42,70 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
{
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
DisplayOrder = { Value = long.MaxValue }
|
||||
});
|
||||
TotalScoreTiebreaker = long.MaxValue
|
||||
};
|
||||
localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
|
||||
scores.Add(localScore);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
}
|
||||
|
||||
private void sort()
|
||||
{
|
||||
if (sorting.IsValid)
|
||||
return;
|
||||
|
||||
var orderedByScore = scores
|
||||
.OrderByDescending(i => i.TotalScore.Value)
|
||||
.ThenBy(i => i.TotalScoreTiebreaker)
|
||||
.ToList();
|
||||
|
||||
int delta = 0;
|
||||
|
||||
for (int i = 0; i < orderedByScore.Count; i++)
|
||||
{
|
||||
var score = orderedByScore[i];
|
||||
|
||||
score.DisplayOrder.Value = i + 1;
|
||||
|
||||
// if we know we have all scores there can ever be, we can do the simple and obvious thing.
|
||||
if (!isPartial)
|
||||
score.Position.Value = i + 1;
|
||||
else
|
||||
{
|
||||
// we have a partial leaderboard, with potential gaps.
|
||||
// we have initial score positions which were valid at the point of starting play.
|
||||
// the assumption here is that non-tracked scores here cannot move around, only tracked ones can.
|
||||
if (score.Tracked)
|
||||
{
|
||||
int? previousScorePosition = i > 0 ? orderedByScore[i - 1].InitialPosition : 0;
|
||||
int? nextScorePosition = i < orderedByScore.Count - 1 ? orderedByScore[i + 1].InitialPosition : null;
|
||||
|
||||
// if the tracked score is perfectly between two scores which have known neighbouring initial positions,
|
||||
// we can assign it the position of the previous score plus one...
|
||||
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
|
||||
{
|
||||
score.Position.Value = previousScorePosition + 1;
|
||||
// but we also need to ensure all subsequent scores get shifted down one position, too.
|
||||
delta++;
|
||||
}
|
||||
// conversely, if the tracked score is not between neighbouring two scores and the leaderboard is partial,
|
||||
// we can't really assign a valid position at all. it could be any number between the two neighbours.
|
||||
else
|
||||
score.Position.Value = null;
|
||||
}
|
||||
// for non-tracked scores, we just need to apply any delta that might have come from the tracked scores
|
||||
// which might have been encountered and assigned a position earlier.
|
||||
else
|
||||
score.Position.Value = score.InitialPosition + delta;
|
||||
}
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardScore
|
||||
{
|
||||
public partial class LeaderboardScoreTooltip : VisibilityContainer, ITooltip<ScoreInfo>
|
||||
{
|
||||
private const float spacing = 20f;
|
||||
|
||||
private DateAndStatisticsPanel dateAndStatistics = null!;
|
||||
private ModsPanel modsPanel = null!;
|
||||
private TotalScoreRankPanel totalScoreRankPanel = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider;
|
||||
|
||||
public LeaderboardScoreTooltip(OverlayColourProvider colourProvider)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Width = 170;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, -spacing),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
dateAndStatistics = new DateAndStatisticsPanel(),
|
||||
modsPanel = new ModsPanel(),
|
||||
totalScoreRankPanel = new TotalScoreRankPanel(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private ScoreInfo? lastContent;
|
||||
|
||||
public void SetContent(ScoreInfo content)
|
||||
{
|
||||
if (lastContent != null && lastContent.Equals(content))
|
||||
return;
|
||||
|
||||
dateAndStatistics.Score = content;
|
||||
modsPanel.Score = content;
|
||||
totalScoreRankPanel.Score = content;
|
||||
lastContent = content;
|
||||
}
|
||||
|
||||
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
private partial class DateAndStatisticsPanel : CompositeDrawable
|
||||
{
|
||||
private OsuSpriteText absoluteDate = null!;
|
||||
private DrawableDate relativeDate = null!;
|
||||
private FillFlowContainer statistics = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt");
|
||||
relativeDate.Date = value.Date;
|
||||
|
||||
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
|
||||
new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0")));
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in value.Mods)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
|
||||
var generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")),
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()),
|
||||
};
|
||||
|
||||
if (value.PP != null)
|
||||
{
|
||||
generalStatistics = new[]
|
||||
{
|
||||
new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0"))
|
||||
}.Concat(generalStatistics).ToArray();
|
||||
}
|
||||
|
||||
statistics.ChildrenEnumerable = judgementsStatistics
|
||||
.Append(Empty().With(d => d.Height = 20))
|
||||
.Concat(generalStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Margin = new MarginPadding { Top = 8f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
absoluteDate = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
relativeDate = new DrawableDate(default, OsuFont.Style.Caption1.Size)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colourProvider.Content2,
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
CornerRadius = corner_radius,
|
||||
Masking = true,
|
||||
Margin = new MarginPadding { Top = 4f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
statistics = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Padding = new MarginPadding(8f),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class StatisticRow : CompositeDrawable
|
||||
{
|
||||
public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = label,
|
||||
Colour = labelColour,
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = value,
|
||||
Colour = Color4.White,
|
||||
Font = OsuFont.Style.Caption2,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ModsPanel : CompositeDrawable
|
||||
{
|
||||
private FillFlowContainer modsFlow = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
var mods = value.Mods;
|
||||
|
||||
if (!mods.Any())
|
||||
Hide();
|
||||
else
|
||||
{
|
||||
Show();
|
||||
|
||||
modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModIcon(m)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.3f),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background4,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Transparent,
|
||||
},
|
||||
modsFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing },
|
||||
Padding = new MarginPadding { Horizontal = 16f },
|
||||
Spacing = new Vector2(2f, -4f),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TotalScoreRankPanel : CompositeDrawable
|
||||
{
|
||||
private Box rankBackground = null!;
|
||||
private Container<DrawableRank> rankContainer = null!;
|
||||
private OsuSpriteText totalScore = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
public ScoreInfo Score
|
||||
{
|
||||
set
|
||||
{
|
||||
rankBackground.Colour = ColourInfo.GradientVertical(
|
||||
OsuColour.ForRank(value.Rank).Opacity(0f),
|
||||
OsuColour.ForRank(value.Rank).Opacity(0.5f));
|
||||
rankContainer.Child = new DrawableRank(value.Rank);
|
||||
totalScore.Current = scoreManager.GetBindableTotalScoreString(value);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = corner_radius;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 4f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#353535"),
|
||||
},
|
||||
rankBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
rankContainer = new Container<DrawableRank>
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(25f, 14f),
|
||||
Margin = new MarginPadding { Bottom = 5f },
|
||||
},
|
||||
totalScore = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Margin = new MarginPadding { Bottom = 25f, Top = 10f + spacing },
|
||||
Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true),
|
||||
Spacing = new Vector2(-1.5f),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// 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 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.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardWedge : VisibilityContainer
|
||||
{
|
||||
public IBindable<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
private Container<Placeholder> placeholderContainer = null!;
|
||||
private Placeholder? placeholder;
|
||||
|
||||
private Container scoresContainer = null!;
|
||||
|
||||
private OsuScrollContainer scoresScroll = null!;
|
||||
private Container personalBestDisplay = null!;
|
||||
|
||||
private Container<BeatmapLeaderboardScore> personalBestScoreContainer = null!;
|
||||
private LoadingLayer loading = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
private const float personal_best_height = 80;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
scoresScroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Child = scoresContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 5,
|
||||
// Left padding offsets the shear to create a visually appealing list display.
|
||||
Left = 80f,
|
||||
// Bottom padding ensures the last entry's full width is displayed
|
||||
// (ie it is fully on screen after shear is considered).
|
||||
Bottom = BeatmapLeaderboardScore.HEIGHT * 3
|
||||
},
|
||||
},
|
||||
},
|
||||
personalBestDisplay = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = personal_best_height,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Left = -40f },
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
// push the personal best 1px down to hide masking issues
|
||||
Y = 1f,
|
||||
X = -100f,
|
||||
Alpha = 0f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Text = "Personal Best",
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
personalBestScoreContainer = new Container<BeatmapLeaderboardScore>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 20f },
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
placeholderContainer = new Container<Placeholder>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
loading = new LoadingLayer(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scope.BindValueChanged(_ => refetchScores());
|
||||
FilterBySelectedMods.BindValueChanged(_ => refetchScores());
|
||||
beatmap.BindValueChanged(_ => refetchScores());
|
||||
ruleset.BindValueChanged(_ => refetchScores());
|
||||
mods.BindValueChanged(_ => refetchScoresFromMods());
|
||||
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void refetchScoresFromMods()
|
||||
{
|
||||
if (FilterBySelectedMods.Value)
|
||||
refetchScores();
|
||||
}
|
||||
|
||||
private bool initialFetchComplete;
|
||||
|
||||
private void refetchScores()
|
||||
{
|
||||
SetScores(Array.Empty<ScoreInfo>(), null);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
SetState(LeaderboardState.NoneSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
SetState(LeaderboardState.Retrieving);
|
||||
|
||||
var fetchBeatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
|
||||
|
||||
// For now, we forcefully refresh to keep things simple.
|
||||
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
|
||||
// (like returning from gameplay after setting a new score, returning to song select after main menu).
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true);
|
||||
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
// only bind this after the first fetch to avoid reading stale scores.
|
||||
fetchedScores.BindTo(leaderboardManager.Scores);
|
||||
fetchedScores.BindValueChanged(_ => updateScores(), true);
|
||||
initialFetchComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScores()
|
||||
{
|
||||
var scores = fetchedScores.Value;
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
if (scores.FailState != null)
|
||||
SetState((LeaderboardState)scores.FailState);
|
||||
else
|
||||
SetScores(scores.TopScores, scores.UserScore);
|
||||
}
|
||||
|
||||
protected void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo? userScore)
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
clearScores();
|
||||
SetState(LeaderboardState.Success);
|
||||
|
||||
if (!scores.Any())
|
||||
{
|
||||
SetState(LeaderboardState.NoScores);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s)
|
||||
{
|
||||
Rank = i + 1,
|
||||
IsPersonalBest = s.OnlineID == userScore?.OnlineID,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
}), loadedScores =>
|
||||
{
|
||||
int delay = 200;
|
||||
int i = 0;
|
||||
|
||||
foreach (var d in loadedScores)
|
||||
{
|
||||
d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i;
|
||||
|
||||
// This is a bit of a weird one. We're already in a sheared state and don't want top-level
|
||||
// shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor).
|
||||
d.Shear = Vector2.Zero;
|
||||
|
||||
scoresContainer.Add(d);
|
||||
|
||||
d.FadeOut()
|
||||
.MoveToX(-20f)
|
||||
.Delay(delay)
|
||||
.FadeIn(300, Easing.OutQuint)
|
||||
.MoveToX(0f, 300, Easing.OutQuint);
|
||||
|
||||
delay += 30;
|
||||
i++;
|
||||
}
|
||||
}, cancellation: cancellationTokenSource.Token);
|
||||
|
||||
if (userScore != null)
|
||||
{
|
||||
personalBestDisplay.MoveToX(0, 600, Easing.OutQuint);
|
||||
personalBestDisplay.FadeIn(600, Easing.OutQuint);
|
||||
personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore)
|
||||
{
|
||||
IsPersonalBest = true,
|
||||
Rank = userScore.Position,
|
||||
SelectedMods = { BindTarget = mods },
|
||||
};
|
||||
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearScores()
|
||||
{
|
||||
float delay = 0;
|
||||
|
||||
foreach (var d in scoresContainer)
|
||||
{
|
||||
// Avoid applying animations a second time to drawables which are already fading out.
|
||||
if (d.LifetimeEnd != double.MaxValue)
|
||||
continue;
|
||||
|
||||
d.Delay(delay)
|
||||
.MoveToX(-10f, 120, Easing.Out)
|
||||
.FadeOut(120, Easing.Out)
|
||||
.Expire();
|
||||
|
||||
delay += 20;
|
||||
}
|
||||
|
||||
personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint);
|
||||
personalBestDisplay.FadeOut(300, Easing.OutQuint);
|
||||
scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding(), 300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private LeaderboardState displayedState;
|
||||
|
||||
protected void SetState(LeaderboardState state)
|
||||
{
|
||||
if (state == displayedState)
|
||||
return;
|
||||
|
||||
if (state == LeaderboardState.Retrieving)
|
||||
loading.Show();
|
||||
else
|
||||
loading.Hide();
|
||||
|
||||
displayedState = state;
|
||||
|
||||
placeholder?.FadeOut(150, Easing.OutQuint).Expire();
|
||||
placeholder = getPlaceholderFor(state);
|
||||
|
||||
if (placeholder == null)
|
||||
return;
|
||||
|
||||
clearScores();
|
||||
|
||||
placeholderContainer.Child = placeholder;
|
||||
|
||||
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint);
|
||||
placeholder.FadeInFromZero(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Placeholder? getPlaceholderFor(LeaderboardState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case LeaderboardState.NetworkFailure:
|
||||
return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
|
||||
{
|
||||
Action = refetchScores
|
||||
};
|
||||
|
||||
case LeaderboardState.NoneSelected:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
|
||||
|
||||
case LeaderboardState.RulesetUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
|
||||
|
||||
case LeaderboardState.BeatmapUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
|
||||
|
||||
case LeaderboardState.NoScores:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
|
||||
|
||||
case LeaderboardState.NotLoggedIn:
|
||||
return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
|
||||
|
||||
case LeaderboardState.NotSupporter:
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
|
||||
|
||||
case LeaderboardState.NoTeam:
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoTeam);
|
||||
|
||||
case LeaderboardState.Retrieving:
|
||||
return null;
|
||||
|
||||
case LeaderboardState.Success:
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Drawable failRetryWedge = null!;
|
||||
private FailRetryDisplay failRetryDisplay = null!;
|
||||
|
||||
public bool RatingsVisible => ratingsWedge.Alpha > 0;
|
||||
public bool FailRetryVisible => failRetryWedge.Alpha > 0;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
[Resolved]
|
||||
@@ -250,7 +253,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
// We could consider hiding individual wedges based on zero data in the future.
|
||||
// Needs some experimentation on what looks good.
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null)
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmap != null)
|
||||
{
|
||||
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@@ -163,7 +164,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
Text = "...",
|
||||
Colour = colourProvider.Background4,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Button),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -308,18 +308,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap != null)
|
||||
{
|
||||
playCount.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
playCount.FadeOut(300, Easing.OutQuint);
|
||||
playCount.Value = null;
|
||||
}
|
||||
|
||||
favouritesStatistic.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1);
|
||||
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Shear = OsuGame.SHEAR,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
Size = new Vector2(BUTTON_WIDTH, bar_height),
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
new Container
|
||||
{
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
CornerRadius = Y_OFFSET,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = mod_display_portion,
|
||||
Masking = true,
|
||||
@@ -264,7 +264,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Depth = float.MaxValue;
|
||||
Origin = Anchor.BottomLeft;
|
||||
Shear = OsuGame.SHEAR;
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
CornerRadius = Y_OFFSET;
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = bar_height;
|
||||
Masking = true;
|
||||
|
||||
@@ -59,11 +59,8 @@ namespace osu.Game.Utils
|
||||
/// <summary>
|
||||
/// Applies rounding to the given BPM value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Double-rounding is applied intentionally (see https://github.com/ppy/osu/pull/18345#issue-1243311382 for rationale).
|
||||
/// </remarks>
|
||||
/// <param name="baseBpm">The base BPM to round.</param>
|
||||
/// <param name="rate">Rate adjustment, if applicable.</param>
|
||||
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate);
|
||||
public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(baseBpm * rate);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user