2019-01-24 17:43:03 +09: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 18:19:50 +09:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2019-10-21 17:56:39 +09:00
using JetBrains.Annotations ;
2018-04-13 18:19:50 +09:00
using osu.Framework.Allocation ;
2019-02-21 19:04:31 +09:00
using osu.Framework.Bindables ;
2018-08-06 11:07:05 +09:00
using osu.Framework.Extensions.TypeExtensions ;
2019-08-28 18:12:47 +09:00
using osu.Framework.Graphics ;
2018-09-12 15:09:10 +09:00
using osu.Framework.Graphics.Primitives ;
2019-09-02 15:02:16 +09:00
using osu.Framework.Threading ;
2020-04-09 18:12:15 +02:00
using osu.Framework.Audio ;
2018-04-13 18:19:50 +09:00
using osu.Game.Audio ;
using osu.Game.Rulesets.Judgements ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Scoring ;
using osu.Game.Skinning ;
2020-04-09 18:12:15 +02:00
using osu.Game.Configuration ;
2018-11-20 16:51:59 +09:00
using osuTK.Graphics ;
2018-04-13 18:19:50 +09:00
namespace osu.Game.Rulesets.Objects.Drawables
{
2019-07-19 19:12:41 +09:00
[Cached(typeof(DrawableHitObject))]
2019-07-22 14:45:25 +09:00
public abstract class DrawableHitObject : SkinReloadableDrawable
2018-04-13 18:19:50 +09:00
{
public readonly HitObject HitObject ;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
2019-07-22 14:45:25 +09:00
public readonly Bindable < Color4 > AccentColour = new Bindable < Color4 > ( Color4 . Gray ) ;
2018-04-13 18:19:50 +09:00
2019-11-08 16:19:55 +09:00
protected SkinnableSound Samples { get ; private set ; }
2018-04-13 18:19:50 +09:00
2019-06-30 21:58:30 +09:00
protected virtual IEnumerable < HitSampleInfo > GetSamples ( ) = > HitObject . Samples ;
2018-04-13 18:19:50 +09:00
private readonly Lazy < List < DrawableHitObject > > nestedHitObjects = new Lazy < List < DrawableHitObject > > ( ) ;
2019-10-16 22:10:50 +09:00
public IReadOnlyList < DrawableHitObject > NestedHitObjects = > nestedHitObjects . IsValueCreated ? nestedHitObjects . Value : ( IReadOnlyList < DrawableHitObject > ) Array . Empty < DrawableHitObject > ( ) ;
2018-04-13 18:19:50 +09:00
2020-03-29 14:30:45 +09:00
/// <summary>
/// Whether this object should handle any user input events.
/// </summary>
public bool HandleUserInput { get ; set ; } = true ;
public override bool PropagatePositionalInputSubTree = > HandleUserInput ;
public override bool PropagateNonPositionalInputSubTree = > HandleUserInput ;
2018-08-06 10:54:16 +09:00
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary>
public event Action < DrawableHitObject , JudgementResult > OnNewResult ;
/// <summary>
2018-08-06 12:29:22 +09:00
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
2018-08-06 10:54:16 +09:00
/// </summary>
2018-08-06 12:29:22 +09:00
public event Action < DrawableHitObject , JudgementResult > OnRevertResult ;
2018-04-13 18:19:50 +09:00
/// <summary>
2018-08-06 11:31:54 +09:00
/// Whether a visual indicator should be displayed when a scoring result occurs.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-06 11:31:46 +09:00
public virtual bool DisplayResult = > true ;
2018-04-13 18:19:50 +09:00
/// <summary>
2018-08-03 16:07:20 +09:00
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-03 16:07:20 +09:00
public bool AllJudged = > Judged & & NestedHitObjects . All ( h = > h . AllJudged ) ;
2018-04-13 18:19:50 +09:00
/// <summary>
2019-04-25 17:36:17 +09:00
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
2018-08-03 16:07:20 +09:00
/// Note: This does NOT include nested hitobjects.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-03 16:07:20 +09:00
public bool IsHit = > Result ? . IsHit ? ? false ;
2018-04-13 18:19:50 +09:00
/// <summary>
2018-08-01 21:04:03 +09:00
/// Whether this <see cref="DrawableHitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-03 15:38:48 +09:00
public bool Judged = > Result ? . HasResult ? ? true ;
2018-08-02 20:35:54 +09:00
2018-08-06 10:55:38 +09:00
/// <summary>
/// The scoring result of this <see cref="DrawableHitObject"/>.
/// </summary>
2018-08-06 11:07:05 +09:00
public JudgementResult Result { get ; private set ; }
2018-08-02 21:07:31 +09:00
2020-04-09 18:12:15 +02:00
/// <summary>
2020-04-13 13:00:03 +09:00
/// The relative X position of this hit object for sample playback balance adjustment.
2020-04-09 18:12:15 +02:00
/// </summary>
2020-04-13 13:00:03 +09:00
/// <remarks>
/// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right).
/// Dampening is post-applied to ensure the effect is not too intense.
/// </remarks>
protected virtual float SamplePlaybackPosition = > 0.5f ;
2020-04-12 01:33:25 +02:00
2020-04-13 13:00:03 +09:00
private readonly BindableDouble balanceAdjust = new BindableDouble ( ) ;
2020-04-09 18:12:15 +02:00
2019-11-08 14:59:47 +09:00
private BindableList < HitSampleInfo > samplesBindable ;
2019-10-18 13:18:41 +09:00
private Bindable < double > startTimeBindable ;
2020-04-09 18:12:15 +02:00
private Bindable < bool > userPositionalHitSounds ;
2019-09-26 17:04:38 +09:00
private Bindable < int > comboIndexBindable ;
2018-04-13 18:19:50 +09:00
public override bool RemoveWhenNotAlive = > false ;
public override bool RemoveCompletedTransforms = > false ;
protected override bool RequiresChildrenUpdate = > true ;
2019-07-23 21:08:41 +09:00
public override bool IsPresent = > base . IsPresent | | ( State . Value = = ArmedState . Idle & & Clock ? . CurrentTime > = LifetimeStart ) ;
2018-04-13 18:19:50 +09:00
2019-07-22 15:05:56 +09:00
private readonly Bindable < ArmedState > state = new Bindable < ArmedState > ( ) ;
2019-07-23 21:08:41 +09:00
public IBindable < ArmedState > State = > state ;
2019-10-21 17:56:39 +09:00
protected DrawableHitObject ( [ NotNull ] HitObject hitObject )
2018-04-13 18:19:50 +09:00
{
2019-10-21 17:56:39 +09:00
HitObject = hitObject ? ? throw new ArgumentNullException ( nameof ( hitObject ) ) ;
2018-04-13 18:19:50 +09:00
}
[BackgroundDependencyLoader]
2020-04-09 18:12:15 +02:00
private void load ( OsuConfigManager config )
2018-04-13 18:19:50 +09:00
{
2020-04-12 01:33:25 +02:00
userPositionalHitSounds = config . GetBindable < bool > ( OsuSetting . PositionalHitSounds ) ;
2018-08-06 11:50:18 +09:00
var judgement = HitObject . CreateJudgement ( ) ;
2019-04-01 12:16:05 +09:00
2020-02-23 14:30:08 +09:00
Result = CreateResult ( judgement ) ;
if ( Result = = null )
throw new InvalidOperationException ( $"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}." ) ;
2018-08-06 11:07:05 +09:00
2019-11-08 14:59:47 +09:00
loadSamples ( ) ;
2018-04-13 18:19:50 +09:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2019-09-26 17:04:38 +09:00
2019-10-18 13:18:41 +09:00
HitObject . DefaultsApplied + = onDefaultsApplied ;
startTimeBindable = HitObject . StartTimeBindable . GetBoundCopy ( ) ;
2020-02-12 19:02:25 +09:00
startTimeBindable . BindValueChanged ( _ = > updateState ( State . Value , true ) ) ;
2019-10-16 22:10:50 +09:00
2019-09-26 17:04:38 +09:00
if ( HitObject is IHasComboInformation combo )
{
comboIndexBindable = combo . ComboIndexBindable . GetBoundCopy ( ) ;
2020-02-19 15:05:04 +09:00
comboIndexBindable . BindValueChanged ( _ = > updateComboColour ( ) , true ) ;
2019-09-26 17:04:38 +09:00
}
2019-11-08 14:59:47 +09:00
samplesBindable = HitObject . SamplesBindable . GetBoundCopy ( ) ;
2020-02-17 15:06:14 +09:00
samplesBindable . CollectionChanged + = ( _ , __ ) = > loadSamples ( ) ;
2019-11-08 14:59:47 +09:00
2019-07-23 21:08:41 +09:00
updateState ( ArmedState . Idle , true ) ;
2019-10-18 13:18:41 +09:00
onDefaultsApplied ( ) ;
2019-07-22 15:05:56 +09:00
}
2019-11-08 14:59:47 +09:00
private void loadSamples ( )
{
2019-11-08 16:19:55 +09:00
if ( Samples ! = null )
{
RemoveInternal ( Samples ) ;
Samples = null ;
}
2019-11-08 14:59:47 +09:00
var samples = GetSamples ( ) . ToArray ( ) ;
if ( samples . Length < = 0 )
return ;
if ( HitObject . SampleControlPoint = = null )
2019-11-12 13:41:54 +09:00
{
2019-11-28 22:21:21 +08:00
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}." ) ;
2019-11-12 13:41:54 +09:00
}
2019-11-08 14:59:47 +09:00
2020-04-12 01:33:25 +02:00
Samples = new SkinnableSound ( samples . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) ) ;
2020-04-13 13:00:03 +09:00
Samples . AddAdjustment ( AdjustableProperty . Balance , balanceAdjust ) ;
2020-04-12 01:33:25 +02:00
AddInternal ( Samples ) ;
2019-11-08 14:59:47 +09:00
}
2019-10-18 13:18:41 +09:00
private void onDefaultsApplied ( ) = > apply ( HitObject ) ;
private void apply ( HitObject hitObject )
2019-10-16 22:10:50 +09:00
{
if ( nestedHitObjects . IsValueCreated )
{
nestedHitObjects . Value . Clear ( ) ;
2019-10-17 13:52:21 +09:00
ClearNestedHitObjects ( ) ;
2019-10-16 22:10:50 +09:00
}
foreach ( var h in hitObject . NestedHitObjects )
{
2019-10-17 13:52:21 +09:00
var drawableNested = CreateNestedHitObject ( h ) ? ? throw new InvalidOperationException ( $"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}." ) ;
2020-04-22 18:32:59 +09:00
drawableNested . OnNewResult + = ( d , r ) = > OnNewResult ? . Invoke ( d , r ) ;
drawableNested . OnRevertResult + = ( d , r ) = > OnRevertResult ? . Invoke ( d , r ) ;
drawableNested . ApplyCustomUpdateState + = ( d , j ) = > ApplyCustomUpdateState ? . Invoke ( d , j ) ;
nestedHitObjects . Value . Add ( drawableNested ) ;
2019-10-17 13:52:21 +09:00
AddNestedHitObject ( drawableNested ) ;
2019-10-16 22:10:50 +09:00
}
}
2019-10-17 13:52:21 +09:00
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to be added.</param>
protected virtual void AddNestedHitObject ( DrawableHitObject hitObject )
2019-10-16 22:10:50 +09:00
{
}
2019-10-17 13:52:21 +09:00
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual void ClearNestedHitObjects ( )
{
}
/// <summary>
/// Creates the drawable representation for a nested <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/>.</param>
/// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
protected virtual DrawableHitObject CreateNestedHitObject ( HitObject hitObject ) = > null ;
2019-10-16 22:10:50 +09:00
2019-07-22 15:33:12 +09:00
#region State / Transform Management
2019-07-22 16:08:38 +09:00
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// </summary>
public event Action < DrawableHitObject , ArmedState > ApplyCustomUpdateState ;
2019-07-22 15:33:12 +09:00
protected override void ClearInternal ( bool disposeChildren = true ) = > throw new InvalidOperationException ( $"Should never clear a {nameof(DrawableHitObject)}" ) ;
2019-07-23 21:08:41 +09:00
private void updateState ( ArmedState newState , bool force = false )
2019-07-22 15:05:56 +09:00
{
2019-07-23 21:08:41 +09:00
if ( State . Value = = newState & & ! force )
return ;
2020-02-27 11:28:29 +09:00
LifetimeEnd = double . MaxValue ;
2019-09-07 14:44:44 +09:00
2020-02-27 11:28:29 +09:00
double transformTime = HitObject . StartTime - InitialLifetimeOffset ;
2019-07-22 15:05:56 +09:00
2020-02-27 11:28:29 +09:00
base . ApplyTransformsAt ( double . MinValue , true ) ;
base . ClearTransformsAfter ( double . MinValue , true ) ;
2019-07-22 15:05:56 +09:00
2020-02-27 11:28:29 +09:00
using ( BeginAbsoluteSequence ( transformTime , true ) )
{
UpdateInitialTransforms ( ) ;
2018-04-13 18:19:50 +09:00
2020-02-27 11:28:29 +09:00
var judgementOffset = Result ? . TimeOffset ? ? 0 ;
2019-07-22 15:05:56 +09:00
2020-02-27 11:28:29 +09:00
using ( BeginDelayedSequence ( InitialLifetimeOffset + judgementOffset , true ) )
{
UpdateStateTransforms ( newState ) ;
state . Value = newState ;
2019-07-22 15:05:56 +09:00
}
}
2020-02-27 11:28:29 +09:00
if ( state . Value ! = ArmedState . Idle & & LifetimeEnd = = double . MaxValue | | HitObject . HitWindows = = null )
Expire ( ) ;
2019-07-27 00:22:40 +03:00
// apply any custom state overrides
ApplyCustomUpdateState ? . Invoke ( this , newState ) ;
if ( newState = = ArmedState . Hit )
PlaySamples ( ) ;
2018-04-13 18:19:50 +09:00
}
2019-07-22 16:08:38 +09:00
/// <summary>
2019-07-23 21:15:55 +09:00
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
2019-07-22 16:08:38 +09:00
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
2019-08-28 18:12:47 +09:00
///
/// By default this will fade in the object from zero with no duration.
2019-07-22 16:08:38 +09:00
/// </summary>
/// <remarks>
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
/// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
/// </remarks>
2019-07-22 15:33:12 +09:00
protected virtual void UpdateInitialTransforms ( )
2019-07-22 15:05:56 +09:00
{
2019-08-28 18:12:47 +09:00
this . FadeInFromZero ( ) ;
2019-07-22 15:05:56 +09:00
}
2019-07-22 16:08:38 +09:00
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
2019-09-12 19:29:08 +09:00
/// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
2019-07-22 16:08:38 +09:00
/// </summary>
/// <param name="state">The new armed state.</param>
2019-07-22 15:33:12 +09:00
protected virtual void UpdateStateTransforms ( ArmedState state )
2019-07-22 15:05:56 +09:00
{
}
public override void ClearTransformsAfter ( double time , bool propagateChildren = false , string targetMember = null )
{
2020-02-27 11:28:29 +09:00
// Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
2019-07-22 15:05:56 +09:00
}
public override void ApplyTransformsAt ( double time , bool propagateChildren = false )
{
2020-02-27 11:28:29 +09:00
// Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
2019-07-22 15:05:56 +09:00
}
2019-07-22 15:33:12 +09:00
#endregion
2019-09-18 20:19:57 +09:00
protected sealed override void SkinChanged ( ISkinSource skin , bool allowFallback )
2019-07-22 15:33:12 +09:00
{
base . SkinChanged ( skin , allowFallback ) ;
2020-02-19 15:05:04 +09:00
updateComboColour ( ) ;
2019-09-18 20:19:57 +09:00
ApplySkin ( skin , allowFallback ) ;
2019-09-19 14:15:06 +09:00
if ( IsLoaded )
updateState ( State . Value , true ) ;
2019-09-18 20:19:57 +09:00
}
2020-02-19 15:05:04 +09:00
private void updateComboColour ( )
2019-09-26 17:04:38 +09:00
{
2020-02-20 15:14:40 +09:00
if ( ! ( HitObject is IHasComboInformation ) ) return ;
var comboColours = CurrentSkin . GetConfig < GlobalSkinColours , IReadOnlyList < Color4 > > ( GlobalSkinColours . ComboColours ) ? . Value ;
AccentColour . Value = GetComboColour ( comboColours ) ;
2019-09-26 17:04:38 +09:00
}
2020-02-19 15:05:04 +09:00
/// <summary>
2020-02-20 15:14:40 +09:00
/// Called to retrieve the combo colour. Automatically assigned to <see cref="AccentColour"/>.
/// Defaults to using <see cref="IHasComboInformation.ComboIndex"/> to decide on a colour.
2020-02-19 15:05:04 +09:00
/// </summary>
2020-02-20 15:14:40 +09:00
/// <remarks>
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks>
2020-02-19 15:05:04 +09:00
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
2020-02-20 15:14:40 +09:00
protected virtual Color4 GetComboColour ( IReadOnlyList < Color4 > comboColours )
2020-02-19 15:05:04 +09:00
{
2020-02-20 15:14:40 +09:00
if ( ! ( HitObject is IHasComboInformation combo ) )
throw new InvalidOperationException ( $"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}" ) ;
return comboColours ? . Count > 0 ? comboColours [ combo . ComboIndex % comboColours . Count ] : Color4 . White ;
2020-02-19 15:05:04 +09:00
}
2019-09-18 20:19:57 +09:00
/// <summary>
/// Called when a change is made to the skin.
/// </summary>
/// <param name="skin">The new skin.</param>
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param>
protected virtual void ApplySkin ( ISkinSource skin , bool allowFallback )
{
2019-07-22 15:33:12 +09:00
}
2018-04-13 18:19:50 +09:00
/// <summary>
2018-09-15 07:30:11 -07:00
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
2018-12-03 17:21:27 +09:00
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
2018-04-13 18:19:50 +09:00
/// </summary>
2020-04-10 00:01:35 +02:00
public virtual void PlaySamples ( )
{
2020-04-13 13:00:03 +09:00
const float balance_adjust_amount = 0.4f ;
balanceAdjust . Value = balance_adjust_amount * ( userPositionalHitSounds . Value ? SamplePlaybackPosition - 0.5f : 0 ) ;
2020-04-10 00:01:35 +02:00
Samples ? . Play ( ) ;
}
2018-04-13 18:19:50 +09:00
protected override void Update ( )
{
base . Update ( ) ;
2018-08-14 18:31:32 +09:00
if ( Result ! = null & & Result . HasResult )
2018-04-13 18:19:50 +09:00
{
2019-11-25 19:01:24 +09:00
var endTime = HitObject . GetEndTime ( ) ;
2018-04-13 18:19:50 +09:00
2018-08-14 18:31:32 +09:00
if ( Result . TimeOffset + endTime > Time . Current )
2018-08-01 21:04:03 +09:00
{
2018-08-06 12:29:22 +09:00
OnRevertResult ? . Invoke ( this , Result ) ;
2018-08-01 21:04:03 +09:00
2019-06-13 14:55:52 +09:00
Result . TimeOffset = 0 ;
2018-08-03 15:38:48 +09:00
Result . Type = HitResult . None ;
2019-07-23 21:08:41 +09:00
updateState ( ArmedState . Idle ) ;
2018-08-01 21:04:03 +09:00
}
2018-04-13 18:19:50 +09:00
}
}
2020-04-20 20:48:35 +09:00
public override bool UpdateSubTreeMasking ( Drawable source , RectangleF maskingBounds ) = > false ;
2018-09-12 15:09:10 +09:00
2018-04-13 18:19:50 +09:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2018-08-06 11:31:46 +09:00
UpdateResult ( false ) ;
2018-04-13 18:19:50 +09:00
}
2019-09-02 15:02:16 +09:00
/// <summary>
/// Schedules an <see cref="Action"/> to this <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// Only provided temporarily until hitobject pooling is implemented.
/// </remarks>
protected internal new ScheduledDelegate Schedule ( Action action ) = > base . Schedule ( action ) ;
2019-07-16 13:45:59 +09:00
private double? lifetimeStart ;
public override double LifetimeStart
{
get = > lifetimeStart ? ? ( HitObject . StartTime - InitialLifetimeOffset ) ;
set
{
lifetimeStart = value ;
2019-09-24 16:47:34 +09:00
base . LifetimeStart = value ;
2019-07-16 13:45:59 +09:00
}
}
/// <summary>
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
2019-07-22 16:08:38 +09:00
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
2019-09-12 19:30:27 +09:00
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
2019-07-16 13:45:59 +09:00
/// </remarks>
protected virtual double InitialLifetimeOffset = > 10000 ;
/// <summary>
2019-07-16 18:19:13 +09:00
/// Will be called at least once after this <see cref="DrawableHitObject"/> has become not alive.
2019-07-16 13:45:59 +09:00
/// </summary>
2019-07-16 18:19:13 +09:00
public virtual void OnKilled ( )
2019-07-16 13:45:59 +09:00
{
foreach ( var nested in NestedHitObjects )
2019-07-16 18:19:13 +09:00
nested . OnKilled ( ) ;
2019-07-16 13:45:59 +09:00
UpdateResult ( false ) ;
}
2018-04-13 18:19:50 +09:00
/// <summary>
2018-08-06 10:55:38 +09:00
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-06 10:55:38 +09:00
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
2018-08-03 15:38:48 +09:00
protected void ApplyResult ( Action < JudgementResult > application )
2018-04-13 18:19:50 +09:00
{
2018-08-03 15:38:48 +09:00
application ? . Invoke ( Result ) ;
2018-08-02 20:35:54 +09:00
2018-08-06 11:07:05 +09:00
if ( ! Result . HasResult )
throw new InvalidOperationException ( $"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}." ) ;
2018-04-13 18:19:50 +09:00
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
2019-11-25 19:01:24 +09:00
var endTime = HitObject . GetEndTime ( ) ;
2019-10-09 19:08:55 +09:00
Result . TimeOffset = Math . Min ( HitObject . HitWindows . WindowFor ( HitResult . Miss ) , Time . Current - endTime ) ;
2018-04-13 18:19:50 +09:00
2018-08-03 15:38:48 +09:00
switch ( Result . Type )
2018-04-13 18:19:50 +09:00
{
2018-08-03 15:38:48 +09:00
case HitResult . None :
break ;
2019-04-01 12:16:05 +09:00
2018-08-03 15:38:48 +09:00
case HitResult . Miss :
2019-07-23 21:08:41 +09:00
updateState ( ArmedState . Miss ) ;
2018-08-03 15:38:48 +09:00
break ;
2019-04-01 12:16:05 +09:00
2018-08-03 15:38:48 +09:00
default :
2019-07-23 21:08:41 +09:00
updateState ( ArmedState . Hit ) ;
2018-08-03 15:38:48 +09:00
break ;
2018-04-13 18:19:50 +09:00
}
2018-08-06 10:54:16 +09:00
OnNewResult ? . Invoke ( this , Result ) ;
2018-04-13 18:19:50 +09:00
}
/// <summary>
2018-08-06 11:31:54 +09:00
/// Processes this <see cref="DrawableHitObject"/>, checking if a scoring result has occurred.
2018-04-13 18:19:50 +09:00
/// </summary>
/// <param name="userTriggered">Whether the user triggered this process.</param>
2018-08-06 11:31:54 +09:00
/// <returns>Whether a scoring result has occurred from this <see cref="DrawableHitObject"/> or any nested <see cref="DrawableHitObject"/>.</returns>
2018-08-06 11:31:46 +09:00
protected bool UpdateResult ( bool userTriggered )
2018-04-13 18:19:50 +09:00
{
2019-06-13 12:21:49 +09:00
// It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed
if ( Time . Elapsed < 0 )
return false ;
2019-09-04 18:14:55 +09:00
if ( Judged )
2018-04-13 18:19:50 +09:00
return false ;
2019-11-25 19:01:24 +09:00
var endTime = HitObject . GetEndTime ( ) ;
2018-08-06 11:31:46 +09:00
CheckForResult ( userTriggered , Time . Current - endTime ) ;
2018-04-13 18:19:50 +09:00
2019-09-04 18:14:55 +09:00
return Judged ;
2018-04-13 18:19:50 +09:00
}
/// <summary>
2018-08-06 11:31:54 +09:00
/// Checks if a scoring result has occurred for this <see cref="DrawableHitObject"/>.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-08-06 11:31:54 +09:00
/// <remarks>
/// If a scoring result has occurred, this method must invoke <see cref="ApplyResult"/> to update the result and notify responders.
/// </remarks>
2018-04-13 18:19:50 +09:00
/// <param name="userTriggered">Whether the user triggered this check.</param>
2018-08-06 11:31:54 +09:00
/// <param name="timeOffset">The offset from the end time of the <see cref="HitObject"/> at which this check occurred.
/// A <paramref name="timeOffset"/> > 0 implies that this check occurred after the end time of the <see cref="HitObject"/>. </param>
2018-08-06 11:31:46 +09:00
protected virtual void CheckForResult ( bool userTriggered , double timeOffset )
2018-04-13 18:19:50 +09:00
{
}
2018-08-02 20:35:54 +09:00
2018-08-06 10:55:38 +09:00
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
2019-09-02 17:14:40 +09:00
protected virtual JudgementResult CreateResult ( Judgement judgement ) = > new JudgementResult ( HitObject , judgement ) ;
2019-10-18 13:18:41 +09:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
HitObject . DefaultsApplied - = onDefaultsApplied ;
}
2018-04-13 18:19:50 +09:00
}
public abstract class DrawableHitObject < TObject > : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject ;
protected DrawableHitObject ( TObject hitObject )
: base ( hitObject )
{
HitObject = hitObject ;
}
}
}