HeavenStudioPlus/Assets/Scripts/Games/Minigame.cs
minenice55 35832c2dfc last of the assetbundle definitions
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
2024-01-14 02:18:46 -05:00

499 lines
18 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HeavenStudio.Util;
using HeavenStudio.Common;
using HeavenStudio.InputSystem;
using System;
using System.Linq;
using Jukebox;
namespace HeavenStudio.Games
{
public class Minigame : MonoBehaviour
{
public static double ngEarlyTimeBase = 0.1, justEarlyTimeBase = 0.05, aceEarlyTimeBase = 0.01, aceLateTimeBase = 0.01, justLateTimeBase = 0.05, ngLateTimeBase = 0.1;
public static double rankHiThreshold = 0.8, rankOkThreshold = 0.6;
public static double ngEarlyTime => ngEarlyTimeBase * Conductor.instance?.SongPitch ?? 1;
public static double justEarlyTime => justEarlyTimeBase * Conductor.instance?.SongPitch ?? 1;
public static double aceEarlyTime => aceEarlyTimeBase * Conductor.instance?.SongPitch ?? 1;
public static double aceLateTime => aceLateTimeBase * Conductor.instance?.SongPitch ?? 1;
public static double justLateTime => justLateTimeBase * Conductor.instance?.SongPitch ?? 1;
public static double ngLateTime => ngLateTimeBase * Conductor.instance?.SongPitch ?? 1;
[SerializeField] public SoundSequence.SequenceKeyValue[] SoundSequences;
[NonSerialized] public string minigameName;
#region Premade Input Actions
protected const int IAEmptyCat = -1;
protected const int IAPressCat = 0;
protected const int IAReleaseCat = 1;
protected const int IAPressingCat = 2;
protected const int IAFlickCat = 3;
protected const int IAMAXCAT = 4;
protected static bool IA_Empty(out double dt)
{
dt = 0;
return false;
}
protected static bool IA_PadBasicPress(out double dt)
{
return PlayerInput.GetPadDown(InputController.ActionsPad.East, out dt);
}
protected static bool IA_TouchBasicPress(out double dt)
{
return PlayerInput.GetTouchDown(InputController.ActionsTouch.Tap, out dt);
}
protected static bool IA_BatonBasicPress(out double dt)
{
return PlayerInput.GetBatonDown(InputController.ActionsBaton.Face, out dt);
}
protected static bool IA_PadBasicRelease(out double dt)
{
return PlayerInput.GetPadUp(InputController.ActionsPad.East, out dt);
}
protected static bool IA_TouchBasicRelease(out double dt)
{
return PlayerInput.GetTouchUp(InputController.ActionsTouch.Tap, out dt) && !PlayerInput.GetFlick(out _);
}
protected static bool IA_BatonBasicRelease(out double dt)
{
return PlayerInput.GetBatonUp(InputController.ActionsBaton.Face, out dt);
}
protected static bool IA_PadBasicPressing(out double dt)
{
dt = 0;
return PlayerInput.GetPad(InputController.ActionsPad.East);
}
protected static bool IA_TouchBasicPressing(out double dt)
{
dt = 0;
return PlayerInput.GetTouch(InputController.ActionsTouch.Tap);
}
protected static bool IA_BatonBasicPressing(out double dt)
{
dt = 0;
return PlayerInput.GetBaton(InputController.ActionsBaton.Face);
}
protected static bool IA_TouchFlick(out double dt)
{
return PlayerInput.GetFlick(out dt);
}
public static PlayerInput.InputAction InputAction_BasicPress =
new("BasicPress", new int[] { IAPressCat, IAPressCat, IAPressCat },
IA_PadBasicPress, IA_TouchBasicPress, IA_BatonBasicPress);
public static PlayerInput.InputAction InputAction_BasicRelease =
new("BasicRelease", new int[] { IAReleaseCat, IAReleaseCat, IAReleaseCat },
IA_PadBasicRelease, IA_TouchBasicRelease, IA_BatonBasicRelease);
public static PlayerInput.InputAction InputAction_BasicPressing =
new("BasicRelease", new int[] { IAReleaseCat, IAReleaseCat, IAReleaseCat },
IA_PadBasicPressing, IA_TouchBasicPressing, IA_BatonBasicPressing);
public static PlayerInput.InputAction InputAction_FlickPress =
new("FlickPress", new int[] { IAPressCat, IAFlickCat, IAPressCat },
IA_PadBasicPress, IA_TouchFlick, IA_BatonBasicPress);
public static PlayerInput.InputAction InputAction_FlickRelease =
new("FlickRelease", new int[] { IAReleaseCat, IAFlickCat, IAReleaseCat },
IA_PadBasicRelease, IA_TouchFlick, IA_BatonBasicRelease);
#endregion
public List<PlayerActionEvent> scheduledInputs = new List<PlayerActionEvent>();
/// <summary>
/// Schedule an Input for a later time in the minigame. Executes the methods put in parameters
/// </summary>
/// <param name="startBeat">When the scheduling started (in beats)</param>
/// <param name="timer">How many beats later should the input be expected</param>
/// <param name="inputAction">The input action that's expected</param>
/// <param name="OnHit">Method to run if the Input has been Hit</param>
/// <param name="OnMiss">Method to run if the Input has been Missed</param>
/// <param name="OnBlank">Method to run whenever there's an Input while this is Scheduled (Shouldn't be used too much)</param>
/// <returns></returns>
public PlayerActionEvent ScheduleInput(
double startBeat,
double timer,
PlayerInput.InputAction inputAction,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank,
PlayerActionEvent.ActionEventHittableQuery HittableQuery = null
)
{
// List<RiqEntity> gameSwitches = GameManager.instance.Beatmap.Entities.FindAll(c => c.beat <= startBeat + timer && c.datamodel.Split("/")[0] == "switchGame");
// if (gameSwitches != null && gameSwitches[^1].datamodel.Split("/")[1] != gameObject.name)
// {
// return null;
// }
GameObject evtObj = new("ActionEvent" + (startBeat + timer));
PlayerActionEvent evt = evtObj.AddComponent<PlayerActionEvent>();
evt.startBeat = startBeat;
evt.timer = timer;
evt.InputAction = inputAction;
evt.OnHit = OnHit;
evt.OnMiss = OnMiss;
evt.OnBlank = OnBlank;
evt.IsHittable = HittableQuery;
evt.OnDestroy = RemoveScheduledInput;
evt.canHit = true;
evt.enabled = true;
evt.transform.parent = this.transform.parent;
evt.minigame = minigameName;
evtObj.SetActive(true);
scheduledInputs.Add(evt);
return evt;
}
public PlayerActionEvent ScheduleAutoplayInput(double startBeat,
double timer,
PlayerInput.InputAction inputAction,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank)
{
PlayerActionEvent evt = ScheduleInput(startBeat, timer, inputAction, OnHit, OnMiss, OnBlank);
evt.autoplayOnly = true;
evt.minigame = minigameName;
return evt;
}
public PlayerActionEvent ScheduleUserInput(double startBeat,
double timer,
PlayerInput.InputAction inputAction,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank,
PlayerActionEvent.ActionEventHittableQuery HittableQuery = null)
{
PlayerActionEvent evt = ScheduleInput(startBeat, timer, inputAction, OnHit, OnMiss, OnBlank, HittableQuery);
evt.noAutoplay = true;
evt.minigame = minigameName;
return evt;
}
//Clean up method used whenever a PlayerActionEvent has finished
public void RemoveScheduledInput(PlayerActionEvent evt)
{
scheduledInputs.Remove(evt);
}
//Get the scheduled input that should happen the **Soonest**
//Can return null if there's no scheduled inputs
// remark: need a check for specific button(s)
[Obsolete("Use GetClosestScheduledInput InputAction or InputAction category instead")]
public PlayerActionEvent GetClosestScheduledInput(InputType input = InputType.ANY)
{
PlayerActionEvent closest = null;
foreach (PlayerActionEvent toCompare in scheduledInputs)
{
// ignore inputs that are for sequencing in autoplay
if (toCompare.autoplayOnly) continue;
if (closest == null)
{
if (input == InputType.ANY || (toCompare.inputType & input) != 0)
closest = toCompare;
}
else
{
double t1 = closest.startBeat + closest.timer;
double t2 = toCompare.startBeat + toCompare.timer;
// Debug.Log("t1=" + t1 + " -- t2=" + t2);
if (t2 < t1)
{
if (input == InputType.ANY || (toCompare.inputType & input) != 0)
closest = toCompare;
}
}
}
return closest;
}
public PlayerActionEvent GetClosestScheduledInput(int[] actionCats)
{
int catIdx = (int)PlayerInput.CurrentControlStyle;
int cat = actionCats[catIdx];
PlayerActionEvent closest = null;
foreach (PlayerActionEvent toCompare in scheduledInputs)
{
// ignore inputs that are for sequencing in autoplay
if (toCompare.autoplayOnly) continue;
if (toCompare.InputAction == null) continue;
if (closest == null)
{
if (toCompare.InputAction.inputLockCategory[catIdx] == cat)
closest = toCompare;
}
else
{
double t1 = closest.startBeat + closest.timer;
double t2 = toCompare.startBeat + toCompare.timer;
if (t2 < t1)
{
if (toCompare.InputAction.inputLockCategory[catIdx] == cat)
closest = toCompare;
}
}
}
return closest;
}
public PlayerActionEvent GetClosestScheduledInput(PlayerInput.InputAction action)
{
return GetClosestScheduledInput(action.inputLockCategory);
}
//Hasn't been tested yet. *Should* work.
//Can be used to detect if the user is expected to input something now or not
//Useful for strict call and responses games like Tambourine
[Obsolete("Use IsExpectingInputNow InputAction or InputAction category instead")]
public bool IsExpectingInputNow(InputType wantInput = InputType.ANY)
{
PlayerActionEvent input = GetClosestScheduledInput(wantInput);
if (input == null) return false;
return input.IsExpectingInputNow();
}
public bool IsExpectingInputNow(int[] wantActionCategory)
{
PlayerActionEvent input = GetClosestScheduledInput(wantActionCategory);
if (input == null) return false;
return input.IsExpectingInputNow();
}
public bool IsExpectingInputNow(PlayerInput.InputAction wantAction)
{
return IsExpectingInputNow(wantAction.inputLockCategory);
}
// now should fix the fast bpm problem
public static double NgEarlyTime(float pitch = -1)
{
if (pitch < 0)
return 1f - ngEarlyTime;
return 1f - (ngEarlyTimeBase * pitch);
}
public static double JustEarlyTime(float pitch = -1)
{
if (pitch < 0)
return 1f - justEarlyTime;
return 1f - (justEarlyTimeBase * pitch);
}
public static double JustLateTime(float pitch = -1)
{
if (pitch < 0)
return 1f + justLateTime;
return 1f + (justLateTimeBase * pitch);
}
public static double NgLateTime(float pitch = -1)
{
if (pitch < 0)
return 1f + ngLateTime;
return 1f + (ngLateTimeBase * pitch);
}
public static double AceEarlyTime(float pitch = -1)
{
if (pitch < 0)
return 1f - aceEarlyTime;
return 1f - (aceEarlyTimeBase * pitch);
}
public static double AceLateTime(float pitch = -1)
{
if (pitch < 0)
return 1f + aceLateTime;
return 1f + (aceLateTimeBase * pitch);
}
public virtual void OnGameSwitch(double beat)
{
}
public virtual void OnTimeChange()
{
}
public virtual void OnPlay(double beat)
{
}
public virtual void OnStop(double beat)
{
foreach (var evt in scheduledInputs)
{
evt.Disable();
}
}
// mainly for bopping logic
public virtual void OnBeatPulse(double beat)
{
}
// added because OnBeatPulse had some animation issues going on
// if your bopping overlaps with other animations, use this instead
public virtual void OnLateBeatPulse(double beat)
{
}
public static MultiSound PlaySoundSequence(string game, string name, double startBeat, params SoundSequence.SequenceParams[] args)
{
Minigames.Minigame gameInfo = GameManager.instance.GetGameInfo(game);
foreach (SoundSequence.SequenceKeyValue pair in gameInfo.LoadedSoundSequences)
{
if (pair.name == name)
{
Debug.Log($"Playing sound sequence {pair.name} at beat {startBeat}");
return pair.sequence.Play(startBeat);
}
}
Debug.LogWarning($"Sound sequence {name} not found in game {game} (did you build AssetBundles?)");
return null;
}
public void ScoreMiss(float weight = 1f)
{
double beat = Conductor.instance?.songPositionInBeatsAsDouble ?? -1;
GameManager.instance.ScoreInputAccuracy(beat, 0, true, NgLateTime(), weight, false);
if (weight > 0)
{
GoForAPerfect.instance.Miss();
SectionMedalsManager.instance.MakeIneligible();
}
}
public void ToggleSplitColoursDisplay(bool on)
{
}
#region Bop
protected enum DefaultBopEnum
{
Off = 0,
On = 1,
}
protected Dictionary<double, int> bopRegion = new();
public bool BeatIsInBopRegion(double beat)
{
if (bopRegion.Count == 0) return true;
int bop = 0;
foreach (var item in bopRegion)
{
if (beat < item.Key) break;
if (beat >= item.Key) bop = item.Value;
}
return (DefaultBopEnum)bop == DefaultBopEnum.On;
}
public int BeatIsInBopRegionInt(double beat)
{
if (bopRegion.Count == 0) return 0;
int bop = 0;
foreach (var item in bopRegion)
{
if (beat < item.Key) break;
if (beat >= item.Key) bop = item.Value;
}
return bop;
}
protected void SetupBopRegion(string gameName, string eventName, string toggleName, bool isBool = true)
{
var allEvents = EventCaller.GetAllInGameManagerList(gameName, new string[] { eventName });
if (allEvents.Count == 0) return;
allEvents.Sort((x, y) => x.beat.CompareTo(y.beat));
foreach (var e in allEvents)
{
if (bopRegion.ContainsKey(e.beat))
{
Debug.Log("Two bops on the same beat, ignoring this one");
continue;
}
if (isBool)
{
bopRegion.Add(e.beat, e[toggleName] ? 1 : 0);
}
else
{
bopRegion.Add(e.beat, e[toggleName]);
}
}
}
protected void AddBopRegionEvents(string gameName, string eventName, bool allowBop)
{
var allEvents = EventCaller.GetAllInGameManagerList(gameName, new string[] { eventName });
foreach (var e in allEvents)
{
bopRegion.Add(e.beat, allowBop ? 1 : 0);
}
bopRegion = bopRegion.OrderBy(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
}
protected void AddBopRegionEventsInt(string gameName, string eventName, int allowBop)
{
var allEvents = EventCaller.GetAllInGameManagerList(gameName, new string[] { eventName });
foreach (var e in allEvents)
{
bopRegion.Add(e.beat, allowBop);
}
bopRegion = bopRegion.OrderBy(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
}
#endregion
private void OnDestroy()
{
foreach (var evt in scheduledInputs)
{
evt.Disable();
}
}
protected void OnDrawGizmos()
{
Gizmos.color = Color.magenta;
Gizmos.DrawWireCube(Vector3.zero, new Vector3(17.77695f, 10, 0));
}
}
}