1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 22:47:24 +08:00

Merge pull request #25062 from smoogipoo/remove-hold-note-ticks

Remove osu!mania hold note ticks
This commit is contained in:
Dean Herbert 2023-10-12 13:55:50 +09:00 committed by GitHub
commit f26e3afd29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 327 additions and 295 deletions

View File

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

View File

@ -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);
}
/// <summary>
/// -----[ ]-----
/// xox o
/// </summary>
[Test]
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
{
performTest(new List<ReplayFrame>
{
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);
}
/// <summary>
/// -----[ ]-----
/// 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<ManiaHitObject>
{
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<ReplayFrame>
{
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!;

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@ -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<DrawableHoldNoteHead> headContainer;
private Container<DrawableHoldNoteTail> tailContainer;
private Container<DrawableHoldNoteTick> tickContainer;
private Container<DrawableHoldNoteBody> bodyContainer;
private PausableSkinnableSound slidingSample;
@ -60,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
/// <summary>
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
/// </summary>
public double? HoldBrokenTime { get; private set; }
/// <summary>
/// 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.
/// </summary>
private double? releaseTime;
@ -103,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
}
},
bodyContainer = new Container<DrawableHoldNoteBody> { 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<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
tailContainer = new Container<DrawableHoldNoteTail> { 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()

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<HoldNoteBody>
{
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);
}
}
}

View File

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

View File

@ -1,110 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Visualises a <see cref="HoldNoteTick"/> hit object.
/// </summary>
public partial class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{
/// <summary>
/// References the time at which the user started holding the hold note.
/// </summary>
private Func<double?> 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);
}
}
}

View File

@ -3,6 +3,9 @@
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// The head note of a <see cref="HoldNote"/>.
/// </summary>
public class HeadNote : Note
{
}

View File

@ -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
/// </summary>
public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary>
/// 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 <see cref="HoldNote"/>.
/// </summary>
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();

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// The body of a <see cref="HoldNote"/>.
/// Mostly a dummy hitobject that provides the judgement for the "holding" state.<br />
/// On hit - the hold note was held correctly for the full duration.<br />
/// On miss - the hold note was released at some point during its judgement period.
/// </summary>
public class HoldNoteBody : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A scoring tick of a hold note.
/// </summary>
public class HoldNoteTick : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}

View File

@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// The tail note of a <see cref="HoldNote"/>.
/// </summary>
public class TailNote : Note
{
/// <summary>

View File

@ -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);

View File

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

View File

@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
}
private void onSourceChanged()

View File

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

View File

@ -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 =>
{

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ArgumentOutOfRangeException>(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported.");
}
});
}
}
}

View File

@ -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<HitObject>
{
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;
}
}

View File

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

View File

@ -35,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements
/// <summary>
/// The minimum <see cref="HitResult"/> that can be achieved - the inverse of <see cref="MaxResult"/>.
/// </summary>
public HitResult MinResult
/// <remarks>
/// Defaults to a sane value for the given <see cref="MaxResult"/>. May be overridden to provide a supported custom value:
/// <list type="table">
/// <listheader>
/// <term><see cref="MaxResult"/>s</term>
/// <description>Valid <see cref="MinResult"/>s</description>
/// </listheader>
/// <item>
/// <term><see cref="HitResult.Perfect"/>, <see cref="HitResult.Great"/>, <see cref="HitResult.Good"/>, <see cref="HitResult.Ok"/>, <see cref="HitResult.Meh"/></term>
/// <description><see cref="HitResult.Miss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.LargeBonus"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.SmallBonus"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.SmallTickHit"/></term>
/// <description><see cref="HitResult.SmallTickMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.LargeTickHit"/></term>
/// <description><see cref="HitResult.LargeTickMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.IgnoreHit"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></description>
/// </item>
/// </list>
/// </remarks>
public virtual HitResult MinResult
{
get
{

View File

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

View File

@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.Scoring
[Order(12)]
IgnoreHit,
/// <summary>
/// Indicates that a combo break should occur, but does not otherwise affect score.
/// </summary>
/// <remarks>
/// May be paired with <see cref="IgnoreHit"/>.
/// </remarks>
[EnumMember(Value = "combo_break")]
[Order(15)]
ComboBreak,
/// <summary>
/// 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).
/// </summary>
@ -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
/// </summary>
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);
}
}
/// <summary>
@ -189,11 +208,19 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
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);
}
}
/// <summary>
@ -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
/// </summary>
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;
}
}
/// <summary>
@ -291,6 +328,30 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
/// <returns>The index of <paramref name="result"/>.</returns>
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
}