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
2019-11-20 12:19:49 +00:00
using System ;
2021-04-08 11:19:41 +00:00
using System.Collections.Generic ;
2020-07-22 07:37:38 +00:00
using System.Linq ;
2020-11-06 14:09:23 +00:00
using JetBrains.Annotations ;
2018-11-20 07:51:59 +00:00
using osuTK ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics ;
using osu.Game.Rulesets.Objects.Drawables ;
using osu.Framework.Allocation ;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics.Containers ;
2020-11-19 11:40:30 +00:00
using osu.Game.Audio ;
2018-11-14 05:29:22 +00:00
using osu.Game.Rulesets.Objects ;
2019-09-03 08:57:34 +00:00
using osu.Game.Rulesets.Osu.Skinning ;
2020-12-04 11:21:53 +00:00
using osu.Game.Rulesets.Osu.Skinning.Default ;
2020-07-22 07:37:38 +00:00
using osu.Game.Rulesets.Osu.UI ;
2021-02-03 13:12:20 +00:00
using osu.Game.Rulesets.Scoring ;
2018-11-20 07:51:59 +00:00
using osuTK.Graphics ;
2018-12-07 21:24:24 +00:00
using osu.Game.Skinning ;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
2020-11-22 09:36:10 +00:00
public class DrawableSlider : DrawableOsuHitObject
2018-04-13 09:19:50 +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
2020-11-05 04:51:46 +00:00
public SliderBall Ball { get ; private set ; }
public SkinnableDrawable Body { get ; private set ; }
2018-04-13 09:19:50 +00:00
2021-02-10 09:46:26 +00:00
public override bool DisplayResult = > ! HitObject . OnlyJudgeNestedObjects ;
2020-03-19 05:44:48 +00:00
2019-12-17 12:26:23 +00:00
private 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 )
2018-04-13 09:19:50 +00:00
: base ( s )
{
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 ( )
{
2018-04-13 09:19:50 +00:00
InternalChildren = new Drawable [ ]
{
2019-12-17 10:29:27 +00:00
Body = new SkinnableDrawable ( new OsuSkinComponent ( OsuSkinComponents . SliderBody ) , _ = > new DefaultSliderBody ( ) , confineMode : ConfineMode . NoScaling ) ,
2020-10-02 04:38:48 +00:00
tailContainer = new Container < DrawableSliderTail > { RelativeSizeAxes = Axes . Both } ,
2019-10-16 13:10:50 +00:00
tickContainer = new Container < DrawableSliderTick > { RelativeSizeAxes = Axes . Both } ,
2020-03-19 05:26:24 +00:00
repeatContainer = new Container < DrawableSliderRepeat > { RelativeSizeAxes = Axes . Both } ,
2020-11-05 05:40:48 +00:00
Ball = new SliderBall ( this )
2018-04-13 09:19:50 +00:00
{
2019-01-21 01:57:14 +00:00
GetInitialHitAction = ( ) = > HeadCircle . HitAction ,
2018-04-13 09:19:50 +00:00
BypassAutoSizeAxes = Axes . Both ,
AlwaysPresent = true ,
Alpha = 0
} ,
2019-10-16 13:10:50 +00:00
headContainer = new Container < DrawableSliderHead > { RelativeSizeAxes = Axes . Both } ,
2020-11-19 11:40:30 +00:00
slidingSample = new PausableSkinnableSound { Looping = true }
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-04-13 09:19:50 +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 ;
2020-11-16 13:40:25 +00:00
updateBallTint ( ) ;
2019-07-22 05:45:25 +00:00
} , 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
}
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
2020-11-19 11:40:30 +00:00
slidingSample . Samples = null ;
}
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 ( ) ;
2020-07-22 07:37:38 +00:00
2021-04-08 14:13:16 +00:00
var slidingSamples = new List < ISampleInfo > ( ) ;
2020-07-22 07:37:38 +00:00
2021-04-09 06:28:08 +00:00
var normalSample = HitObject . Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ;
2021-04-08 14:13:16 +00:00
if ( normalSample ! = null )
slidingSamples . Add ( HitObject . SampleControlPoint . ApplyTo ( normalSample ) . With ( "sliderslide" ) ) ;
2020-07-22 07:37:38 +00:00
2021-04-09 06:28:08 +00:00
var whistleSample = HitObject . Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_WHISTLE ) ;
2021-04-08 14:13:16 +00:00
if ( whistleSample ! = null )
slidingSamples . Add ( HitObject . SampleControlPoint . ApplyTo ( whistleSample ) . With ( "sliderwhistle" ) ) ;
2021-04-08 11:19:41 +00:00
2021-04-08 14:13:16 +00:00
slidingSample . Samples = slidingSamples . 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-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 ) ;
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
protected override void Update ( )
{
base . Update ( ) ;
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
slidingSample . Balance . Value = CalculateSamplePlaybackBalance ( Ball . X / OsuPlayfield . BASE_SIZE . X ) ;
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 ) ;
2019-12-17 12:26:23 +00:00
sliderBody ? . UpdateProgress ( completionProgress ) ;
2019-10-16 13:10:50 +00:00
foreach ( DrawableHitObject hitObject in NestedHitObjects )
{
2020-11-05 04:51:46 +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
2019-12-17 12:26:23 +00:00
Size = sliderBody ? . Size ? ? Vector2 . Zero ;
OriginPosition = sliderBody ? . PathOffset ? ? Vector2 . Zero ;
2018-04-13 09:19:50 +00: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 09:19:13 +00:00
public override void OnKilled ( )
{
base . OnKilled ( ) ;
2019-12-17 12:26:23 +00:00
sliderBody ? . RecyclePath ( ) ;
2019-07-16 09:19:13 +00:00
}
2019-09-18 11:19:57 +00:00
protected override void ApplySkin ( ISkinSource skin , bool allowFallback )
2018-12-07 21:24:24 +00:00
{
2019-09-18 11:19:57 +00:00
base . ApplySkin ( skin , allowFallback ) ;
2019-01-07 11:12:39 +00:00
2020-11-16 13:40:25 +00:00
updateBallTint ( ) ;
}
private void updateBallTint ( )
{
if ( CurrentSkin = = null )
return ;
bool allowBallTint = CurrentSkin . GetConfig < OsuSkinConfiguration , bool > ( OsuSkinConfiguration . AllowSliderBallTint ) ? . Value ? ? false ;
2020-04-04 18:17:11 +00:00
Ball . AccentColour = allowBallTint ? AccentColour . Value : Color4 . White ;
2018-12-07 21:24:24 +00:00
}
2018-08-06 02:31:46 +00:00
protected override void CheckForResult ( bool userTriggered , double timeOffset )
2018-04-13 09:19:50 +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 ( ) ;
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 ( )
2018-04-13 09:19:50 +00:00
{
2020-11-04 07:19:07 +00:00
base . UpdateStartTimeStateTransforms ( ) ;
2019-09-13 09:49:21 +00:00
2018-04-13 09:19:50 +00:00
Ball . FadeIn ( ) ;
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
2020-11-04 07:19:07 +00:00
const float fade_out_time = 450 ;
2018-04-13 09:19:50 +00:00
2020-11-04 07:19:07 +00:00
// intentionally pile on an extra FadeOut to make it happen much faster.
Ball . FadeOut ( fade_out_time / 4 , Easing . Out ) ;
2018-04-13 09:19:50 +00:00
2020-11-04 07:19:07 +00:00
switch ( state )
{
case ArmedState . Hit :
Ball . ScaleTo ( HitObject . Scale * 1.4f , fade_out_time , Easing . Out ) ;
2020-12-01 05:56:41 +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 ;
2018-04-13 09:19:50 +00:00
}
2020-11-04 07:19:07 +00:00
2020-11-17 14:19:59 +00:00
this . FadeOut ( fade_out_time , Easing . OutQuint ) . Expire ( ) ;
2018-04-13 09:19:50 +00:00
}
2019-12-17 12:26:23 +00:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > sliderBody ? . ReceivePositionalInputAt ( screenSpacePos ) ? ? base . ReceivePositionalInputAt ( screenSpacePos ) ;
2019-12-17 10:29:27 +00:00
private class DefaultSliderBody : PlaySliderBody
{
}
2018-04-13 09:19:50 +00:00
}
}