Jamie Greunbaum de0a866e6d - Lightning round is essentially complete, with scoring fully implemented.
- Podium name tag can no longer be blocked by score card.
- Choice cards don't display text for anyone but the owner until locked in.
- Minor corrections to the ACME Crimenet logo.
2025-06-04 19:28:17 -04:00

635 lines
18 KiB
C#

using UdonSharp;
using UnityEngine;
using VRC.SDK3.Data;
using VRC.SDK3.UdonNetworkCalling;
using VRC.Udon.Common.Interfaces;
using VRC.SDKBase;
using VRC.SDK3.StringLoading;
using TMPro;
using VRC.SDK3.Components;
public enum QuestionType
{
None,
MultipleChoice,
LightningRound,
DumpsterDive,
TheChase,
FinalRound,
Tiebreaker
}
public enum MusicEventType
{
None,
WhereInTheWorld,
RockapellaIdent
}
public enum SFXEventType
{
None,
Ding,
Buzzer
}
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class GameManager : UdonSharpBehaviour
{
[UdonSynced] private bool _BuzzInAllowed = false;
[UdonSynced] private bool[] _PlayerBuzzInAllowed;
[UdonSynced] private int _BuzzedInPlayer = 0;
private DataList _QuestionsList = new DataList();
private int _QuestionIndex = 0;
private DataDictionary _CurrentQuestion;
private int _LightningRoundQuestionIndex = 0;
[UdonSynced] private int _QuestionStage = 0;
[UdonSynced] private int _QuestionCorrectResponse = 0;
[SerializeField] private PlayerPodium[] _PlayerPodiums;
[UdonSynced] public VRCUrl QuestionURL;
[SerializeField] private HostCardPregameInterface _PregameInterface;
[SerializeField] private HostCardMultipleChoiceInterface _MultipleChoiceInterface;
[SerializeField] private HostCardLightningRoundInterface _LightningRoundInterface;
[Header("Audio")]
[SerializeField] private AudioClip _Ding = null;
[SerializeField] private AudioClip _Buzzer = null;
[SerializeField] private AudioClip _WhereInTheWorld = null;
[SerializeField] private AudioClip _RockapellaIdent = null;
[SerializeField] private AudioSource _MusicPlayer = null;
[SerializeField] private AudioSource _SFXPlayer = null;
void Start()
{
_PlayerBuzzInAllowed = new bool[_PlayerPodiums.Length];
ResetBuzzers();
// Download our test question.
VRCStringDownloader.LoadUrl(QuestionURL, (IUdonEventReceiver)this);
}
public override void OnStringLoadSuccess(IVRCStringDownload DownloadedString)
{
string JSONString = DownloadedString.Result;
if (VRCJson.TryDeserializeFromJson(JSONString, out DataToken JSONResult))
{
if (JSONResult.TokenType == TokenType.DataList)
{
_QuestionsList.Clear();
_QuestionIndex = 0;
for (int i = 0; i < JSONResult.DataList.Count; i++)
{
if (JSONResult.DataList[i].TokenType == TokenType.DataDictionary)
{
_QuestionsList.Add(JSONResult.DataList[i]);
}
}
if (_QuestionsList.Count == 0)
{
_PregameInterface.HeaderUI.text = "Unable to find any questions. Ensure the root array elements are all objects.";
return;
}
_CurrentQuestion = _QuestionsList[0].DataDictionary;
_PregameInterface.HeaderUI.text = "Found " + _QuestionsList.Count + " questions in this case file.";
}
else
{
Debug.LogError("Malformed case file. Ensure the first element is an array of objects.");
}
}
}
private void NewMultipleChoiceQuestion()
{
_MultipleChoiceInterface.HeaderUI.text = "Multiple Choice";
DataList ClueStrings = _CurrentQuestion["Clues"].DataList;
for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length && i < ClueStrings.Count; i++)
{
_MultipleChoiceInterface.CluesUI[i].text = ClueStrings[i].ToString();
}
DataList Choices = _CurrentQuestion["Choices"].DataList;
for (int i = 0; i < Choices.Count && i < _MultipleChoiceInterface.ChoiceUI.Length; i++)
{
_MultipleChoiceInterface.ChoiceUI[i].text = Choices[i].ToString();
}
_QuestionCorrectResponse = (int)_CurrentQuestion["Correct Response"].Number;
EnableHostCardDisplay(QuestionType.MultipleChoice);
EnableInteraction("Reveal Choice 1");
}
private void MultipleChoiceRevealChoices()
{
SendCustomEvent(nameof(MultipleChoiceRevealChoice1));
SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice2), 1.25f);
SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice3), 2.5f);
}
public void MultipleChoiceRevealChoice1()
{
_MultipleChoiceInterface.ChoiceButtonImages[0].color = (_QuestionCorrectResponse == 1) ? Color.green : Color.red;
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, As5);
EnableInteraction("Reveal Choice 2");
}
public void MultipleChoiceRevealChoice2()
{
_MultipleChoiceInterface.ChoiceButtonImages[1].color = (_QuestionCorrectResponse == 2) ? Color.green : Color.red;
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, C6);
EnableInteraction("Reveal Choice 3");
}
public void MultipleChoiceRevealChoice3()
{
_MultipleChoiceInterface.ChoiceButtonImages[2].color = (_QuestionCorrectResponse == 3) ? Color.green : Color.red;
DataList Choices = _CurrentQuestion["Choices"].DataList;
EnableChoiceCards();
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
int[] Indices = { 0, 1, 2 };
int[] ChoiceOrder = { -1, -1, -1 };
int Choice1Index = Indices[Random.Range(0, 3)];
ChoiceOrder[0] = Choice1Index;
Indices[Choice1Index] = -1;
int Choice2Index = -1;
while (Choice2Index == -1) { Choice2Index = Indices[Random.Range(0, 3)]; }
ChoiceOrder[1] = Choice2Index;
Indices[Choice2Index] = -1;
int Choice3Index = -1;
while (Choice3Index == -1) { Choice3Index = Indices[Random.Range(0, 3)]; }
ChoiceOrder[2] = Choice3Index;
Indices[Choice3Index] = -1;
string[] ChoiceStrings = { Choices[0].ToString(), Choices[1].ToString(), Choices[2].ToString() };
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "SetCardChoices", ChoiceStrings, ChoiceOrder);
}
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, D6);
EnableInteraction("Lock Answers");
}
private void MultipleChoiceLockAnswers()
{
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "LockInChoice");
}
_MultipleChoiceInterface.HeaderUI.text = "LOCKED IN";
for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length; i++)
{
_MultipleChoiceInterface.CluesUI[i].text = "";
}
for (int i = 0; i < _MultipleChoiceInterface.ChoiceUI.Length; i++)
{
if (i != (_QuestionCorrectResponse - 1))
{
_MultipleChoiceInterface.ChoiceUI[i].text = "";
}
}
EnableInteraction("Reveal Answers And Assign Points");
}
private void MultipleChoiceRevealAnswersAndAssignPoints()
{
_MultipleChoiceInterface.HeaderUI.text = "ANSWER REVEALED";
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent(
(IUdonEventReceiver)_PlayerPodiums[i],
NetworkEventTarget.All,
"VerifyMultipleChoiceResponse", _QuestionCorrectResponse);
}
EnableInteraction("Next Question");
}
private void BeginLightningRound()
{
_LightningRoundQuestionIndex = 0;
_LightningRoundInterface.HeaderUI.text = "Lightning Round | " + _CurrentQuestion["Location"].ToString();
_LightningRoundInterface.QuestionUI.text = "";
for (int i = 0; i < _LightningRoundInterface.ChoiceUI.Length && i < _LightningRoundInterface.ChoiceButtons.Length; i++)
{
_LightningRoundInterface.ChoiceUI[i].text = "";
_LightningRoundInterface.ChoiceButtonImages[i].color = Color.red;
}
EnableHostCardDisplay(QuestionType.LightningRound);
EnableBuzzers();
EnableInteraction("Next Question");
}
private void NewLightningRoundQuestion(int Question)
{
_LightningRoundQuestionIndex = Question;
DataDictionary CurrentQuestion = _CurrentQuestion["Questions"].DataList[Question - 1].DataDictionary;
_LightningRoundInterface.QuestionUI.text = CurrentQuestion["Question"].ToString();
DataList Choices = CurrentQuestion["Choices"].DataList;
for (int i = 0; i < Choices.Count && i < _LightningRoundInterface.ChoiceUI.Length; i++)
{
_LightningRoundInterface.ChoiceUI[i].text = Choices[i].ToString();
}
_QuestionCorrectResponse = (int)CurrentQuestion["Correct Response"].Number;
for (int i = 0; i < _LightningRoundInterface.ChoiceButtons.Length && i < _LightningRoundInterface.ChoiceButtonImages.Length; i++)
{
_LightningRoundInterface.ChoiceButtonImages[i].color = (_QuestionCorrectResponse == (i + 1)) ? Color.green : Color.red;
_LightningRoundInterface.ChoiceButtons[i].interactable = true;
}
EnableBuzzInPeriodForAllPlayers();
}
private void LightningRoundCheckAnswer(int Answer)
{
if (_QuestionCorrectResponse == Answer)
{
for (int i = 0; i < _LightningRoundInterface.ChoiceButtons.Length; i++)
{
_LightningRoundInterface.ChoiceButtons[i].interactable = false;
}
int PodiumIndex = _BuzzedInPlayer - 1;
if (PodiumIndex >= 0 && PodiumIndex < _PlayerPodiums.Length)
{
NetworkCalling.SendCustomNetworkEvent(
(IUdonEventReceiver)_PlayerPodiums[PodiumIndex],
NetworkEventTarget.All,
"IncreaseScoreBy5");
}
EndBuzzInPeriod();
EnableInteraction("Next Question");
}
else
{
WaitForBuzzInsWithoutLastPlayer();
}
}
public void LightningRoundCheckAnswer1()
{
LightningRoundCheckAnswer(1);
}
public void LightningRoundCheckAnswer2()
{
LightningRoundCheckAnswer(2);
}
public void LightningRoundCheckAnswer3()
{
LightningRoundCheckAnswer(3);
}
private void AdvanceToNextQuestion()
{
DisableChoiceCards();
DisableBuzzers();
_QuestionIndex++;
if (_QuestionIndex >= _QuestionsList.Count)
{
_PregameInterface.HeaderUI.text = "No More Questions";
EnableHostCardDisplay(QuestionType.None);
DisableInteraction();
return;
}
_CurrentQuestion = _QuestionsList[_QuestionIndex].DataDictionary;
_QuestionStage = 0;
ResetInfoCard();
EnableInteraction("Show Next Question");
}
private void ResetInfoCard(string Header = "")
{
_MultipleChoiceInterface.HeaderUI.text = Header;
for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length; i++)
{
_MultipleChoiceInterface.CluesUI[i].text = "";
}
for (int i = 0; i < _MultipleChoiceInterface.ChoiceButtonImages.Length; i++)
{
_MultipleChoiceInterface.ChoiceButtonImages[i].color = Color.white;
}
for (int i = 0; i < _MultipleChoiceInterface.ChoiceUI.Length; i++)
{
_MultipleChoiceInterface.ChoiceUI[i].text = "";
}
}
private void EnableChoiceCards()
{
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableChoiceCards", true);
}
}
private void DisableChoiceCards()
{
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableChoiceCards", false);
}
}
public void EnableBuzzers()
{
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzer", true);
}
}
public void DisableBuzzers()
{
for (int i = 0; i < _PlayerPodiums.Length; i++)
{
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzer", false);
}
}
public void EnableBuzzInPeriodForAllPlayers()
{
_BuzzInAllowed = true;
ResetBuzzers();
}
public void WaitForBuzzInsWithoutLastPlayer() {
_BuzzInAllowed = true;
int PodiumIndex = _BuzzedInPlayer - 1;
if (PodiumIndex >= 0 && PodiumIndex < _PlayerPodiums.Length)
{
NetworkCalling.SendCustomNetworkEvent(
(IUdonEventReceiver)_PlayerPodiums[_BuzzedInPlayer - 1],
NetworkEventTarget.All,
"EnableBuzzInEffect", false);
_BuzzedInPlayer = -1;
RequestSerialization();
}
}
[NetworkCallable]
public void PlayerBuzzedIn(int PlayerNumber)
{
int PlayerIndex = PlayerNumber - 1;
if (!_BuzzInAllowed || !_PlayerBuzzInAllowed[PlayerIndex]) { return; }
// Prevent new buzz-ins and store which player is currently buzzed in.
_BuzzInAllowed = false;
_PlayerBuzzInAllowed[PlayerIndex] = false;
_BuzzedInPlayer = PlayerNumber;
RequestSerialization();
NetworkCalling.SendCustomNetworkEvent(
(IUdonEventReceiver)_PlayerPodiums[PlayerIndex],
NetworkEventTarget.All,
"EnableBuzzInEffect", true);
// Play the buzzer sound globally.
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFX), SFXEventType.Buzzer);
}
public void EndBuzzInPeriod()
{
_BuzzInAllowed = false;
ResetBuzzers();
}
public void ResetBuzzers()
{
for (int i = 0; i < _PlayerPodiums.Length; i++) {
_PlayerBuzzInAllowed[i] = true;
NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzInEffect", false);
}
_BuzzedInPlayer = -1;
}
private void AdvanceQuestion()
{
// Disable interaction until we unlock it again later.
DisableInteraction();
_QuestionStage++;
// TO-DO: Ask someone at either Microsoft or VRChat why the VM crashes if you cast an int
// to an enum in a switch parameter, but not if you cast an enum to an int in a case
// statement. I'm starting to wonder if either C# or U# are just fucking terrible
// languages. C++ figured this problem out in at least 1985, and it turns out the proper
// solution was "it's not a problem, it's two numbers, they're the same fucking thing".
switch ((int)_CurrentQuestion["Type"].Number)
{
case (int)QuestionType.MultipleChoice: AdvanceMultipleChoiceStage(); break;
case (int)QuestionType.LightningRound: AdvanceLightningRoundQuestion(); break;
}
RequestSerialization();
}
private void AdvanceMultipleChoiceStage()
{
switch(_QuestionStage)
{
case 1: NewMultipleChoiceQuestion(); break;
case 2: MultipleChoiceRevealChoice1(); break;
case 3: MultipleChoiceRevealChoice2(); break;
case 4: MultipleChoiceRevealChoice3(); break;
case 5: MultipleChoiceLockAnswers(); break;
case 6: MultipleChoiceRevealAnswersAndAssignPoints(); break;
case 7: AdvanceToNextQuestion(); break;
default: break;
}
}
private void AdvanceLightningRoundQuestion()
{
switch(_QuestionStage)
{
case 1: BeginLightningRound(); break;
case 2: NewLightningRoundQuestion(1); break;
case 3: NewLightningRoundQuestion(2); break;
case 4: NewLightningRoundQuestion(3); break;
case 5: AdvanceToNextQuestion(); break;
default: break;
}
}
[NetworkCallable]
public void PlayMusic(MusicEventType MusicEvent)
{
_MusicPlayer.Stop();
switch (MusicEvent)
{
case MusicEventType.WhereInTheWorld: _MusicPlayer.clip = _WhereInTheWorld; break;
case MusicEventType.RockapellaIdent: _MusicPlayer.clip = _RockapellaIdent; break;
default: _MusicPlayer.clip = null; break;
}
if (_MusicPlayer.clip != null)
_MusicPlayer.Play();
}
[NetworkCallable]
public void PlaySFX(SFXEventType SFXEvent)
{
PlaySFXInternal(SFXEvent);
}
[NetworkCallable]
public void PlaySFXAtPitch(SFXEventType SFXEvent, float Pitch)
{
PlaySFXInternal(SFXEvent, Pitch);
}
private void PlaySFXInternal(SFXEventType SFXEvent, float Pitch = 1.0f)
{
_SFXPlayer.Stop();
switch (SFXEvent)
{
case SFXEventType.Ding: _SFXPlayer.clip = _Ding; break;
case SFXEventType.Buzzer: _SFXPlayer.clip = _Buzzer; break;
default: _SFXPlayer.clip = null; break;
}
if (_SFXPlayer.clip != null)
_SFXPlayer.pitch = Pitch;
_SFXPlayer.Play();
}
public override void OnPickupUseDown()
{
if (IsInteractionDisabled()) { return; }
AdvanceQuestion();
base.OnPickupUseDown();
}
private void EnableHostCardDisplay(QuestionType Type)
{
_PregameInterface.gameObject.SetActive(false);
_MultipleChoiceInterface.gameObject.SetActive(false);
_LightningRoundInterface.gameObject.SetActive(false);
switch(Type)
{
case QuestionType.None:
_PregameInterface.gameObject.SetActive(true);
break;
case QuestionType.MultipleChoice:
_MultipleChoiceInterface.gameObject.SetActive(true);
break;
case QuestionType.LightningRound:
_LightningRoundInterface.gameObject.SetActive(true);
break;
case QuestionType.DumpsterDive:
case QuestionType.TheChase:
case QuestionType.FinalRound:
case QuestionType.Tiebreaker:
default:
break;
}
}
private void EnableInteraction(string InteractionText = "Advance")
{
DisableInteractive = false;
VRCPickup Pickup = GetComponent<VRCPickup>();
if (Pickup != null)
{
Pickup.UseText = InteractionText;
}
}
private void DisableInteraction()
{
DisableInteractive = true;
}
private bool IsInteractionDisabled()
{
return DisableInteractive;
}
// A messy group of variables that are used for pitch correction of sound effects
// Since Udon doesn't support accessing public static values in structs, we're doing it here
// As5 = 932.33 Hz, PitchShift = TargetNoteFreq / As5Freq
private const float As4 = 0.49999463709201677517617152724894f;
private const float B4 = 0.52972659895101519848122445915073f;
private const float C5 = 0.56122832044447781364967339890382f;
private const float Cs5 = 0.5946070597320691171580878015295f;
private const float D5 = 0.62995934915748715583538017654693f;
private const float Ds5 = 0.66741389851232932545343386998166f;
private const float E5 = 0.70709941758819302178413222785923f;
private const float F5 = 0.74915534199264209024701554170734f;
private const float Fs5 = 0.79369965570130747696631021204938f;
private const float G5 = 0.84089324595368592665687042141731f;
private const float Gs5 = 0.89089699998927418403355035234305f;
private const float A5 = 0.94387180504756899381120418735855f;
private const float As5 = 1.0f;
private const float B5 = 1.0594639237179968466101058638036f;
private const float C6 = 1.1224566408889556272993467978076f;
private const float Cs6 = 1.1892033936481717846685186575569f;
private const float D6 = 1.2599186983149743116707603530939f;
private const float Ds6 = 1.3348385228406251005545246854654f;
private const float E6 = 1.4142095609923524932159214012206f;
private const float F6 = 1.4982999581693177308463741379125f;
private const float Fs6 = 1.5873993114026149539326204240988f;
private const float G6 = 1.6817864919073718533137408428346f;
private const float Gs6 = 1.7817939999785483680671007046861f;
private const float A6 = 1.8877436100951379876224083747171f;
private const float As6 = 2.0f;
private const float B6 = 2.1183486533738054122467366704922f;
private const float C7 = 2.2449132817779112545986935956153f;
private const float Cs7 = 2.3784067872963435693370373151137f;
}