// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// /// Visualises a hit object. /// public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { public override bool DisplayResult => false; public IBindable IsHitting => isHitting; private readonly Bindable isHitting = new Bindable(); public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; private readonly Container headContainer; private readonly Container tailContainer; private readonly Container tickContainer; private readonly SkinnableDrawable bodyPiece; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// public double? HoldStartTime { get; private set; } /// /// Whether the hold note has been released too early and shouldn't give full score for the release. /// public bool HasBroken { get; private set; } public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { RelativeSizeAxes = Axes.X; AddRangeInternal(new Drawable[] { bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.X }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); switch (hitObject) { case DrawableHoldNoteHead head: headContainer.Child = head; break; case DrawableHoldNoteTail tail: tailContainer.Child = tail; break; case DrawableHoldNoteTick tick: tickContainer.Add(tick); break; } } protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); headContainer.Clear(); tailContainer.Clear(); tickContainer.Clear(); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) { switch (hitObject) { case TailNote _: return new DrawableHoldNoteTail(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AccentColour = { BindTarget = AccentColour } }; case Note _: return new DrawableHoldNoteHead(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AccentColour = { BindTarget = AccentColour } }; case HoldNoteTick tick: return new DrawableHoldNoteTick(tick) { HoldStartTime = () => HoldStartTime, AccentColour = { BindTarget = AccentColour } }; } return base.CreateNestedHitObject(hitObject); } protected override void OnDirectionChanged(ValueChangedEvent e) { base.OnDirectionChanged(e); bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } public override void PlaySamples() { // Samples are played by the head/tail notes. } public override void OnKilled() { base.OnKilled(); (bodyPiece.Drawable as IHoldNoteBody)?.Recycle(); } protected override void Update() { base.Update(); // Make the body piece not lie under the head note bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; } protected override void UpdateStateTransforms(ArmedState state) { using (BeginDelayedSequence(HitObject.Duration, true)) base.UpdateStateTransforms(state); } protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) ApplyResult(r => r.Type = HitResult.Perfect); if (Tail.Result.Type == HitResult.Miss) HasBroken = true; } public bool OnPressed(ManiaAction action) { if (AllJudged) return false; if (action != Action.Value) return false; // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed). // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time. // Note: Unlike below, we use the tail's start time to determine the time offset. if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime)) return false; beginHoldAt(Time.Current - Head.HitObject.StartTime); Head.UpdateResult(); return true; } private void beginHoldAt(double timeOffset) { if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss)) return; HoldStartTime = Time.Current; isHitting.Value = true; } public void OnReleased(ManiaAction action) { if (AllJudged) return; if (action != Action.Value) return; // Make sure a hold was started if (HoldStartTime == null) return; Tail.UpdateResult(); endHold(); // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) HasBroken = true; } private void endHold() { HoldStartTime = null; isHitting.Value = false; } } }