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
2019-11-20 20:19:49 +08:00
using System ;
2020-07-22 15:37:38 +08:00
using System.Linq ;
2020-11-06 22:09:23 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2022-03-14 14:45:57 +08:00
using osu.Framework.Graphics ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.Containers ;
2020-11-19 19:40:30 +08:00
using osu.Game.Audio ;
2018-11-14 13:29:22 +08:00
using osu.Game.Rulesets.Objects ;
2022-03-14 14:45:57 +08:00
using osu.Game.Rulesets.Objects.Drawables ;
2019-09-03 16:57:34 +08:00
using osu.Game.Rulesets.Osu.Skinning ;
2020-12-04 19:21:53 +08:00
using osu.Game.Rulesets.Osu.Skinning.Default ;
2021-02-03 21:12:20 +08:00
using osu.Game.Rulesets.Scoring ;
2018-12-08 05:24:24 +08:00
using osu.Game.Skinning ;
2022-03-14 14:45:57 +08:00
using osuTK ;
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
2020-11-22 17:36:10 +08:00
public class DrawableSlider : DrawableOsuHitObject
2018-04-13 17:19:50 +08:00
{
2020-11-05 12:51:46 +08:00
public new Slider HitObject = > ( Slider ) base . HitObject ;
2019-10-17 13:02:23 +08:00
public DrawableSliderHead HeadCircle = > headContainer . Child ;
public DrawableSliderTail TailCircle = > tailContainer . Child ;
2018-04-13 17:19:50 +08:00
2022-06-29 16:23:35 +08:00
[Cached]
public DrawableSliderBall Ball { get ; private set ; }
2020-11-05 12:51:46 +08:00
public SkinnableDrawable Body { get ; private set ; }
2018-04-13 17:19:50 +08:00
2021-09-01 18:34:57 +08: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 17:46:26 +08:00
public override bool DisplayResult = > ! HitObject . OnlyJudgeNestedObjects ;
2020-03-19 13:44:48 +08:00
2021-05-24 16:15:57 +08:00
[CanBeNull]
public PlaySliderBody SliderBody = > Body . Drawable as PlaySliderBody ;
2019-12-17 17:16:25 +08:00
2020-11-06 22:09:23 +08:00
public IBindable < int > PathVersion = > pathVersion ;
private readonly Bindable < int > pathVersion = new Bindable < int > ( ) ;
2019-10-16 21:10:50 +08:00
2020-11-05 12:51:46 +08:00
private Container < DrawableSliderHead > headContainer ;
private Container < DrawableSliderTail > tailContainer ;
private Container < DrawableSliderTick > tickContainer ;
private Container < DrawableSliderRepeat > repeatContainer ;
2020-11-19 19:40:30 +08:00
private PausableSkinnableSound slidingSample ;
2019-03-05 13:40:27 +08:00
2020-11-10 23:22:06 +08:00
public DrawableSlider ( )
: this ( null )
{
}
2020-11-06 22:09:23 +08:00
public DrawableSlider ( [ CanBeNull ] Slider s = null )
2018-04-13 17:19:50 +08:00
: base ( s )
{
2022-06-29 16:23:35 +08:00
Ball = new DrawableSliderBall
{
GetInitialHitAction = ( ) = > HeadCircle . HitAction ,
BypassAutoSizeAxes = Axes . Both ,
AlwaysPresent = true ,
Alpha = 0
} ;
2020-11-05 12:51:46 +08:00
}
2018-04-13 17:19:50 +08:00
2020-11-05 12:51:46 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2018-04-13 17:19:50 +08:00
InternalChildren = new Drawable [ ]
{
2019-12-17 18:29:27 +08:00
Body = new SkinnableDrawable ( new OsuSkinComponent ( OsuSkinComponents . SliderBody ) , _ = > new DefaultSliderBody ( ) , confineMode : ConfineMode . NoScaling ) ,
2020-10-02 12:38:48 +08:00
tailContainer = new Container < DrawableSliderTail > { RelativeSizeAxes = Axes . Both } ,
2019-10-16 21:10:50 +08:00
tickContainer = new Container < DrawableSliderTick > { RelativeSizeAxes = Axes . Both } ,
2020-03-19 13:26:24 +08:00
repeatContainer = new Container < DrawableSliderRepeat > { RelativeSizeAxes = Axes . Both } ,
2021-09-01 18:34:57 +08:00
headContainer = new Container < DrawableSliderHead > { RelativeSizeAxes = Axes . Both } ,
OverlayElementContainer = new Container { RelativeSizeAxes = Axes . Both , } ,
2022-06-29 16:23:35 +08:00
Ball ,
2020-11-19 19:40:30 +08:00
slidingSample = new PausableSkinnableSound { Looping = true }
2018-04-13 17:19:50 +08:00
} ;
2020-11-06 22:35:47 +08:00
PositionBindable . BindValueChanged ( _ = > Position = HitObject . StackedPosition ) ;
StackHeightBindable . BindValueChanged ( _ = > Position = HitObject . StackedPosition ) ;
ScaleBindable . BindValueChanged ( scale = > Ball . Scale = new Vector2 ( scale . NewValue ) ) ;
2018-04-13 17:19:50 +08:00
2019-07-22 13:45:25 +08:00
AccentColour . BindValueChanged ( colour = >
2018-04-13 17:19:50 +08:00
{
2018-07-02 15:10:56 +08:00
foreach ( var drawableHitObject in NestedHitObjects )
2019-07-22 13:45:25 +08:00
drawableHitObject . AccentColour . Value = colour . NewValue ;
2020-11-16 21:40:25 +08:00
updateBallTint ( ) ;
2019-07-22 13:45:25 +08:00
} , true ) ;
2020-07-22 15:37:38 +08:00
Tracking . BindValueChanged ( updateSlidingSample ) ;
}
2020-11-27 09:13:05 +08:00
protected override void OnApply ( )
2020-11-06 22:09:23 +08:00
{
2020-11-27 09:13:05 +08:00
base . OnApply ( ) ;
2020-11-06 22:09:23 +08:00
2020-11-06 23:40:26 +08:00
// Ensure that the version will change after the upcoming BindTo().
pathVersion . Value = int . MaxValue ;
PathVersion . BindTo ( HitObject . Path . Version ) ;
2020-11-06 22:09:23 +08:00
}
2020-11-27 09:13:05 +08:00
protected override void OnFree ( )
2020-11-06 22:09:23 +08:00
{
2020-11-27 09:13:05 +08:00
base . OnFree ( ) ;
2020-11-06 22:09:23 +08:00
2020-11-06 23:40:26 +08:00
PathVersion . UnbindFrom ( HitObject . Path . Version ) ;
2020-11-06 22:09:23 +08:00
2020-11-19 19:40:30 +08:00
slidingSample . Samples = null ;
}
2020-07-22 15:37:38 +08:00
protected override void LoadSamples ( )
{
2021-04-09 14:28:08 +08: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 14:45:57 +08:00
slidingSample . Samples = HitObject . CreateSlidingSamples ( ) . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
2020-07-22 15:37:38 +08:00
}
2020-09-29 14:07:55 +08:00
public override void StopAllSamples ( )
{
base . StopAllSamples ( ) ;
slidingSample ? . Stop ( ) ;
}
2020-07-22 15:37:38 +08:00
private void updateSlidingSample ( ValueChangedEvent < bool > tracking )
{
2020-09-29 11:45:20 +08:00
if ( tracking . NewValue )
2020-07-22 15:37:38 +08:00
slidingSample ? . Play ( ) ;
else
slidingSample ? . Stop ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-10-17 12:52:21 +08:00
protected override void AddNestedHitObject ( DrawableHitObject hitObject )
2019-10-16 21:10:50 +08:00
{
2019-10-17 12:52:21 +08:00
base . AddNestedHitObject ( hitObject ) ;
2019-10-16 21:10:50 +08:00
2019-10-17 11:53:54 +08:00
switch ( hitObject )
2019-10-16 21:10:50 +08:00
{
case DrawableSliderHead head :
2019-10-17 13:02:23 +08:00
headContainer . Child = head ;
2019-10-16 21:10:50 +08:00
break ;
case DrawableSliderTail tail :
2019-10-17 13:02:23 +08:00
tailContainer . Child = tail ;
2019-10-16 21:10:50 +08:00
break ;
case DrawableSliderTick tick :
tickContainer . Add ( tick ) ;
break ;
2020-03-19 13:26:24 +08:00
case DrawableSliderRepeat repeat :
2019-10-16 21:10:50 +08:00
repeatContainer . Add ( repeat ) ;
break ;
}
}
2019-10-17 12:52:21 +08:00
protected override void ClearNestedHitObjects ( )
2019-10-16 21:10:50 +08:00
{
2019-10-17 12:52:21 +08:00
base . ClearNestedHitObjects ( ) ;
2019-10-16 21:10:50 +08:00
2020-11-12 14:59:48 +08:00
headContainer . Clear ( false ) ;
tailContainer . Clear ( false ) ;
repeatContainer . Clear ( false ) ;
tickContainer . Clear ( false ) ;
2021-09-01 18:34:57 +08:00
2021-09-18 22:27:30 +08:00
OverlayElementContainer . Clear ( false ) ;
2019-10-16 21:10:50 +08:00
}
2019-10-17 12:52:21 +08:00
protected override DrawableHitObject CreateNestedHitObject ( HitObject hitObject )
2019-10-16 21:10:50 +08:00
{
switch ( hitObject )
{
case SliderTailCircle tail :
2020-11-05 12:51:46 +08:00
return new DrawableSliderTail ( tail ) ;
2019-10-16 21:10:50 +08:00
2020-03-30 15:14:56 +08:00
case SliderHeadCircle head :
2020-11-12 14:59:48 +08:00
return new DrawableSliderHead ( head ) ;
2019-10-16 21:10:50 +08:00
case SliderTick tick :
2020-11-12 14:59:48 +08:00
return new DrawableSliderTick ( tick ) ;
2019-10-16 21:10:50 +08:00
2020-03-19 13:42:02 +08:00
case SliderRepeat repeat :
2020-11-12 14:59:48 +08:00
return new DrawableSliderRepeat ( repeat ) ;
2019-10-16 21:10:50 +08:00
}
2019-10-17 12:52:21 +08:00
return base . CreateNestedHitObject ( hitObject ) ;
2019-10-16 21:10:50 +08:00
}
2019-04-12 09:47:22 +08:00
public readonly Bindable < bool > Tracking = new Bindable < bool > ( ) ;
2018-04-13 17:19:50 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2019-04-12 09:47:22 +08:00
Tracking . Value = Ball . Tracking ;
2018-04-13 17:19:50 +08:00
2020-07-22 15:37:38 +08:00
if ( Tracking . Value & & slidingSample ! = null )
// keep the sliding sample playing at the current tracking position
2022-04-18 14:18:56 +08:00
slidingSample . Balance . Value = CalculateSamplePlaybackBalance ( CalculateDrawableRelativePosition ( Ball ) ) ;
2020-07-22 15:37:38 +08:00
2020-11-05 12:51:46 +08:00
double completionProgress = Math . Clamp ( ( Time . Current - HitObject . StartTime ) / HitObject . Duration , 0 , 1 ) ;
2018-04-13 17:19:50 +08:00
2019-10-16 21:10:50 +08:00
Ball . UpdateProgress ( completionProgress ) ;
2021-05-24 16:15:57 +08:00
SliderBody ? . UpdateProgress ( completionProgress ) ;
2019-10-16 21:10:50 +08:00
foreach ( DrawableHitObject hitObject in NestedHitObjects )
{
2021-05-24 16:15:57 +08:00
if ( hitObject is ITrackSnaking s ) s . UpdateSnakingPosition ( HitObject . Path . PositionAt ( SliderBody ? . SnakedStart ? ? 0 ) , HitObject . Path . PositionAt ( SliderBody ? . SnakedEnd ? ? 0 ) ) ;
2019-10-16 21:10:50 +08:00
if ( hitObject is IRequireTracking t ) t . Tracking = Ball . Tracking ;
}
2018-04-13 17:19:50 +08:00
2021-05-24 16:15:57 +08:00
Size = SliderBody ? . Size ? ? Vector2 . Zero ;
OriginPosition = SliderBody ? . PathOffset ? ? Vector2 . Zero ;
2018-04-13 17:19:50 +08:00
if ( DrawSize ! = Vector2 . Zero )
{
var childAnchorPosition = Vector2 . Divide ( OriginPosition , DrawSize ) ;
foreach ( var obj in NestedHitObjects )
obj . RelativeAnchorPosition = childAnchorPosition ;
Ball . RelativeAnchorPosition = childAnchorPosition ;
}
}
2019-07-16 17:19:13 +08:00
public override void OnKilled ( )
{
base . OnKilled ( ) ;
2021-05-24 16:15:57 +08:00
SliderBody ? . RecyclePath ( ) ;
2019-07-16 17:19:13 +08:00
}
2019-09-18 19:19:57 +08:00
protected override void ApplySkin ( ISkinSource skin , bool allowFallback )
2018-12-08 05:24:24 +08:00
{
2019-09-18 19:19:57 +08:00
base . ApplySkin ( skin , allowFallback ) ;
2019-01-07 19:12:39 +08:00
2020-11-16 21:40:25 +08:00
updateBallTint ( ) ;
}
private void updateBallTint ( )
{
if ( CurrentSkin = = null )
return ;
bool allowBallTint = CurrentSkin . GetConfig < OsuSkinConfiguration , bool > ( OsuSkinConfiguration . AllowSliderBallTint ) ? . Value ? ? false ;
2020-04-05 02:17:11 +08:00
Ball . AccentColour = allowBallTint ? AccentColour . Value : Color4 . White ;
2018-12-08 05:24:24 +08:00
}
2018-08-06 10:31:46 +08:00
protected override void CheckForResult ( bool userTriggered , double timeOffset )
2018-04-13 17:19:50 +08:00
{
2020-11-05 12:51:46 +08:00
if ( userTriggered | | Time . Current < HitObject . EndTime )
2018-08-01 20:46:22 +08:00
return ;
2021-02-10 17:52:39 +08: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 17:46:26 +08:00
if ( HitObject . OnlyJudgeNestedObjects )
2021-02-03 21:12:20 +08:00
{
ApplyResult ( r = > r . Type = NestedHitObjects . Any ( h = > h . Result . IsHit ) ? r . Judgement . MaxResult : r . Judgement . MinResult ) ;
return ;
}
2021-02-10 20:27:12 +08: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 21:12:20 +08:00
ApplyResult ( r = >
{
int totalTicks = NestedHitObjects . Count ;
int hitTicks = NestedHitObjects . Count ( h = > h . IsHit ) ;
if ( hitTicks = = totalTicks )
r . Type = HitResult . Great ;
2021-02-10 20:24:41 +08:00
else if ( hitTicks = = 0 )
2021-02-03 21:12:20 +08:00
r . Type = HitResult . Miss ;
2021-02-10 20:24:41 +08:00
else
{
2021-02-10 21:09:24 +08:00
double hitFraction = ( double ) hitTicks / totalTicks ;
2021-02-10 20:25:31 +08:00
r . Type = hitFraction > = 0.5 ? HitResult . Ok : HitResult . Meh ;
2021-02-10 20:24:41 +08:00
}
2021-02-03 21:12:20 +08:00
} ) ;
2020-03-26 18:51:02 +08: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 16:56:23 +08:00
if ( ! TailCircle . SamplePlaysOnlyOnHit | | TailCircle . IsHit )
2020-03-26 18:51:02 +08:00
base . PlaySamples ( ) ;
2018-04-13 17:19:50 +08:00
}
2020-11-17 22:19:59 +08:00
protected override void UpdateInitialTransforms ( )
{
base . UpdateInitialTransforms ( ) ;
Body . FadeInFromZero ( HitObject . TimeFadeIn ) ;
}
2020-11-04 15:19:07 +08:00
protected override void UpdateStartTimeStateTransforms ( )
2018-04-13 17:19:50 +08:00
{
2020-11-04 15:19:07 +08:00
base . UpdateStartTimeStateTransforms ( ) ;
2019-09-13 17:49:21 +08:00
2018-04-13 17:19:50 +08:00
Ball . FadeIn ( ) ;
Ball . ScaleTo ( HitObject . Scale ) ;
2020-11-04 15:19:07 +08:00
}
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
protected override void UpdateHitStateTransforms ( ArmedState state )
{
base . UpdateHitStateTransforms ( state ) ;
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
const float fade_out_time = 450 ;
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
switch ( state )
{
case ArmedState . Hit :
2021-05-24 16:15:57 +08:00
if ( SliderBody ? . SnakingOut . Value = = true )
2020-12-01 14:21:32 +08:00
Body . FadeOut ( 40 ) ; // short fade to allow for any body colour to smoothly disappear.
2020-11-04 15:19:07 +08:00
break ;
2018-04-13 17:19:50 +08:00
}
2020-11-04 15:19:07 +08:00
2020-11-17 22:19:59 +08:00
this . FadeOut ( fade_out_time , Easing . OutQuint ) . Expire ( ) ;
2018-04-13 17:19:50 +08:00
}
2021-05-24 16:15:57 +08:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > SliderBody ? . ReceivePositionalInputAt ( screenSpacePos ) ? ? base . ReceivePositionalInputAt ( screenSpacePos ) ;
2019-12-17 18:29:27 +08:00
private class DefaultSliderBody : PlaySliderBody
{
}
2018-04-13 17:19:50 +08:00
}
}