2019-01-24 08:43:03 +00: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 09:19:50 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
2019-11-20 12:19:49 +00:00
using System ;
2020-07-22 07:37:38 +00:00
using System.Linq ;
2020-11-06 14:09:23 +00:00
using JetBrains.Annotations ;
2018-02-20 11:52:12 +00:00
using osu.Framework.Allocation ;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables ;
2022-03-14 06:45:57 +00:00
using osu.Framework.Graphics ;
2017-02-16 08:02:36 +00:00
using osu.Framework.Graphics.Containers ;
2020-11-19 11:40:30 +00:00
using osu.Game.Audio ;
2022-09-22 05:30:01 +00:00
using osu.Game.Graphics.Containers ;
2018-11-14 05:29:22 +00:00
using osu.Game.Rulesets.Objects ;
2022-03-14 06:45:57 +00:00
using osu.Game.Rulesets.Objects.Drawables ;
2020-12-04 11:21:53 +00:00
using osu.Game.Rulesets.Osu.Skinning.Default ;
2021-02-03 13:12:20 +00:00
using osu.Game.Rulesets.Scoring ;
2018-12-07 21:24:24 +00:00
using osu.Game.Skinning ;
2022-03-14 06:45:57 +00:00
using osuTK ;
2018-04-13 09:19:50 +00:00
2017-04-18 07:05:58 +00:00
namespace osu.Game.Rulesets.Osu.Objects.Drawables
2016-11-17 08:20:51 +00:00
{
2020-11-22 09:36:10 +00:00
public partial class DrawableSlider : DrawableOsuHitObject
2016-11-17 08:20:51 +00:00
{
2020-11-05 04:51:46 +00:00
public new Slider HitObject = > ( Slider ) base . HitObject ;
2019-10-17 05:02:23 +00:00
public DrawableSliderHead HeadCircle = > headContainer . Child ;
public DrawableSliderTail TailCircle = > tailContainer . Child ;
2018-04-13 09:19:50 +00:00
2022-06-29 08:23:35 +00:00
[Cached]
public DrawableSliderBall Ball { get ; private set ; }
2020-11-05 04:51:46 +00:00
public SkinnableDrawable Body { get ; private set ; }
2018-04-13 09:19:50 +00:00
2022-09-22 05:30:01 +00:00
private ShakeContainer shakeContainer ;
2021-09-01 10:34:57 +00:00
/// <summary>
/// A target container which can be used to add top level elements to the slider's display.
/// Intended to be used for proxy purposes only.
/// </summary>
public Container OverlayElementContainer { get ; private set ; }
2021-02-10 09:46:26 +00:00
public override bool DisplayResult = > ! HitObject . OnlyJudgeNestedObjects ;
2020-03-19 05:44:48 +00:00
2021-05-24 08:15:57 +00:00
[CanBeNull]
public PlaySliderBody SliderBody = > Body . Drawable as PlaySliderBody ;
2019-12-17 09:16:25 +00:00
2020-11-06 14:09:23 +00:00
public IBindable < int > PathVersion = > pathVersion ;
private readonly Bindable < int > pathVersion = new Bindable < int > ( ) ;
2019-10-16 13:10:50 +00:00
2020-11-05 04:51:46 +00:00
private Container < DrawableSliderHead > headContainer ;
private Container < DrawableSliderTail > tailContainer ;
private Container < DrawableSliderTick > tickContainer ;
private Container < DrawableSliderRepeat > repeatContainer ;
2020-11-19 11:40:30 +00:00
private PausableSkinnableSound slidingSample ;
2019-03-05 05:40:27 +00:00
2020-11-10 15:22:06 +00:00
public DrawableSlider ( )
: this ( null )
{
}
2020-11-06 14:09:23 +00:00
public DrawableSlider ( [ CanBeNull ] Slider s = null )
2017-12-30 06:21:25 +00:00
: base ( s )
2016-11-17 08:20:51 +00:00
{
2022-06-29 08:23:35 +00:00
Ball = new DrawableSliderBall
{
GetInitialHitAction = ( ) = > HeadCircle . HitAction ,
BypassAutoSizeAxes = Axes . Both ,
AlwaysPresent = true ,
Alpha = 0
} ;
2020-11-05 04:51:46 +00:00
}
2018-04-13 09:19:50 +00:00
2020-11-05 04:51:46 +00:00
[BackgroundDependencyLoader]
private void load ( )
{
2022-09-22 05:46:37 +00:00
AddRangeInternal ( new Drawable [ ]
2016-11-28 03:15:25 +00:00
{
2022-09-22 05:30:01 +00:00
shakeContainer = new ShakeContainer
{
ShakeDuration = 30 ,
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
2022-11-09 07:04:56 +00:00
Body = new SkinnableDrawable ( new OsuSkinComponentLookup ( OsuSkinComponents . SliderBody ) , _ = > new DefaultSliderBody ( ) , confineMode : ConfineMode . NoScaling ) ,
2022-09-22 05:30:01 +00:00
tailContainer = new Container < DrawableSliderTail > { RelativeSizeAxes = Axes . Both } ,
tickContainer = new Container < DrawableSliderTick > { RelativeSizeAxes = Axes . Both } ,
repeatContainer = new Container < DrawableSliderRepeat > { RelativeSizeAxes = Axes . Both } ,
}
} ,
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
2021-09-01 10:34:57 +00:00
headContainer = new Container < DrawableSliderHead > { RelativeSizeAxes = Axes . Both } ,
OverlayElementContainer = new Container { RelativeSizeAxes = Axes . Both , } ,
2022-06-29 08:23:35 +00:00
Ball ,
2020-11-19 11:40:30 +00:00
slidingSample = new PausableSkinnableSound { Looping = true }
2022-09-22 05:46:37 +00:00
} ) ;
2018-04-13 09:19:50 +00:00
2020-11-06 14:35:47 +00:00
PositionBindable . BindValueChanged ( _ = > Position = HitObject . StackedPosition ) ;
StackHeightBindable . BindValueChanged ( _ = > Position = HitObject . StackedPosition ) ;
ScaleBindable . BindValueChanged ( scale = > Ball . Scale = new Vector2 ( scale . NewValue ) ) ;
2018-04-13 09:19:50 +00:00
2019-07-22 05:45:25 +00:00
AccentColour . BindValueChanged ( colour = >
2018-03-15 06:58:04 +00:00
{
2018-07-02 07:10:56 +00:00
foreach ( var drawableHitObject in NestedHitObjects )
2019-07-22 05:45:25 +00:00
drawableHitObject . AccentColour . Value = colour . NewValue ;
} , true ) ;
2020-07-22 07:37:38 +00:00
Tracking . BindValueChanged ( updateSlidingSample ) ;
}
2020-11-27 01:13:05 +00:00
protected override void OnApply ( )
2020-11-06 14:09:23 +00:00
{
2020-11-27 01:13:05 +00:00
base . OnApply ( ) ;
2020-11-06 14:09:23 +00:00
2020-11-06 15:40:26 +00:00
// Ensure that the version will change after the upcoming BindTo().
pathVersion . Value = int . MaxValue ;
PathVersion . BindTo ( HitObject . Path . Version ) ;
2020-11-06 14:09:23 +00:00
}
2022-09-22 05:30:01 +00:00
public override void Shake ( ) = > shakeContainer . Shake ( ) ;
2020-11-27 01:13:05 +00:00
protected override void OnFree ( )
2020-11-06 14:09:23 +00:00
{
2020-11-27 01:13:05 +00:00
base . OnFree ( ) ;
2020-11-06 14:09:23 +00:00
2020-11-06 15:40:26 +00:00
PathVersion . UnbindFrom ( HitObject . Path . Version ) ;
2020-11-06 14:09:23 +00:00
2023-01-27 10:32:30 +00:00
slidingSample ? . ClearSamples ( ) ;
2020-11-19 11:40:30 +00:00
}
2020-07-22 07:37:38 +00:00
protected override void LoadSamples ( )
{
2021-04-09 06:28:08 +00:00
// 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}." ) ;
}
Samples . Samples = HitObject . TailSamples . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
2022-03-14 06:45:57 +00:00
slidingSample . Samples = HitObject . CreateSlidingSamples ( ) . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
2020-07-22 07:37:38 +00:00
}
2020-09-29 06:07:55 +00:00
public override void StopAllSamples ( )
{
base . StopAllSamples ( ) ;
slidingSample ? . Stop ( ) ;
}
2020-07-22 07:37:38 +00:00
private void updateSlidingSample ( ValueChangedEvent < bool > tracking )
{
2020-09-29 03:45:20 +00:00
if ( tracking . NewValue )
2020-07-22 07:37:38 +00:00
slidingSample ? . Play ( ) ;
else
slidingSample ? . Stop ( ) ;
2018-03-15 06:58:04 +00:00
}
2018-04-13 09:19:50 +00:00
2019-10-17 04:52:21 +00:00
protected override void AddNestedHitObject ( DrawableHitObject hitObject )
2019-10-16 13:10:50 +00:00
{
2019-10-17 04:52:21 +00:00
base . AddNestedHitObject ( hitObject ) ;
2019-10-16 13:10:50 +00:00
2019-10-17 03:53:54 +00:00
switch ( hitObject )
2019-10-16 13:10:50 +00:00
{
case DrawableSliderHead head :
2019-10-17 05:02:23 +00:00
headContainer . Child = head ;
2019-10-16 13:10:50 +00:00
break ;
case DrawableSliderTail tail :
2019-10-17 05:02:23 +00:00
tailContainer . Child = tail ;
2019-10-16 13:10:50 +00:00
break ;
case DrawableSliderTick tick :
tickContainer . Add ( tick ) ;
break ;
2020-03-19 05:26:24 +00:00
case DrawableSliderRepeat repeat :
2019-10-16 13:10:50 +00:00
repeatContainer . Add ( repeat ) ;
break ;
}
}
2019-10-17 04:52:21 +00:00
protected override void ClearNestedHitObjects ( )
2019-10-16 13:10:50 +00:00
{
2019-10-17 04:52:21 +00:00
base . ClearNestedHitObjects ( ) ;
2019-10-16 13:10:50 +00:00
2020-11-12 06:59:48 +00:00
headContainer . Clear ( false ) ;
tailContainer . Clear ( false ) ;
repeatContainer . Clear ( false ) ;
tickContainer . Clear ( false ) ;
2021-09-01 10:34:57 +00:00
2021-09-18 14:27:30 +00:00
OverlayElementContainer . Clear ( false ) ;
2019-10-16 13:10:50 +00:00
}
2019-10-17 04:52:21 +00:00
protected override DrawableHitObject CreateNestedHitObject ( HitObject hitObject )
2019-10-16 13:10:50 +00:00
{
switch ( hitObject )
{
case SliderTailCircle tail :
2020-11-05 04:51:46 +00:00
return new DrawableSliderTail ( tail ) ;
2019-10-16 13:10:50 +00:00
2020-03-30 07:14:56 +00:00
case SliderHeadCircle head :
2020-11-12 06:59:48 +00:00
return new DrawableSliderHead ( head ) ;
2019-10-16 13:10:50 +00:00
case SliderTick tick :
2020-11-12 06:59:48 +00:00
return new DrawableSliderTick ( tick ) ;
2019-10-16 13:10:50 +00:00
2020-03-19 05:42:02 +00:00
case SliderRepeat repeat :
2020-11-12 06:59:48 +00:00
return new DrawableSliderRepeat ( repeat ) ;
2019-10-16 13:10:50 +00:00
}
2019-10-17 04:52:21 +00:00
return base . CreateNestedHitObject ( hitObject ) ;
2019-10-16 13:10:50 +00:00
}
2019-04-12 01:47:22 +00:00
public readonly Bindable < bool > Tracking = new Bindable < bool > ( ) ;
2018-04-13 09:19:50 +00:00
2016-12-06 09:54:32 +00:00
protected override void Update ( )
2016-11-17 12:29:35 +00:00
{
2016-12-06 09:54:32 +00:00
base . Update ( ) ;
2018-04-13 09:19:50 +00:00
2019-04-12 01:47:22 +00:00
Tracking . Value = Ball . Tracking ;
2018-04-13 09:19:50 +00:00
2020-07-22 07:37:38 +00:00
if ( Tracking . Value & & slidingSample ! = null )
// keep the sliding sample playing at the current tracking position
2022-04-18 06:18:56 +00:00
slidingSample . Balance . Value = CalculateSamplePlaybackBalance ( CalculateDrawableRelativePosition ( Ball ) ) ;
2020-07-22 07:37:38 +00:00
2020-11-05 04:51:46 +00:00
double completionProgress = Math . Clamp ( ( Time . Current - HitObject . StartTime ) / HitObject . Duration , 0 , 1 ) ;
2018-04-13 09:19:50 +00:00
2019-10-16 13:10:50 +00:00
Ball . UpdateProgress ( completionProgress ) ;
2021-05-24 08:15:57 +00:00
SliderBody ? . UpdateProgress ( completionProgress ) ;
2019-10-16 13:10:50 +00:00
foreach ( DrawableHitObject hitObject in NestedHitObjects )
{
2021-05-24 08:15:57 +00:00
if ( hitObject is ITrackSnaking s ) s . UpdateSnakingPosition ( HitObject . Path . PositionAt ( SliderBody ? . SnakedStart ? ? 0 ) , HitObject . Path . PositionAt ( SliderBody ? . SnakedEnd ? ? 0 ) ) ;
2019-10-16 13:10:50 +00:00
if ( hitObject is IRequireTracking t ) t . Tracking = Ball . Tracking ;
}
2018-04-13 09:19:50 +00:00
2021-05-24 08:15:57 +00:00
Size = SliderBody ? . Size ? ? Vector2 . Zero ;
OriginPosition = SliderBody ? . PathOffset ? ? Vector2 . Zero ;
2018-04-13 09:19:50 +00:00
2018-02-26 07:11:26 +00:00
if ( DrawSize ! = Vector2 . Zero )
2018-02-23 17:53:02 +00:00
{
var childAnchorPosition = Vector2 . Divide ( OriginPosition , DrawSize ) ;
foreach ( var obj in NestedHitObjects )
obj . RelativeAnchorPosition = childAnchorPosition ;
Ball . RelativeAnchorPosition = childAnchorPosition ;
}
2016-12-03 13:40:35 +00:00
}
2018-04-13 09:19:50 +00:00
2019-07-16 09:19:13 +00:00
public override void OnKilled ( )
{
base . OnKilled ( ) ;
2021-05-24 08:15:57 +00:00
SliderBody ? . RecyclePath ( ) ;
2019-07-16 09:19:13 +00:00
}
2018-08-06 02:31:46 +00:00
protected override void CheckForResult ( bool userTriggered , double timeOffset )
2016-11-29 12:40:24 +00:00
{
2020-11-05 04:51:46 +00:00
if ( userTriggered | | Time . Current < HitObject . EndTime )
2018-08-01 12:46:22 +00:00
return ;
2021-02-10 09:52:39 +00:00
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
2021-02-10 09:46:26 +00:00
if ( HitObject . OnlyJudgeNestedObjects )
2021-02-03 13:12:20 +00:00
{
ApplyResult ( r = > r . Type = NestedHitObjects . Any ( h = > h . Result . IsHit ) ? r . Judgement . MaxResult : r . Judgement . MinResult ) ;
return ;
}
2021-02-10 12:27:12 +00:00
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
2021-02-03 13:12:20 +00:00
ApplyResult ( r = >
{
int totalTicks = NestedHitObjects . Count ;
int hitTicks = NestedHitObjects . Count ( h = > h . IsHit ) ;
if ( hitTicks = = totalTicks )
r . Type = HitResult . Great ;
2021-02-10 12:24:41 +00:00
else if ( hitTicks = = 0 )
2021-02-03 13:12:20 +00:00
r . Type = HitResult . Miss ;
2021-02-10 12:24:41 +00:00
else
{
2021-02-10 13:09:24 +00:00
double hitFraction = ( double ) hitTicks / totalTicks ;
2021-02-10 12:25:31 +00:00
r . Type = hitFraction > = 0.5 ? HitResult . Ok : HitResult . Meh ;
2021-02-10 12:24:41 +00:00
}
2021-02-03 13:12:20 +00:00
} ) ;
2020-03-26 10:51:02 +00:00
}
public override void PlaySamples ( )
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
2021-04-02 08:56:23 +00:00
if ( ! TailCircle . SamplePlaysOnlyOnHit | | TailCircle . IsHit )
2020-03-26 10:51:02 +00:00
base . PlaySamples ( ) ;
2016-11-29 12:40:24 +00:00
}
2018-04-13 09:19:50 +00:00
2020-11-17 14:19:59 +00:00
protected override void UpdateInitialTransforms ( )
{
base . UpdateInitialTransforms ( ) ;
Body . FadeInFromZero ( HitObject . TimeFadeIn ) ;
}
2020-11-04 07:19:07 +00:00
protected override void UpdateStartTimeStateTransforms ( )
2016-11-17 08:20:51 +00:00
{
2020-11-04 07:19:07 +00:00
base . UpdateStartTimeStateTransforms ( ) ;
2019-09-13 09:49:21 +00:00
2017-12-26 16:25:18 +00:00
Ball . FadeIn ( ) ;
2018-01-04 11:53:33 +00:00
Ball . ScaleTo ( HitObject . Scale ) ;
2020-11-04 07:19:07 +00:00
}
2018-04-13 09:19:50 +00:00
2020-11-04 07:19:07 +00:00
protected override void UpdateHitStateTransforms ( ArmedState state )
{
base . UpdateHitStateTransforms ( state ) ;
2018-04-13 09:19:50 +00:00
2022-10-18 20:43:31 +00:00
const float fade_out_time = 240 ;
2018-04-13 09:19:50 +00:00
2020-11-04 07:19:07 +00:00
switch ( state )
{
case ArmedState . Hit :
2021-05-24 08:15:57 +00:00
if ( SliderBody ? . SnakingOut . Value = = true )
2020-12-01 06:21:32 +00:00
Body . FadeOut ( 40 ) ; // short fade to allow for any body colour to smoothly disappear.
2020-11-04 07:19:07 +00:00
break ;
2017-07-17 14:05:24 +00:00
}
2020-11-04 07:19:07 +00:00
2022-10-18 20:43:42 +00:00
this . FadeOut ( fade_out_time ) . Expire ( ) ;
2016-11-17 08:20:51 +00:00
}
2018-04-13 09:19:50 +00:00
2021-05-24 08:15:57 +00:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > SliderBody ? . ReceivePositionalInputAt ( screenSpacePos ) ? ? base . ReceivePositionalInputAt ( screenSpacePos ) ;
2019-12-17 10:29:27 +00:00
private partial class DefaultSliderBody : PlaySliderBody
{
}
2016-12-06 09:54:32 +00:00
}
2017-04-27 08:37:38 +00:00
}