HeavenStudio/Assets/Scripts/Games/RhythmRally/RhythmRally.cs
minenice55 bb2ae74339
Integration of Jukebox Library (#451)
* add Jukebox library

todo:
- saving / loading of new format
- inferrence of unknown data like past versions
- move the temporary float casts to proper use of double
- make sound related functions take double for timing
- inform people that the Jukebox sound player was renamed to SoundByte lol

* make sound, input scheduling, and super curve use double precision

* successfully load charts

* editor works again

v1 riqs can be saved and loaded

* first tempo and volume markers are unmovable

fix loading of charts' easing values

* use gsync / freesync

* update Jukebox refs to SoundByte

* game events use double part 1

Air Rally - Glee Club converted

* don't load song if chart load fails

* finish conversion of all minigames

* remove editor waveform toggle

* timeline now respects added song offset length

clear cache files on app close
prepped notes for dsp sync

* update timeline length when offset changed

* update to latest Jukebox

* make error panel object in global game manager

* improve conductor music scheduling

* added error message box

fix first game events sometimes not playing
2023-06-10 15:13:29 -04:00

544 lines
21 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NaughtyBezierCurves;
using DG.Tweening;
using HeavenStudio.Util;
namespace HeavenStudio.Games.Loaders
{
using static Minigames;
public static class NtrPingpongLoader
{
public static Minigame AddGame(EventCaller eventCaller) {
return new Minigame("rhythmRally", "Rhythm Rally", "ffffff", false, false, new List<GameAction>()
{
new GameAction("bop", "Bop")
{
function = delegate {var e = eventCaller.currentEntity; RhythmRally.instance.Bop(e.beat, e.length, e["bop"], e["bopAuto"]); },
resizable = true,
parameters = new List<Param>()
{
new Param("bop", true, "Bop", "Will the paddlers bop?"),
new Param("bopAuto", false, "Bop (Auto)", "Will the paddlers auto bop?")
}
},
new GameAction("whistle", "Whistle")
{
preFunction = delegate { RhythmRally.PlayWhistle(eventCaller.currentEntity.beat); },
defaultLength = 0.5f
},
new GameAction("toss ball", "Toss Ball")
{
function = delegate { RhythmRally.instance.Toss(eventCaller.currentEntity.beat, eventCaller.currentEntity.length, 6f, true); },
defaultLength = 2f,
resizable = true
},
new GameAction("rally", "Rally")
{
function = delegate { RhythmRally.instance.Serve(eventCaller.currentEntity.beat, RhythmRally.RallySpeed.Normal); },
defaultLength = 4f,
resizable = true
},
new GameAction("slow rally", "Slow Rally")
{
function = delegate { RhythmRally.instance.Serve(eventCaller.currentEntity.beat, RhythmRally.RallySpeed.Slow); },
defaultLength = 8f,
resizable = true
},
new GameAction("fast rally", "Fast Rally")
{
function = delegate { RhythmRally.instance.PrepareFastRally(eventCaller.currentEntity.beat, RhythmRally.RallySpeed.Fast, eventCaller.currentEntity["muteAudio"]); },
defaultLength = 6f,
parameters = new List<Param>()
{
new Param("muteAudio", false, "Mute Cowbell", "Whether the cowbell sound should play or not.")
}
},
new GameAction("superfast rally", "Superfast Rally")
{
function = delegate { RhythmRally.instance.PrepareFastRally(eventCaller.currentEntity.beat, RhythmRally.RallySpeed.SuperFast, eventCaller.currentEntity["muteAudio"]); },
defaultLength = 12f,
parameters = new List<Param>()
{
new Param("muteAudio", false, "Mute Cowbell", "Whether the cowbell sound should play or not.")
}
},
new GameAction("tonktinktonk", "Tonk-Tink-Tonk (Stretchable)")
{
preFunction = delegate {var e = eventCaller.currentEntity; RhythmRally.TonkTinkTonkStretchable(e.beat, e.length); },
defaultLength = 4f,
resizable = true
},
new GameAction("superfast stretchable", "Superfast Rally (Stretchable)")
{
function = delegate { var e = eventCaller.currentEntity; RhythmRally.instance.SuperFastRallyStretchable(e.beat, e.length); },
defaultLength = 8f,
resizable = true
},
new GameAction("pose", "End Pose")
{
function = delegate { RhythmRally.instance.Pose(); },
defaultLength = 0.5f
},
new GameAction("camera", "Camera Controls")
{
function = delegate {
var e = eventCaller.currentEntity;
var rotation = new Vector3(0, e["valA"], 0);
RhythmRally.instance.ChangeCameraAngle(rotation, e["valB"], e.length, (Ease)e["type"], (RotateMode)e["type2"]);
},
defaultLength = 4,
resizable = true,
parameters = new List<Param>() {
new Param("valA", new EntityTypes.Integer(-360, 360, 0), "Angle", "The rotation of the camera around the center of the table"),
new Param("valB", new EntityTypes.Float(0.5f, 4f, 1), "Zoom", "The camera's level of zoom (Lower value = Zoomed in)"),
new Param("type", Ease.Linear, "Ease", "The easing function to use"),
new Param("type2", RotateMode.Fast, "Rotation Mode", "The rotation mode to use")
}
},
},
new List<string>() {"ntr", "keep"},
"ntrpingpong", "en",
new List<string>() {}
);
}
}
}
namespace HeavenStudio.Games
{
using Scripts_RhythmRally;
public class RhythmRally : Minigame
{
public enum RallySpeed { Slow, Normal, Fast, SuperFast }
[Header("Camera")]
[SerializeField] Transform cameraPivot;
[SerializeField] Transform cameraPos;
[SerializeField] float cameraFOV;
[Header("Ball and curve info")]
public GameObject ball;
public GameObject ballShadow;
public TrailRenderer ballTrail;
public BezierCurve3D serveCurve;
public BezierCurve3D returnCurve;
public BezierCurve3D tossCurve;
public BezierCurve3D missCurve;
public GameObject ballHitFX;
[Header("Animators")]
public Animator playerAnim;
public Animator opponentAnim;
[Header("Properties")]
public RallySpeed rallySpeed = RallySpeed.Normal;
public bool started;
public bool missed;
public bool served;
public bool tossing;
public double serveBeat;
public double targetBeat;
public double tossBeat;
public double missBeat;
public float tossLength;
private bool inPose;
public Paddlers paddlers;
public GameEvent bop = new GameEvent();
private bool goBop = true;
public static RhythmRally instance;
private void Awake()
{
instance = this;
paddlers.Init();
playerAnim.Play("Idle", 0, 0);
opponentAnim.Play("Idle", 0, 0);
}
const float tableHitTime = 0.58f;
bool opponentServing = false; // Opponent serving this frame?
void Update()
{
var cond = Conductor.instance;
var currentBeat = cond.songPositionInBeatsAsDouble;
var hitBeat = serveBeat; // Beat when the last paddler hit the ball
var beatDur1 = 1f; // From paddle to table
var beatDur2 = 1f; // From table to other paddle
var playerState = playerAnim.GetCurrentAnimatorStateInfo(0);
var opponentState = opponentAnim.GetCurrentAnimatorStateInfo(0);
if (started)
{
// Determine hitBeat and beatDurs.
switch (rallySpeed)
{
case RallySpeed.Normal:
if (!served)
{
hitBeat = serveBeat + 2f;
}
break;
case RallySpeed.Fast:
if (!served)
{
hitBeat = serveBeat + 1f;
beatDur1 = 1f;
beatDur2 = 2f;
}
else
{
beatDur1 = 0.5f;
beatDur2 = 0.5f;
}
break;
case RallySpeed.SuperFast:
if (!served)
{
hitBeat = serveBeat + 1f;
}
beatDur1 = 0.5f;
beatDur2 = 0.5f;
break;
case RallySpeed.Slow:
if (!served)
{
hitBeat = serveBeat + 4f;
}
beatDur1 = 2f;
beatDur2 = 2f;
break;
}
// Ball position.
var curveToUse = served ? serveCurve : returnCurve;
float curvePosition;
var hitPosition1 = cond.GetPositionFromBeat(hitBeat, beatDur1);
if (hitPosition1 >= 1f)
{
var hitPosition2 = cond.GetPositionFromBeat(hitBeat + beatDur1, beatDur2);
curvePosition = tableHitTime + hitPosition2 * (1f - tableHitTime);
}
else
{
curvePosition = hitPosition1 * tableHitTime;
}
if (!missed)
{
float curveHeight = 1.25f;
if ((rallySpeed == RallySpeed.Fast && served) || rallySpeed == RallySpeed.SuperFast)
curveHeight = 0.75f;
else if (rallySpeed == RallySpeed.Fast && !served && hitPosition1 >= 1f)
curveHeight = 2f;
else if (rallySpeed == RallySpeed.Slow)
curveHeight = 3f;
curveToUse.transform.localScale = new Vector3(1f, curveHeight, 1f);
ball.transform.position = curveToUse.GetPoint(Mathf.Max(0, curvePosition));
// Make ball inactive before it passes through the floor.
if (curvePosition > 1.05f)
ball.SetActive(false);
}
else
{
if (tossing)
{
TossUpdate(tossBeat, tossLength);
}
else
{
var missPosition = cond.GetPositionFromBeat(missBeat, 1f);
ball.transform.position = missCurve.GetPoint(Mathf.Max(0, missPosition));
if (missPosition > 1f)
ball.SetActive(false);
}
}
// TODO: Make conditional so ball shadow only appears when over table.
ballShadow.transform.position = new Vector3(ball.transform.position.x, -0.399f, ball.transform.position.z);
var timeBeforeNextHit = hitBeat + beatDur1 + beatDur2 - currentBeat;
// Check if the opponent should swing.
if (!served && timeBeforeNextHit <= 0f)
{
var rallies = GameManager.instance.Beatmap.Entities.FindAll(c => c.datamodel == "rhythmRally/rally" || c.datamodel == "rhythmRally/slow rally");
for (int i = 0; i < rallies.Count; i++)
{
var rally = rallies[i];
if (rally.beat - currentBeat <= 0f && rally.beat + rally.length - currentBeat > 0f)
{
Serve(hitBeat + beatDur1 + beatDur2, rallySpeed);
opponentServing = true;
break;
}
}
}
// Check if paddler should do ready animation.
bool readyToPrep;
switch (rallySpeed)
{
case RallySpeed.Slow:
case RallySpeed.Fast:
readyToPrep = timeBeforeNextHit <= 2f;
break;
case RallySpeed.SuperFast:
readyToPrep = timeBeforeNextHit <= 0.5f;
break;
default:
readyToPrep = timeBeforeNextHit <= 1f;
break;
}
// Paddler ready animation.
if (readyToPrep && !opponentServing && !inPose)
{
if (served)
{
if ((playerState.IsName("Swing") && playerAnim.IsAnimationNotPlaying()) || (!playerState.IsName("Swing") && !playerState.IsName("Ready1")))
playerAnim.Play("Ready1");
}
else if (!opponentServing)
{
if ((opponentState.IsName("Swing") && opponentAnim.IsAnimationNotPlaying()) || (!opponentState.IsName("Swing") && !opponentState.IsName("Ready1")))
{
opponentAnim.Play("Ready1");
// Toss ball if it fell off the table.
if (missed && !tossing)
{
float tossHeight = 3f;
if (rallySpeed == RallySpeed.Slow || rallySpeed == RallySpeed.Fast)
tossHeight = 6f;
Toss(hitBeat + beatDur1, beatDur2, tossHeight);
}
}
// If player never swung and is still in ready state, snap them out of it.
if (missed && playerState.IsName("Ready1"))
playerAnim.Play("Beat");
}
}
}
else
{
if (tossing)
{
TossUpdate(tossBeat, tossLength);
}
}
// Paddler bop animation.
if (cond.ReportBeat(ref bop.lastReportedBeat, bop.startBeat % 1))
{
if (goBop && !inPose)
{
BopSingle();
}
}
opponentServing = false;
//update camera
GameCamera.additionalPosition = cameraPos.position + (Quaternion.Euler(cameraPos.rotation.eulerAngles) * Vector3.forward * 10f);
GameCamera.additionalRotEluer = cameraPos.rotation.eulerAngles;
GameCamera.additionalFoV = cameraFOV;
}
public void Bop(double beat, float length, bool bop, bool bopAuto)
{
goBop = bopAuto;
if (bop)
{
for (int i = 0; i < length; i++)
{
BeatAction.New(instance.gameObject, new List<BeatAction.Action>()
{
new BeatAction.Action(beat + i, delegate
{
BopSingle();
})
});
}
}
}
void BopSingle()
{
var playerState = playerAnim.GetCurrentAnimatorStateInfo(0);
var opponentState = opponentAnim.GetCurrentAnimatorStateInfo(0);
bool playerPrepping = false; // Player using prep animation?
bool opponentPrepping = false; // Opponent using prep animation?
if (!playerPrepping && (playerAnim.IsAnimationNotPlaying() || playerState.IsName("Idle") || playerState.IsName("Beat")))
playerAnim.DoScaledAnimationAsync("Beat", 0.5f);
if (!opponentPrepping && !opponentServing && !tossing && (opponentAnim.IsAnimationNotPlaying() || opponentState.IsName("Idle") || opponentState.IsName("Beat")))
opponentAnim.DoScaledAnimationAsync("Beat", 0.5f);
}
public void Serve(double beat, RallySpeed speed)
{
if (!ball.activeSelf)
ball.SetActive(true);
if (!ballTrail.gameObject.activeSelf)
ballTrail.gameObject.SetActive(true);
served = true;
missed = false;
started = true;
opponentServing = true;
tossing = false;
serveBeat = beat;
rallySpeed = speed;
double bounceBeat = 0f;
switch (rallySpeed)
{
case RallySpeed.Normal:
targetBeat = 2f;
bounceBeat = serveBeat + 1f;
break;
case RallySpeed.Fast:
case RallySpeed.SuperFast:
targetBeat = 1f;
bounceBeat = serveBeat + 0.5f;
break;
case RallySpeed.Slow:
targetBeat = 4f;
bounceBeat = serveBeat + 2f;
break;
}
opponentAnim.DoScaledAnimationAsync("Swing", 0.5f);
MultiSound.Play(new MultiSound.Sound[] { new MultiSound.Sound("rhythmRally/Serve", serveBeat), new MultiSound.Sound("rhythmRally/ServeBounce", bounceBeat) });
paddlers.BounceFX(bounceBeat);
ScheduleInput(serveBeat, targetBeat, InputType.STANDARD_DOWN, paddlers.Just, paddlers.Miss, paddlers.Out);
}
public void Toss(double beat, float length, float height, bool firstToss = false)
{
// Hide trail while tossing to prevent weirdness while teleporting ball.
ballTrail.gameObject.SetActive(false);
if (firstToss)
height *= length/2f;
tossCurve.transform.localScale = new Vector3(1f, height, 1f);
tossBeat = beat;
tossLength = length;
tossing = true;
if (firstToss)
{
opponentAnim.Play("Ready1");
}
if (!ball.activeSelf)
ball.SetActive(true);
}
private void TossUpdate(double beat, float duration)
{
var tossPosition = Conductor.instance.GetPositionFromBeat(beat, duration);
ball.transform.position = tossCurve.GetPoint(Mathf.Clamp(tossPosition, 0, 1));
if (tossPosition > 1.05f)
ball.SetActive(false);
}
public static void PlayWhistle(double beat)
{
MultiSound.Play(new MultiSound.Sound[]
{
new MultiSound.Sound("rhythmRally/Whistle", beat),
}, forcePlay: true);
}
public void Pose()
{
playerAnim.Play("Pose", 0, 0);
opponentAnim.Play("Pose", 0, 0);
ball.SetActive(false); // temporary solution, should realistically just fall down
inPose = true;
}
public void ChangeCameraAngle(Vector3 rotation, float camZoom, float length, Ease ease, RotateMode rotateMode)
{
var len = length * Conductor.instance.secPerBeat;
cameraPivot.DORotate(rotation, len, rotateMode).SetEase(ease);
cameraPivot.DOScale(camZoom, len).SetEase(ease);
}
public void PrepareFastRally(double beat, RallySpeed speedChange, bool muteAudio = false)
{
if (speedChange == RallySpeed.Fast)
{
BeatAction.New(gameObject, new List<BeatAction.Action>()
{
new BeatAction.Action(beat + 2f, delegate { Serve(beat + 2f, RallySpeed.Fast); })
});
if (muteAudio) return;
TonkTinkTonkStretchable(beat, 1.5f);
}
else if (speedChange == RallySpeed.SuperFast)
{
SuperFastRallyStretchable(beat + 4f, 8f);
if (muteAudio) return;
TonkTinkTonkStretchable(beat, 4f);
}
}
public static void TonkTinkTonkStretchable(double beat, float length)
{
List<MultiSound.Sound> soundsToPlay = new List<MultiSound.Sound>();
bool tink = false;
for (float i = 0; i < length; i += 0.5f)
{
soundsToPlay.Add(new MultiSound.Sound(tink ? "rhythmRally/Tink" : "rhythmRally/Tonk", beat + i));
tink = !tink;
}
MultiSound.Play(soundsToPlay.ToArray(), forcePlay: true);
}
public void SuperFastRallyStretchable(double beat, float length)
{
List<BeatAction.Action> servesToPerform = new List<BeatAction.Action>();
for (int i = 0; i < length; i += 2)
{
double beatToSpawn = beat + i;
servesToPerform.Add( new BeatAction.Action(beatToSpawn, delegate { Serve(beatToSpawn, RallySpeed.SuperFast); }) );
}
BeatAction.New(gameObject, servesToPerform);
}
}
}