mirror of
https://github.com/ppy/osu.git
synced 2024-09-22 01:27:29 +08:00
Merge pull request #24720 from bdach/overlapping-hit-circle-input
Block input to objects lying under already-hit hitcircles when classic note lock is active
This commit is contained in:
commit
4dafd4c4a2
@ -142,6 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
drawableHitCircle.Scale = new Vector2(2f);
|
drawableHitCircle.Scale = new Vector2(2f);
|
||||||
|
|
||||||
|
LoadComponent(drawableHitCircle);
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||||
|
|
||||||
|
@ -505,6 +505,56 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addClickActionAssert(0, ClickAction.Shake);
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputDoesNotFallThroughOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
|
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
||||||
|
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToUnderlyingObjects()`),
|
||||||
|
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||||
@ -523,6 +573,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
private void addClickActionAssert(int inputIndex, ClickAction action)
|
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||||
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||||
|
|
||||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputFallsThroughJudgedSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200);
|
||||||
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||||
@ -354,6 +400,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||||
private List<JudgementResult> judgementResults;
|
private List<JudgementResult> judgementResults;
|
||||||
|
|
||||||
|
@ -74,6 +74,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||||
applyEarlyFading(head);
|
applyEarlyFading(head);
|
||||||
|
|
||||||
|
if (ClassicNoteLock.Value)
|
||||||
|
blockInputToUnderlyingObjects(head);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSliderTail tail:
|
case DrawableSliderTail tail:
|
||||||
@ -83,10 +87,29 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
case DrawableHitCircle circle:
|
case DrawableHitCircle circle:
|
||||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||||
applyEarlyFading(circle);
|
applyEarlyFading(circle);
|
||||||
|
|
||||||
|
if (ClassicNoteLock.Value)
|
||||||
|
blockInputToUnderlyingObjects(circle);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On stable, hitcircles that have already been hit block input from reaching objects that may be underneath them.
|
||||||
|
/// The purpose of this method is to restore that behaviour.
|
||||||
|
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
|
||||||
|
/// </summary>
|
||||||
|
private static void blockInputToUnderlyingObjects(DrawableHitCircle circle)
|
||||||
|
{
|
||||||
|
var oldHitAction = circle.HitArea.Hit;
|
||||||
|
circle.HitArea.Hit = () =>
|
||||||
|
{
|
||||||
|
oldHitAction?.Invoke();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void applyEarlyFading(DrawableHitCircle circle)
|
private void applyEarlyFading(DrawableHitCircle circle)
|
||||||
{
|
{
|
||||||
circle.ApplyCustomUpdateState += (dho, state) =>
|
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
case OsuAction.RightButton:
|
case OsuAction.RightButton:
|
||||||
if (IsHovered && (Hit?.Invoke() ?? false))
|
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||||
{
|
{
|
||||||
HitAction = e.Action;
|
HitAction ??= e.Action;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user