2018-04-13 17:19:50 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System ;
using System.Collections.Generic ;
using System.Linq ;
using osu.Framework.Allocation ;
using osu.Framework.Configuration ;
using osu.Game.Audio ;
using osu.Game.Graphics ;
using osu.Game.Rulesets.Judgements ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Scoring ;
using osu.Game.Skinning ;
using OpenTK.Graphics ;
namespace osu.Game.Rulesets.Objects.Drawables
{
public abstract class DrawableHitObject : SkinReloadableDrawable , IHasAccentColour
{
public readonly HitObject HitObject ;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
public virtual Color4 AccentColour { get ; set ; } = Color4 . Gray ;
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
protected virtual string SampleNamespace = > null ;
protected SkinnableSound Samples ;
protected virtual IEnumerable < SampleInfo > GetSamples ( ) = > HitObject . Samples ;
private readonly Lazy < List < DrawableHitObject > > nestedHitObjects = new Lazy < List < DrawableHitObject > > ( ) ;
2018-07-02 15:10:56 +08:00
public IEnumerable < DrawableHitObject > NestedHitObjects = > nestedHitObjects . IsValueCreated ? nestedHitObjects . Value : Enumerable . Empty < DrawableHitObject > ( ) ;
2018-04-13 17:19:50 +08:00
2018-08-02 19:35:54 +08:00
public event Action < DrawableHitObject , JudgementResult > OnJudgement ;
public event Action < DrawableHitObject , JudgementResult > OnJudgementRemoved ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether a visible judgement should be displayed when this representation is hit.
/// </summary>
public virtual bool DisplayJudgement = > true ;
/// <summary>
2018-08-03 15:07:20 +08:00
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 15:07:20 +08:00
public bool AllJudged = > Judged & & NestedHitObjects . All ( h = > h . AllJudged ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-03 15:07:20 +08:00
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result.IsHit"/> is <see cref="true"/>.
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 15:07:20 +08:00
public bool IsHit = > Result ? . IsHit ? ? false ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-01 20:04:03 +08:00
/// Whether this <see cref="DrawableHitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 14:38:48 +08:00
public bool Judged = > Result ? . HasResult ? ? true ;
2018-08-02 19:35:54 +08:00
2018-08-03 14:38:48 +08:00
public readonly JudgementResult Result ;
2018-08-02 20:07:31 +08:00
2018-04-13 17:19:50 +08:00
private bool judgementOccurred ;
public bool Interactive = true ;
public override bool HandleKeyboardInput = > Interactive ;
public override bool HandleMouseInput = > Interactive ;
public override bool RemoveWhenNotAlive = > false ;
public override bool RemoveCompletedTransforms = > false ;
protected override bool RequiresChildrenUpdate = > true ;
public readonly Bindable < ArmedState > State = new Bindable < ArmedState > ( ) ;
protected DrawableHitObject ( HitObject hitObject )
{
HitObject = hitObject ;
2018-08-02 19:35:54 +08:00
2018-08-03 14:38:48 +08:00
if ( hitObject . Judgement ! = null )
Result = CreateJudgementResult ( hitObject . Judgement ) ;
2018-04-13 17:19:50 +08:00
}
[BackgroundDependencyLoader]
private void load ( )
{
var samples = GetSamples ( ) . ToArray ( ) ;
if ( samples . Any ( ) )
{
if ( HitObject . SampleControlPoint = = null )
throw new ArgumentNullException ( nameof ( HitObject . SampleControlPoint ) , $"{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}." ) ;
2018-06-28 17:20:29 +08:00
samples = samples . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . ToArray ( ) ;
foreach ( var s in samples )
s . Namespace = SampleNamespace ;
AddInternal ( Samples = new SkinnableSound ( samples ) ) ;
2018-04-13 17:19:50 +08:00
}
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
State . ValueChanged + = state = >
{
UpdateState ( state ) ;
// apply any custom state overrides
ApplyCustomUpdateState ? . Invoke ( this , state ) ;
if ( State = = ArmedState . Hit )
PlaySamples ( ) ;
} ;
State . TriggerChange ( ) ;
}
protected abstract void UpdateState ( ArmedState state ) ;
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// </summary>
public event Action < DrawableHitObject , ArmedState > ApplyCustomUpdateState ;
/// <summary>
/// Plays all the hitsounds for this <see cref="DrawableHitObject"/>.
/// </summary>
public void PlaySamples ( ) = > Samples ? . Play ( ) ;
2018-08-01 20:04:03 +08:00
private double lastUpdateTime ;
2018-04-13 17:19:50 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2018-08-03 14:38:48 +08:00
if ( Result ! = null & & lastUpdateTime > Time . Current )
2018-04-13 17:19:50 +08:00
{
2018-08-01 20:04:03 +08:00
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
2018-04-13 17:19:50 +08:00
2018-08-03 14:38:48 +08:00
if ( Result . TimeOffset + endTime < Time . Current )
2018-08-01 20:04:03 +08:00
{
2018-08-03 14:38:48 +08:00
OnJudgementRemoved ? . Invoke ( this , Result ) ;
2018-08-01 20:04:03 +08:00
2018-08-03 14:38:48 +08:00
Result . Type = HitResult . None ;
2018-08-02 19:35:54 +08:00
State . Value = ArmedState . Idle ;
2018-08-01 20:04:03 +08:00
}
2018-04-13 17:19:50 +08:00
}
2018-08-01 20:04:03 +08:00
lastUpdateTime = Time . Current ;
2018-04-13 17:19:50 +08:00
}
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
UpdateJudgement ( false ) ;
}
protected virtual void AddNested ( DrawableHitObject h )
{
2018-08-02 19:35:54 +08:00
h . OnJudgement + = ( d , r ) = > OnJudgement ? . Invoke ( d , r ) ;
h . OnJudgementRemoved + = ( d , r ) = > OnJudgementRemoved ? . Invoke ( d , r ) ;
2018-04-13 17:19:50 +08:00
h . ApplyCustomUpdateState + = ( d , j ) = > ApplyCustomUpdateState ? . Invoke ( d , j ) ;
nestedHitObjects . Value . Add ( h ) ;
}
/// <summary>
/// Notifies that a new judgement has occurred for this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/>.</param>
2018-08-03 14:38:48 +08:00
protected void ApplyResult ( Action < JudgementResult > application )
2018-04-13 17:19:50 +08:00
{
2018-08-03 14:38:48 +08:00
application ? . Invoke ( Result ) ;
2018-08-02 19:35:54 +08:00
judgementOccurred = true ;
2018-08-01 20:04:03 +08:00
2018-04-13 17:19:50 +08:00
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
2018-08-03 14:38:48 +08:00
Result . TimeOffset = Time . Current - endTime ;
2018-04-13 17:19:50 +08:00
2018-08-03 14:38:48 +08:00
switch ( Result . Type )
2018-04-13 17:19:50 +08:00
{
2018-08-03 14:38:48 +08:00
case HitResult . None :
break ;
case HitResult . Miss :
State . Value = ArmedState . Miss ;
break ;
default :
State . Value = ArmedState . Hit ;
break ;
2018-04-13 17:19:50 +08:00
}
2018-08-03 14:38:48 +08:00
OnJudgement ? . Invoke ( this , Result ) ;
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Processes this <see cref="DrawableHitObject"/>, checking if any judgements have occurred.
/// </summary>
/// <param name="userTriggered">Whether the user triggered this process.</param>
/// <returns>Whether a judgement has occurred from this <see cref="DrawableHitObject"/> or any nested <see cref="DrawableHitObject"/>s.</returns>
protected bool UpdateJudgement ( bool userTriggered )
{
judgementOccurred = false ;
if ( AllJudged )
return false ;
2018-07-02 15:10:56 +08:00
foreach ( var d in NestedHitObjects )
judgementOccurred | = d . UpdateJudgement ( userTriggered ) ;
2018-04-13 17:19:50 +08:00
2018-08-01 20:04:03 +08:00
if ( judgementOccurred | | Judged )
2018-04-13 17:19:50 +08:00
return judgementOccurred ;
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
CheckForJudgements ( userTriggered , Time . Current - endTime ) ;
return judgementOccurred ;
}
/// <summary>
/// Checks if any judgements have occurred for this <see cref="DrawableHitObject"/>. This method must construct
2018-08-02 19:35:54 +08:00
/// all <see cref="Judgement"/>s and notify of them through <see cref="ApplyResult{T}"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="userTriggered">Whether the user triggered this check.</param>
/// <param name="timeOffset">The offset from the <see cref="HitObject"/> end time at which this check occurred. A <paramref name="timeOffset"/> > 0
/// implies that this check occurred after the end time of <see cref="HitObject"/>. </param>
protected virtual void CheckForJudgements ( bool userTriggered , double timeOffset )
{
}
2018-08-02 19:35:54 +08:00
protected virtual JudgementResult CreateJudgementResult ( Judgement judgement ) = > new JudgementResult ( judgement ) ;
2018-04-13 17:19:50 +08:00
}
public abstract class DrawableHitObject < TObject > : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject ;
protected DrawableHitObject ( TObject hitObject )
: base ( hitObject )
{
HitObject = hitObject ;
}
}
}