2019-01-24 16:43:03 +08:00
// 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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2020-08-17 23:29:00 +08:00
using System ;
2022-10-11 15:31:37 +08:00
using System.Linq ;
2021-05-12 15:35:05 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Input.Bindings ;
2021-09-16 17:26:12 +08:00
using osu.Framework.Input.Events ;
2022-10-11 15:31:37 +08:00
using osu.Game.Audio ;
2020-12-07 11:32:52 +08:00
using osu.Game.Rulesets.Mania.Skinning.Default ;
2019-10-17 11:37:20 +08:00
using osu.Game.Rulesets.Objects ;
2019-08-27 11:59:57 +08:00
using osu.Game.Rulesets.Objects.Drawables ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Scoring ;
2018-06-08 17:16:55 +08:00
using osu.Game.Rulesets.UI.Scrolling ;
2020-03-31 15:42:35 +08:00
using osu.Game.Skinning ;
2021-05-12 16:07:42 +08:00
using osuTK ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNote"/> hit object.
/// </summary>
2022-11-24 13:32:20 +08:00
public partial class DrawableHoldNote : DrawableManiaHitObject < HoldNote > , IKeyBindingHandler < ManiaAction >
2018-04-13 17:19:50 +08:00
{
2018-08-06 10:31:46 +08:00
public override bool DisplayResult = > false ;
2018-05-28 17:12:49 +08:00
2020-03-31 15:42:35 +08:00
public IBindable < bool > IsHitting = > isHitting ;
private readonly Bindable < bool > isHitting = new Bindable < bool > ( ) ;
2019-12-23 16:48:48 +08:00
public DrawableHoldNoteHead Head = > headContainer . Child ;
public DrawableHoldNoteTail Tail = > tailContainer . Child ;
2019-10-17 13:02:23 +08:00
2021-05-12 15:35:05 +08:00
private Container < DrawableHoldNoteHead > headContainer ;
private Container < DrawableHoldNoteTail > tailContainer ;
private Container < DrawableHoldNoteTick > tickContainer ;
2019-10-17 11:37:20 +08:00
2022-10-11 15:31:37 +08:00
private PausableSkinnableSound slidingSample ;
2020-08-18 23:05:05 +08:00
/// <summary>
2020-08-21 16:52:42 +08:00
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
2020-08-18 23:05:05 +08:00
/// </summary>
2021-05-12 15:35:05 +08:00
private Container sizingContainer ;
2020-08-18 23:05:05 +08:00
/// <summary>
2020-08-21 16:52:42 +08:00
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>.
2020-08-18 23:05:05 +08:00
/// </summary>
2021-05-12 15:35:05 +08:00
private Container maskingContainer ;
2020-08-19 00:40:26 +08:00
2021-05-12 15:35:05 +08:00
private SkinnableDrawable bodyPiece ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
/// </summary>
2019-12-23 17:48:14 +08:00
public double? HoldStartTime { get ; private set ; }
2018-04-13 17:19:50 +08:00
/// <summary>
2020-11-14 04:14:34 +08:00
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-11-14 04:14:34 +08:00
public double? HoldBrokenTime { get ; private set ; }
2018-04-13 17:19:50 +08:00
2020-08-17 23:29:00 +08:00
/// <summary>
/// Whether the hold note has been released potentially without having caused a break.
/// </summary>
2020-08-19 00:37:24 +08:00
private double? releaseTime ;
2020-08-17 23:29:00 +08:00
2021-05-12 15:35:05 +08:00
public DrawableHoldNote ( )
: this ( null )
{
}
2018-07-02 11:31:41 +08:00
public DrawableHoldNote ( HoldNote hitObject )
: base ( hitObject )
2018-04-13 17:19:50 +08:00
{
2021-05-12 15:35:05 +08:00
}
2018-04-13 17:19:50 +08:00
2021-05-12 15:35:05 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2020-08-21 16:52:42 +08:00
Container maskedContents ;
AddRangeInternal ( new Drawable [ ]
2018-04-13 17:19:50 +08:00
{
2020-08-21 16:52:42 +08:00
sizingContainer = new Container
2020-04-23 11:53:23 +08:00
{
2020-08-18 23:05:05 +08:00
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2020-08-17 23:29:00 +08:00
{
2020-08-21 16:52:42 +08:00
maskingContainer = new Container
2020-08-18 23:05:05 +08:00
{
2020-08-21 16:52:42 +08:00
RelativeSizeAxes = Axes . Both ,
Child = maskedContents = new Container
2020-08-18 23:05:05 +08:00
{
2020-08-21 16:52:42 +08:00
RelativeSizeAxes = Axes . Both ,
Masking = true ,
}
2020-08-18 23:05:05 +08:00
} ,
headContainer = new Container < DrawableHoldNoteHead > { RelativeSizeAxes = Axes . Both }
}
2020-03-31 15:42:35 +08:00
} ,
2022-11-09 15:04:56 +08:00
bodyPiece = new SkinnableDrawable ( new ManiaSkinComponentLookup ( ManiaSkinComponents . HoldNoteBody ) , _ = > new DefaultBodyPiece
2020-08-19 00:40:26 +08:00
{
2020-08-21 16:52:42 +08:00
RelativeSizeAxes = Axes . Both ,
} )
{
RelativeSizeAxes = Axes . X
2020-08-19 00:40:26 +08:00
} ,
2020-08-21 16:52:42 +08:00
tickContainer = new Container < DrawableHoldNoteTick > { RelativeSizeAxes = Axes . Both } ,
tailContainer = new Container < DrawableHoldNoteTail > { RelativeSizeAxes = Axes . Both } ,
2022-10-11 15:31:37 +08:00
slidingSample = new PausableSkinnableSound { Looping = true }
2020-08-21 16:52:42 +08:00
} ) ;
maskedContents . AddRange ( new [ ]
{
bodyPiece . CreateProxy ( ) ,
tickContainer . CreateProxy ( ) ,
tailContainer . CreateProxy ( ) ,
2019-03-25 12:47:28 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
}
2022-10-11 15:31:37 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
isHitting . BindValueChanged ( updateSlidingSample , true ) ;
}
2021-05-12 16:07:42 +08:00
protected override void OnApply ( )
{
base . OnApply ( ) ;
sizingContainer . Size = Vector2 . One ;
HoldStartTime = null ;
HoldBrokenTime = null ;
releaseTime = null ;
}
2019-10-17 12:52:21 +08:00
protected override void AddNestedHitObject ( DrawableHitObject hitObject )
2019-10-17 11:37:20 +08:00
{
2019-10-17 12:52:21 +08:00
base . AddNestedHitObject ( hitObject ) ;
2019-10-17 11:37:20 +08:00
2019-10-17 11:53:54 +08:00
switch ( hitObject )
2019-10-17 11:37:20 +08:00
{
2019-12-23 14:43:58 +08:00
case DrawableHoldNoteHead head :
2019-10-17 11:37:20 +08:00
headContainer . Child = head ;
break ;
2019-12-23 14:43:58 +08:00
case DrawableHoldNoteTail tail :
2019-10-17 11:37:20 +08:00
tailContainer . Child = tail ;
break ;
case DrawableHoldNoteTick tick :
tickContainer . Add ( tick ) ;
break ;
}
}
2019-10-17 12:52:21 +08:00
protected override void ClearNestedHitObjects ( )
2019-10-17 11:37:20 +08:00
{
2019-10-17 12:52:21 +08:00
base . ClearNestedHitObjects ( ) ;
2021-05-12 15:35:05 +08:00
headContainer . Clear ( false ) ;
tailContainer . Clear ( false ) ;
tickContainer . Clear ( false ) ;
2019-10-17 11:37:20 +08:00
}
2019-10-17 12:52:21 +08:00
protected override DrawableHitObject CreateNestedHitObject ( HitObject hitObject )
2019-10-17 11:37:20 +08:00
{
switch ( hitObject )
{
2021-05-12 15:35:05 +08:00
case TailNote tail :
return new DrawableHoldNoteTail ( tail ) ;
2019-10-17 11:37:20 +08:00
2021-05-12 15:35:05 +08:00
case HeadNote head :
return new DrawableHoldNoteHead ( head ) ;
2019-10-17 11:37:20 +08:00
case HoldNoteTick tick :
2021-05-12 15:35:05 +08:00
return new DrawableHoldNoteTick ( tick ) ;
2019-10-17 11:37:20 +08:00
}
2019-10-17 12:52:21 +08:00
return base . CreateNestedHitObject ( hitObject ) ;
2019-10-17 11:37:20 +08:00
}
2019-02-21 17:56:34 +08:00
protected override void OnDirectionChanged ( ValueChangedEvent < ScrollingDirection > e )
2018-06-08 17:16:55 +08:00
{
2019-02-21 17:56:34 +08:00
base . OnDirectionChanged ( e ) ;
2018-06-08 17:16:55 +08:00
2020-08-18 23:05:36 +08:00
if ( e . NewValue = = ScrollingDirection . Up )
{
2020-08-21 16:52:42 +08:00
bodyPiece . Anchor = bodyPiece . Origin = Anchor . TopLeft ;
sizingContainer . Anchor = sizingContainer . Origin = Anchor . BottomLeft ;
2020-08-18 23:05:36 +08:00
}
else
{
2020-08-21 16:52:42 +08:00
bodyPiece . Anchor = bodyPiece . Origin = Anchor . BottomLeft ;
sizingContainer . Anchor = sizingContainer . Origin = Anchor . TopLeft ;
2020-08-18 23:05:36 +08:00
}
2018-06-08 17:16:55 +08:00
}
2020-04-21 15:33:19 +08:00
public override void PlaySamples ( )
{
2020-04-21 16:20:37 +08:00
// Samples are played by the head/tail notes.
2020-04-21 15:33:19 +08:00
}
2020-08-18 16:56:48 +08:00
public override void OnKilled ( )
{
base . OnKilled ( ) ;
( bodyPiece . Drawable as IHoldNoteBody ) ? . Recycle ( ) ;
}
2018-04-13 17:19:50 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2020-08-19 00:37:24 +08:00
if ( Time . Current < releaseTime )
releaseTime = null ;
2020-08-21 16:52:42 +08:00
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
// This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
sizingContainer . Padding = new MarginPadding
2020-08-17 23:29:00 +08:00
{
2020-08-21 16:52:42 +08:00
Top = Direction . Value = = ScrollingDirection . Down ? - Tail . Height : 0 ,
Bottom = Direction . Value = = ScrollingDirection . Up ? - Tail . Height : 0 ,
} ;
2020-08-18 23:05:05 +08:00
2020-08-21 16:52:42 +08:00
// Pad the masking container to the starting position of the body piece (half-way under the head).
2020-08-21 18:38:59 +08:00
// This is required to make the body start getting masked immediately as soon as the note is held.
2020-08-21 16:52:42 +08:00
maskingContainer . Padding = new MarginPadding
{
Top = Direction . Value = = ScrollingDirection . Up ? Head . Height / 2 : 0 ,
Bottom = Direction . Value = = ScrollingDirection . Down ? Head . Height / 2 : 0 ,
} ;
2020-08-19 00:40:26 +08:00
2020-08-21 16:52:42 +08:00
// Position and resize the body to lie half-way under the head and the tail notes.
2023-03-09 19:26:48 +08:00
// The rationale for this is account for heads/tails with corner radius.
2020-08-21 16:52:42 +08:00
bodyPiece . Y = ( Direction . Value = = ScrollingDirection . Up ? 1 : - 1 ) * Head . Height / 2 ;
bodyPiece . Height = DrawHeight - Head . Height / 2 + Tail . Height / 2 ;
2020-08-19 00:40:26 +08:00
2020-08-21 18:38:59 +08:00
// As the note is being held, adjust the size of the sizing container. This has two effects:
2020-08-21 16:52:42 +08:00
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
2021-04-22 18:51:33 +08:00
if ( Head . IsHit & & releaseTime = = null & & DrawHeight > 0 )
2020-08-21 16:52:42 +08:00
{
2020-08-21 22:11:15 +08:00
// How far past the hit target this hold note is. Always a positive value.
float yOffset = Math . Max ( 0 , Direction . Value = = ScrollingDirection . Up ? - Y : Y ) ;
sizingContainer . Height = Math . Clamp ( 1 - yOffset / DrawHeight , 0 , 1 ) ;
2020-08-21 16:52:42 +08:00
}
2018-04-13 17:19:50 +08:00
}
2019-12-23 16:48:48 +08:00
protected override void CheckForResult ( bool userTriggered , double timeOffset )
2018-09-06 22:49:54 +08:00
{
2019-12-23 16:48:48 +08:00
if ( Tail . AllJudged )
2020-08-26 19:21:56 +08:00
{
2021-03-31 11:21:14 +08:00
foreach ( var tick in tickContainer )
{
if ( ! tick . Judged )
tick . MissForcefully ( ) ;
}
2022-11-22 15:15:32 +08:00
if ( Tail . IsHit )
ApplyResult ( r = > r . Type = r . Judgement . MaxResult ) ;
else
MissForcefully ( ) ;
2020-08-26 19:21:56 +08:00
}
2018-09-06 22:49:54 +08:00
2020-10-03 04:57:49 +08:00
if ( Tail . Judged & & ! Tail . IsHit )
2020-11-14 04:14:34 +08:00
HoldBrokenTime = Time . Current ;
2018-09-06 22:49:54 +08:00
}
2022-11-22 15:15:32 +08:00
public override void MissForcefully ( )
{
base . MissForcefully ( ) ;
// Important that this is always called when a result is applied.
endHold ( ) ;
}
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < ManiaAction > e )
2018-04-13 17:19:50 +08:00
{
2019-12-23 16:48:48 +08:00
if ( AllJudged )
2018-04-13 17:19:50 +08:00
return false ;
2021-09-16 17:26:12 +08:00
if ( e . Action ! = Action . Value )
2018-04-13 17:19:50 +08:00
return false ;
2020-11-14 03:49:06 +08:00
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
if ( Time . Elapsed < 0 )
return false ;
2020-08-27 19:24:08 +08:00
if ( CheckHittable ? . Invoke ( this , Time . Current ) = = false )
return false ;
2020-07-20 21:26:58 +08:00
// 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 ;
2019-12-23 16:48:48 +08:00
beginHoldAt ( Time . Current - Head . HitObject . StartTime ) ;
2021-08-24 18:05:45 +08:00
return Head . UpdateResult ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-12-23 16:48:48 +08:00
private void beginHoldAt ( double timeOffset )
{
if ( timeOffset < - Head . HitObject . HitWindows . WindowFor ( HitResult . Miss ) )
return ;
HoldStartTime = Time . Current ;
2020-03-31 15:42:35 +08:00
isHitting . Value = true ;
2019-12-23 16:48:48 +08:00
}
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < ManiaAction > e )
2018-04-13 17:19:50 +08:00
{
2019-12-23 16:48:48 +08:00
if ( AllJudged )
2020-01-22 12:22:34 +08:00
return ;
2018-04-13 17:19:50 +08:00
2021-09-16 17:26:12 +08:00
if ( e . Action ! = Action . Value )
2020-01-22 12:22:34 +08:00
return ;
2018-04-13 17:19:50 +08:00
2020-11-14 03:49:06 +08:00
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
if ( Time . Elapsed < 0 )
return ;
2019-12-23 16:48:48 +08:00
// Make sure a hold was started
if ( HoldStartTime = = null )
2020-01-22 12:22:34 +08:00
return ;
2019-12-23 16:48:48 +08:00
Tail . UpdateResult ( ) ;
endHold ( ) ;
2018-04-13 17:19:50 +08:00
// If the key has been released too early, the user should not receive full score for the release
2018-05-20 18:22:42 +08:00
if ( ! Tail . IsHit )
2020-11-14 04:14:34 +08:00
HoldBrokenTime = Time . Current ;
2020-08-17 23:29:00 +08:00
2020-08-19 00:37:24 +08:00
releaseTime = Time . Current ;
2018-04-13 17:19:50 +08:00
}
2019-12-23 16:48:48 +08:00
private void endHold ( )
{
HoldStartTime = null ;
2020-03-31 15:42:35 +08:00
isHitting . Value = false ;
2019-12-23 16:48:48 +08:00
}
2022-10-11 15:31:37 +08:00
protected override void LoadSamples ( )
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if ( HitObject . SampleControlPoint = = null )
{
throw new InvalidOperationException ( $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}." ) ;
}
slidingSample . Samples = HitObject . CreateSlidingSamples ( ) . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
}
public override void StopAllSamples ( )
{
base . StopAllSamples ( ) ;
slidingSample ? . Stop ( ) ;
}
private void updateSlidingSample ( ValueChangedEvent < bool > tracking )
{
if ( tracking . NewValue )
slidingSample ? . Play ( ) ;
else
slidingSample ? . Stop ( ) ;
}
protected override void OnFree ( )
{
2023-01-27 18:32:30 +08:00
slidingSample . ClearSamples ( ) ;
2022-10-11 15:31:37 +08:00
base . OnFree ( ) ;
}
2018-04-13 17:19:50 +08:00
}
}