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
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 ;
2023-07-07 14:21:24 +08:00
using osu.Game.Screens.Play ;
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 ;
2023-10-09 08:47:00 +08:00
public DrawableHoldNoteBody Body = > bodyContainer . 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 ;
2023-10-09 08:47:00 +08:00
private Container < DrawableHoldNoteBody > bodyContainer ;
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
2020-08-17 23:29:00 +08:00
/// <summary>
2023-10-10 15:59:18 +08:00
/// Used to decide whether to visually clamp the hold note to the judgement line.
2020-08-17 23:29:00 +08:00
/// </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
} ,
2023-10-09 08:47:00 +08:00
bodyContainer = new Container < DrawableHoldNoteBody > { RelativeSizeAxes = Axes . Both } ,
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
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 ( ) ,
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 ;
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 ;
2023-10-09 08:47:00 +08:00
case DrawableHoldNoteBody body :
bodyContainer . Child = body ;
break ;
2019-10-17 11:37:20 +08:00
}
}
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 ) ;
2023-10-09 08:47:00 +08:00
bodyContainer . 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 ) ;
2023-10-09 08:47:00 +08:00
case HoldNoteBody body :
return new DrawableHoldNoteBody ( body ) ;
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 ;
2023-05-03 14:18:37 +08:00
if ( Time . Current < HoldStartTime )
endHold ( ) ;
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
2023-06-17 00:37:09 +08:00
if ( Time . Current > = HitObject . StartTime )
2020-08-21 16:52:42 +08:00
{
2023-06-17 00:37:09 +08:00
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 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.
//
// As per stable, this should not apply for early hits, waiting until the object starts to touch the
// judgement area first.
if ( Head . IsHit & & releaseTime = = null & & DrawHeight > 0 )
{
// How far past the hit target this hold note is.
float yOffset = Direction . Value = = ScrollingDirection . Up ? - Y : Y ;
sizingContainer . Height = 1 - yOffset / DrawHeight ;
}
2020-08-21 16:52:42 +08:00
}
2023-06-16 13:54:19 +08:00
else
sizingContainer . Height = 1 ;
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
{
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
2023-10-09 08:47:00 +08:00
// Make sure that the hold note is fully judged by giving the body a judgement.
if ( Tail . AllJudged & & ! Body . AllJudged )
Body . TriggerResult ( Tail . IsHit ) ;
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.
2023-07-07 14:21:24 +08:00
if ( ( Clock as IGameplayClock ) ? . IsRewinding = = true )
2020-11-14 03:49:06 +08:00
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
2023-05-08 14:43:11 +08:00
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
2023-07-08 19:31:21 +08:00
if ( ( Clock as IGameplayClock ) ? . IsRewinding = = true )
2023-05-08 14:43:11 +08:00
return ;
2023-10-10 15:59:18 +08:00
// When our action is released and we are in the middle of a hold, there's a chance that
// the user has released too early (before the tail).
//
// In such a case, we want to record this against the DrawableHoldNoteBody.
if ( HoldStartTime ! = null )
{
Tail . UpdateResult ( ) ;
Body . TriggerResult ( Tail . IsHit ) ;
2020-08-17 23:29:00 +08:00
2023-10-10 15:59:18 +08:00
endHold ( ) ;
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.
2023-04-26 19:55:39 +08:00
slidingSample . Samples = HitObject . CreateSlidingSamples ( ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
2022-10-11 15:31:37 +08:00
}
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
}
}