diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index 483c468c1e..a0833ff91f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
c.Add(hitExplosionPools[poolIndex].Get(e =>
{
- e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
+ e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement()));
e.Anchor = Anchor.Centre;
e.Origin = Anchor.Centre;
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 77db1b0bd8..93128c512f 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
@@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
@@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
@@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
}
@@ -188,10 +181,31 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
+ ///
+ /// -----[ ]-----
+ /// xox o
+ ///
+ [Test]
+ public void TestPressAtStartThenReleaseAndImmediatelyRepress()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 1),
+ new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertComboAtJudgement(0, 1);
+ assertTailJudgement(HitResult.Meh);
+ assertComboAtJudgement(1, 0);
+ assertComboAtJudgement(2, 1);
+ }
+
///
/// -----[ ]-----
/// xo x o
@@ -208,7 +222,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -228,7 +241,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -246,7 +258,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -264,7 +275,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -358,7 +368,6 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Good);
@@ -405,7 +414,6 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Great);
@@ -425,7 +433,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Meh);
}
@@ -476,42 +483,6 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
- [Test]
- public void TestHitTailBeforeLastTick()
- {
- const int tick_rate = 8;
- const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
- const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
-
- var beatmap = new Beatmap
- {
- HitObjects =
- {
- new HoldNote
- {
- StartTime = time_head,
- Duration = time_tail - time_head,
- Column = 0,
- }
- },
- BeatmapInfo =
- {
- Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
- Ruleset = new ManiaRuleset().RulesetInfo
- },
- };
-
- performTest(new List
- {
- new ManiaReplayFrame(time_head, ManiaAction.Key1),
- new ManiaReplayFrame(time_last_tick - 5)
- }, beatmap);
-
- assertHeadJudgement(HitResult.Perfect);
- assertLastTickJudgement(HitResult.LargeTickMiss);
- assertTailJudgement(HitResult.Ok);
- }
-
[Test]
public void TestZeroLength()
{
@@ -551,11 +522,8 @@ namespace osu.Game.Rulesets.Mania.Tests
private void assertNoteJudgement(HitResult result)
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
- private void assertTickJudgement(HitResult result)
- => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
-
- private void assertLastTickJudgement(HitResult result)
- => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
+ private void assertComboAtJudgement(int judgementIndex, int combo)
+ => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
private ScoreAccessibleReplayPlayer currentPlayer = null!;
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs
new file mode 100644
index 0000000000..6719665cbe
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs
@@ -0,0 +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 osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Judgements
+{
+ public class HoldNoteBodyJudgement : ManiaJudgement
+ {
+ public override HitResult MaxResult => HitResult.IgnoreHit;
+ public override HitResult MinResult => HitResult.ComboBreak;
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
deleted file mode 100644
index ae9e8bd287..0000000000
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Judgements
-{
- public class HoldNoteTickJudgement : ManiaJudgement
- {
- public override HitResult MaxResult => HitResult.LargeTickHit;
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index c3fec92b92..86920927dc 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -35,10 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
+ public DrawableHoldNoteBody Body => bodyContainer.Child;
private Container headContainer;
private Container tailContainer;
- private Container tickContainer;
+ private Container bodyContainer;
private PausableSkinnableSound slidingSample;
@@ -60,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
///
- /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
- ///
- public double? HoldBrokenTime { get; private set; }
-
- ///
- /// Whether the hold note has been released potentially without having caused a break.
+ /// Used to decide whether to visually clamp the hold note to the judgement line.
///
private double? releaseTime;
@@ -103,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container { RelativeSizeAxes = Axes.Both }
}
},
+ bodyContainer = new Container { RelativeSizeAxes = Axes.Both },
bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
{
RelativeSizeAxes = Axes.Both,
@@ -110,7 +107,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
RelativeSizeAxes = Axes.X
},
- tickContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
slidingSample = new PausableSkinnableSound { Looping = true }
});
@@ -118,7 +114,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
maskedContents.AddRange(new[]
{
bodyPiece.CreateProxy(),
- tickContainer.CreateProxy(),
tailContainer.CreateProxy(),
});
}
@@ -136,7 +131,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
sizingContainer.Size = Vector2.One;
HoldStartTime = null;
- HoldBrokenTime = null;
releaseTime = null;
}
@@ -154,8 +148,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tailContainer.Child = tail;
break;
- case DrawableHoldNoteTick tick:
- tickContainer.Add(tick);
+ case DrawableHoldNoteBody body:
+ bodyContainer.Child = body;
break;
}
}
@@ -165,7 +159,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.ClearNestedHitObjects();
headContainer.Clear(false);
tailContainer.Clear(false);
- tickContainer.Clear(false);
+ bodyContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@@ -178,8 +172,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
case HeadNote head:
return new DrawableHoldNoteHead(head);
- case HoldNoteTick tick:
- return new DrawableHoldNoteTick(tick);
+ case HoldNoteBody body:
+ return new DrawableHoldNoteBody(body);
}
return base.CreateNestedHitObject(hitObject);
@@ -266,20 +260,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (Tail.AllJudged)
{
- foreach (var tick in tickContainer)
- {
- if (!tick.Judged)
- tick.MissForcefully();
- }
-
if (Tail.IsHit)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else
MissForcefully();
}
- if (Tail.Judged && !Tail.IsHit)
- HoldBrokenTime = Time.Current;
+ // Make sure that the hold note is fully judged by giving the body a judgement.
+ if (Tail.AllJudged && !Body.AllJudged)
+ Body.TriggerResult(Tail.IsHit);
}
public override void MissForcefully()
@@ -333,22 +322,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (e.Action != Action.Value)
return;
- // Make sure a hold was started
- if (HoldStartTime == null)
- return;
-
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
if ((Clock as IGameplayClock)?.IsRewinding == true)
return;
- Tail.UpdateResult();
- endHold();
+ // When our action is released and we are in the middle of a hold, there's a chance that
+ // the user has released too early (before the tail).
+ //
+ // In such a case, we want to record this against the DrawableHoldNoteBody.
+ if (HoldStartTime != null)
+ {
+ Tail.UpdateResult();
+ Body.TriggerResult(Tail.IsHit);
- // If the key has been released too early, the user should not receive full score for the release
- if (!Tail.IsHit)
- HoldBrokenTime = Time.Current;
-
- releaseTime = Time.Current;
+ endHold();
+ releaseTime = Time.Current;
+ }
}
private void endHold()
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
new file mode 100644
index 0000000000..1b2efbafdf
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables
+{
+ public partial class DrawableHoldNoteBody : DrawableManiaHitObject
+ {
+ public bool HasHoldBreak => AllJudged && !IsHit;
+
+ public override bool DisplayResult => false;
+
+ public DrawableHoldNoteBody()
+ : this(null)
+ {
+ }
+
+ public DrawableHoldNoteBody(HoldNoteBody hitObject)
+ : base(hitObject)
+ {
+ }
+
+ internal void TriggerResult(bool hit)
+ {
+ if (AllJudged) return;
+
+ ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index e7326df07d..a559e91f1b 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
- if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
+ bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
+
+ if (result > HitResult.Meh && hasComboBreak)
result = HitResult.Meh;
r.Type = result;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
deleted file mode 100644
index ce6a83f79f..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System;
-using System.Diagnostics;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-
-namespace osu.Game.Rulesets.Mania.Objects.Drawables
-{
- ///
- /// Visualises a hit object.
- ///
- public partial class DrawableHoldNoteTick : DrawableManiaHitObject
- {
- ///
- /// References the time at which the user started holding the hold note.
- ///
- private Func holdStartTime;
-
- private Container glowContainer;
-
- public DrawableHoldNoteTick()
- : this(null)
- {
- }
-
- public DrawableHoldNoteTick(HoldNoteTick hitObject)
- : base(hitObject)
- {
- Anchor = Anchor.TopCentre;
- Origin = Anchor.TopCentre;
-
- RelativeSizeAxes = Axes.X;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- AddInternal(glowContainer = new CircularContainer
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
- }
- });
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- AccentColour.BindValueChanged(colour =>
- {
- glowContainer.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 2f,
- Roundness = 15f,
- Colour = colour.NewValue.Opacity(0.3f)
- };
- }, true);
- }
-
- protected override void OnApply()
- {
- base.OnApply();
-
- Debug.Assert(ParentHitObject != null);
-
- var holdNote = (DrawableHoldNote)ParentHitObject;
- holdStartTime = () => holdNote.HoldStartTime;
- }
-
- protected override void OnFree()
- {
- base.OnFree();
-
- holdStartTime = null;
- }
-
- protected override void CheckForResult(bool userTriggered, double timeOffset)
- {
- if (Time.Current < HitObject.StartTime)
- return;
-
- double? startTime = holdStartTime?.Invoke();
-
- if (startTime == null || startTime > HitObject.StartTime)
- ApplyResult(r => r.Type = r.Judgement.MinResult);
- else
- ApplyResult(r => r.Type = r.Judgement.MaxResult);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
index e69cc62aed..a2e89ea560 100644
--- a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
@@ -3,6 +3,9 @@
namespace osu.Game.Rulesets.Mania.Objects
{
+ ///
+ /// The head note of a .
+ ///
public class HeadNote : Note
{
}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index c367886efe..3f930a310b 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -6,8 +6,6 @@
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -81,27 +79,18 @@ namespace osu.Game.Rulesets.Mania.Objects
///
public TailNote Tail { get; private set; }
- public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
-
///
- /// The time between ticks of this hold.
+ /// The body of the hold.
+ /// This is an invisible and silent object that tracks the holding state of the .
///
- private double tickSpacing = 50;
+ public HoldNoteBody Body { get; private set; }
- protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
- {
- base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
-
- TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
- }
+ public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
- createTicks(cancellationToken);
-
AddNested(Head = new HeadNote
{
StartTime = StartTime,
@@ -115,23 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
- }
- private void createTicks(CancellationToken cancellationToken)
- {
- if (tickSpacing == 0)
- return;
-
- for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
+ AddNested(Body = new HoldNoteBody
{
- cancellationToken.ThrowIfCancellationRequested();
-
- AddNested(new HoldNoteTick
- {
- StartTime = t,
- Column = Column
- });
- }
+ StartTime = StartTime,
+ Column = Column
+ });
}
public override Judgement CreateJudgement() => new IgnoreJudgement();
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs
new file mode 100644
index 0000000000..47163d0d81
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs
@@ -0,0 +1,21 @@
+// 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.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Objects
+{
+ ///
+ /// The body of a .
+ /// Mostly a dummy hitobject that provides the judgement for the "holding" state.
+ /// On hit - the hold note was held correctly for the full duration.
+ /// On miss - the hold note was released at some point during its judgement period.
+ ///
+ public class HoldNoteBody : ManiaHitObject
+ {
+ public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
deleted file mode 100644
index e5c5260a49..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Judgements;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Objects
-{
- ///
- /// A scoring tick of a hold note.
- ///
- public class HoldNoteTick : ManiaHitObject
- {
- public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
-
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs
index 71a594c6ce..def32880f1 100644
--- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs
@@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
+ ///
+ /// The tail note of a .
+ ///
public class TailNote : Note
{
///
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
index ef4810c40d..660f72e565 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
@@ -209,7 +209,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
protected override void Update()
{
base.Update();
- missFadeTime.Value ??= holdNote.HoldBrokenTime;
+
+ if (holdNote.Body.HasHoldBreak)
+ missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 6c56db613c..1ec218644c 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -69,9 +68,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public void Animate(JudgementResult result)
{
- if (result.Judgement is HoldNoteTickJudgement)
- return;
-
(explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(FADE_IN_DURATION)
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index f38571a6d3..6cd55bb099 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(10, 50);
- RegisterPool(50, 250);
+ RegisterPool(10, 50);
}
private void onSourceChanged()
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index e0663e9878..e588951624 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -150,9 +149,6 @@ namespace osu.Game.Rulesets.Mania.UI
// scale roughly in-line with visual appearance of notes
Vector2 scale = new Vector2(1, 0.6f);
- if (result.Judgement is HoldNoteTickJudgement)
- scale *= 0.5f;
-
this.ScaleTo(scale);
largeFaint
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index 4382f8e84a..fa9af6d157 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -195,10 +195,6 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- // Tick judgements should not display text.
- if (judgedObject is DrawableHoldNoteTick)
- return;
-
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{
diff --git a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs
new file mode 100644
index 0000000000..68d7335055
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs
@@ -0,0 +1,36 @@
+// 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.Linq;
+using NUnit.Framework;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Tests.Rulesets.Scoring
+{
+ [TestFixture]
+ public class HitResultTest
+ {
+ [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })]
+ [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })]
+ [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })]
+ [TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
+ [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
+ public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)
+ {
+ HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray();
+
+ Assert.Multiple(() =>
+ {
+ foreach (var max in maxResults)
+ {
+ foreach (var min in minResults)
+ Assert.DoesNotThrow(() => HitResultExtensions.ValidateHitResultPair(max, min), $"{max} + {min} should be supported.");
+
+ foreach (var unsupported in unsupportedResults)
+ Assert.Throws(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported.");
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index 17a4c80f7f..92e94bd02d 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
for (int i = 0; i < 4; i++)
{
- var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult))
+ var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement())
{
Type = i == 2 ? minResult : hitResult
};
@@ -259,6 +259,41 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
#pragma warning restore CS0618
+ [Test]
+ public void TestComboBreak()
+ {
+ Assert.That(HitResult.ComboBreak.IncreasesCombo(), Is.False);
+ Assert.That(HitResult.ComboBreak.BreaksCombo(), Is.True);
+ Assert.That(HitResult.ComboBreak.AffectsCombo(), Is.True);
+ Assert.That(HitResult.ComboBreak.AffectsAccuracy(), Is.False);
+ Assert.That(HitResult.ComboBreak.IsBasic(), Is.False);
+ Assert.That(HitResult.ComboBreak.IsTick(), Is.False);
+ Assert.That(HitResult.ComboBreak.IsBonus(), Is.False);
+ Assert.That(HitResult.ComboBreak.IsHit(), Is.False);
+ Assert.That(HitResult.ComboBreak.IsScorable(), Is.True);
+ Assert.That(HitResultExtensions.ALL_TYPES, Does.Contain(HitResult.ComboBreak));
+
+ beatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = new List
+ {
+ new TestHitObject(HitResult.Great),
+ new TestHitObject(HitResult.IgnoreHit, HitResult.ComboBreak),
+ }
+ };
+
+ scoreProcessor = new TestScoreProcessor();
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great });
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
+
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak });
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
+ }
+
[Test]
public void TestAccuracyWhenNearPerfect()
{
@@ -275,7 +310,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
- scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement())
{
Type = i == 0 ? HitResult.Miss : HitResult.Great
});
@@ -293,24 +328,31 @@ namespace osu.Game.Tests.Rulesets.Scoring
{
public override HitResult MaxResult { get; }
- public TestJudgement(HitResult maxResult)
+ public override HitResult MinResult => minResult ?? base.MinResult;
+
+ private readonly HitResult? minResult;
+
+ public TestJudgement(HitResult maxResult, HitResult? minResult = null)
{
MaxResult = maxResult;
+ this.minResult = minResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
+ private readonly HitResult? minResult;
public override Judgement CreateJudgement()
{
- return new TestJudgement(maxResult);
+ return new TestJudgement(maxResult, minResult);
}
- public TestHitObject(HitResult maxResult)
+ public TestHitObject(HitResult maxResult, HitResult? minResult = null)
{
this.maxResult = maxResult;
+ this.minResult = minResult;
}
}
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 1b21f79c0a..d0e07a9e66 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -78,6 +78,7 @@ namespace osu.Game.Graphics
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.Miss:
+ case HitResult.ComboBreak:
return Red;
case HitResult.Meh:
diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs
index 99dce82ec2..f60b3a6c02 100644
--- a/osu.Game/Rulesets/Judgements/Judgement.cs
+++ b/osu.Game/Rulesets/Judgements/Judgement.cs
@@ -35,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements
///
/// The minimum that can be achieved - the inverse of .
///
- public HitResult MinResult
+ ///
+ /// Defaults to a sane value for the given . May be overridden to provide a supported custom value:
+ ///
+ ///
+ /// s
+ /// Valid s
+ ///
+ /// -
+ /// , , , ,
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// -
+ ///
+ /// ,
+ ///
+ ///
+ ///
+ public virtual HitResult MinResult
{
get
{
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index e31656e0ff..3bb0e3dfb8 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -672,6 +672,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
+ HitResultExtensions.ValidateHitResultPair(Result.Judgement.MaxResult, Result.Judgement.MinResult);
+
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
{
throw new InvalidOperationException(
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index 0013a9f20d..ccd1f49de4 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.Scoring
[Order(12)]
IgnoreHit,
+ ///
+ /// Indicates that a combo break should occur, but does not otherwise affect score.
+ ///
+ ///
+ /// May be paired with .
+ ///
+ [EnumMember(Value = "combo_break")]
+ [Order(15)]
+ ComboBreak,
+
///
/// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
///
@@ -165,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
case HitResult.LegacyComboIncrease:
+ case HitResult.ComboBreak:
return true;
default:
@@ -177,11 +188,19 @@ namespace osu.Game.Rulesets.Scoring
///
public static bool AffectsAccuracy(this HitResult result)
{
- // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
- if (result == HitResult.LegacyComboIncrease)
- return false;
+ switch (result)
+ {
+ // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
+ case HitResult.LegacyComboIncrease:
+ return false;
- return IsScorable(result) && !IsBonus(result);
+ // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
+ case HitResult.ComboBreak:
+ return false;
+
+ default:
+ return IsScorable(result) && !IsBonus(result);
+ }
}
///
@@ -189,11 +208,19 @@ namespace osu.Game.Rulesets.Scoring
///
public static bool IsBasic(this HitResult result)
{
- // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
- if (result == HitResult.LegacyComboIncrease)
- return false;
+ switch (result)
+ {
+ // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
+ case HitResult.LegacyComboIncrease:
+ return false;
- return IsScorable(result) && !IsTick(result) && !IsBonus(result);
+ // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
+ case HitResult.ComboBreak:
+ return false;
+
+ default:
+ return IsScorable(result) && !IsTick(result) && !IsBonus(result);
+ }
}
///
@@ -242,6 +269,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.Miss:
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
+ case HitResult.ComboBreak:
return false;
default:
@@ -254,11 +282,20 @@ namespace osu.Game.Rulesets.Scoring
///
public static bool IsScorable(this HitResult result)
{
- // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
- if (result == HitResult.LegacyComboIncrease)
- return true;
+ switch (result)
+ {
+ // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
+ case HitResult.LegacyComboIncrease:
+ return true;
- return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
+ // ComboBreak is its own type that affects score via combo.
+ case HitResult.ComboBreak:
+ return true;
+
+ default:
+ // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score.
+ return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
+ }
}
///
@@ -291,6 +328,30 @@ namespace osu.Game.Rulesets.Scoring
/// The to get the index of.
/// The index of .
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
+
+ public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult)
+ {
+ if (maxResult == HitResult.None || !IsHit(maxResult))
+ throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result.");
+
+ if (minResult == HitResult.None || IsHit(minResult))
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result.");
+
+ if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak))
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
+
+ if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss)
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
+
+ if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss)
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement.");
+
+ if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss)
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement.");
+
+ if (maxResult.IsBasic() && minResult != HitResult.Miss)
+ throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement.");
+ }
}
#pragma warning restore CS0618
}