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 @@
-
+