HeavenStudio/Assets/Scripts/Games/Tambourine/Tambourine.cs
minenice55 60d29f19c6
Timekeeping Improvements and Small Optimizations (#544)
* make BeatActions coroutines instead of componentrs

* pooled scheduled sounds

implement S' entity seek

* remove debug prints from last two changes

* implement absolute time tracking

implement DSP time resyncing

* optimize GameManager

* update TMPro

* update IDE packages

* fix dsp sync making the drift worse

* fix issue with the JSL dll

* relocate debug print

* make scheduled pitch setter functional

* any cpu
2023-09-11 22:28:04 +00:00

511 lines
19 KiB
C#

using HeavenStudio.Util;
using System;
using System.Collections.Generic;
using UnityEngine;
using Jukebox;
namespace HeavenStudio.Games.Loaders
{
using static Minigames;
public static class RvlTambourineLoader
{
public static Minigame AddGame(EventCaller eventCaller)
{
return new Minigame("tambourine", "Tambourine", "388cd0", false, false, new List<GameAction>()
{
new GameAction("beat intervals", "Start Interval")
{
preFunction = delegate {var e = eventCaller.currentEntity; Tambourine.PreInterval(e.beat, e.length, e["auto"]); },
defaultLength = 7f,
resizable = true,
parameters = new List<Param>()
{
new Param("auto", true, "Auto Pass Turn")
}
},
new GameAction("shake", "Shake")
{
defaultLength = 0.5f,
},
new GameAction("hit", "Hit")
{
defaultLength = 0.5f,
},
new GameAction("pass turn", "Pass Turn")
{
preFunction = delegate
{
Tambourine.PrePassTurn(eventCaller.currentEntity.beat);
},
resizable = true,
preFunctionLength = 1f
},
new GameAction("bop", "Bop")
{
function = delegate {var e = eventCaller.currentEntity; Tambourine.instance.Bop(e.beat, e.length, e["whoBops"], e["whoBopsAuto"]); },
parameters = new List<Param>()
{
new Param("whoBops", Tambourine.WhoBops.Both, "Who Bops", "Who will bop."),
new Param("whoBopsAuto", Tambourine.WhoBops.None, "Who Bops (Auto)", "Who will auto bop."),
},
resizable = true,
priority = 4
},
new GameAction("success", "Success")
{
function = delegate {var e = eventCaller.currentEntity; Tambourine.instance.SuccessFace(e.beat); },
defaultLength = 1f,
priority = 4,
},
new GameAction("fade background", "Background Color")
{
function = delegate {var e = eventCaller.currentEntity; Tambourine.instance.BackgroundColor(e.beat, e.length, e["colorStart"], e["colorEnd"], e["ease"]); },
defaultLength = 4f,
resizable = true,
parameters = new List<Param>()
{
new Param("colorStart", Color.white, "Start Color", "The starting color of the fade."),
new Param("colorEnd", Tambourine.defaultBGColor, "End Color", "The ending color of the fade."),
new Param("ease", Util.EasingFunction.Ease.Linear, "Ease")
}
},
},
new List<string>() {"rvl", "repeat"},
"rvldrum", "en",
new List<string>() {}
);
}
}
}
namespace HeavenStudio.Games
{
public class Tambourine : Minigame
{
private static Color _defaultBGColor;
public static Color defaultBGColor
{
get
{
ColorUtility.TryParseHtmlString("#388cd0", out _defaultBGColor);
return _defaultBGColor;
}
}
[Header("Components")]
[SerializeField] Animator handsAnimator;
[SerializeField] SpriteRenderer bg;
[SerializeField] Animator monkeyAnimator;
[SerializeField] ParticleSystem flowerParticles;
[SerializeField] GameObject happyFace;
[SerializeField] GameObject sadFace;
[SerializeField] Animator sweatAnimator;
[SerializeField] Animator frogAnimator;
[Header("Variables")]
float misses;
bool frogPresent;
bool monkeyGoBop;
bool handsGoBop;
public GameEvent bop = new GameEvent();
public enum WhoBops
{
Monkey,
Player,
Both,
None
}
public static Tambourine instance;
void Awake()
{
instance = this;
sweatAnimator.Play("NoSweat", 0, 0);
frogAnimator.Play("FrogExited", 0, 0);
handsAnimator.Play("Idle", 0, 0);
monkeyAnimator.Play("MonkeyIdle", 0, 0);
colorStart = defaultBGColor;
colorEnd = defaultBGColor;
}
void Update()
{
BackgroundColorUpdate();
if (Conductor.instance.ReportBeat(ref bop.lastReportedBeat, bop.startBeat % 1))
{
if (monkeyGoBop)
{
monkeyAnimator.Play("MonkeyBop", 0, 0);
}
if (handsGoBop)
{
handsAnimator.Play("Bop", 0, 0);
}
}
if (PlayerInput.Pressed() && !IsExpectingInputNow(InputType.STANDARD_DOWN))
{
handsAnimator.Play("Shake", 0, 0);
SoundByte.PlayOneShotGame($"tambourine/player/shake/{UnityEngine.Random.Range(1, 6)}");
sweatAnimator.Play("Sweating", 0, 0);
SummonFrog();
ScoreMiss();
if (!IntervalIsGoingOn())
{
sadFace.SetActive(true);
}
}
else if (PlayerInput.AltPressed() && !IsExpectingInputNow(InputType.STANDARD_ALT_DOWN))
{
handsAnimator.Play("Smack", 0, 0);
SoundByte.PlayOneShotGame($"tambourine/player/hit/{UnityEngine.Random.Range(1, 6)}");
sweatAnimator.Play("Sweating", 0, 0);
SummonFrog();
ScoreMiss();
if (!IntervalIsGoingOn())
{
sadFace.SetActive(true);
}
}
if (passedTurns.Count > 0)
{
foreach (var pass in passedTurns)
{
PassTurnStandalone(pass);
}
passedTurns.Clear();
}
}
private bool IntervalIsGoingOn()
{
double beat = Conductor.instance.songPositionInBeats;
return EventCaller.GetAllInGameManagerList("tambourine", new string[] { "beat intervals" }).Find(x => beat >= x.beat && beat < x.beat + x.length) != null;
}
private List<RiqEntity> GetAllInputsBetweenBeat(double beat, double endBeat)
{
return EventCaller.GetAllInGameManagerList("tambourine", new string[] { "shake", "hit" }).FindAll(x => x.beat >= beat && x.beat < endBeat);
}
public static void PreInterval(double beat, float interval, bool autoPassTurn)
{
if (GameManager.instance.currentGame == "tambourine")
{
instance.StartInterval(beat, interval, beat, autoPassTurn);
}
else
{
queuedIntervals.Add(new QueuedInterval()
{
beat = beat,
interval = interval,
autoPassTurn = autoPassTurn
});
}
}
public override void OnGameSwitch(double beat)
{
if (queuedIntervals.Count > 0)
{
foreach (var interval in queuedIntervals)
{
StartInterval(interval.beat, interval.interval, beat, interval.autoPassTurn);
}
queuedIntervals.Clear();
}
PersistColor(beat);
}
private struct QueuedInterval
{
public double beat;
public float interval;
public bool autoPassTurn;
}
private static List<QueuedInterval> queuedIntervals = new();
private void StartInterval(double beat, float interval, double gameSwitchBeat, bool autoPassTurn)
{
List<BeatAction.Action> actions = new()
{
new BeatAction.Action(beat, delegate
{
DesummonFrog();
sadFace.SetActive(false);
})
};
var relevantInputs = GetAllInputsBetweenBeat(beat, beat + interval);
relevantInputs.Sort((x, y) => x.beat.CompareTo(y.beat));
for (int i = 0; i < relevantInputs.Count; i++)
{
bool isHit = relevantInputs[i].datamodel == "tambourine/hit";
double inputBeat = relevantInputs[i].beat;
if (inputBeat >= gameSwitchBeat)
{
actions.Add(new BeatAction.Action(inputBeat, delegate
{
MonkeyInput(inputBeat, isHit);
}));
}
}
BeatAction.New(this, actions);
if (autoPassTurn)
{
PassTurn(beat + interval, beat, interval, 1);
}
}
public void MonkeyInput(double beat, bool hit)
{
if (hit)
{
monkeyAnimator.DoScaledAnimationAsync("MonkeySmack", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/monkey/hit/{UnityEngine.Random.Range(1, 6)}");
}
else
{
monkeyAnimator.DoScaledAnimationAsync("MonkeyShake", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/monkey/shake/{UnityEngine.Random.Range(1, 6)}");
}
}
private RiqEntity GetLastIntervalBeforeBeat(double beat)
{
return EventCaller.GetAllInGameManagerList("tambourine", new string[] { "beat intervals" }).FindLast(x => x.beat <= beat);
}
public static void PrePassTurn(double beat)
{
if (GameManager.instance.currentGame == "tambourine")
{
instance.PassTurnStandalone(beat);
}
else
{
passedTurns.Add(beat);
}
}
private static List<double> passedTurns = new();
private void PassTurnStandalone(double beat)
{
var lastInterval = GetLastIntervalBeforeBeat(beat);
float length = EventCaller.GetAllInGameManagerList("tambourine", new string[] { "pass turn" }).Find(x => x.beat == beat).length;
if (lastInterval != null) PassTurn(beat, lastInterval.beat, lastInterval.length, length);
}
private void PassTurn(double beat, double intervalBeat, float intervalLength, float length)
{
SoundByte.PlayOneShotGame($"tambourine/monkey/turnPass/{UnityEngine.Random.Range(1, 6)}", beat);
List<BeatAction.Action> actions = new()
{
new BeatAction.Action(beat, delegate
{
monkeyAnimator.DoScaledAnimationAsync("MonkeyPassTurn", 0.5f);
happyFace.SetActive(true);
}),
new BeatAction.Action(beat + 0.3f, delegate { happyFace.SetActive(false); })
};
var relevantInputs = GetAllInputsBetweenBeat(intervalBeat, intervalBeat + intervalLength);
relevantInputs.Sort((x, y) => x.beat.CompareTo(y.beat));
for (int i = 0; i < relevantInputs.Count; i++)
{
bool isHit = relevantInputs[i].datamodel == "tambourine/hit";
double inputBeat = relevantInputs[i].beat - intervalBeat;
actions.Add(new BeatAction.Action(inputBeat, delegate
{
if (isHit) ScheduleInput(beat + length, inputBeat, InputType.STANDARD_ALT_DOWN, JustHit, Miss, Nothing);
else ScheduleInput(beat + length, inputBeat, InputType.STANDARD_DOWN, JustShake, Miss, Nothing);
Bop(beat + length + inputBeat, 1, (int)WhoBops.Monkey, (int)WhoBops.None);
}));
}
BeatAction.New(this, actions);
}
public void Bop(double beat, float length, int whoBops, int whoBopsAuto)
{
monkeyGoBop = whoBopsAuto == (int)WhoBops.Monkey || whoBopsAuto == (int)WhoBops.Both;
handsGoBop = whoBopsAuto == (int)WhoBops.Player || whoBopsAuto == (int)WhoBops.Both;
for (int i = 0; i < length; i++)
{
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(beat + i, delegate
{
switch (whoBops)
{
case (int) WhoBops.Monkey:
monkeyAnimator.DoScaledAnimationAsync("MonkeyBop", 0.5f);
break;
case (int) WhoBops.Player:
handsAnimator.DoScaledAnimationAsync("Bop", 0.5f);
break;
case (int) WhoBops.Both:
monkeyAnimator.DoScaledAnimationAsync("MonkeyBop", 0.5f);
handsAnimator.DoScaledAnimationAsync("Bop", 0.5f);
break;
default:
break;
}
})
});
}
}
public void SuccessFace(double beat)
{
DesummonFrog();
if (misses > 0) return;
flowerParticles.Play();
SoundByte.PlayOneShotGame($"tambourine/player/turnPass/sweep");
MultiSound.Play(new MultiSound.Sound[]
{
new MultiSound.Sound("tambourine/player/turnPass/note1", beat),
new MultiSound.Sound("tambourine/player/turnPass/note2", beat + 0.1f),
new MultiSound.Sound("tambourine/player/turnPass/note3", beat + 0.2f),
new MultiSound.Sound("tambourine/player/turnPass/note3", beat + 0.3f),
}, forcePlay: true);
happyFace.SetActive(true);
BeatAction.New(instance, new List<BeatAction.Action>()
{
new BeatAction.Action(beat + 1, delegate { happyFace.SetActive(false); }),
});
}
public void JustHit(PlayerActionEvent caller, float state)
{
if (state >= 1f || state <= -1f)
{
handsAnimator.DoScaledAnimationAsync("Smack", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/player/hit/{UnityEngine.Random.Range(1, 6)}");
SoundByte.PlayOneShotGame("tambourine/miss");
sweatAnimator.DoScaledAnimationAsync("Sweating", 0.5f);
misses++;
if (!IntervalIsGoingOn())
{
sadFace.SetActive(true);
}
return;
}
Success(true);
}
public void JustShake(PlayerActionEvent caller, float state)
{
if (state >= 1f || state <= -1f)
{
handsAnimator.DoScaledAnimationAsync("Shake", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/player/shake/{UnityEngine.Random.Range(1, 6)}");
SoundByte.PlayOneShotGame("tambourine/miss");
sweatAnimator.DoScaledAnimationAsync("Sweating", 0.5f);
misses++;
if (!IntervalIsGoingOn())
{
sadFace.SetActive(true);
}
return;
}
Success(false);
}
public void Success(bool hit)
{
sadFace.SetActive(false);
if (hit)
{
handsAnimator.DoScaledAnimationAsync("Smack", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/player/hit/{UnityEngine.Random.Range(1, 6)}");
}
else
{
handsAnimator.DoScaledAnimationAsync("Shake", 0.5f);
SoundByte.PlayOneShotGame($"tambourine/player/shake/{UnityEngine.Random.Range(1, 6)}");
}
}
public void Miss(PlayerActionEvent caller)
{
SummonFrog();
sweatAnimator.DoScaledAnimationAsync("Sweating", 0.5f);
misses++;
if (!IntervalIsGoingOn())
{
sadFace.SetActive(true);
}
}
private double colorStartBeat = -1;
private float colorLength = 0f;
private Color colorStart = Color.white; //obviously put to the default color of the game
private Color colorEnd = Color.white;
private Util.EasingFunction.Ease colorEase; //putting Util in case this game is using jukebox
//call this in update
private void BackgroundColorUpdate()
{
float normalizedBeat = Mathf.Clamp01(Conductor.instance.GetPositionFromBeat(colorStartBeat, colorLength));
var func = Util.EasingFunction.GetEasingFunction(colorEase);
float newR = func(colorStart.r, colorEnd.r, normalizedBeat);
float newG = func(colorStart.g, colorEnd.g, normalizedBeat);
float newB = func(colorStart.b, colorEnd.b, normalizedBeat);
bg.color = new Color(newR, newG, newB);
}
public void BackgroundColor(double beat, float length, Color colorStartSet, Color colorEndSet, int ease)
{
colorStartBeat = beat;
colorLength = length;
colorStart = colorStartSet;
colorEnd = colorEndSet;
colorEase = (Util.EasingFunction.Ease)ease;
}
//call this in OnPlay(double beat) and OnGameSwitch(double beat)
private void PersistColor(double beat)
{
var allEventsBeforeBeat = EventCaller.GetAllInGameManagerList("tambourine", new string[] { "fade background" }).FindAll(x => x.beat < beat);
if (allEventsBeforeBeat.Count > 0)
{
allEventsBeforeBeat.Sort((x, y) => x.beat.CompareTo(y.beat)); //just in case
var lastEvent = allEventsBeforeBeat[^1];
BackgroundColor(lastEvent.beat, lastEvent.length, lastEvent["colorStart"], lastEvent["colorEnd"], lastEvent["ease"]);
}
}
public override void OnPlay(double beat)
{
PersistColor(beat);
}
public void SummonFrog()
{
if (frogPresent) return;
SoundByte.PlayOneShotGame("tambourine/frog");
frogAnimator.Play("FrogEnter", 0, 0);
frogPresent = true;
}
public void DesummonFrog()
{
if (!frogPresent) return;
frogAnimator.Play("FrogExit", 0, 0);
frogPresent = false;
}
public void Nothing(PlayerActionEvent caller) {}
}
}