diff --git a/osu.Android.props b/osu.Android.props index 78ceaa8616..d7817cf4cf 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 9839d16030..db73bb7e7f 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -9,7 +9,7 @@ using osu.Framework.Android; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { protected override Framework.Game CreateGame() => new OsuGameAndroid(); diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index cc01009dd9..75feb21298 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Type.AffectsCombo() || !result.HasResult) return; - if (result.Type == HitResult.Miss) + if (!result.IsHit) { updateCombo(0, null); return; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 95e86de884..9c4c2b3d5b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -2,6 +2,7 @@ // 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.Testing; using osu.Game.Beatmaps; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneHoldNote : ManiaHitObjectTestScene { - public TestSceneHoldNote() + [Test] + public void TestHoldNote() { AddToggleStep("toggle hitting", v => { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index dd5fd93710..6b8f5d5d9d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class TestSceneNotes : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestVariousNotes() { - Child = new FillFlowContainer + DrawableNote note1 = null; + DrawableNote note2 = null; + DrawableHoldNote holdNote1 = null; + DrawableHoldNote holdNote2 = null; + + AddStep("create notes", () => { - Clock = new FramedClock(new ManualClock()), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new[] + Child = new FillFlowContainer { - createNoteDisplay(ScrollingDirection.Down, 1, out var note1), - createNoteDisplay(ScrollingDirection.Up, 2, out var note2), - createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), - createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), - } - }; + Clock = new FramedClock(new ManualClock()), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new[] + { + createNoteDisplay(ScrollingDirection.Down, 1, out note1), + createNoteDisplay(ScrollingDirection.Up, 2, out note2), + createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1), + createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2), + } + }; + }); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 220dedc4a4..d28b7bdf58 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -2,10 +2,40 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { + protected override double HealthIncreaseFor(HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.LargeTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.Meh: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + + case HitResult.Ok: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; + + case HitResult.Good: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.Great: + return DEFAULT_MAX_HEALTH_INCREASE * 0.8; + + case HitResult.Perfect: + return DEFAULT_MAX_HEALTH_INCREASE; + + default: + return base.HealthIncreaseFor(result); + } + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index ba6cad978d..f6d539c91b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables endHold(); } - if (Tail.Result.Type == HitResult.Miss) + if (Tail.Judged && !Tail.IsHit) HasBroken = true; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 31e43d3ee2..c780c0836e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 08c41b0d75..27960b3f3a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); } public abstract class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 973dc06e05..b3402d13e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png new file mode 100644 index 0000000000..c6c3771593 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png new file mode 100644 index 0000000000..232560a1d4 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 37df0d6e37..596bc06c68 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneHitCircle() + [Test] + public void TestVariousHitCircles() { AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index f3221ffe32..39deba2f57 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests { HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } }, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Autoplay = false, Beatmap = beatmap, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c79cae2fe5..c9e112f76d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneSlider() + [Test] + public void TestVariousSliders() { AddStep("Big Single", () => SetContents(() => testSimpleBig())); AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d5c3538c81..0164fb8bf4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit; + private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; - private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.IgnoreMiss; + private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; private ScoreAccessibleReplayPlayer currentPlayer; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9418565907..a0f70ce408 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -1,7 +1,13 @@ // 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -10,40 +16,180 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuSelectionHandler : SelectionHandler { - public override bool HandleMovement(MoveSelectionEvent moveEvent) + protected override void OnSelectionChanged() { - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + base.OnSelectionChanged(); - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var h in SelectedHitObjects.OfType()) + bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider); + + SelectionBox.CanRotate = canOperate; + SelectionBox.CanScaleX = canOperate; + SelectionBox.CanScaleY = canOperate; + } + + protected override void OnDragOperationEnded() + { + base.OnDragOperationEnded(); + referenceOrigin = null; + } + + public override bool HandleMovement(MoveSelectionEvent moveEvent) => + moveSelection(moveEvent.InstantDelta); + + /// + /// During a transform, the initial origin is stored so it can be used throughout the operation. + /// + private Vector2? referenceOrigin; + + public override bool HandleScale(Vector2 scale, Anchor reference) + { + adjustScaleFromAnchor(ref scale, reference); + + var hitObjects = selectedMovableObjects; + + // for the time being, allow resizing of slider paths only if the slider is + // the only hit object selected. with a group selection, it's likely the user + // is not looking to change the duration of the slider but expand the whole pattern. + if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } + Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height); - // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); + foreach (var point in slider.Path.ControlPoints) + point.Position.Value *= pathRelativeDeltaScale; } - - if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) - return false; - - foreach (var h in SelectedHitObjects.OfType()) + else { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } + // move the selection before scaling if dragging from top or left anchors. + if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; + if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; - h.Position += moveEvent.InstantDelta; + Quad quad = getSurroundingQuad(hitObjects); + + foreach (var h in hitObjects) + { + h.Position = new Vector2( + quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X), + quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y) + ); + } } return true; } + + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) + { + // cancel out scale in axes we don't care about (based on which drag handle was used). + if ((reference & Anchor.x1) > 0) scale.X = 0; + if ((reference & Anchor.y1) > 0) scale.Y = 0; + + // reverse the scale direction if dragging from top or left. + if ((reference & Anchor.x0) > 0) scale.X = -scale.X; + if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; + } + + public override bool HandleRotation(float delta) + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + + referenceOrigin ??= quad.Centre; + + foreach (var h in hitObjects) + { + h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); + + if (h is IHasPath path) + { + foreach (var point in path.Path.ControlPoints) + point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); + } + } + + // this isn't always the case but let's be lenient for now. + return true; + } + + private bool moveSelection(Vector2 delta) + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + + if (quad.TopLeft.X + delta.X < 0 || + quad.TopLeft.Y + delta.Y < 0 || + quad.BottomRight.X + delta.X > DrawWidth || + quad.BottomRight.Y + delta.Y > DrawHeight) + return false; + + foreach (var h in hitObjects) + h.Position += delta; + + return true; + } + + /// + /// Returns a gamefield-space quad surrounding the provided hit objects. + /// + /// The hit objects to calculate a quad for. + private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => + getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition })); + + /// + /// Returns a gamefield-space quad surrounding the provided points. + /// + /// The points to calculate a quad for. + private Quad getSurroundingQuad(IEnumerable points) + { + if (!SelectedHitObjects.Any()) + return new Quad(); + + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var p in points) + { + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); + } + + Vector2 size = maxPosition - minPosition; + + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + } + + /// + /// All osu! hitobjects which can be moved/rotated/scaled. + /// + private OsuHitObject[] selectedMovableObjects => SelectedHitObjects + .OfType() + .Where(h => !(h is Spinner)) + .ToArray(); + + /// + /// Rotate a point around an arbitrary origin. + /// + /// The point. + /// The centre origin to rotate around. + /// The angle to rotate (in degrees). + private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) + { + angle = -angle; + + point.X -= origin.X; + point.Y -= origin.Y; + + Vector2 ret; + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + + ret.X += origin.X; + ret.Y += origin.Y; + + return ret; + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 08fd13915d..80e40af717 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToDrawableHitObjects(drawables); } + private double lastSliderHeadFadeOutStartTime; + private double lastSliderHeadFadeOutDuration; + protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject d)) @@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { + case DrawableSliderTail sliderTail: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + sliderTail.FadeOut(fadeOutDuration); + + break; + + case DrawableSliderRepeat sliderRepeat: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + // only apply to circle piece – reverse arrow is not affected by hidden. + sliderRepeat.CirclePiece.FadeOut(fadeOutDuration); + + break; + case DrawableHitCircle circle: + + if (circle is DrawableSliderHead) + { + lastSliderHeadFadeOutDuration = fadeOutDuration; + lastSliderHeadFadeOutStartTime = fadeOutStartTime; + } + // we don't want to see the approach circle using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) circle.ApproachCircle.Hide(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index a438dc8be4..b5ac26c824 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var circleResult = (OsuHitCircleJudgementResult)r; // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (result != HitResult.Miss) + if (result.IsHit()) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2946331bc6..45c664ba3b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 012d9f8878..49535e7fff 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK.Graphics; @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject != null) { lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true); } else { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 9abcef83c4..b00d12983d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) @@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }; } @@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // rather than doing it this way, we should probably attach the sample to the tail circle. // this can only be done after we stop using LegacyLastTick. - if (TailCircle.Result.Type != HitResult.Miss) + if (TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index f65077685f..2a88f11f69 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Drawable scaleContainer; + public readonly Drawable CirclePiece; + public override bool DisplayResult => false; public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) @@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - InternalChild = scaleContainer = new ReverseArrowPiece(); + InternalChild = scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + // no default for this; only visible in legacy skins. + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), + arrow = new ReverseArrowPiece(), + } + }; } private readonly IBindable scaleBindable = new BindableFloat(); @@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private bool hasRotation; + private readonly ReverseArrowPiece arrow; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) { // When the repeat is hit, the arrow should fade out on spot rather than following the slider @@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); - while (Math.Abs(aimRotation - Rotation) > 180) - aimRotation += aimRotation < Rotation ? 360 : -360; + while (Math.Abs(aimRotation - arrow.Rotation) > 180) + aimRotation += aimRotation < arrow.Rotation ? 360 : -360; if (!hasRotation) { - Rotation = aimRotation; + arrow.Rotation = aimRotation; hasRotation = true; } else { // If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly). - Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint); + arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 0939e2847a..f5bcecccdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -1,15 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking + public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { - private readonly Slider slider; + private readonly SliderTailCircle tailCircle; /// /// The judgement text is provided by the . @@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - private readonly IBindable positionBindable = new Bindable(); - private readonly IBindable pathVersion = new Bindable(); + private readonly IBindable scaleBindable = new BindableFloat(); - public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) - : base(hitCircle) + private readonly SkinnableDrawable circlePiece; + + private readonly Container scaleContainer; + + public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle) + : base(tailCircle) { - this.slider = slider; - + this.tailCircle = tailCircle; Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - AlwaysPresent = true; + InternalChildren = new Drawable[] + { + scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + // no default for this; only visible in legacy skins. + circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()) + } + }, + }; + } - positionBindable.BindTo(hitCircle.PositionBindable); - pathVersion.BindTo(slider.Path.Version); + [BackgroundDependencyLoader] + private void load() + { + scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + scaleBindable.BindTo(HitObject.ScaleBindable); + } - positionBindable.BindValueChanged(_ => updatePosition()); - pathVersion.BindValueChanged(_ => updatePosition(), true); + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); - // TODO: This has no drawable content. Support for skins should be added. + circlePiece.FadeInFromZero(HitObject.TimeFadeIn); + } + + protected override void UpdateStateTransforms(ArmedState state) + { + base.UpdateStateTransforms(state); + + Debug.Assert(HitObject.HitWindows != null); + + switch (state) + { + case ArmedState.Idle: + this.Delay(HitObject.TimePreempt).FadeOut(500); + + Expire(true); + break; + + case ArmedState.Miss: + this.FadeOut(100); + break; + + case ArmedState.Hit: + // todo: temporary / arbitrary + this.Delay(800).FadeOut(); + break; + } } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } - private void updatePosition() => Position = HitObject.Position - slider.Position; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) => + Position = tailCircle.RepeatIndex % 2 == 0 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index fe7cb278b0..936bfaeb86 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; // Trigger a miss result for remaining ticks to avoid infinite gameplay. - foreach (var tick in ticks.Where(t => !t.IsHit)) + foreach (var tick in ticks.Where(t => !t.Result.HasResult)) tick.TriggerResult(false); ApplyResult(r => @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables else if (Progress > .75) r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) - r.Type = HitResult.Miss; + r.Type = r.Judgement.MinResult; }); } @@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables while (wholeSpins != spins) { - var tick = ticks.FirstOrDefault(t => !t.IsHit); + var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); // tick may be null if we've hit the spin limit. if (tick != null) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index aab01f45d4..e95cdc7ee3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, DrawableHitObject drawableHitObject) { InternalChildren = new Drawable[] { @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece + new TrianglesPiece((int)drawableHitObject.HitObject.StartTime) { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 587bd415ee..e855317544 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private Spinner spinner; + private const float initial_scale = 1.3f; private const float idle_alpha = 0.2f; private const float tracking_alpha = 0.4f; @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // we are slightly bigger than our parent, to clip the top and bottom of the circle // this should probably be revisited when scaled spinners are a thing. - Scale = new Vector2(1.3f); + Scale = new Vector2(initial_scale); Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -117,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime)); } - const float initial_scale = 0.2f; - float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; + const float initial_fill_scale = 0.2f; + float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress; fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; @@ -129,41 +130,54 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (!(drawableHitObject is DrawableSpinner)) return; - centre.ScaleTo(0); - mainContainer.ScaleTo(0); - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { - // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); - - centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); - mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + this.ScaleTo(initial_scale); + this.RotateTo(0); using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); - mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + // constant ambient rotation to give the spinner "spinning" character. + this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + } + + using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true)) + { + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out); + this.RotateTo(mainContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + this.ScaleTo(initial_scale * 0.8f, 320, Easing.In); + break; + } + } + } + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + centre.ScaleTo(0); + mainContainer.ScaleTo(0); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + } } } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - updateComplete(state == ArmedState.Hit, 0); - - using (BeginDelayedSequence(spinner.Duration, true)) - { - switch (state) - { - case ArmedState.Hit: - this.ScaleTo(Scale * 1.2f, 320, Easing.Out); - this.RotateTo(mainContainer.Rotation + 180, 320); - break; - - case ArmedState.Miss: - this.ScaleTo(Scale * 0.8f, 320, Easing.In); - break; - } - } + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + updateComplete(state == ArmedState.Hit, 0); } private void updateComplete(bool complete, double duration) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index 0e29a1dcd8..6cdb0d3df3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; - public TrianglesPiece() + public TrianglesPiece(int? seed = null) + : base(seed) { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 51f6a44a87..917382eccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects // if this is to change, we should revisit this. AddNested(TailCircle = new SliderTailCircle(this) { + RepeatIndex = e.SpanIndex, StartTime = e.Time, Position = EndPosition, StackHeight = StackHeight @@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeat + AddNested(new SliderRepeat(this) { RepeatIndex = e.SpanIndex, - SpanDuration = SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs deleted file mode 100644 index 151902a752..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Osu.Objects -{ - public class SliderCircle : HitCircle - { - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs new file mode 100644 index 0000000000..a6aed2c00e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects +{ + /// + /// A hit circle which is at the end of a slider path (either repeat or final tail). + /// + public abstract class SliderEndCircle : HitCircle + { + private readonly Slider slider; + + protected SliderEndCircle(Slider slider) + { + this.slider = slider; + } + + public int RepeatIndex { get; set; } + + public double SpanDuration => slider.SpanDuration; + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + if (RepeatIndex > 0) + { + // Repeat points after the first span should appear behind the still-visible one. + TimeFadeIn = 0; + + // The next end circle should appear exactly after the previous circle (on the same end) is hit. + TimePreempt = SpanDuration * 2; + } + else + { + // taken from osu-stable + const float first_end_circle_preempt_adjust = 2 / 3f; + + // The first end circle should fade in with the slider. + TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust; + } + } + + protected override HitWindows CreateHitWindows() => HitWindows.Empty; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index b6c58a75d1..cca86361c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -1,35 +1,19 @@ // 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 osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class SliderRepeat : OsuHitObject + public class SliderRepeat : SliderEndCircle { - public int RepeatIndex { get; set; } - public double SpanDuration { get; set; } - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + public SliderRepeat(Slider slider) + : base(slider) { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - // Out preempt should be one span early to give the user ample warning. - TimePreempt += SpanDuration; - - // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders - // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. - if (RepeatIndex > 0) - TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeatJudgement(); public class SliderRepeatJudgement : OsuJudgement diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index aff3f38e17..f9450062f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -13,23 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects /// Note that this should not be used for timing correctness. /// See usage in for more information. /// - public class SliderTailCircle : SliderCircle + public class SliderTailCircle : SliderEndCircle { - private readonly IBindable pathVersion = new Bindable(); - public SliderTailCircle(Slider slider) + : base(slider) { - pathVersion.BindTo(slider.Path.Version); - pathVersion.BindValueChanged(_ => Position = slider.EndPosition); } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderTailJudgement(); public class SliderTailJudgement : OsuJudgement { - public override HitResult MaxResult => HitResult.IgnoreHit; + public override HitResult MaxResult => HitResult.SmallTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 5468764692..2883f0c187 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu ReverseArrow, HitCircleText, SliderHeadHitCircle, + SliderTailHitCircle, SliderFollowCircle, SliderBall, SliderBody, diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs index 1885c76fcc..e6cd7bc59d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs @@ -1,9 +1,12 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private bool disjointTrail; private double lastTrailTime; + private IBindable cursorSize; public LegacyCursorTrail() { @@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, OsuConfigManager config) { Texture = skin.GetTexture("cursortrail"); disjointTrail = skin.GetTexture("cursormiddle") == null; @@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. Texture.ScaleAdjust *= 1.6f; } + + cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); } protected override double FadeDuration => disjointTrail ? 150 : 500; protected override bool InterpolateMovements => !disjointTrail; + protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); + protected override bool OnMouseMove(MouseMoveEvent e) { if (!disjointTrail) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index d15a0a3203..f051cbfa3b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning public class LegacyMainCirclePiece : CompositeDrawable { private readonly string priorityLookup; + private readonly bool hasNumber; - public LegacyMainCirclePiece(string priorityLookup = null) + public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true) { this.priorityLookup = priorityLookup; + this.hasNumber = hasNumber; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } @@ -70,7 +72,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } } }, - hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + }; + + if (hasNumber) + { + AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -78,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - }; + }); + } bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; @@ -107,7 +113,8 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); - indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + if (hasNumber) + indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } private void updateState(ValueChangedEvent state) @@ -120,16 +127,19 @@ namespace osu.Game.Rulesets.Osu.Skinning circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. - hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); - else + if (hasNumber) { - // old skins scale and fade it normally along other pieces. - hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); - hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; + + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } } break; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 1dfc9c0772..56b5571ce1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -70,9 +70,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } @@ -83,12 +81,19 @@ namespace osu.Game.Rulesets.Osu.Skinning var spinner = (Spinner)drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); - fixedMiddle.FadeColour(Color4.White); - using (BeginAbsoluteSequence(spinner.StartTime, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + fixedMiddle.FadeColour(Color4.White); + + using (BeginDelayedSequence(spinner.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + } } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index eba9abda0b..7b0d7acbbc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -88,9 +88,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } @@ -101,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Skinning var spinner = drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 851a8d56c9..78bc26eff7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderTailHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderendcircle", false); + + return null; + case OsuSkinComponents.SliderHeadHitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece("sliderstartcircle"); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 9bcb3abc63..0b30c28b8d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected virtual bool InterpolateMovements => true; + protected virtual float IntervalMultiplier => 1.0f; + private Vector2? lastPosition; private readonly InputResampler resampler = new InputResampler(); @@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f; + float interval = partSize.X / 2.5f * IntervalMultiplier; for (float d = interval; d < distance; d += interval) { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 0f605be8f9..e4c0766844 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests private readonly Random rng = new Random(1337); - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestVariousHits() { AddStep("Hit", () => addHitJudgement(false)); AddStep("Strong hit", () => addStrongHitJudgement(false)); diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index 0551df3211..647ad7853d 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements { switch (result) { - case HitResult.Great: + case HitResult.SmallTickHit: return 0.15; default: diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 286feac5ba..8f268dc1c7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!(obj is DrawableDrumRollTick)) return; - if (result.Type > HitResult.Miss) + if (result.IsHit) rollingHits++; else rollingHits--; @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 03df28f850..bb42240f25 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } @@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; if (!validActionPressed) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); else ApplyResult(r => r.Type = result); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 11ff0729e2..8ee4a5db71 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -211,9 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; - - ApplyResult(r => r.Type = hitResult); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index dd3c2289ea..f7a1d130eb 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpMultiplier; /// - /// HP multiplier for a . + /// HP multiplier for a that does not satisfy . /// private double hpMissMultiplier; @@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring } protected override double GetHealthIncreaseFor(JudgementResult result) - => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); + => base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 928072c491..e029040ef3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (r?.Type.AffectsCombo() == false) return; - passing = r == null || r.Type > HitResult.Miss; + passing = r == null || r.IsHit; foreach (var sprite in InternalChildren.OfType()) sprite.Passing = passing; diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 7b8ab89233..3bd20e4bb4 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0.15f; Masking = true; - if (result == HitResult.Miss) + if (!result.IsHit()) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 5966b24b34..1ca1be1bdf 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI target = centreHit; back = centre; - if (gameplayClock?.IsSeeking != true) - drumSample.Centre?.Play(); + drumSample.Centre?.Play(); } else if (action == RimAction) { target = rimHit; back = rim; - if (gameplayClock?.IsSeeking != true) - drumSample.Rim?.Play(); + drumSample.Rim?.Play(); } if (target != null) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index ff2c9fb1a9..b7a41ffd1c 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing [TestFixture] public class EditorChangeHandlerTest { + private int stateChangedFired; + + [SetUp] + public void SetUp() + { + stateChangedFired = 0; + } + [Test] public void TestSaveRestoreState() { @@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing addArbitraryChange(beatmap); handler.SaveState(); + Assert.That(stateChangedFired, Is.EqualTo(1)); + Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.False); @@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.True); + + Assert.That(stateChangedFired, Is.EqualTo(2)); } [Test] @@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.False); + Assert.That(stateChangedFired, Is.EqualTo(1)); string hash = handler.CurrentStateHash; @@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing handler.SaveState(); Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + Assert.That(stateChangedFired, Is.EqualTo(1)); handler.RestoreState(-1); @@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing // we should only be able to restore once even though we saved twice. Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.True); + Assert.That(stateChangedFired, Is.EqualTo(2)); } [Test] @@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) { + Assert.That(stateChangedFired, Is.EqualTo(i)); + addArbitraryChange(beatmap); handler.SaveState(); } @@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing { var beatmap = new EditorBeatmap(new Beatmap()); - return (new EditorChangeHandler(beatmap), beatmap); + var changeHandler = new EditorChangeHandler(beatmap); + + changeHandler.OnStateChange += () => stateChangedFired++; + return (changeHandler, beatmap); } private void addArbitraryChange(EditorBeatmap beatmap) diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs new file mode 100644 index 0000000000..3fd7c364b7 --- /dev/null +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Timing; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class GameplayClockTest + { + [TestCase(0)] + [TestCase(1)] + public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate) + { + var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); + var gameplayClock = new TestGameplayClock(framedClock); + + gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble()); + + Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0)); + } + + private class TestGameplayClock : GameplayClock + { + public List> MutableNonGameplayAdjustments { get; } = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public TestGameplayClock(IFrameBasedClock underlyingClock) + : base(underlyingClock) + { + } + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs new file mode 100644 index 0000000000..a1fb91024b --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneComposeSelectBox : OsuTestScene + { + private Container selectionArea; + + public TestSceneComposeSelectBox() + { + ComposeSelectionBox selectionBox = null; + + AddStep("create box", () => + Child = selectionArea = new Container + { + Size = new Vector2(300), + Position = -new Vector2(150), + Anchor = Anchor.Centre, + Children = new Drawable[] + { + selectionBox = new ComposeSelectionBox + { + CanRotate = true, + CanScaleX = true, + CanScaleY = true, + + OnRotation = handleRotation, + OnScale = handleScale + } + } + }); + + AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state); + AddToggleStep("toggle x", state => selectionBox.CanScaleX = state); + AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); + } + + private void handleScale(DragEvent e, Anchor reference) + { + if ((reference & Anchor.y1) == 0) + { + int directionY = (reference & Anchor.y0) > 0 ? -1 : 1; + if (directionY < 0) + selectionArea.Y += e.Delta.Y; + selectionArea.Height += directionY * e.Delta.Y; + } + + if ((reference & Anchor.x1) == 0) + { + int directionX = (reference & Anchor.x0) > 0 ? -1 : 1; + if (directionX < 0) + selectionArea.X += e.Delta.X; + selectionArea.Width += directionX * e.Delta.X; + } + } + + private void handleRotation(DragEvent e) + { + // kinda silly and wrong, but just showing that the drag handles work. + selectionArea.Rotation += e.Delta.X; + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 720cf51f2c..13a3195824 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -5,6 +5,8 @@ using System; using System.IO; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; @@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; + [Resolved] + private BeatmapManager beatmapManager { get; set; } + public override void SetUpSteps() { AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); @@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); + AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false); + } + + [Test] + public void TestExitWithoutSave() + { + AddStep("exit without save", () => Editor.Exit()); + AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); + AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs index 039a21fd94..f182023c0e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -19,12 +19,14 @@ namespace osu.Game.Tests.Visual.Editing public void TestSlidingSampleStopsOnSeek() { DrawableSlider slider = null; - DrawableSample[] samples = null; + DrawableSample[] loopingSamples = null; + DrawableSample[] onceOffSamples = null; AddStep("get first slider", () => { slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - samples = slider.ChildrenOfType().ToArray(); + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); }); AddStep("start playback", () => EditorClock.Start()); @@ -34,14 +36,15 @@ namespace osu.Game.Tests.Visual.Editing if (!slider.Tracking.Value) return false; - if (!samples.Any(s => s.Playing)) + if (!loopingSamples.Any(s => s.Playing)) return false; EditorClock.Seek(20000); return true; }); - AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing)); + AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing)); + AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs new file mode 100644 index 0000000000..3ab4df20df --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . 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.Graphics.Audio; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneGameplaySamplePlayback : PlayerTestScene + { + [Test] + public void TestAllSamplesStopDuringSeek() + { + DrawableSlider slider = null; + DrawableSample[] samples = null; + ISamplePlaybackDisabler gameplayClock = null; + + AddStep("get variables", () => + { + gameplayClock = Player.ChildrenOfType().First().GameplayClock; + slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); + samples = slider.ChildrenOfType().ToArray(); + }); + + AddUntilStep("wait for slider sliding then seek", () => + { + if (!slider.Tracking.Value) + return false; + + if (!samples.Any(s => s.Playing)) + return false; + + Player.ChildrenOfType().First().Seek(40000); + return true; + }); + + AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. + // the important thing is that at least one started, and that sample has since stopped. + AddAssert("no samples are playing", () => Player.ChildrenOfType().All(s => !s.IsPlaying)); + + AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value); + AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); + } + + protected override bool Autoplay => true; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 27027202ce..5b0fa44444 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds /// public float Velocity = 1; + private readonly Random stableRandom; + + private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); + private readonly SortedList parts = new SortedList(Comparer.Default); private IShader shader; private readonly Texture texture; - public Triangles() + /// + /// Construct a new triangle visualisation. + /// + /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time. + public Triangles(int? seed = null) { + if (seed != null) + stableRandom = new Random(seed.Value); + texture = Texture.WhitePixel; } @@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds { TriangleParticle particle = CreateTriangle(); - particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); - particle.ColourShade = RNG.NextSingle(); + particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1); + particle.ColourShade = nextRandom(); particle.Colour = CreateTriangleShade(particle.ColourShade); return particle; @@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds const float std_dev = 0.16f; const float mean = 0.5f; - float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats - float u2 = 1 - RNG.NextSingle(); + float u1 = 1 - nextRandom(); //uniform(0,1] random floats + float u2 = 1 - nextRandom(); float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1) var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 1070b8cbd2..8ed7885101 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -40,17 +40,28 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + protected override void LoadComplete() { base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; - beatmap.HitObjectUpdated += updateReplay; beatmap.HitObjectRemoved += removeHitObject; + + if (changeHandler != null) + { + // for now only regenerate replay on a finalised state change, not HitObjectUpdated. + changeHandler.OnStateChange += updateReplay; + } + else + { + beatmap.HitObjectUpdated += _ => updateReplay(); + } } - private void updateReplay(HitObject obj = null) => - drawableRuleset.RegenerateAutoplay(); + private void updateReplay() => drawableRuleset.RegenerateAutoplay(); private void addHitObject(HitObject hitObject) { @@ -58,8 +69,6 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.PostProcess(); - - updateReplay(); } private void removeHitObject(HitObject hitObject) @@ -68,8 +77,6 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); - - drawableRuleset.RegenerateAutoplay(); } public override bool PropagatePositionalInputSubTree => false; diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 4ee0ce437c..89a3a2b855 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -109,40 +109,40 @@ namespace osu.Game.Rulesets.Judgements return 0; case HitResult.SmallTickHit: - return DEFAULT_MAX_HEALTH_INCREASE * 0.05; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.SmallTickMiss: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.LargeTickHit: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE; case HitResult.LargeTickMiss: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return -DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Miss: return -DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Meh: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; case HitResult.Ok: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.Good: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE * 0.75; case HitResult.Great: - return DEFAULT_MAX_HEALTH_INCREASE * 0.8; - - case HitResult.Perfect: return DEFAULT_MAX_HEALTH_INCREASE; + case HitResult.Perfect: + return DEFAULT_MAX_HEALTH_INCREASE * 1.05; + case HitResult.SmallBonus: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.LargeBonus: - return DEFAULT_MAX_HEALTH_INCREASE * 0.2; + return DEFAULT_MAX_HEALTH_INCREASE; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 29c25f20a4..8012b4d95c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -385,9 +385,14 @@ namespace osu.Game.Rulesets.Objects.Drawables } /// - /// Stops playback of all samples. Automatically called when 's lifetime has been exceeded. + /// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out. + /// Automatically called when 's lifetime has been exceeded. /// - public virtual void StopAllSamples() => Samples?.Stop(); + public virtual void StopAllSamples() + { + if (Samples?.Looping == true) + Samples.Stop(); + } protected override void Update() { @@ -457,6 +462,8 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var nested in NestedHitObjects) nested.OnKilled(); + // failsafe to ensure looping samples don't get stuck in a playing state. + // this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped. StopAllSamples(); UpdateResult(false); @@ -506,19 +513,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime); - switch (Result.Type) - { - case HitResult.None: - break; - - case HitResult.Miss: - updateState(ArmedState.Miss); - break; - - default: - updateState(ArmedState.Hit); - break; - } + if (Result.HasResult) + updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); OnNewResult?.Invoke(this, Result); } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 55c4edfbd1..70b3d0c7d4 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.UI public GameplayClock GameplayClock => stabilityGameplayClock; [Cached(typeof(GameplayClock))] + [Cached(typeof(ISamplePlaybackDisabler))] private readonly StabilityGameplayClock stabilityGameplayClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) @@ -58,13 +59,16 @@ namespace osu.Game.Rulesets.UI private int direction; [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) + private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) { if (clock != null) { parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; GameplayClock.IsPaused.BindTo(clock.IsPaused); } + + // this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes). + stabilityGameplayClock.ParentSampleDisabler = sampleDisabler; } protected override void LoadComplete() @@ -207,11 +211,15 @@ namespace osu.Game.Rulesets.UI private void setClock() { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - - Clock = GameplayClock; - ProcessCustomClock = false; + if (parentGameplayClock == null) + { + // in case a parent gameplay clock isn't available, just use the parent clock. + parentGameplayClock ??= Clock; + } + else + { + Clock = GameplayClock; + } } public ReplayInputHandler ReplayInputHandler { get; set; } @@ -220,6 +228,8 @@ namespace osu.Game.Rulesets.UI { public GameplayClock ParentGameplayClock; + public ISamplePlaybackDisabler ParentSampleDisabler; + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); public StabilityGameplayClock(FramedClock underlyingClock) @@ -227,7 +237,11 @@ namespace osu.Game.Rulesets.UI { } - public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200; + protected override bool ShouldDisableSamplePlayback => + // handle the case where playback is catching up to real-time. + base.ShouldDisableSamplePlayback + || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true + || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs new file mode 100644 index 0000000000..530c6007cf --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -0,0 +1,312 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class ComposeSelectionBox : CompositeDrawable + { + public Action OnRotation; + public Action OnScale; + + public Action OperationStarted; + public Action OperationEnded; + + private bool canRotate; + + /// + /// Whether rotation support should be enabled. + /// + public bool CanRotate + { + get => canRotate; + set + { + canRotate = value; + recreate(); + } + } + + private bool canScaleX; + + /// + /// Whether vertical scale support should be enabled. + /// + public bool CanScaleX + { + get => canScaleX; + set + { + canScaleX = value; + recreate(); + } + } + + private bool canScaleY; + + /// + /// Whether horizontal scale support should be enabled. + /// + public bool CanScaleY + { + get => canScaleY; + set + { + canScaleY = value; + recreate(); + } + } + + public const float BORDER_RADIUS = 3; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + recreate(); + } + + private void recreate() + { + if (LoadState < LoadState.Loading) + return; + + InternalChildren = new Drawable[] + { + new Container + { + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + + AlwaysPresent = true, + Alpha = 0 + }, + } + }, + }; + + if (CanRotate) + { + const float separation = 40; + + AddRangeInternal(new Drawable[] + { + new Box + { + Colour = colours.YellowLight, + Blending = BlendingParameters.Additive, + Alpha = 0.3f, + Size = new Vector2(BORDER_RADIUS, separation), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new RotationDragHandle + { + Anchor = Anchor.TopCentre, + Y = -separation, + HandleDrag = e => OnRotation?.Invoke(e), + OperationStarted = operationStarted, + OperationEnded = operationEnded + } + }); + } + + if (CanScaleY) + { + AddRangeInternal(new[] + { + createDragHandle(Anchor.TopCentre), + createDragHandle(Anchor.BottomCentre), + }); + } + + if (CanScaleX) + { + AddRangeInternal(new[] + { + createDragHandle(Anchor.CentreLeft), + createDragHandle(Anchor.CentreRight), + }); + } + + if (CanScaleX && CanScaleY) + { + AddRangeInternal(new[] + { + createDragHandle(Anchor.TopLeft), + createDragHandle(Anchor.TopRight), + createDragHandle(Anchor.BottomLeft), + createDragHandle(Anchor.BottomRight), + }); + } + + ScaleDragHandle createDragHandle(Anchor anchor) => + new ScaleDragHandle(anchor) + { + HandleDrag = e => OnScale?.Invoke(e, anchor), + OperationStarted = operationStarted, + OperationEnded = operationEnded + }; + } + + private int activeOperations; + + private void operationEnded() + { + if (--activeOperations == 0) + OperationEnded?.Invoke(); + } + + private void operationStarted() + { + if (activeOperations++ == 0) + OperationStarted?.Invoke(); + } + + private class ScaleDragHandle : DragHandle + { + public ScaleDragHandle(Anchor anchor) + { + Anchor = anchor; + } + } + + private class RotationDragHandle : DragHandle + { + private SpriteIcon icon; + + [BackgroundDependencyLoader] + private void load() + { + Size *= 2; + + AddInternal(icon = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Icon = FontAwesome.Solid.Redo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + + protected override void UpdateHoverState() + { + base.UpdateHoverState(); + icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; + } + } + + private class DragHandle : Container + { + public Action OperationStarted; + public Action OperationEnded; + + public Action HandleDrag { get; set; } + + private Circle circle; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(10); + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateHoverState(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + UpdateHoverState(); + } + + protected bool HandlingMouse; + + protected override bool OnMouseDown(MouseDownEvent e) + { + HandlingMouse = true; + UpdateHoverState(); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) + { + OperationStarted?.Invoke(); + return true; + } + + protected override void OnDrag(DragEvent e) + { + HandleDrag?.Invoke(e); + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + HandlingMouse = false; + OperationEnded?.Invoke(); + + UpdateHoverState(); + base.OnDragEnd(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + HandlingMouse = false; + UpdateHoverState(); + base.OnMouseUp(e); + } + + protected virtual void UpdateHoverState() + { + circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); + this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 0615ebfc20..0ec981203a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Masking = true, BorderColour = Color4.White, - BorderThickness = SelectionHandler.BORDER_RADIUS, + BorderThickness = ComposeSelectionBox.BORDER_RADIUS, Child = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index a0220cf987..ee094c6246 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { - public const float BORDER_RADIUS = 2; - public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; @@ -45,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuSpriteText selectionDetailsText; + protected ComposeSelectionBox SelectionBox { get; private set; } + [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -69,19 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Children = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = BORDER_RADIUS, - BorderColour = colours.YellowDark, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - } - }, + // todo: should maybe be inside the SelectionBox? new Container { Name = "info text", @@ -100,11 +88,38 @@ namespace osu.Game.Screens.Edit.Compose.Components Font = OsuFont.Default.With(size: 11) } } - } + }, + SelectionBox = CreateSelectionBox(), } }; } + public ComposeSelectionBox CreateSelectionBox() + => new ComposeSelectionBox + { + OperationStarted = OnDragOperationBegan, + OperationEnded = OnDragOperationEnded, + + OnRotation = e => HandleRotation(e.Delta.X), + OnScale = (e, anchor) => HandleScale(e.Delta, anchor), + }; + + /// + /// Fired when a drag operation ends from the selection box. + /// + protected virtual void OnDragOperationBegan() + { + ChangeHandler.BeginChange(); + } + + /// + /// Fired when a drag operation begins from the selection box. + /// + protected virtual void OnDragOperationEnded() + { + ChangeHandler.EndChange(); + } + #region User Input Handling /// @@ -119,7 +134,22 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be moved. /// Returning true will also propagate StartTime changes provided by the closest . /// - public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true; + public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; + + /// + /// Handles the selected s being rotated. + /// + /// The delta angle to apply to the selection. + /// Whether any s could be moved. + public virtual bool HandleRotation(float angle) => false; + + /// + /// Handles the selected s being scaled. + /// + /// The delta scale to apply, in playfield local coordinates. + /// The point of reference where the scale is originating from. + /// Whether any s could be moved. + public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; public bool OnPressed(PlatformAction action) { @@ -222,11 +252,22 @@ namespace osu.Game.Screens.Edit.Compose.Components selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; if (count > 0) + { Show(); + OnSelectionChanged(); + } else Hide(); } + /// + /// Triggered whenever more than one object is selected, on each change. + /// Should update the selection box's state to match supported operations. + /// + protected virtual void OnSelectionChanged() + { + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index be3bca3242..0bb3d25011 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -13,6 +13,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -62,6 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private WaveformGraph waveform; + private ControlPointPart controlPoints; + private TimelineTickDisplay ticks; private TimelineTickDisplay ticks; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a0692d94e6..875ab25003 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit private DependencyContainer dependencies; + private bool isNewBeatmap; + protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -113,8 +115,6 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - bool isNewBeatmap = false; - if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; @@ -287,6 +287,9 @@ namespace osu.Game.Screens.Edit protected void Save() { + // no longer new after first user-triggered save. + isNewBeatmap = false; + // apply any set-level metadata changes. beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); @@ -435,10 +438,20 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) + if (!exitConfirmed) { - dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); - return true; + // if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save. + if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) + { + confirmExit(); + return true; + } + + if (isNewBeatmap || HasUnsavedChanges) + { + dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + return true; + } } Background.FadeColour(Color4.White, 500); @@ -456,6 +469,12 @@ namespace osu.Game.Screens.Edit private void confirmExit() { + if (isNewBeatmap) + { + // confirming exit without save means we should delete the new beatmap completely. + beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + } + exitConfirmed = true; this.Exit(); } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 617c436ee0..b69e9c4c51 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); + public event Action OnStateChange; + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); @@ -79,9 +81,6 @@ namespace osu.Game.Screens.Edit SaveState(); } - /// - /// Saves the current state. - /// public void SaveState() { if (bulkChangesStarted > 0) @@ -109,6 +108,8 @@ namespace osu.Game.Screens.Edit savedStates.Add(newState); currentState = savedStates.Count - 1; + + OnStateChange?.Invoke(); updateBindables(); } } @@ -136,6 +137,7 @@ namespace osu.Game.Screens.Edit isRestoring = false; + OnStateChange?.Invoke(); updateBindables(); } diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index c1328252d4..0283421b8c 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -1,6 +1,7 @@ // 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 osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit @@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit /// public interface IEditorChangeHandler { + /// + /// Fired whenever a state change occurs. + /// + event Action OnStateChange; + /// /// Begins a bulk state change event. should be invoked soon after. /// @@ -29,5 +35,11 @@ namespace osu.Game.Screens.Edit /// This should be invoked as soon as possible after to cause a state change. /// void EndChange(); + + /// + /// Immediately saves the current state. + /// Note that this will be a no-op if there is a change in progress via . + /// + void SaveState(); } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index e1182d9fa4..c40061b97c 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing private IReadOnlyList createSections() => new Drawable[] { + new GroupSection(), new TimingSection(), new DifficultySection(), new SampleSection(), diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 78766d9777..b55d74e3b4 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -28,6 +28,7 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; + multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 71e7f42713..2f143108a9 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { kiai.Current = point.NewValue.KiaiModeBindable; + kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; + omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs new file mode 100644 index 0000000000..c77d48ef0a --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.Edit.Timing +{ + internal class GroupSection : CompositeDrawable + { + private LabelledTextBox textBox; + + private TriangleButton button; + + [Resolved] + protected Bindable SelectedGroup { get; private set; } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } + + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding(10); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textBox = new LabelledTextBox + { + Label = "Time" + }, + button = new TriangleButton + { + Text = "Use current time", + RelativeSizeAxes = Axes.X, + Action = () => changeSelectedGroupTime(clock.CurrentTime) + } + } + }, + }; + + textBox.OnCommit += (sender, isNew) => + { + if (!isNew) + return; + + if (double.TryParse(sender.Text, out var newTime)) + { + changeSelectedGroupTime(newTime); + } + else + { + SelectedGroup.TriggerChange(); + } + }; + + SelectedGroup.BindValueChanged(group => + { + if (group.NewValue == null) + { + textBox.Text = string.Empty; + + textBox.Current.Disabled = true; + button.Enabled.Value = false; + return; + } + + textBox.Current.Disabled = false; + button.Enabled.Value = true; + + textBox.Text = $"{group.NewValue.Time:n0}"; + }, true); + } + + private void changeSelectedGroupTime(in double time) + { + if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time) + return; + + changeHandler?.BeginChange(); + + var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); + + Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); + + foreach (var cp in currentGroupItems) + Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); + + SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time); + + changeHandler?.EndChange(); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs index de986e28ca..280e19c99a 100644 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs @@ -35,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bank.Current = point.NewValue.SampleBankBindable; + bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + volume.Current = point.NewValue.SampleVolumeBindable; + volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs index 603fb77f31..7a81eeb1a4 100644 --- a/osu.Game/Screens/Edit/Timing/Section.cs +++ b/osu.Game/Screens/Edit/Timing/Section.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] protected Bindable SelectedGroup { get; private set; } + [Resolved(canBeNull: true)] + protected IEditorChangeHandler ChangeHandler { get; private set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index 14023b0c35..d5afc8978d 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -38,6 +38,7 @@ namespace osu.Game.Screens.Edit.Timing }, slider = new SettingsSlider { + TransferValueOnCommit = true, RelativeSizeAxes = Axes.X, } } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 269874fea8..0796097186 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -81,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -140,6 +143,7 @@ namespace osu.Game.Screens.Edit.Timing controlGroups.BindCollectionChanged((sender, args) => { table.ControlGroups = controlGroups; + changeHandler.SaveState(); }, true); } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 0202441537..2ab8703cc4 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -37,8 +37,13 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bpmSlider.Bindable = point.NewValue.BeatLengthBindable; + bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; + // no need to hook change handler here as it's the same bindable as above + timeSignature.Bindable = point.NewValue.TimeSignatureBindable; + timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -117,6 +122,8 @@ namespace osu.Game.Screens.Edit.Timing bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); base.Bindable = bpmBindable; + + TransferValueOnCommit = true; } public override Bindable Bindable diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 9d04722c12..eeea6777c6 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Timing; +using osu.Framework.Utils; namespace osu.Game.Screens.Play { @@ -27,6 +28,8 @@ namespace osu.Game.Screens.Play /// public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public GameplayClock(IFrameBasedClock underlyingClock) { this.underlyingClock = underlyingClock; @@ -47,7 +50,12 @@ namespace osu.Game.Screens.Play double baseRate = Rate; foreach (var adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment.Value, 0)) + return 0; + baseRate /= adjustment.Value; + } return baseRate; } @@ -56,13 +64,15 @@ namespace osu.Game.Screens.Play public bool IsRunning => underlyingClock.IsRunning; /// - /// Whether an ongoing seek operation is active. + /// Whether nested samples supporting the interface should be paused. /// - public virtual bool IsSeeking => false; + protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value; public void ProcessFrame() { - // we do not want to process the underlying clock. + // intentionally not updating the underlying clock (handled externally). + + samplePlaybackDisabled.Value = ShouldDisableSamplePlayback; } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -73,6 +83,6 @@ namespace osu.Game.Screens.Play public IClock Source => underlyingClock; - public IBindable SamplePlaybackDisabled => IsPaused; + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } } diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index 7736541c92..fc4a1a5d83 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -13,7 +13,6 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { @@ -106,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD public void Flash(JudgementResult result) { - if (result.Type == HitResult.Miss) + if (!result.IsHit) return; Scheduler.AddOnce(flash); diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 45fdc3ff33..aa2a83774e 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index 18a2238784..055db143d1 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Statistics public UnstableRate(IEnumerable hitEvents) : base("Unstable Rate") { - var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss) + var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()) .Select(ev => ev.TimeOffset).ToArray(); Value = 10 * standardDeviation(timeOffsets); } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 9819574b1d..d340f67575 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -34,21 +34,21 @@ namespace osu.Game.Skinning samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); samplePlaybackDisabled.BindValueChanged(disabled => { - if (RequestedPlaying) + if (!RequestedPlaying) return; + + // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). + if (!Looping) return; + + if (disabled.NewValue) + base.Stop(); + else { - if (disabled.NewValue) - base.Stop(); - // it's not easy to know if a sample has finished playing (to end). - // to keep things simple only resume playing looping samples. - else if (Looping) + // schedule so we don't start playing a sample which is no longer alive. + Schedule(() => { - // schedule so we don't start playing a sample which is no longer alive. - Schedule(() => - { - if (RequestedPlaying) - base.Play(); - }); - } + if (RequestedPlaying) + base.Play(); + }); } }); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3a839ac1a4..fa2135580d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 31f1af135d..20a51e5feb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - +