// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// /// Visualises a hit object. /// public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { private readonly DrawableNote head; private readonly DrawableNote tail; private readonly GlowPiece glowPiece; private readonly BodyPiece bodyPiece; private readonly Container fullHeightContainer; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// private double? holdStartTime; /// /// Whether the hold note has been released too early and shouldn't give full score for the release. /// private bool hasBroken; public DrawableHoldNote(HoldNote hitObject, ManiaAction action) : base(hitObject, action) { Container tickContainer; RelativeSizeAxes = Axes.X; InternalChildren = new Drawable[] { // The hit object itself cannot be used for various elements because the tail overshoots it // So a specialized container that is updated to contain the tail height is used fullHeightContainer = new Container { RelativeSizeAxes = Axes.X, Child = glowPiece = new GlowPiece() }, bodyPiece = new BodyPiece { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, }, tickContainer = new Container { RelativeSizeAxes = Axes.Both, ChildrenEnumerable = HitObject.NestedHitObjects.OfType().Select(tick => new DrawableHoldNoteTick(tick) { HoldStartTime = () => holdStartTime }) }, head = new DrawableHeadNote(this, action) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, tail = new DrawableTailNote(this, action) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } }; foreach (var tick in tickContainer) AddNested(tick); AddNested(head); AddNested(tail); } public override Color4 AccentColour { get { return base.AccentColour; } set { base.AccentColour = value; glowPiece.AccentColour = value; bodyPiece.AccentColour = value; head.AccentColour = value; tail.AccentColour = value; } } protected override void UpdateState(ArmedState state) { switch (state) { case ArmedState.Hit: // Good enough for now, we just want them to have a lifetime end this.Delay(2000).Expire(); break; } } protected override void CheckForJudgements(bool userTriggered, double timeOffset) { if (tail.AllJudged) AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect }); } protected override void Update() { base.Update(); // Make the body piece not lie under the head note bodyPiece.Y = head.Height; bodyPiece.Height = DrawHeight - head.Height; // Make the fullHeightContainer "contain" the height of the tail note, keeping in mind // that the tail note overshoots the height of this hit object fullHeightContainer.Height = DrawHeight + tail.Height; } public bool OnPressed(ManiaAction action) { // Make sure the action happened within the body of the hold note if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) return false; if (action != Action) return false; // The user has pressed during the body of the hold note, after the head note and its hit windows have passed // and within the limited range of the above if-statement. This state will be managed by the head note if the // user has pressed during the hit windows of the head note. holdStartTime = Time.Current; return true; } public bool OnReleased(ManiaAction action) { // Make sure that the user started holding the key during the hold note if (!holdStartTime.HasValue) return false; if (action != Action) return false; holdStartTime = null; // If the key has been released too early, the user should not receive full score for the release if (!tail.IsHit) hasBroken = true; return true; } /// /// The head note of a hold. /// private class DrawableHeadNote : DrawableNote { private readonly DrawableHoldNote holdNote; public DrawableHeadNote(DrawableHoldNote holdNote, ManiaAction action) : base(holdNote.HitObject.Head, action) { this.holdNote = holdNote; GlowPiece.Alpha = 0; } public override bool OnPressed(ManiaAction action) { if (!base.OnPressed(action)) return false; // If the key has been released too early, the user should not receive full score for the release if (Judgements.Any(j => j.Result == HitResult.Miss)) holdNote.hasBroken = true; // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held // The body doesn't handle these early early hits, so we have to explicitly set the holding state here holdNote.holdStartTime = Time.Current; return true; } protected override void UpdateState(ArmedState state) { // The holdnote keeps scrolling through for now, so having the head disappear looks weird } } /// /// The tail note of a hold. /// private class DrawableTailNote : DrawableNote { /// /// Lenience of release hit windows. This is to make cases where the hold note release /// is timed alongside presses of other hit objects less awkward. /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps /// private const double release_window_lenience = 1.5; private readonly DrawableHoldNote holdNote; public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action) : base(holdNote.HitObject.Tail, action) { this.holdNote = holdNote; GlowPiece.Alpha = 0; } protected override void CheckForJudgements(bool userTriggered, double timeOffset) { // Factor in the release lenience timeOffset /= release_window_lenience; if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) { AddJudgement(new HoldNoteTailJudgement { Result = HitResult.Miss, HasBroken = holdNote.hasBroken }); } return; } var result = HitObject.HitWindows.ResultFor(timeOffset); if (result == HitResult.None) return; AddJudgement(new HoldNoteTailJudgement { Result = result, HasBroken = holdNote.hasBroken }); } protected override void UpdateState(ArmedState state) { // The holdnote keeps scrolling through, so having the tail disappear looks weird } public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down public override bool OnReleased(ManiaAction action) { // Make sure that the user started holding the key during the hold note if (!holdNote.holdStartTime.HasValue) return false; if (action != Action) return false; UpdateJudgement(true); // Handled by the hold note, which will set holding = false return false; } } } }