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 }