1
0
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:
Dan Balasescu 2023-12-14 11:51:50 +09:00
parent 38e7c03500
commit 599fdb0128
No known key found for this signature in database
7 changed files with 565 additions and 41 deletions

View 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,
})
{
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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