Add mania hold note body + combo break judgement

This commit is contained in:
Dan Balasescu 2023-10-09 09:47:00 +09:00
parent b2d9c95441
commit 9415fe4446
No known key found for this signature in database
13 changed files with 246 additions and 17 deletions

View File

@ -39,7 +39,7 @@ public TestSceneHitExplosion()
{
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

@ -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 HoldNoteHoldJudgement : ManiaJudgement
{
public override HitResult MaxResult => HitResult.IgnoreHit;
public override HitResult MinResult => HitResult.ComboBreak;
}
}

View File

@ -35,9 +35,11 @@ public partial class DrawableHoldNote : DrawableManiaHitObject<HoldNote>, IKeyBi
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<DrawableHoldNoteBody> bodyContainer;
private PausableSkinnableSound slidingSample;
@ -58,11 +60,6 @@ public partial class DrawableHoldNote : DrawableManiaHitObject<HoldNote>, IKeyBi
/// </summary>
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.
/// </summary>
@ -102,6 +99,7 @@ private void load()
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,
@ -133,7 +131,6 @@ protected override void OnApply()
sizingContainer.Size = Vector2.One;
HoldStartTime = null;
HoldBrokenTime = null;
releaseTime = null;
}
@ -150,6 +147,10 @@ protected override void AddNestedHitObject(DrawableHitObject hitObject)
case DrawableHoldNoteTail tail:
tailContainer.Child = tail;
break;
case DrawableHoldNoteBody body:
bodyContainer.Child = body;
break;
}
}
@ -158,6 +159,7 @@ protected override void ClearNestedHitObjects()
base.ClearNestedHitObjects();
headContainer.Clear(false);
tailContainer.Clear(false);
bodyContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@ -169,6 +171,9 @@ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
case HeadNote head:
return new DrawableHoldNoteHead(head);
case HoldNoteBody body:
return new DrawableHoldNoteBody(body);
}
return base.CreateNestedHitObject(hitObject);
@ -261,8 +266,9 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
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()
@ -325,12 +331,9 @@ public void OnReleased(KeyBindingReleaseEvent<ManiaAction> e)
return;
Tail.UpdateResult();
Body.TriggerResult(Tail.IsHit);
endHold();
// 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;
}

View File

@ -0,0 +1,30 @@
// 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)
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
}

View File

@ -55,7 +55,9 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
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

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

@ -79,6 +79,12 @@ public override int Column
/// </summary>
public TailNote Tail { get; private set; }
/// <summary>
/// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
/// </summary>
public HoldNoteBody Body { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@ -98,6 +104,12 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
AddNested(Body = new HoldNoteBody
{
StartTime = StartTime,
Column = Column
});
}
public override Judgement CreateJudgement() => new IgnoreJudgement();

View File

@ -0,0 +1,15 @@
// 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.
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
{
}
}

View File

@ -6,6 +6,9 @@
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,8 @@ private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange
protected override void Update()
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
missFadeTime.Value = holdNote.Body.HasHoldBreak ? holdNote.Body.Result.TimeAbsolute : null;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);

View File

@ -112,6 +112,7 @@ private void load(GameHost host)
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
}
private void onSourceChanged()

View File

@ -35,7 +35,40 @@ public class Judgement
/// <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>Valid <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"/>, <see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></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"/>, <see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.LargeTickHit"/></term>
/// <description><see cref="HitResult.LargeTickMiss"/>, <see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></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
{
@ -58,6 +91,10 @@ public HitResult MinResult
}
}
public Judgement()
{
}
/// <summary>
/// The numeric score representation for the maximum achievable result.
/// </summary>

View File

@ -120,6 +120,16 @@ public enum HitResult
[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>
@ -291,6 +301,105 @@ public static bool IsValidHitResult(this HitResult result, HitResult minResult,
/// <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)
{
// Check valid maximum judgements.
switch (maxResult)
{
case HitResult.Meh:
case HitResult.Ok:
case HitResult.Good:
case HitResult.Great:
case HitResult.Perfect:
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.SmallBonus:
case HitResult.LargeBonus:
case HitResult.IgnoreHit:
break;
default:
throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result.");
}
// Check valid minimum judgements.
switch (minResult)
{
case HitResult.Miss:
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
case HitResult.ComboBreak:
break;
default:
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result.");
}
// Check valid category pairings.
switch (maxResult)
{
case HitResult.SmallBonus:
case HitResult.LargeBonus:
switch (minResult)
{
case HitResult.IgnoreMiss:
break;
default:
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
}
break;
case HitResult.SmallTickHit:
switch (minResult)
{
case HitResult.SmallTickMiss:
case HitResult.IgnoreMiss:
case HitResult.ComboBreak:
break;
default:
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
}
break;
case HitResult.LargeTickHit:
switch (minResult)
{
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
case HitResult.ComboBreak:
break;
default:
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
}
break;
case HitResult.Meh:
case HitResult.Ok:
case HitResult.Good:
case HitResult.Great:
case HitResult.Perfect:
switch (minResult)
{
case HitResult.Miss:
case HitResult.IgnoreMiss:
case HitResult.ComboBreak:
break;
default:
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
}
break;
}
}
}
#pragma warning restore CS0618
}