2023-05-07 20:33:15 +00:00
using System ;
2022-05-03 20:36:55 +00:00
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
using DG.Tweening ;
using HeavenStudio.Util ;
using Starpelly ;
2023-03-11 04:51:22 +00:00
using HeavenStudio.Common ;
2022-05-03 20:36:55 +00:00
namespace HeavenStudio.Games
{
2023-05-07 20:33:15 +00:00
public class PlayerActionEvent : MonoBehaviour
2022-05-03 20:36:55 +00:00
{
2023-05-07 20:33:15 +00:00
static List < PlayerActionEvent > allEvents = new List < PlayerActionEvent > ( ) ;
2023-03-12 02:56:26 +00:00
public static bool EnableAutoplayCheat = true ;
2022-05-06 20:05:19 +00:00
public delegate void ActionEventCallback ( PlayerActionEvent caller ) ;
public delegate void ActionEventCallbackState ( PlayerActionEvent caller , float state ) ;
2022-05-04 18:05:51 +00:00
2022-05-03 20:36:55 +00:00
public ActionEventCallbackState OnHit ; //Function to trigger when an input has been done perfectly
public ActionEventCallback OnMiss ; //Function to trigger when an input has been missed
public ActionEventCallback OnBlank ; //Function to trigger when an input has been recorded while this is pending
2022-05-06 20:05:19 +00:00
public ActionEventCallback OnDestroy ; //Function to trigger whenever this event gets destroyed. /!\ Shouldn't be used for a minigame! Use OnMiss instead /!\
2022-05-04 18:05:51 +00:00
2022-05-03 20:36:55 +00:00
public float startBeat ;
public float timer ;
2023-05-07 20:33:15 +00:00
public bool isEligible = true ;
2022-05-04 17:21:11 +00:00
public bool canHit = true ; //Indicates if you can still hit the cue or not. If set to false, it'll guarantee a miss
2022-05-06 20:05:19 +00:00
public bool enabled = true ; //Indicates if the PlayerActionEvent is enabled. If set to false, it'll not trigger any events and destroy itself AFTER it's not relevant anymore
2023-05-07 20:33:15 +00:00
public bool triggersAutoplay = true ;
bool lockedByEvent = false ;
bool markForDeletion = false ;
2022-05-04 17:21:11 +00:00
public bool autoplayOnly = false ; //Indicates if the input event only triggers when it's autoplay. If set to true, NO Miss or Blank events will be triggered when you're not autoplaying.
public bool noAutoplay = false ; //Indicates if this PlayerActionEvent is recognized by the autoplay. /!\ Overrides autoPlayOnly /!\
2022-05-06 20:05:19 +00:00
public InputType inputType ; //The type of input. Check the InputType class to see a list of all of them
2022-05-03 20:36:55 +00:00
2022-05-06 20:05:19 +00:00
public bool perfectOnly = false ; //Indicates that the input only recognize perfect inputs.
2023-01-25 03:54:19 +00:00
public bool countsForAccuracy = true ; //Indicates if the input counts for the accuracy or not. If set to false, it'll not be counted in the accuracy calculation
2022-05-03 20:36:55 +00:00
public void setHitCallback ( ActionEventCallbackState OnHit )
{
this . OnHit = OnHit ;
}
public void setMissCallback ( ActionEventCallback OnMiss )
{
this . OnMiss = OnMiss ;
}
public void Enable ( ) { enabled = true ; }
public void Disable ( ) { enabled = false ; }
2023-05-07 20:33:15 +00:00
public void QueueDeletion ( ) { markForDeletion = true ; }
public bool IsCorrectInput ( ) = >
//General inputs, both down and up
( PlayerInput . Pressed ( ) & & inputType . HasFlag ( InputType . STANDARD_DOWN ) ) | |
( PlayerInput . AltPressed ( ) & & inputType . HasFlag ( InputType . STANDARD_ALT_DOWN ) ) | |
( PlayerInput . GetAnyDirectionDown ( ) & & inputType . HasFlag ( InputType . DIRECTION_DOWN ) ) | |
( PlayerInput . PressedUp ( ) & & inputType . HasFlag ( InputType . STANDARD_UP ) ) | |
( PlayerInput . AltPressedUp ( ) & & inputType . HasFlag ( InputType . STANDARD_ALT_UP ) ) | |
( PlayerInput . GetAnyDirectionUp ( ) & & inputType . HasFlag ( InputType . DIRECTION_UP ) ) | |
//Specific directional inputs
( PlayerInput . GetSpecificDirectionDown ( PlayerInput . DOWN ) & & inputType . HasFlag ( InputType . DIRECTION_DOWN_DOWN ) ) | |
( PlayerInput . GetSpecificDirectionDown ( PlayerInput . UP ) & & inputType . HasFlag ( InputType . DIRECTION_UP_DOWN ) ) | |
( PlayerInput . GetSpecificDirectionDown ( PlayerInput . LEFT ) & & inputType . HasFlag ( InputType . DIRECTION_LEFT_DOWN ) ) | |
( PlayerInput . GetSpecificDirectionDown ( PlayerInput . RIGHT ) & & inputType . HasFlag ( InputType . DIRECTION_RIGHT_DOWN ) ) | |
( PlayerInput . GetSpecificDirectionUp ( PlayerInput . DOWN ) & & inputType . HasFlag ( InputType . DIRECTION_DOWN_UP ) ) | |
( PlayerInput . GetSpecificDirectionUp ( PlayerInput . UP ) & & inputType . HasFlag ( InputType . DIRECTION_UP_UP ) ) | |
( PlayerInput . GetSpecificDirectionUp ( PlayerInput . LEFT ) & & inputType . HasFlag ( InputType . DIRECTION_LEFT_UP ) ) | |
( PlayerInput . GetSpecificDirectionUp ( PlayerInput . RIGHT ) & & inputType . HasFlag ( InputType . DIRECTION_RIGHT_UP ) ) ;
2022-05-03 20:36:55 +00:00
public void CanHit ( bool canHit )
{
this . canHit = canHit ;
}
2023-05-07 20:33:15 +00:00
public void Start ( )
{
allEvents . Add ( this ) ;
}
2022-09-18 20:48:14 +00:00
2022-05-03 20:36:55 +00:00
public void Update ( )
{
2023-05-07 20:33:15 +00:00
if ( markForDeletion ) CleanUp ( ) ;
if ( ! Conductor . instance . NotStopped ( ) ) CleanUp ( ) ; // If the song is stopped entirely in the editor, destroy itself as we don't want duplicates
2022-05-03 20:36:55 +00:00
2022-05-04 17:21:11 +00:00
if ( noAutoplay & & autoplayOnly ) autoplayOnly = false ;
2023-05-07 20:33:15 +00:00
if ( noAutoplay & & triggersAutoplay ) triggersAutoplay = false ;
2023-03-11 04:51:22 +00:00
if ( ! enabled ) return ;
2022-05-04 17:21:11 +00:00
2023-01-25 03:54:19 +00:00
double normalizedTime = GetNormalizedTime ( ) ;
2023-05-07 20:33:15 +00:00
if ( GameManager . instance . autoplay )
{
AutoplayInput ( normalizedTime ) ;
return ;
}
2022-05-03 20:36:55 +00:00
2022-05-28 02:40:16 +00:00
//BUGFIX: ActionEvents destroyed too early
2023-01-25 03:54:19 +00:00
if ( normalizedTime > Minigame . EndTime ( ) ) Miss ( ) ;
2022-05-03 20:36:55 +00:00
2023-05-07 20:33:15 +00:00
if ( lockedByEvent )
2022-05-03 20:36:55 +00:00
{
2023-05-07 20:33:15 +00:00
return ;
}
if ( ! CheckEventLock ( ) )
{
return ;
}
if ( ! autoplayOnly & & IsCorrectInput ( ) )
{
if ( IsExpectingInputNow ( ) )
2022-05-03 20:36:55 +00:00
{
2023-05-07 20:33:15 +00:00
double stateProg = ( ( normalizedTime - Minigame . PerfectTime ( ) ) / ( Minigame . LateTime ( ) - Minigame . PerfectTime ( ) ) - 0.5f ) * 2 ;
2023-01-25 03:54:19 +00:00
Hit ( stateProg , normalizedTime ) ;
2022-05-03 20:36:55 +00:00
}
else
{
Blank ( ) ;
}
}
2022-05-04 18:37:52 +00:00
}
2023-05-07 20:33:15 +00:00
public void LateUpdate ( ) {
if ( markForDeletion ) {
CleanUp ( ) ;
Destroy ( this . gameObject ) ;
}
foreach ( PlayerActionEvent evt in allEvents )
{
evt . lockedByEvent = false ;
}
}
private bool CheckEventLock ( )
{
foreach ( PlayerActionEvent toCompare in allEvents )
{
if ( toCompare = = this ) continue ;
if ( toCompare . autoplayOnly ) continue ;
if ( ( toCompare . inputType & this . inputType ) = = 0 ) continue ;
if ( ! toCompare . IsExpectingInputNow ( ) ) continue ;
double t1 = this . startBeat + this . timer ;
double t2 = toCompare . startBeat + toCompare . timer ;
double songPos = Conductor . instance . songPositionInBeatsAsDouble ;
// compare distance between current time and the events
// events that happen at the exact same time with the exact same inputs will return true
if ( Math . Abs ( t1 - songPos ) > Math . Abs ( t2 - songPos ) )
return false ;
else if ( t1 ! = t2 ) // if they are the same time, we don't want to lock the event
toCompare . lockedByEvent = true ;
}
return true ;
}
private void AutoplayInput ( double normalizedTime , bool autoPlay = false )
{
if ( triggersAutoplay & & ( GameManager . instance . autoplay | | autoPlay ) & & GameManager . instance . canInput & & normalizedTime > = 1f - ( Time . deltaTime * 0.5f ) )
{
AutoplayEvent ( ) ;
if ( ! autoPlay )
TimelineAutoplay ( ) ;
}
}
// TODO: move this to timeline code instead
private void TimelineAutoplay ( )
{
if ( Editor . Editor . instance = = null ) return ;
if ( Editor . Track . Timeline . instance ! = null & & ! Editor . Editor . instance . fullscreen )
{
Editor . Track . Timeline . instance . AutoplayBTN . GetComponent < Animator > ( ) . Play ( "Ace" , 0 , 0 ) ;
}
}
2022-05-04 18:37:52 +00:00
public bool IsExpectingInputNow ( )
{
2023-01-25 03:54:19 +00:00
double normalizedBeat = GetNormalizedTime ( ) ;
2022-05-04 18:37:52 +00:00
return normalizedBeat > Minigame . EarlyTime ( ) & & normalizedBeat < Minigame . EndTime ( ) ;
}
2022-05-03 20:36:55 +00:00
2023-01-25 03:54:19 +00:00
double GetNormalizedTime ( )
2023-01-14 04:53:25 +00:00
{
var cond = Conductor . instance ;
2023-05-07 20:33:15 +00:00
double currTime = cond . songPositionAsDouble ;
2023-01-14 04:53:25 +00:00
double targetTime = cond . GetSongPosFromBeat ( startBeat + timer ) ;
double min = targetTime - 1f ;
double max = targetTime + 1f ;
2023-01-25 03:54:19 +00:00
return 1f + ( ( ( currTime - min ) / ( max - min ) ) - 0.5f ) * 2 ;
2023-01-14 04:53:25 +00:00
}
2022-05-03 20:36:55 +00:00
//For the Autoplay
2023-05-07 20:33:15 +00:00
public void AutoplayEvent ( )
2022-05-03 20:36:55 +00:00
{
2023-01-25 03:54:19 +00:00
if ( EnableAutoplayCheat )
{
Hit ( 0f , 1f ) ;
}
else
{
double normalizedBeat = GetNormalizedTime ( ) ;
double stateProg = ( ( normalizedBeat - Minigame . PerfectTime ( ) ) / ( Minigame . LateTime ( ) - Minigame . PerfectTime ( ) ) - 0.5f ) * 2 ;
Hit ( stateProg , normalizedBeat ) ;
}
2022-05-03 20:36:55 +00:00
}
2022-05-04 17:21:11 +00:00
//The state parameter is either -1 -> Early, 0 -> Perfect, 1 -> Late
2023-01-25 03:54:19 +00:00
public void Hit ( double state , double time )
2022-05-03 20:36:55 +00:00
{
if ( OnHit ! = null & & enabled )
{
if ( canHit )
{
2023-01-25 03:54:19 +00:00
double normalized = time - 1f ;
2023-01-14 04:53:25 +00:00
int offset = Mathf . CeilToInt ( ( float ) normalized * 1000 ) ;
GameManager . instance . AvgInputOffset = offset ;
2023-05-07 20:33:15 +00:00
state = System . Math . Max ( - 1.0 , System . Math . Min ( 1.0 , state ) ) ;
2023-01-25 03:54:19 +00:00
OnHit ( this , ( float ) state ) ;
2023-02-05 03:05:43 +00:00
CleanUp ( ) ;
2023-03-11 04:51:22 +00:00
if ( countsForAccuracy & & ! ( noAutoplay | | autoplayOnly ) & & isEligible )
{
GameManager . instance . ScoreInputAccuracy ( TimeToAccuracy ( time ) , time > 1.0 , time ) ;
if ( state > = 1f | | state < = - 1f )
{
GoForAPerfect . instance . Miss ( ) ;
SectionMedalsManager . instance . MakeIneligible ( ) ;
}
else
{
GoForAPerfect . instance . Hit ( ) ;
}
}
2022-05-03 20:36:55 +00:00
} else
{
2022-05-06 20:05:19 +00:00
Blank ( ) ;
2022-05-03 20:36:55 +00:00
}
}
2023-01-25 03:54:19 +00:00
}
double TimeToAccuracy ( double time )
{
if ( time > = Minigame . AceStartTime ( ) & & time < = Minigame . AceEndTime ( ) )
{
// Ace
return 1.0 ;
}
double state = 0 ;
if ( time > = Minigame . PerfectTime ( ) & & time < = Minigame . LateTime ( ) )
{
// Good Hit
if ( time > 1.0 )
{
// late half of timing window
state = 1.0 - ( ( time - Minigame . AceEndTime ( ) ) / ( Minigame . LateTime ( ) - Minigame . AceEndTime ( ) ) ) ;
state * = 1.0 - Minigame . rankHiThreshold ;
state + = Minigame . rankHiThreshold ;
}
else
{
//early half of timing window
state = ( ( time - Minigame . PerfectTime ( ) ) / ( Minigame . AceStartTime ( ) - Minigame . PerfectTime ( ) ) ) ;
state * = 1.0 - Minigame . rankHiThreshold ;
state + = Minigame . rankHiThreshold ;
}
}
else
{
if ( time > 1.0 )
{
// late half of timing window
state = 1.0 - ( ( time - Minigame . LateTime ( ) ) / ( Minigame . EndTime ( ) - Minigame . LateTime ( ) ) ) ;
state * = Minigame . rankOkThreshold ;
}
else
{
//early half of timing window
state = ( ( time - Minigame . PerfectTime ( ) ) / ( Minigame . AceStartTime ( ) - Minigame . PerfectTime ( ) ) ) ;
state * = Minigame . rankOkThreshold ;
}
}
return state ;
2022-05-03 20:36:55 +00:00
}
public void Miss ( )
{
2022-05-04 17:21:11 +00:00
if ( OnMiss ! = null & & enabled & & ! autoplayOnly )
2022-05-03 20:36:55 +00:00
{
2022-05-06 20:05:19 +00:00
OnMiss ( this ) ;
2022-05-03 20:36:55 +00:00
}
2022-05-04 17:21:11 +00:00
CleanUp ( ) ;
2023-01-25 03:54:19 +00:00
if ( countsForAccuracy & & ! ( noAutoplay | | autoplayOnly ) )
2023-03-11 04:51:22 +00:00
{
GameManager . instance . ScoreInputAccuracy ( 0 , true , 2.0 , 1.0 , false ) ;
GoForAPerfect . instance . Miss ( ) ;
SectionMedalsManager . instance . MakeIneligible ( ) ;
}
2022-05-03 20:36:55 +00:00
}
public void Blank ( )
{
2022-05-04 17:21:11 +00:00
if ( OnBlank ! = null & & enabled & & ! autoplayOnly )
2022-05-03 20:36:55 +00:00
{
2022-05-06 20:05:19 +00:00
OnBlank ( this ) ;
2022-05-03 20:36:55 +00:00
}
}
public void CleanUp ( )
{
2023-05-07 20:33:15 +00:00
if ( markForDeletion ) return ;
allEvents . Remove ( this ) ;
2022-05-04 18:05:51 +00:00
OnDestroy ( this ) ;
2023-05-07 20:33:15 +00:00
markForDeletion = true ;
2022-05-03 20:36:55 +00:00
}
}
}