35832c2dfc
fix input scheduling not taking into account what minigame is actually active at the target time fix input disable and autoplay jank prep "friendly program name" define title screen adjustments remove bread2unity
364 lines
13 KiB
C#
364 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
using HeavenStudio.Common;
|
|
|
|
namespace HeavenStudio.Games
|
|
{
|
|
|
|
public class PlayerActionEvent : MonoBehaviour
|
|
{
|
|
static List<PlayerActionEvent> allEvents = new List<PlayerActionEvent>();
|
|
public static bool EnableAutoplayCheat = true;
|
|
public delegate void ActionEventCallback(PlayerActionEvent caller);
|
|
public delegate void ActionEventCallbackState(PlayerActionEvent caller, float state);
|
|
public delegate bool ActionEventHittableQuery();
|
|
|
|
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
|
|
public ActionEventHittableQuery IsHittable; //Checks if an input can be hit. Returning false will skip button checks.
|
|
|
|
public ActionEventCallback OnDestroy; //Function to trigger whenever this event gets destroyed. /!\ Shouldn't be used for a minigame! Use OnMiss instead /!\
|
|
|
|
public PlayerInput.InputAction InputAction;
|
|
|
|
public double startBeat;
|
|
public double timer;
|
|
public float weight = 1f;
|
|
|
|
public bool isEligible = true;
|
|
public bool canHit = true; //Indicates if you can still hit the cue or not. If set to false, it'll guarantee a miss
|
|
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
|
|
public bool triggersAutoplay = true;
|
|
public string minigame;
|
|
bool lockedByEvent = false;
|
|
bool markForDeletion = false;
|
|
|
|
float pitchWhenHit = 1f;
|
|
|
|
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 /!\
|
|
|
|
public InputType inputType; //The type of input. Check the InputType class to see a list of all of them
|
|
|
|
public bool perfectOnly = false; //Indicates that the input only recognize perfect inputs.
|
|
|
|
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
|
|
|
|
public void setHitCallback(ActionEventCallbackState OnHit)
|
|
{
|
|
this.OnHit = OnHit;
|
|
}
|
|
|
|
public void setMissCallback(ActionEventCallback OnMiss)
|
|
{
|
|
this.OnMiss = OnMiss;
|
|
}
|
|
|
|
public void setHittableQuery(ActionEventHittableQuery IsHittable)
|
|
{
|
|
this.IsHittable = IsHittable;
|
|
}
|
|
|
|
public void Enable() { enabled = true; }
|
|
public void Disable() { enabled = false; }
|
|
public void QueueDeletion() { markForDeletion = true; }
|
|
|
|
public bool IsCorrectInput(out double dt)
|
|
{
|
|
dt = 0;
|
|
if (InputAction != null)
|
|
{
|
|
return PlayerInput.GetIsAction(InputAction, out dt);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void CanHit(bool canHit)
|
|
{
|
|
this.canHit = canHit;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
allEvents.Add(this);
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
Conductor cond = Conductor.instance;
|
|
GameManager gm = GameManager.instance;
|
|
if (markForDeletion) CleanUp();
|
|
if (!cond.NotStopped()) CleanUp(); // If the song is stopped entirely in the editor, destroy itself as we don't want duplicates
|
|
|
|
if (noAutoplay && autoplayOnly) autoplayOnly = false;
|
|
if (noAutoplay && triggersAutoplay) triggersAutoplay = false;
|
|
if (!enabled) return;
|
|
if (minigame != GameManager.instance.currentGame) return;
|
|
|
|
double normalizedTime = GetNormalizedTime();
|
|
if (gm.autoplay && gm.canInput)
|
|
{
|
|
AutoplayInput(normalizedTime);
|
|
return;
|
|
}
|
|
|
|
//BUGFIX: ActionEvents destroyed too early
|
|
if (normalizedTime > Minigame.NgLateTime(cond.SongPitch)) Miss();
|
|
|
|
if (lockedByEvent)
|
|
{
|
|
return;
|
|
}
|
|
if (!CheckEventLock())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!autoplayOnly && (IsHittable == null || IsHittable != null && IsHittable()) && IsCorrectInput(out double dt))
|
|
{
|
|
normalizedTime -= dt;
|
|
if (IsExpectingInputNow())
|
|
{
|
|
double stateProg = ((normalizedTime - Minigame.JustEarlyTime()) / (Minigame.JustLateTime() - Minigame.JustEarlyTime()) - 0.5f) * 2;
|
|
Hit(stateProg, normalizedTime);
|
|
}
|
|
else
|
|
{
|
|
Blank();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void LateUpdate()
|
|
{
|
|
if (markForDeletion)
|
|
{
|
|
allEvents.Remove(this);
|
|
OnDestroy(this);
|
|
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 (InputAction != null)
|
|
{
|
|
if (toCompare.InputAction == null) continue;
|
|
int catIdx = (int)PlayerInput.CurrentControlStyle;
|
|
if (toCompare.InputAction != null
|
|
&& toCompare.InputAction.inputLockCategory[catIdx] != InputAction.inputLockCategory[catIdx]) continue;
|
|
}
|
|
else
|
|
{
|
|
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) && 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 (!GameManager.instance.canInput) return;
|
|
if (Editor.Track.Timeline.instance != null && !Editor.Editor.instance.fullscreen)
|
|
{
|
|
Editor.Track.Timeline.instance.AutoplayBTN.GetComponent<Animator>().Play("Ace", 0, 0);
|
|
}
|
|
}
|
|
|
|
public bool IsExpectingInputNow()
|
|
{
|
|
if (IsHittable != null)
|
|
{
|
|
if (!IsHittable()) return false;
|
|
}
|
|
if (!enabled) return false;
|
|
if (!isEligible) return false;
|
|
|
|
double normalizedBeat = GetNormalizedTime();
|
|
return normalizedBeat > Minigame.NgEarlyTime() && normalizedBeat < Minigame.NgLateTime();
|
|
}
|
|
|
|
double GetNormalizedTime()
|
|
{
|
|
var cond = Conductor.instance;
|
|
double currTime = cond.songPositionAsDouble;
|
|
double targetTime = cond.GetSongPosFromBeat(startBeat + timer);
|
|
|
|
// HS timing window uses 1 as the middle point instead of 0
|
|
return 1 + (currTime - targetTime);
|
|
}
|
|
|
|
//For the Autoplay
|
|
public void AutoplayEvent()
|
|
{
|
|
if (!GameManager.instance.canInput)
|
|
{
|
|
CleanUp();
|
|
return;
|
|
}
|
|
if (EnableAutoplayCheat)
|
|
{
|
|
Hit(0f, 1f);
|
|
}
|
|
else
|
|
{
|
|
double normalizedBeat = GetNormalizedTime();
|
|
double stateProg = ((normalizedBeat - Minigame.JustEarlyTime()) / (Minigame.JustLateTime() - Minigame.JustEarlyTime()) - 0.5f) * 2;
|
|
Hit(stateProg, normalizedBeat);
|
|
}
|
|
}
|
|
|
|
//The state parameter is either -1 -> Early, 0 -> Perfect, 1 -> Late
|
|
public void Hit(double state, double time)
|
|
{
|
|
GameManager gm = GameManager.instance;
|
|
if (OnHit != null && enabled)
|
|
{
|
|
if (canHit)
|
|
{
|
|
CleanUp();
|
|
pitchWhenHit = Conductor.instance.SongPitch;
|
|
double normalized = time - 1f;
|
|
int offset = Mathf.CeilToInt((float)normalized * 1000);
|
|
if (gm.canInput)
|
|
{
|
|
gm.AvgInputOffset = offset;
|
|
}
|
|
state = System.Math.Max(-1.0, System.Math.Min(1.0, state));
|
|
|
|
if (countsForAccuracy && gm.canInput && !(noAutoplay || autoplayOnly) && isEligible)
|
|
{
|
|
gm.ScoreInputAccuracy(startBeat + timer, TimeToAccuracy(time, pitchWhenHit), time > 1.0, time, weight, true);
|
|
if (state >= 1f || state <= -1f)
|
|
{
|
|
GoForAPerfect.instance.Miss();
|
|
SectionMedalsManager.instance.MakeIneligible();
|
|
}
|
|
else
|
|
{
|
|
GoForAPerfect.instance.Hit();
|
|
}
|
|
}
|
|
OnHit(this, (float)state);
|
|
}
|
|
else
|
|
{
|
|
Blank();
|
|
}
|
|
}
|
|
}
|
|
|
|
double TimeToAccuracy(double time, float pitch = -1)
|
|
{
|
|
if (pitch < 0) pitch = pitchWhenHit;
|
|
if (time >= Minigame.AceEarlyTime(pitch) && time <= Minigame.AceLateTime(pitch))
|
|
{
|
|
// Ace
|
|
return 1.0;
|
|
}
|
|
|
|
double state = 0;
|
|
if (time >= Minigame.JustEarlyTime(pitch) && time <= Minigame.JustLateTime(pitch))
|
|
{
|
|
// Good Hit
|
|
if (time > 1.0)
|
|
{
|
|
// late half of timing window
|
|
state = 1.0 - ((time - Minigame.AceLateTime(pitch)) / (Minigame.JustLateTime(pitch) - Minigame.AceLateTime(pitch)));
|
|
state *= 1.0 - Minigame.rankHiThreshold;
|
|
state += Minigame.rankHiThreshold;
|
|
}
|
|
else
|
|
{
|
|
//early half of timing window
|
|
state = ((time - Minigame.JustEarlyTime(pitch)) / (Minigame.AceEarlyTime(pitch) - Minigame.JustEarlyTime(pitch)));
|
|
state *= 1.0 - Minigame.rankHiThreshold;
|
|
state += Minigame.rankHiThreshold;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (time > 1.0)
|
|
{
|
|
// late half of timing window
|
|
state = 1.0 - ((time - Minigame.JustLateTime(pitch)) / (Minigame.NgLateTime(pitch) - Minigame.JustLateTime(pitch)));
|
|
state *= Minigame.rankOkThreshold;
|
|
}
|
|
else
|
|
{
|
|
//early half of timing window
|
|
state = ((time - Minigame.JustEarlyTime(pitch)) / (Minigame.AceEarlyTime(pitch) - Minigame.JustEarlyTime(pitch)));
|
|
state *= Minigame.rankOkThreshold;
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
public void Miss()
|
|
{
|
|
GameManager gm = GameManager.instance;
|
|
CleanUp();
|
|
if (OnMiss != null && enabled && !autoplayOnly)
|
|
{
|
|
OnMiss(this);
|
|
}
|
|
|
|
if (countsForAccuracy && gm.canInput && !(noAutoplay || autoplayOnly))
|
|
{
|
|
gm.ScoreInputAccuracy(startBeat + timer, 0, true, 2.0, weight, false);
|
|
GoForAPerfect.instance.Miss();
|
|
SectionMedalsManager.instance.MakeIneligible();
|
|
}
|
|
}
|
|
|
|
public void Blank()
|
|
{
|
|
if (OnBlank != null && enabled && !autoplayOnly)
|
|
{
|
|
OnBlank(this);
|
|
}
|
|
}
|
|
|
|
public void CleanUp()
|
|
{
|
|
if (markForDeletion) return;
|
|
markForDeletion = true;
|
|
}
|
|
}
|
|
} |