diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs new file mode 100644 index 0000000000..c969cb11b4 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -0,0 +1,147 @@ +// Copyright (c) ppy Pty Ltd . 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.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene + { + private const double time_spinner_start = 2000; + private const double time_spinner_end = 4000; + + private List judgementResults = new List(); + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + [Test] + public void TestHitNothing() + { + performTest(new List()); + + AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult)); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public void TestNumberOfSpins(int spins) + { + performTest(generateReplay(spins)); + + for (int i = 0; i < spins; ++i) + assertResult(i, HitResult.SmallBonus); + + assertResult(spins, HitResult.IgnoreMiss); + } + + [Test] + public void TestHitEverything() + { + performTest(generateReplay(20)); + + AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); + } + + private static List generateReplay(int spins) + { + var replayFrames = new List(); + + const int frames_per_spin = 30; + + for (int i = 0; i < spins * frames_per_spin; ++i) + { + float totalProgress = i / (float)(spins * frames_per_spin); + float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; + double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; + float posX = MathF.Cos(2 * MathF.PI * spinProgress); + float posY = MathF.Sin(2 * MathF.PI * spinProgress); + Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; + + replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); + } + + return replayFrames; + } + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private void assertResult(int index, HitResult expectedResult) + { + AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}", + () => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type, + () => Is.EqualTo(expectedResult)); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index bf06f513b7..719cf57d98 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default lastAngle = thisAngle; - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f; + IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; - Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed)); + Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } /// diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cff5731181..3e63d624e7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoHitObjectComposer : HitObjectComposer { + protected override bool ApplyHorizontalCentering => false; + public TaikoHitObjectComposer(TaikoRuleset ruleset) : base(ruleset) { diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 6933eafc29..49478a2174 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -136,11 +136,11 @@ namespace osu.Game.Tournament.Components if (match.NewValue != null) match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged; - updateState(); + Scheduler.AddOnce(updateState); } private void picksBansOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - => updateState(); + => Scheduler.AddOnce(updateState); private BeatmapChoice? choice; diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 62d18ac9e5..9411892dc5 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tournament.Screens.Editors private WarningBox rightClickMessage; + private RectangularPositionSnapGrid grid; + [Resolved(canBeNull: true)] [CanBeNull] private IDialogOverlay dialogOverlay { get; set; } @@ -53,10 +55,12 @@ namespace osu.Game.Tournament.Screens.Editors AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); - ScrollContent.Add(new RectangularPositionSnapGrid(Vector2.Zero) + ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero) { Spacing = new Vector2(GRID_SPACING), - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, Depth = float.MaxValue }); @@ -64,6 +68,22 @@ namespace osu.Game.Tournament.Screens.Editors updateMessage(); } + protected override void Update() + { + base.Update(); + + // Expand grid with the content to allow going beyond the bounds of the screen. + grid.Size = ScrollContent.Size + new Vector2(GRID_SPACING * 2); + } + + private Vector2 lastMatchesContainerMouseDownPosition; + + protected override bool OnMouseDown(MouseDownEvent e) + { + lastMatchesContainerMouseDownPosition = MatchesContainer.ToLocalSpace(e.ScreenSpaceMouseDownPosition); + return base.OnMouseDown(e); + } + private void updateMessage() { rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; @@ -85,7 +105,8 @@ namespace osu.Game.Tournament.Screens.Editors { new OsuMenuItem("Create new match", MenuItemType.Highlighted, () => { - Vector2 pos = MatchesContainer.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position); + Vector2 pos = MatchesContainer.Count == 0 ? Vector2.Zero : lastMatchesContainerMouseDownPosition; + TournamentMatch newMatch = new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } }; LadderInfo.Matches.Add(newMatch); diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index a74c9a9429..2d5281b893 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -57,12 +57,15 @@ namespace osu.Game.Tournament.Screens.Ladder }, ScrollContent = new LadderDragContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { paths = new Container { RelativeSizeAxes = Axes.Both }, headings = new Container { RelativeSizeAxes = Axes.Both }, - MatchesContainer = new Container { RelativeSizeAxes = Axes.Both }, + MatchesContainer = new Container + { + AutoSizeAxes = Axes.Both + }, } }, } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index bad7c55883..c967187b5c 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -44,6 +44,11 @@ namespace osu.Game.Rulesets.Edit public abstract partial class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject { + /// + /// Whether the playfield should be centered horizontally. Should be disabled for playfields which span the full horizontal width. + /// + protected virtual bool ApplyHorizontalCentering => true; + protected IRulesetConfigManager Config { get; private set; } // Provides `Playfield` @@ -119,8 +124,6 @@ namespace osu.Game.Rulesets.Edit { Name = "Playfield content", RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Children = new Drawable[] { // layers below playfield @@ -241,8 +244,23 @@ namespace osu.Game.Rulesets.Edit { base.Update(); - // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. - PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; + if (ApplyHorizontalCentering) + { + PlayfieldContentContainer.Anchor = Anchor.Centre; + PlayfieldContentContainer.Origin = Anchor.Centre; + + // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; + PlayfieldContentContainer.X = 0; + } + else + { + PlayfieldContentContainer.Anchor = Anchor.CentreLeft; + PlayfieldContentContainer.Origin = Anchor.CentreLeft; + + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT); + PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT; + } } public override Playfield Playfield => drawableRulesetWrapper.Playfield; diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 063ea23281..cfc01fe17b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -54,7 +54,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!gridCache.IsValid) { ClearInternal(); - createContent(); + + if (DrawWidth > 0 && DrawHeight > 0) + createContent(); + gridCache.Validate(); } }