mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Add lenience for late-hit of slider heads
This commit is contained in:
parent
38e7c03500
commit
599fdb0128
419
osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs
Normal file
419
osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs
Normal file
@ -0,0 +1,419 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSliderLateHitJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
// Note: In the following tests, the terminology "in range of the follow circle" is used as meaning
|
||||
// the equivalent of "in range of the follow circle as if it were in its expanded state".
|
||||
|
||||
private const double time_slider_start = 1000;
|
||||
private const double time_slider_end = 1500;
|
||||
|
||||
private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192);
|
||||
private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192);
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private const float slider_path_length = 200;
|
||||
|
||||
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then tracking should be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeTracks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is NOT in range of the follow circle,
|
||||
/// then tracking should NOT be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateOutOfRangeDoesNotTrack()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is in range of the follow circle,
|
||||
/// then all ticks that the follow circle has passed through should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeHitsTicks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickHit);
|
||||
assertTickJudgement(1, HitResult.LargeTickHit);
|
||||
assertTickJudgement(2, HitResult.LargeTickHit);
|
||||
assertTickJudgement(3, HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is NOT in range of the follow circle,
|
||||
/// then all ticks that the follow circle has passed through should NOT be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateOutOfRangeDoesNotHitTicks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is pressed after it's missed and the mouse is in range of the follow circle,
|
||||
/// then tracking should NOT be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestMissHeadInRangeDoesNotTrack()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 151, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 151, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(2, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(3, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late but after the completion of the slider and the mouse is in range of the follow circle,
|
||||
/// then all nested objects (ticks/repeats/tail) should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateShortSliderHitsAll()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(20, 0),
|
||||
}, 20);
|
||||
|
||||
s.TickDistanceMultiplier = 0.01f;
|
||||
s.RepeatCount = 1;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertAllTickJudgements(HitResult.LargeTickHit);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is in range of the follow circle,
|
||||
/// then all the repeats that the mouse has passed through should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeHitsRepeat()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 0),
|
||||
}, 50);
|
||||
|
||||
s.RepeatCount = 1;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then only the ticks that were in range of the follow circle at the head should be hit.
|
||||
/// If any hitobject was outside the follow range, ALL hitobjects after that point should be missed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeDoesNotHitAfterAnyOutOfRange()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(70, 70),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.03f;
|
||||
s.SliderVelocityMultiplier = 6f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
|
||||
// The first few ticks that are in the follow range of the head should be hit.
|
||||
assertTickJudgement(0, HitResult.LargeTickHit); // This tick is hidden under the slider head :(
|
||||
assertTickJudgement(1, HitResult.LargeTickHit);
|
||||
assertTickJudgement(2, HitResult.LargeTickHit);
|
||||
|
||||
// Every other tick should be missed
|
||||
assertTickJudgement(3, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(4, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(5, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(6, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(7, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(8, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(9, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(10, HitResult.LargeTickMiss);
|
||||
|
||||
// In particular, these three are in the follow range of the head, but should not be hit
|
||||
// because the slider was at some point outside the follow range of the head.
|
||||
assertTickJudgement(11, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(12, HitResult.LargeTickMiss);
|
||||
|
||||
// And the tail should be hit because of its leniency.
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then a tick outside the range of the follow circle from the head should not be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeDoesNotHitOutOfRange()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 50),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.3f;
|
||||
s.SliderVelocityMultiplier = 3;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check head result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTickJudgement(int index, HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"check tick({index}) result",
|
||||
() => judgementResults.Where(r => r.HitObject is SliderTick).ElementAtOrDefault(index)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertAllTickJudgements(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check all tick results",
|
||||
() => judgementResults.Where(r => r.HitObject is SliderTick).Select(t => t.Type),
|
||||
() => Has.All.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertRepeatJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check repeat result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTailJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check tail result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertSliderJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check slider result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private Vector2 computePositionFromTime(double time)
|
||||
{
|
||||
Vector2 dist = slider_end_position - slider_start_position;
|
||||
double t = (time - time_slider_start) / (time_slider_end - time_slider_start);
|
||||
return slider_start_position + dist * (float)t;
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Action<Slider>? adjustSliderFunc = null, bool classic = false)
|
||||
{
|
||||
Slider slider = new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(256 - slider_path_length / 2, 192),
|
||||
TickDistanceMultiplier = 3,
|
||||
ClassicSliderBehaviour = classic,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(slider_path_length, 0),
|
||||
}, slider_path_length),
|
||||
};
|
||||
|
||||
adjustSliderFunc?.Invoke(slider);
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = { slider },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderMultiplier = 4,
|
||||
SliderTickRate = 3
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
}
|
||||
});
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults.Clear();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public DrawableSliderHead HeadCircle => headContainer.Child;
|
||||
public DrawableSliderTail TailCircle => tailContainer.Child;
|
||||
public IEnumerable<DrawableSliderTick> Ticks => tickContainer.Children;
|
||||
public IEnumerable<DrawableSliderRepeat> Repeats => repeatContainer.Children;
|
||||
|
||||
[Cached]
|
||||
public DrawableSliderBall Ball { get; private set; }
|
||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public Func<OsuAction?> GetInitialHitAction;
|
||||
|
||||
private Drawable followCircleReceptor;
|
||||
private DrawableSlider drawableSlider;
|
||||
private Drawable ball;
|
||||
|
||||
@ -48,13 +47,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
followCircleReceptor = new CircularContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true
|
||||
},
|
||||
ball = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -86,21 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.ApplyTransformsAt(time, false);
|
||||
}
|
||||
|
||||
private bool tracking;
|
||||
|
||||
public bool Tracking
|
||||
{
|
||||
get => tracking;
|
||||
private set
|
||||
{
|
||||
if (value == tracking)
|
||||
return;
|
||||
|
||||
tracking = value;
|
||||
|
||||
followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
|
||||
}
|
||||
}
|
||||
public bool Tracking { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking.
|
||||
@ -129,6 +107,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
|
||||
|
||||
public bool IsMouseInFollowCircleWithState(bool expanded)
|
||||
{
|
||||
if (lastScreenSpaceMousePosition is not Vector2 mousePos)
|
||||
return false;
|
||||
|
||||
float radius = GetFollowCircleRadius(expanded);
|
||||
|
||||
double followProgress = Math.Clamp((Time.Current - drawableSlider.HitObject.StartTime) / drawableSlider.HitObject.Duration, 0, 1);
|
||||
Vector2 followCirclePosition = drawableSlider.HitObject.CurvePositionAt(followProgress);
|
||||
Vector2 mousePositionInSlider = drawableSlider.ToLocalSpace(mousePos) - drawableSlider.OriginPosition;
|
||||
|
||||
return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius;
|
||||
}
|
||||
|
||||
public float GetFollowCircleRadius(bool expanded)
|
||||
{
|
||||
float radius = (float)drawableSlider.HitObject.Radius;
|
||||
|
||||
if (expanded)
|
||||
radius *= FOLLOW_AREA;
|
||||
|
||||
return radius;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -152,14 +154,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
timeToAcceptAnyKeyAfter = Time.Current;
|
||||
}
|
||||
|
||||
bool validInFollowArea = IsMouseInFollowCircleWithState(Tracking);
|
||||
bool validInHeadCircle = drawableSlider.HeadCircle.IsHit
|
||||
&& IsMouseInFollowCircleWithState(true)
|
||||
&& drawableSlider.HeadCircle.Result.TimeAbsolute == Time.Current;
|
||||
|
||||
Tracking =
|
||||
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
|
||||
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
|
||||
(!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime())
|
||||
// in valid position range
|
||||
&& lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
|
||||
&& (validInFollowArea || validInHeadCircle)
|
||||
// valid action
|
||||
(actions?.Any(isValidTrackingAction) ?? false);
|
||||
&& (actions?.Any(isValidTrackingAction) ?? false);
|
||||
|
||||
lastPressedActions.Clear();
|
||||
if (actions != null)
|
||||
|
@ -3,10 +3,14 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -61,6 +65,50 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
|
||||
if (!Judged || !Result.IsHit)
|
||||
return;
|
||||
|
||||
// If the head is hit and in radius of the would-be-expanded follow circle,
|
||||
// then hit every object that the follow circle has passed through up until the current time.
|
||||
if (DrawableSlider.Ball.IsMouseInFollowCircleWithState(true))
|
||||
{
|
||||
foreach (var nested in DrawableSlider.NestedHitObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
if (nested.Judged)
|
||||
continue;
|
||||
|
||||
if (!check(nested.HitObject))
|
||||
break;
|
||||
|
||||
if (nested is DrawableSliderTick tick)
|
||||
tick.HitForcefully();
|
||||
|
||||
if (nested is DrawableSliderRepeat repeat)
|
||||
repeat.HitForcefully();
|
||||
|
||||
if (nested is DrawableSliderTail tail)
|
||||
tail.HitForcefully();
|
||||
}
|
||||
}
|
||||
|
||||
bool check(OsuHitObject h)
|
||||
{
|
||||
if (h.StartTime > Time.Current)
|
||||
return false;
|
||||
|
||||
float radius = DrawableSlider.Ball.GetFollowCircleRadius(true);
|
||||
|
||||
double objectProgress = Math.Clamp((h.StartTime - DrawableSlider.HitObject.StartTime) / DrawableSlider.HitObject.Duration, 0, 1);
|
||||
Vector2 objectPosition = DrawableSlider.HitObject.CurvePositionAt(objectProgress);
|
||||
|
||||
return objectPosition.LengthSquared <= radius * radius;
|
||||
}
|
||||
}
|
||||
|
||||
protected override HitResult ResultFor(double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject != null);
|
||||
|
@ -85,17 +85,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Position = HitObject.Position - DrawableSlider.Position;
|
||||
}
|
||||
|
||||
public void HitForcefully()
|
||||
{
|
||||
if (Judged)
|
||||
return;
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
// shared implementation with DrawableSliderTick.
|
||||
if (timeOffset >= 0)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
if (!DrawableSlider.HeadCircle.Judged)
|
||||
{
|
||||
if (Tracking)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
@ -125,6 +125,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public void HitForcefully()
|
||||
{
|
||||
if (Judged)
|
||||
return;
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (userTriggered)
|
||||
@ -141,12 +149,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
|
||||
return;
|
||||
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
if (!DrawableSlider.HeadCircle.Judged)
|
||||
{
|
||||
if (Tracking)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
|
||||
// An actual tick miss should only occur if reaching the tick itself.
|
||||
|
@ -73,17 +73,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Position = HitObject.Position - DrawableSlider.HitObject.Position;
|
||||
}
|
||||
|
||||
public void HitForcefully()
|
||||
{
|
||||
if (Judged)
|
||||
return;
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
// shared implementation with DrawableSliderRepeat.
|
||||
if (timeOffset >= 0)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
if (!DrawableSlider.HeadCircle.Judged)
|
||||
{
|
||||
if (Tracking)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
@ -107,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.FadeOut(ANIM_DURATION);
|
||||
this.FadeColour(Color4.Red, ANIM_DURATION / 2);
|
||||
this.TransformBindableTo(AccentColour, Color4.Red, 0);
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
|
Loading…
Reference in New Issue
Block a user