2019-03-08 20:13:11 +09:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 17:43:03 +09:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 18:19:50 +09:00
2018-11-20 16:51:59 +09:00
using osuTK ;
2018-04-13 18:19:50 +09:00
using osu.Game.Rulesets.Objects.Types ;
using System.Collections.Generic ;
using osu.Game.Rulesets.Objects ;
using System.Linq ;
2020-05-15 18:07:41 +09:00
using System.Threading ;
2020-09-14 17:08:22 +09:00
using Newtonsoft.Json ;
2019-01-03 17:43:10 +09:00
using osu.Framework.Caching ;
2018-04-13 18:19:50 +09:00
using osu.Game.Audio ;
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
2018-08-02 20:36:38 +09:00
using osu.Game.Rulesets.Judgements ;
2020-03-19 18:19:10 +09:00
using osu.Game.Rulesets.Osu.Judgements ;
2019-09-06 15:24:00 +09:00
using osu.Game.Rulesets.Scoring ;
2018-04-13 18:19:50 +09:00
namespace osu.Game.Rulesets.Osu.Objects
{
2020-05-26 17:44:47 +09:00
public class Slider : OsuHitObject , IHasPathWithRepeats
2018-04-13 18:19:50 +09:00
{
2020-05-27 12:37:44 +09:00
public double EndTime = > StartTime + this . SpanCount ( ) * Path . Distance / Velocity ;
2020-09-14 17:08:22 +09:00
[JsonIgnore]
2020-05-27 12:37:44 +09:00
public double Duration
2020-02-05 17:12:26 +09:00
{
2020-05-27 12:37:44 +09:00
get = > EndTime - StartTime ;
2020-02-06 13:16:32 +09:00
set = > throw new System . NotSupportedException ( $"Adjust via {nameof(RepeatCount)} instead" ) ; // can be implemented if/when needed.
2020-02-05 17:12:26 +09:00
}
2019-08-09 19:12:29 +09:00
private readonly Cached < Vector2 > endPositionCache = new Cached < Vector2 > ( ) ;
2019-01-03 17:43:10 +09:00
public override Vector2 EndPosition = > endPositionCache . IsValid ? endPositionCache . Value : endPositionCache . Value = Position + this . CurvePositionAt ( 1 ) ;
2018-04-13 18:19:50 +09:00
public Vector2 StackedPositionAt ( double t ) = > StackedPosition + this . CurvePositionAt ( t ) ;
2019-12-09 17:48:27 +09:00
private readonly SliderPath path = new SliderPath ( ) ;
2019-12-06 20:53:40 +09:00
public SliderPath Path
{
get = > path ;
set
{
path . ControlPoints . Clear ( ) ;
path . ExpectedDistance . Value = null ;
if ( value ! = null )
{
2019-12-16 15:27:54 +09:00
path . ControlPoints . AddRange ( value . ControlPoints . Select ( c = > new PathControlPoint ( c . Position . Value , c . Type . Value ) ) ) ;
2019-12-06 20:53:40 +09:00
path . ExpectedDistance . Value = value . ExpectedDistance . Value ;
}
}
}
2018-04-13 18:19:50 +09:00
2018-11-12 14:07:48 +09:00
public double Distance = > Path . Distance ;
2018-04-13 18:19:50 +09:00
2018-10-25 18:16:25 +09:00
public override Vector2 Position
{
get = > base . Position ;
set
{
base . Position = value ;
2019-10-31 15:52:38 +09:00
updateNestedPositions ( ) ;
2018-10-25 18:16:25 +09:00
}
2018-04-13 18:19:50 +09:00
}
2018-06-13 22:20:34 +09:00
public double? LegacyLastTickOffset { get ; set ; }
2018-04-13 18:19:50 +09:00
/// <summary>
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal Vector2 ? LazyEndPosition ;
/// <summary>
/// The distance travelled by the cursor upon completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal float LazyTravelDistance ;
2019-11-08 14:04:57 +09:00
public List < IList < HitSampleInfo > > NodeSamples { get ; set ; } = new List < IList < HitSampleInfo > > ( ) ;
2018-10-16 17:10:24 +09:00
2019-01-03 17:43:10 +09:00
private int repeatCount ;
public int RepeatCount
{
get = > repeatCount ;
set
{
repeatCount = value ;
2020-02-05 17:12:26 +09:00
updateNestedPositions ( ) ;
2019-01-03 17:43:10 +09:00
}
}
2018-04-13 18:19:50 +09:00
/// <summary>
/// The length of one span of this <see cref="Slider"/>.
/// </summary>
public double SpanDuration = > Duration / this . SpanCount ( ) ;
2018-10-15 12:32:59 +09:00
/// <summary>
/// Velocity of this <see cref="Slider"/>.
/// </summary>
2018-10-15 12:31:52 +09:00
public double Velocity { get ; private set ; }
2018-10-15 12:32:59 +09:00
/// <summary>
/// Spacing between <see cref="SliderTick"/>s of this <see cref="Slider"/>.
/// </summary>
2018-10-15 12:31:52 +09:00
public double TickDistance { get ; private set ; }
2018-04-13 18:19:50 +09:00
2018-10-15 12:25:42 +09:00
/// <summary>
2018-10-15 12:32:59 +09:00
/// An extra multiplier that affects the number of <see cref="SliderTick"/>s generated by this <see cref="Slider"/>.
2018-10-15 12:25:42 +09:00
/// An increase in this value increases <see cref="TickDistance"/>, which reduces the number of ticks generated.
/// </summary>
public double TickDistanceMultiplier = 1 ;
2018-04-13 18:19:50 +09:00
2021-02-03 22:12:20 +09:00
/// <summary>
2021-02-10 18:46:26 +09:00
/// Whether this <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <c>false</c>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
2021-02-03 22:12:20 +09:00
/// </summary>
2021-02-10 18:46:26 +09:00
public bool OnlyJudgeNestedObjects = true ;
2021-02-03 22:12:20 +09:00
2020-09-14 17:08:22 +09:00
[JsonIgnore]
2021-02-05 15:56:13 +09:00
public SliderHeadCircle HeadCircle { get ; protected set ; }
2020-09-14 17:08:22 +09:00
[JsonIgnore]
public SliderTailCircle TailCircle { get ; protected set ; }
2018-04-13 18:19:50 +09:00
2019-11-08 15:39:07 +09:00
public Slider ( )
{
2020-02-17 15:06:14 +09:00
SamplesBindable . CollectionChanged + = ( _ , __ ) = > updateNestedSamples ( ) ;
2019-12-06 20:53:40 +09:00
Path . Version . ValueChanged + = _ = > updateNestedPositions ( ) ;
2019-11-08 15:39:07 +09:00
}
2018-04-13 18:19:50 +09:00
protected override void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo , BeatmapDifficulty difficulty )
{
base . ApplyDefaultsToSelf ( controlPointInfo , difficulty ) ;
TimingControlPoint timingPoint = controlPointInfo . TimingPointAt ( StartTime ) ;
DifficultyControlPoint difficultyPoint = controlPointInfo . DifficultyPointAt ( StartTime ) ;
2019-10-17 16:36:47 +09:00
double scoringDistance = BASE_SCORING_DISTANCE * difficulty . SliderMultiplier * difficultyPoint . SpeedMultiplier ;
2018-04-13 18:19:50 +09:00
Velocity = scoringDistance / timingPoint . BeatLength ;
2018-10-15 12:25:42 +09:00
TickDistance = scoringDistance / difficulty . SliderTickRate * TickDistanceMultiplier ;
2020-10-09 20:50:09 +09:00
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
2021-02-11 17:14:49 +09:00
// ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
Samples = this . GetNodeSamples ( repeatCount + 1 ) . ToArray ( ) ;
2018-04-13 18:19:50 +09:00
}
2020-05-15 18:07:41 +09:00
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
2018-04-13 18:19:50 +09:00
{
2020-05-15 18:07:41 +09:00
base . CreateNestedHitObjects ( cancellationToken ) ;
2018-04-13 18:19:50 +09:00
2019-03-08 13:48:45 +09:00
foreach ( var e in
2020-05-15 18:17:39 +09:00
SliderEventGenerator . Generate ( StartTime , SpanDuration , Velocity , TickDistance , Path . Distance , this . SpanCount ( ) , LegacyLastTickOffset , cancellationToken ) )
2018-04-13 18:19:50 +09:00
{
2019-03-08 13:48:45 +09:00
switch ( e . Type )
2018-04-13 18:19:50 +09:00
{
2019-03-08 13:48:45 +09:00
case SliderEventType . Tick :
AddNested ( new SliderTick
{
SpanIndex = e . SpanIndex ,
SpanStartTime = e . SpanStartTime ,
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2019-03-08 13:48:45 +09:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
StackHeight = StackHeight ,
Scale = Scale ,
} ) ;
2018-04-13 18:19:50 +09:00
break ;
2019-04-01 12:44:46 +09:00
2019-03-08 13:48:45 +09:00
case SliderEventType . Head :
2020-03-30 16:14:56 +09:00
AddNested ( HeadCircle = new SliderHeadCircle
2018-04-13 18:19:50 +09:00
{
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2019-03-08 13:48:45 +09:00
Position = Position ,
2019-10-21 16:15:41 +09:00
StackHeight = StackHeight ,
2019-03-08 13:48:45 +09:00
SampleControlPoint = SampleControlPoint ,
2018-04-13 18:19:50 +09:00
} ) ;
2019-03-08 13:48:45 +09:00
break ;
2019-04-01 12:44:46 +09:00
2019-03-08 20:13:11 +09:00
case SliderEventType . LegacyLastTick :
2019-03-08 20:12:48 +09:00
// we need to use the LegacyLastTick here for compatibility reasons (difficulty).
// it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
// if this is to change, we should revisit this.
2020-10-02 15:21:52 +09:00
AddNested ( TailCircle = new SliderTailCircle ( this )
2019-03-08 13:48:45 +09:00
{
2020-10-02 14:20:55 +09:00
RepeatIndex = e . SpanIndex ,
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2019-03-08 13:48:45 +09:00
Position = EndPosition ,
2019-10-21 16:15:41 +09:00
StackHeight = StackHeight
2019-03-08 13:48:45 +09:00
} ) ;
break ;
2019-04-01 12:44:46 +09:00
2019-03-08 13:48:45 +09:00
case SliderEventType . Repeat :
2020-10-02 15:21:52 +09:00
AddNested ( new SliderRepeat ( this )
2019-03-08 13:48:45 +09:00
{
RepeatIndex = e . SpanIndex ,
StartTime = StartTime + ( e . SpanIndex + 1 ) * SpanDuration ,
2019-03-08 15:14:57 +09:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
2019-03-08 13:48:45 +09:00
StackHeight = StackHeight ,
Scale = Scale ,
} ) ;
break ;
2018-04-13 18:19:50 +09:00
}
}
2019-11-08 15:39:07 +09:00
updateNestedSamples ( ) ;
2018-04-13 18:19:50 +09:00
}
2019-10-31 15:52:38 +09:00
private void updateNestedPositions ( )
{
2019-12-06 20:53:40 +09:00
endPositionCache . Invalidate ( ) ;
2019-10-31 15:52:38 +09:00
if ( HeadCircle ! = null )
HeadCircle . Position = Position ;
if ( TailCircle ! = null )
TailCircle . Position = EndPosition ;
}
2019-11-08 15:39:07 +09:00
private void updateNestedSamples ( )
{
var firstSample = Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL )
? ? Samples . FirstOrDefault ( ) ; // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List < HitSampleInfo > ( ) ;
if ( firstSample ! = null )
2020-12-01 15:37:51 +09:00
sampleList . Add ( firstSample . With ( "slidertick" ) ) ;
2019-11-08 15:39:07 +09:00
foreach ( var tick in NestedHitObjects . OfType < SliderTick > ( ) )
tick . Samples = sampleList ;
2020-03-19 14:42:02 +09:00
foreach ( var repeat in NestedHitObjects . OfType < SliderRepeat > ( ) )
2020-10-09 20:50:09 +09:00
repeat . Samples = this . GetNodeSamples ( repeat . RepeatIndex + 1 ) ;
2019-11-08 15:39:07 +09:00
if ( HeadCircle ! = null )
2020-10-09 20:50:09 +09:00
HeadCircle . Samples = this . GetNodeSamples ( 0 ) ;
2019-11-08 15:39:07 +09:00
}
2021-02-10 18:46:26 +09:00
public override Judgement CreateJudgement ( ) = > OnlyJudgeNestedObjects ? new OsuIgnoreJudgement ( ) : new OsuJudgement ( ) ;
2019-09-02 16:10:30 +09:00
2019-10-09 19:08:31 +09:00
protected override HitWindows CreateHitWindows ( ) = > HitWindows . Empty ;
2018-04-13 18:19:50 +09:00
}
}