game: interim
This commit is contained in:
parent
0d57d2b3a9
commit
c5402178f0
|
@ -134,6 +134,7 @@ GameObject:
|
|||
- component: {fileID: 133964671}
|
||||
- component: {fileID: 133964673}
|
||||
- component: {fileID: 133964674}
|
||||
- component: {fileID: 133964675}
|
||||
m_Layer: 5
|
||||
m_Name: UI
|
||||
m_TagString: Untagged
|
||||
|
@ -200,6 +201,18 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
state: 0
|
||||
--- !u!114 &133964675
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 133964670}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6351b7620d84e2d43bc4f59c5f3f8b5c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &447905425
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
@ -1,82 +1,90 @@
|
|||
using System;
|
||||
using System.Net.Mail;
|
||||
using Firebase.Auth;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.UIElements;
|
||||
using Button = UnityEngine.UIElements.Button;
|
||||
|
||||
/// <summary>
|
||||
/// class to handle the account view ui
|
||||
/// </summary>
|
||||
public class AccountUI : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// state of the account view
|
||||
/// </summary>
|
||||
private enum State
|
||||
{
|
||||
UnassociatedState, // (start)
|
||||
NotSignedIn, // initial
|
||||
AfterContinue, // after
|
||||
SignedIn // post
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// current state of the account view
|
||||
/// </summary>
|
||||
[SerializeField] private State state = State.UnassociatedState;
|
||||
|
||||
/// <summary>
|
||||
/// account view header text
|
||||
/// default text colour
|
||||
/// </summary>
|
||||
private Label _header;
|
||||
|
||||
private readonly Color _defaultInputFieldValueTextColour = new(5.88f, 5.1f, 10.59f);
|
||||
|
||||
/// <summary>
|
||||
/// username text field
|
||||
/// error text colour
|
||||
/// </summary>
|
||||
private TextField _usernameField;
|
||||
|
||||
/// <summary>
|
||||
/// email text field
|
||||
/// </summary>
|
||||
private TextField _emailField;
|
||||
|
||||
/// <summary>
|
||||
/// password text field
|
||||
/// </summary>
|
||||
private TextField _passwordField;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the username
|
||||
/// </summary>
|
||||
private Button _usernameUpdateButton;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the email
|
||||
/// </summary>
|
||||
private Button _emailUpdateButton;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the password
|
||||
/// </summary>
|
||||
private Button _passwordUpdateButton;
|
||||
|
||||
private readonly Color _errorInputFieldValueTextColour = new(1f, 50.59f, 50.2f);
|
||||
|
||||
/// <summary>
|
||||
/// accompanying text for the account input fields, used when an error/notice is needed
|
||||
/// </summary>
|
||||
private Label _accompanyingText;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// email text field
|
||||
/// </summary>
|
||||
private TextField _emailField;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the email
|
||||
/// </summary>
|
||||
private Button _emailUpdateButton;
|
||||
|
||||
/// <summary>
|
||||
/// account view header text
|
||||
/// </summary>
|
||||
private Label _header;
|
||||
|
||||
/// <summary>
|
||||
/// password text field
|
||||
/// </summary>
|
||||
private TextField _passwordField;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the password
|
||||
/// </summary>
|
||||
private Button _passwordUpdateButton;
|
||||
|
||||
/// <summary>
|
||||
/// either 'continue', 'log in', or 'sign out' button
|
||||
/// (in order of 'initial', 'after', and 'post' states)
|
||||
/// </summary>
|
||||
private Button _primaryActionButton;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// either 'forgot password' or 'create an account'
|
||||
/// (in order of 'initial' and 'after' states, is hidden in 'post' state)
|
||||
/// </summary>
|
||||
private Button _secondaryActionButton;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// username text field
|
||||
/// </summary>
|
||||
private TextField _usernameField;
|
||||
|
||||
/// <summary>
|
||||
/// button to update the username
|
||||
/// </summary>
|
||||
private Button _usernameUpdateButton;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (state == State.UnassociatedState) throw new Exception("unreachable state");
|
||||
|
||||
// GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to subscribe button events to their respective functions
|
||||
/// </summary>
|
||||
|
@ -85,44 +93,57 @@ public void OnEnable()
|
|||
var ui = GetComponent<UIDocument>().rootVisualElement;
|
||||
|
||||
_header = ui.Q<Label>("AccountHeader");
|
||||
|
||||
|
||||
_usernameField = ui.Q<TextField>("UsernameField");
|
||||
_emailField = ui.Q<TextField>("EmailField");
|
||||
_passwordField = ui.Q<TextField>("PasswordField");
|
||||
|
||||
_usernameUpdateButton = ui.Q<Button>("UsernameUpdateButton");
|
||||
_usernameUpdateButton.clicked += OnUsernameUpdateButtonClick;
|
||||
|
||||
|
||||
_emailUpdateButton = ui.Q<Button>("EmailUpdateButton");
|
||||
_emailUpdateButton.clicked += OnEmailUpdateButtonClick;
|
||||
|
||||
|
||||
_passwordUpdateButton = ui.Q<Button>("PasswordUpdateButton");
|
||||
_passwordUpdateButton.clicked += OnPasswordUpdateButtonClick;
|
||||
|
||||
|
||||
_accompanyingText = ui.Q<Label>("AccompanyingText");
|
||||
|
||||
|
||||
_primaryActionButton = ui.Q<Button>("PrimaryActionButton");
|
||||
_primaryActionButton.clicked += OnPrimaryActionButtonClick;
|
||||
|
||||
|
||||
_secondaryActionButton = ui.Q<Button>("SecondaryActionButton");
|
||||
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
|
||||
|
||||
|
||||
TransitionStateTo(State.NotSignedIn);
|
||||
GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
private void OnSignInCallback(FirebaseUser user)
|
||||
{
|
||||
if (state == State.UnassociatedState)
|
||||
{
|
||||
throw new Exception("unreachable state");
|
||||
}
|
||||
Debug.Log("sign in account ui callback");
|
||||
|
||||
var username = GameManager.Instance.Backend.GetUsername();
|
||||
_usernameField.value = username;
|
||||
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
|
||||
_passwordField.value = "";
|
||||
_header.text = $"Signed in as {username}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// populate the fields with the given username and email, used by GameManager after local player data is loaded
|
||||
/// </summary>
|
||||
public void PopulateFields(string username, string email)
|
||||
{
|
||||
_usernameField.value = username;
|
||||
_emailField.value = email;
|
||||
}
|
||||
|
||||
private void TransitionStateTo(State newState, bool keepAccompanyingText = false)
|
||||
{
|
||||
// if we're transitioning to the same state, do nothing
|
||||
if (state == newState) return;
|
||||
|
||||
|
||||
// else, hide the accompanying text
|
||||
if (!keepAccompanyingText) _accompanyingText.style.display = DisplayStyle.None;
|
||||
|
||||
|
@ -139,78 +160,79 @@ private void TransitionStateTo(State newState, bool keepAccompanyingText = false
|
|||
_passwordUpdateButton.style.display = DisplayStyle.Flex;
|
||||
_emailUpdateButton.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
Debug.Log($"transitioning to {newState}");
|
||||
|
||||
// set primary/secondary buttons
|
||||
switch (newState)
|
||||
{
|
||||
case State.NotSignedIn:
|
||||
_header.text = "You are not signed in.";
|
||||
|
||||
|
||||
_usernameField.style.display = DisplayStyle.None;
|
||||
_emailField.style.display = DisplayStyle.Flex;
|
||||
_passwordField.style.display = DisplayStyle.Flex;
|
||||
|
||||
|
||||
_primaryActionButton.style.display = DisplayStyle.Flex;
|
||||
_secondaryActionButton.style.display = DisplayStyle.Flex;
|
||||
|
||||
_primaryActionButton.text = "Continue \u2192";
|
||||
_secondaryActionButton.text = "Forgot Password \u2192";
|
||||
break;
|
||||
|
||||
|
||||
case State.AfterContinue:
|
||||
_header.text = "You are not signed in.";
|
||||
|
||||
|
||||
_usernameField.style.display = DisplayStyle.None;
|
||||
_emailField.style.display = DisplayStyle.Flex;
|
||||
_passwordField.style.display = DisplayStyle.Flex;
|
||||
|
||||
|
||||
_primaryActionButton.style.display = DisplayStyle.Flex;
|
||||
_secondaryActionButton.style.display = DisplayStyle.Flex;
|
||||
|
||||
|
||||
_primaryActionButton.text = "Retry Log In \u2192";
|
||||
_secondaryActionButton.text = "Create an Account \u2192";
|
||||
break;
|
||||
|
||||
|
||||
case State.SignedIn:
|
||||
Debug.Log("transitioning to signed in state");
|
||||
|
||||
var username = GameManager.Instance.Backend.GetUsername();
|
||||
_header.text = string.IsNullOrEmpty(username) ? "You are signed in." : $"Signed in as {username}";
|
||||
|
||||
|
||||
_usernameField.style.display = DisplayStyle.Flex;
|
||||
_emailField.style.display = DisplayStyle.Flex;
|
||||
_passwordField.style.display = DisplayStyle.Flex;
|
||||
|
||||
_usernameField.value = GameManager.Instance.Backend.GetUsername();
|
||||
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
|
||||
_passwordField.value = "";
|
||||
|
||||
|
||||
_primaryActionButton.style.display = DisplayStyle.Flex;
|
||||
_secondaryActionButton.style.display = DisplayStyle.None;
|
||||
|
||||
_primaryActionButton.text = "Sign Out \u2192";
|
||||
break;
|
||||
|
||||
|
||||
case State.UnassociatedState:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
|
||||
state = newState;
|
||||
}
|
||||
|
||||
|
||||
private void ValidateUsername()
|
||||
{
|
||||
// just has to be min. 5 characters
|
||||
_usernameField.style.color = _defaultInputFieldValueTextColour;
|
||||
if (_usernameField.value.Length >= 5) return;
|
||||
_usernameField.style.color = _errorInputFieldValueTextColour;
|
||||
throw new Exception("Username must be at least 5 characters long.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// validate the email and password fields
|
||||
/// validate the email field
|
||||
/// </summary>
|
||||
/// <param name="emailField">the email field to validate</param>
|
||||
/// <param name="passwordField">the password field to validate</param>
|
||||
/// <exception cref="Exception">if the email or password fields are invalid</exception>
|
||||
private void ValidateFields(TextField emailField, TextField passwordField)
|
||||
/// <exception cref="Exception">if the email field is invalid</exception>
|
||||
private void ValidateEmailField(TextField emailField)
|
||||
{
|
||||
emailField.style.color = Color.black;
|
||||
passwordField.style.color = Color.black;
|
||||
|
||||
// validate email
|
||||
emailField.style.color = _defaultInputFieldValueTextColour;
|
||||
try
|
||||
{
|
||||
var dot = emailField.value.LastIndexOf(".", StringComparison.Ordinal);
|
||||
|
@ -226,34 +248,217 @@ private void ValidateFields(TextField emailField, TextField passwordField)
|
|||
}
|
||||
catch
|
||||
{
|
||||
emailField.style.color = Color.red;
|
||||
emailField.style.color = _errorInputFieldValueTextColour;
|
||||
throw new Exception("Invalid email.");
|
||||
}
|
||||
|
||||
// validate password
|
||||
if (passwordField.value.Length >= 10) return;
|
||||
passwordField.style.color = Color.red;
|
||||
throw new Exception("Invalid password.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// validate the password field
|
||||
/// </summary>
|
||||
/// <param name="passwordField">the password field to validate</param>
|
||||
/// <exception cref="Exception">if the password field is invalid</exception>
|
||||
private void ValidatePasswordField(TextField passwordField)
|
||||
{
|
||||
passwordField.style.color = _defaultInputFieldValueTextColour;
|
||||
if (passwordField.value.Length >= 10) return;
|
||||
passwordField.style.color = _errorInputFieldValueTextColour;
|
||||
throw new Exception("Password must be at least 10 characters long.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// validate both the email and password fields
|
||||
/// </summary>
|
||||
/// <param name="emailField">the email field to validate</param>
|
||||
/// <param name="passwordField">the password field to validate</param>
|
||||
/// <exception cref="Exception">if either the email or password field is invalid</exception>
|
||||
private void ValidateFields(TextField emailField, TextField passwordField)
|
||||
{
|
||||
var invalidEmail = false;
|
||||
var invalidPassword = false;
|
||||
|
||||
try
|
||||
{
|
||||
ValidateEmailField(_emailField);
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
invalidEmail = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ValidatePasswordField(_passwordField);
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
invalidPassword = true;
|
||||
}
|
||||
|
||||
if (!invalidEmail && !invalidPassword) return;
|
||||
var errorMessage = (invalidEmail, invalidPassword) switch
|
||||
{
|
||||
(true, true) => "Invalid email and password is too short.",
|
||||
(true, false) => "Invalid email.",
|
||||
(false, true) => "Passwords must be at least 10 characters long.",
|
||||
_ => null
|
||||
};
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// validate both the email and password fields
|
||||
/// </summary>
|
||||
/// <param name="emailField">the email field to validate</param>
|
||||
/// <param name="passwordField">the password field to validate</param>
|
||||
/// <param name="usernameField">the username field to validate</param>
|
||||
/// <exception cref="Exception">if either the email or password field is invalid</exception>
|
||||
private void ValidateFields(TextField emailField, TextField passwordField, TextField usernameField)
|
||||
{
|
||||
var invalidEmail = false;
|
||||
var invalidPassword = false;
|
||||
var invalidUsername = false;
|
||||
|
||||
try
|
||||
{
|
||||
ValidateEmailField(_emailField);
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
invalidEmail = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ValidatePasswordField(_passwordField);
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
invalidPassword = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ValidateUsername();
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
invalidUsername = true;
|
||||
}
|
||||
|
||||
if (!invalidEmail && !invalidPassword && !invalidUsername) return;
|
||||
var errorMessage = (invalidEmail, invalidPassword, invalidUsername) switch
|
||||
{
|
||||
(true, true, true) => "Invalid email, password is too short, and username is too short.",
|
||||
(true, true, false) => "Invalid email, and password is too short.",
|
||||
(true, false, true) => "Invalid email, and username is too short.",
|
||||
(true, false, false) => "Invalid email.",
|
||||
(false, true, true) => "Password is too short, and username is too short.",
|
||||
(false, true, false) => "Password is too short.",
|
||||
(false, false, true) => "Username is too short.",
|
||||
_ => null
|
||||
};
|
||||
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the username update button click
|
||||
/// </summary>
|
||||
private void OnUsernameUpdateButtonClick()
|
||||
{
|
||||
// TODO
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
ValidateUsername();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Username,
|
||||
_usernameField.value,
|
||||
dtr =>
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = dtr switch
|
||||
{
|
||||
Backend.DatabaseTransactionResult.Ok => "Username updated!",
|
||||
Backend.DatabaseTransactionResult.Unauthenticated =>
|
||||
"You are not signed in. Please sign in to update your username.",
|
||||
_ => "An error occurred updating the username. Please try again."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the email update button click
|
||||
/// </summary>
|
||||
private void OnEmailUpdateButtonClick()
|
||||
{
|
||||
// TODO
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
ValidateEmailField(_emailField);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Email,
|
||||
_emailField.value,
|
||||
callback =>
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = callback switch
|
||||
{
|
||||
Backend.DatabaseTransactionResult.Ok =>
|
||||
$"Verification email sent to {_emailField.value}! You may want to sign in again to see the changes.",
|
||||
Backend.DatabaseTransactionResult.Unauthenticated =>
|
||||
"You are not signed in. Please sign in to update your email.",
|
||||
_ => "An error occurred updating the email. Please try again."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the password update button click
|
||||
/// </summary>
|
||||
private void OnPasswordUpdateButtonClick()
|
||||
{
|
||||
// TODO
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
ValidatePasswordField(_passwordField);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Password,
|
||||
_passwordField.value,
|
||||
callback =>
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = callback switch
|
||||
{
|
||||
Backend.DatabaseTransactionResult.Ok => "Password updated!",
|
||||
Backend.DatabaseTransactionResult.Unauthenticated =>
|
||||
"You are not signed in. Please sign in to update your password.",
|
||||
_ => "An error occurred updating the password. Please try again."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the primary action button click
|
||||
/// </summary>
|
||||
private void OnPrimaryActionButtonClick()
|
||||
{
|
||||
switch (state)
|
||||
|
@ -273,11 +478,11 @@ private void OnPrimaryActionButtonClick()
|
|||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GameManager.Instance.Backend.AuthenticateUser(
|
||||
_emailField.value,
|
||||
_passwordField.value,
|
||||
(result) =>
|
||||
result =>
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
|
@ -289,7 +494,7 @@ private void OnPrimaryActionButtonClick()
|
|||
case Backend.AuthenticationResult.NonExistentUser:
|
||||
TransitionStateTo(State.AfterContinue);
|
||||
break;
|
||||
|
||||
|
||||
case Backend.AuthenticationResult.InvalidEmail:
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = "Invalid email. Please try again.";
|
||||
|
@ -299,44 +504,43 @@ private void OnPrimaryActionButtonClick()
|
|||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = "Invalid credentials. Please try again.";
|
||||
break;
|
||||
|
||||
|
||||
case Backend.AuthenticationResult.AlreadyExistingUser:
|
||||
case Backend.AuthenticationResult.GenericError:
|
||||
default:
|
||||
TransitionStateTo(State.AfterContinue);
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = "Either the account does not exist, or there was an internal error. Try again.";
|
||||
_accompanyingText.text =
|
||||
"There was an error. Either the account does not exist, or the credentials are invalid. Try again.";
|
||||
break;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
|
||||
// sign out button
|
||||
case State.SignedIn:
|
||||
GameManager.Instance.Backend.SignOutUser();
|
||||
TransitionStateTo(State.NotSignedIn);
|
||||
break;
|
||||
|
||||
|
||||
case State.UnassociatedState:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the secondary action button click
|
||||
/// </summary>
|
||||
private void OnSecondaryActionButtonClick()
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
// forgot password button
|
||||
case State.NotSignedIn:
|
||||
throw new NotImplementedException();
|
||||
|
||||
// create an account button
|
||||
case State.AfterContinue:
|
||||
try
|
||||
{
|
||||
ValidateFields(_emailField, _passwordField);
|
||||
ValidateEmailField(_emailField);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -344,7 +548,30 @@ private void OnSecondaryActionButtonClick()
|
|||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GameManager.Instance.Backend.ForgotPassword(_emailField.value, result =>
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text =
|
||||
result
|
||||
? "Password reset email sent. Check your inbox! :D"
|
||||
: "An error occurred sending the email. Please try again.";
|
||||
});
|
||||
break;
|
||||
|
||||
// create an account button
|
||||
case State.AfterContinue:
|
||||
try
|
||||
{
|
||||
ValidateFields(_emailField, _passwordField, _usernameField);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = e.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
// if the username field is hidden, show it and then immediately return
|
||||
if (_usernameField.style.display == DisplayStyle.None)
|
||||
{
|
||||
|
@ -353,11 +580,11 @@ private void OnSecondaryActionButtonClick()
|
|||
_accompanyingText.text = "Set a username for your account.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GameManager.Instance.Backend.AuthenticateUser(
|
||||
_emailField.value,
|
||||
_passwordField.value,
|
||||
(result) =>
|
||||
result =>
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
|
@ -365,7 +592,7 @@ private void OnSecondaryActionButtonClick()
|
|||
case Backend.AuthenticationResult.AlreadyAuthenticated:
|
||||
TransitionStateTo(State.SignedIn);
|
||||
break;
|
||||
|
||||
|
||||
case Backend.AuthenticationResult.InvalidEmail:
|
||||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = "Invalid email. Please try again.";
|
||||
|
@ -375,7 +602,7 @@ private void OnSecondaryActionButtonClick()
|
|||
_accompanyingText.style.display = DisplayStyle.Flex;
|
||||
_accompanyingText.text = "Invalid credentials. Please try again.";
|
||||
break;
|
||||
|
||||
|
||||
case Backend.AuthenticationResult.NonExistentUser:
|
||||
case Backend.AuthenticationResult.AlreadyExistingUser:
|
||||
case Backend.AuthenticationResult.GenericError:
|
||||
|
@ -389,11 +616,22 @@ private void OnSecondaryActionButtonClick()
|
|||
true,
|
||||
_usernameField.value);
|
||||
break;
|
||||
|
||||
|
||||
case State.SignedIn:
|
||||
case State.UnassociatedState:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// state of the account view
|
||||
/// </summary>
|
||||
private enum State
|
||||
{
|
||||
UnassociatedState, // (start)
|
||||
NotSignedIn, // initial
|
||||
AfterContinue, // after
|
||||
SignedIn // post
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* author: mark joshwel
|
||||
* date: 29/5/2024
|
||||
* description: audio manager for handling audio in the game
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// singleton class for handling audio in the game
|
||||
/// </summary>
|
||||
public class AudioManager : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// enum for available audio channels in the game
|
||||
/// </summary>
|
||||
public enum AudioChannel
|
||||
{
|
||||
Music,
|
||||
SoundEffects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// singleton pattern: define instance field for accessing the singleton elsewhere
|
||||
/// </summary>
|
||||
public static AudioManager Instance;
|
||||
|
||||
/// <summary>
|
||||
/// music audio source
|
||||
/// </summary>
|
||||
[SerializeField] private AudioSource musicSource;
|
||||
|
||||
/// <summary>
|
||||
/// sound effects (sfx) audio source
|
||||
/// </summary>
|
||||
[SerializeField] private AudioSource sfxSource;
|
||||
|
||||
/// <summary>
|
||||
/// music source default volume
|
||||
/// </summary>
|
||||
[SerializeField] private float musicSourceDefaultVolume = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// sound effects (sfx) source default volume
|
||||
/// </summary>
|
||||
[SerializeField] private float sfxSourceDefaultVolume = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// check if instance hasn't been set yet
|
||||
if (Instance == null)
|
||||
{
|
||||
Debug.Log("awake as singleton instance, setting self as the forever-alive instance");
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
// check if instance is already set and it's not this instance
|
||||
else if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.Log("awake as non-singleton instance, destroying self");
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to set default volumes for the audio sources
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
// set the default volume for the music source
|
||||
musicSource.volume = musicSourceDefaultVolume;
|
||||
|
||||
// set the default volume for the sfx source
|
||||
sfxSource.volume = sfxSourceDefaultVolume;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// plays the audio clip once on a given channel
|
||||
/// </summary>
|
||||
/// <param name="clip">the audio clip to play</param>
|
||||
/// <param name="channel">the audio channel to play the clip on</param>
|
||||
public void PlayClipOnChannel(AudioClip clip, AudioChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case AudioChannel.Music:
|
||||
musicSource.PlayOneShot(clip);
|
||||
break;
|
||||
case AudioChannel.SoundEffects:
|
||||
sfxSource.PlayOneShot(clip);
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"invalid channel '{channel}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// gets the volume of a given audio channel
|
||||
/// </summary>
|
||||
/// <param name="channel">the AudioManager.AudioChannel to get the volume of</param>
|
||||
/// <returns>volume float value of the channel, from 0.0f-1.0f</returns>
|
||||
public float GetChannelVolume(AudioChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case AudioChannel.Music:
|
||||
return musicSource.volume;
|
||||
case AudioChannel.SoundEffects:
|
||||
return sfxSource.volume;
|
||||
default:
|
||||
Debug.LogError($"invalid channel '{channel}'");
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// sets the pure volume value of a given audio channel
|
||||
/// </summary>
|
||||
/// <param name="volume">volume float value for the channel, from 0.0f-1.0f</param>
|
||||
/// <param name="channel">the AudioManager.AudioChannel to set the volume of</param>
|
||||
public void SetChannelVolumeReal(float volume, AudioChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case AudioChannel.Music:
|
||||
musicSource.volume = Math.Min(volume, 1.0f);
|
||||
break;
|
||||
case AudioChannel.SoundEffects:
|
||||
sfxSource.volume = Math.Min(volume, 1.0f);
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"invalid channel '{channel}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// sets the volume value of a given audio channel based on a logarithmic scale
|
||||
/// for human perception (e.g. 0.5f is half volume, 0.1f is very quiet)
|
||||
/// </summary>
|
||||
/// <param name="volume">volume float value for the channel, from 0.0f-1.0f</param>
|
||||
/// <param name="channel">the AudioManager.AudioChannel to set the volume of</param>
|
||||
public void SetChannelVolumeLog(float volume, AudioChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case AudioChannel.Music:
|
||||
musicSource.volume = Mathf.Log10(Mathf.Max(volume, 0.0001f)) * 20;
|
||||
break;
|
||||
case AudioChannel.SoundEffects:
|
||||
sfxSource.volume = Mathf.Log10(Mathf.Max(volume, 0.0001f)) * 20;
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"invalid channel '{channel}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,13 @@ public enum FirebaseConnectionStatus
|
|||
InternalError // "an unknown error occurred"
|
||||
}
|
||||
|
||||
public enum UserAccountDetailTargetEnum
|
||||
{
|
||||
Username,
|
||||
Email,
|
||||
Password
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// callback functions to be invoked when the user signs in
|
||||
/// </summary>
|
||||
|
@ -171,12 +178,13 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
|
|||
|
||||
// they have signed in, update _user
|
||||
_user = _auth.CurrentUser;
|
||||
if (signedIn)
|
||||
if (!signedIn) return;
|
||||
|
||||
Debug.Log($"signed in successfully as {_user.UserId}");
|
||||
RetrieveUsernameWithCallback((_, _) =>
|
||||
{
|
||||
Debug.Log($"signed in successfully as {_user.UserId}");
|
||||
RetrieveUsername();
|
||||
foreach (var callback in _onSignInCallback) callback(_user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -292,6 +300,7 @@ public void RegisterOnSignOutCallback(Action<FirebaseUser> callback)
|
|||
{
|
||||
if (signInTask.IsCompletedSuccessfully)
|
||||
{
|
||||
RetrieveUsername();
|
||||
callback(AuthenticationResult.Ok);
|
||||
return;
|
||||
}
|
||||
|
@ -332,23 +341,44 @@ public void RegisterOnSignOutCallback(Action<FirebaseUser> callback)
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to retrieve the user's username from the database
|
||||
/// helper function to run RetrieveUsername with no callback
|
||||
/// </summary>
|
||||
private void RetrieveUsername()
|
||||
{
|
||||
RetrieveUsernameWithCallback((result, username) => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to retrieve the user's username from the database
|
||||
/// </summary>
|
||||
private void RetrieveUsernameWithCallback(Action<DatabaseTransactionResult, string> callback)
|
||||
{
|
||||
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
|
||||
_db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWith(task =>
|
||||
|
||||
if (_user == null)
|
||||
{
|
||||
Debug.LogError("receiving username post-authentication but user is null (should be unreachable)");
|
||||
callback(DatabaseTransactionResult.Unauthenticated, "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
_db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWithOnMainThread(task =>
|
||||
{
|
||||
DatabaseTransactionResult result;
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
result = DatabaseTransactionResult.Ok;
|
||||
_username = task.Result.Value.ToString();
|
||||
Debug.Log($"our username is {_username}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_username = "???";
|
||||
result = DatabaseTransactionResult.Error;
|
||||
_username = "Unknown";
|
||||
Debug.LogError("failed to get username");
|
||||
}
|
||||
|
||||
callback(result, _username);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -375,12 +405,46 @@ public void SignOutUser()
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to submit a play to the database
|
||||
/// abstraction function for the user to reset their password
|
||||
/// </summary>
|
||||
/// <param name="playData">play data</param>
|
||||
/// <param name="email">the forgetful user's email lol</param>
|
||||
/// <param name="callback">callback function to be invoked after the password reset email is sent</param>
|
||||
public void ForgotPassword(string email, Action<bool> callback)
|
||||
{
|
||||
_auth.SendPasswordResetEmailAsync(email).ContinueWithOnMainThread(resetTask =>
|
||||
{
|
||||
if (resetTask.IsCompletedSuccessfully)
|
||||
{
|
||||
callback(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(resetTask.Exception);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to get the user's recent scores from the database
|
||||
/// </summary>
|
||||
/// <param name="callback">
|
||||
/// callback function that takes in a DatabaseTransactionResult and List of LocalPlayerData.Score
|
||||
/// argument
|
||||
/// </param>
|
||||
public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
|
||||
{
|
||||
// TODO
|
||||
callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to submit a score to the database
|
||||
/// </summary>
|
||||
/// <param name="score">score</param>
|
||||
/// <param name="callback">callback function that takes in one DatabaseTransactionResult argument</param>
|
||||
public void SubmitPlay(
|
||||
PlayData playData,
|
||||
public void SubmitScore(
|
||||
LocalPlayerData.Score score,
|
||||
Action<DatabaseTransactionResult> callback)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
@ -421,20 +485,79 @@ public void SignOutUser()
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// private void Playground()
|
||||
// {
|
||||
// // update username
|
||||
// _db.Child("users").Child(_user.UserId).Child("username").SetValueAsync("newusername").ContinueWith(task => { });
|
||||
//
|
||||
// // update email
|
||||
// _user.SendEmailVerificationBeforeUpdatingEmailAsync("name@example.com").ContinueWith(task => { });
|
||||
//
|
||||
// // update password
|
||||
// _user.UpdatePasswordAsync("password").ContinueWith(task => { });
|
||||
//
|
||||
// // reset password
|
||||
// _auth.SendPasswordResetEmailAsync("name@example.com").ContinueWith(task => { });
|
||||
// }
|
||||
/// <summary>
|
||||
/// abstraction function to update the user's account details in the database
|
||||
/// </summary>
|
||||
/// <param name="target">the target account detail to update</param>
|
||||
/// <param name="newValue">the new value for the target account detail</param>
|
||||
/// <param name="callback">callback function that takes in one DatabaseTransactionResult argument</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">thrown when the target is not a valid UserAccountDetailTargetEnum</exception>
|
||||
public void UpdateUserAccountDetail(
|
||||
UserAccountDetailTargetEnum target,
|
||||
string newValue,
|
||||
Action<DatabaseTransactionResult> callback)
|
||||
{
|
||||
if (!Status.Equals(FirebaseConnectionStatus.Connected)) callback(DatabaseTransactionResult.Unauthenticated);
|
||||
|
||||
if (_user == null)
|
||||
{
|
||||
callback(DatabaseTransactionResult.Unauthenticated);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case UserAccountDetailTargetEnum.Email:
|
||||
_user.SendEmailVerificationBeforeUpdatingEmailAsync(newValue).ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
callback(DatabaseTransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(DatabaseTransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case UserAccountDetailTargetEnum.Username:
|
||||
_db.Child("users").Child(_user.UserId).Child("username").SetValueAsync(newValue)
|
||||
.ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
_username = newValue;
|
||||
callback(DatabaseTransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(DatabaseTransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case UserAccountDetailTargetEnum.Password:
|
||||
_user.UpdatePasswordAsync(newValue).ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
callback(DatabaseTransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(DatabaseTransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(target), target, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// struct for a leaderboard entry
|
||||
|
@ -452,16 +575,4 @@ public LeaderboardEntry(string username, float rating, int playCount)
|
|||
PlayCount = playCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// struct for play data
|
||||
/// </summary>
|
||||
public struct PlayData
|
||||
{
|
||||
public int RoundsPlayed;
|
||||
public float AverageOverallAccuracy;
|
||||
public float AverageLightnessAccuracy;
|
||||
public float AverageChromaAccuracy;
|
||||
public float AverageHueAccuracy;
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* author: mark joshwel
|
||||
* date: 29/5/2024
|
||||
* description: common menu script for hover and click sound effects on ui toolkit buttons
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// common menu class for hover and click sound effects on ui toolkit buttons
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// override <c>OnEnable()</c> with the first call to <c>base.OnEnable()</c>
|
||||
/// or <c>PostEnable()</c>, and set the variable
|
||||
/// <c>GameManager.DisplayState.associatedState</c> to the respective menu state
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CommonMenu : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// associated display state with the menu for the game manager to filter out menus in a scene
|
||||
/// </summary>
|
||||
public GameManager.DisplayState associatedState = GameManager.DisplayState.UnassociatedState;
|
||||
|
||||
/// <summary>
|
||||
/// audio clip for menu button click
|
||||
/// </summary>
|
||||
[SerializeField] public AudioClip menuButtonClick;
|
||||
|
||||
/// <summary>
|
||||
/// audio clip for menu button hover
|
||||
/// </summary>
|
||||
[SerializeField] public AudioClip menuButtonHover;
|
||||
|
||||
/// <summary>
|
||||
/// manager for the game state
|
||||
/// </summary>
|
||||
protected GameManager Game;
|
||||
|
||||
/// <summary>
|
||||
/// the visual element object for the menu
|
||||
/// </summary>
|
||||
protected VisualElement UI;
|
||||
|
||||
/// <summary>
|
||||
/// checks if The Menu (2022) was set up correctly
|
||||
/// </summary>
|
||||
/// <exception cref="Exception">throws an exception if UI, Game and Audio are not set</exception>
|
||||
private void Start()
|
||||
{
|
||||
if (associatedState == GameManager.DisplayState.UnassociatedState)
|
||||
throw new Exception("associatedState not set");
|
||||
|
||||
if (Game == null)
|
||||
throw new Exception("Game not set (was base.OnEnable() or PostEnable() called?)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// override this class but call <c>base.OnEnable()</c> first.
|
||||
/// also set the <c>associatedState</c> variable to the respective menu state
|
||||
/// </summary>
|
||||
public virtual void OnEnable()
|
||||
{
|
||||
PostEnable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to subscribe to mouse events and assign managers
|
||||
/// </summary>
|
||||
public void PostEnable()
|
||||
{
|
||||
// get audio manager singleton instance from the world
|
||||
UI = GetComponent<UIDocument>().rootVisualElement;
|
||||
Game = GameManager.Instance;
|
||||
|
||||
// subscribe to hover events
|
||||
UI.RegisterCallback<PointerOverEvent>(HoverListener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function listener for <c>PointerOverEvents</c> and plays a hover sound if it's a button
|
||||
/// </summary>
|
||||
/// <param name="evt">event from UIE callback</param>
|
||||
public virtual void HoverListener(PointerOverEvent evt)
|
||||
{
|
||||
// check for button
|
||||
if (evt.target is Button)
|
||||
// play hover sound
|
||||
PlayHover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function listener for <c>ClickEvents</c> and plays a click sound if it's a button
|
||||
/// </summary>
|
||||
/// <param name="evt">event from UIE callback</param>
|
||||
public virtual void ClickListener(ClickEvent evt)
|
||||
{
|
||||
// check for button
|
||||
if (evt.target is Button)
|
||||
// play click sound
|
||||
PlayClick();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// generic decoupled function to play click sound
|
||||
/// </summary>
|
||||
public virtual void PlayClick()
|
||||
{
|
||||
AudioManager.Instance.PlayClipOnChannel(menuButtonClick, AudioManager.AudioChannel.SoundEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// generic decoupled function to play hover sound
|
||||
/// </summary>
|
||||
public virtual void PlayHover()
|
||||
{
|
||||
AudioManager.Instance.PlayClipOnChannel(menuButtonHover, AudioManager.AudioChannel.SoundEffects);
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 26a2a7f124fc93c47aae6ca6569f68b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -1,4 +1,5 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,18 +12,21 @@ public class GameManager : MonoBehaviour
|
|||
/// </summary>
|
||||
public enum DisplayState
|
||||
{
|
||||
ScreenStart,
|
||||
OverlayAccountManagement,
|
||||
OverlaySettings,
|
||||
OverlayLeaderboard,
|
||||
Game,
|
||||
UnassociatedState
|
||||
Nothing,
|
||||
PlayView,
|
||||
LeaderboardView,
|
||||
AccountView,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// singleton pattern: define instance field for accessing the singleton elsewhere
|
||||
/// </summary>
|
||||
public static GameManager Instance;
|
||||
|
||||
/// <summary>
|
||||
/// the current display state of the game
|
||||
/// </summary>
|
||||
[SerializeField] private DisplayState state = DisplayState.Nothing;
|
||||
|
||||
/// <summary>
|
||||
/// the visual element object for game ui (hud/prompts/tooltips)
|
||||
|
@ -33,6 +37,11 @@ public enum DisplayState
|
|||
/// backend object for handling communication with the firebase backend
|
||||
/// </summary>
|
||||
public Backend Backend;
|
||||
|
||||
/// <summary>
|
||||
/// the local player data object for storing player data
|
||||
/// </summary>
|
||||
private LocalPlayerData _localPlayerData;
|
||||
|
||||
/// <summary>
|
||||
/// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
|
||||
|
@ -54,6 +63,21 @@ private void Awake()
|
|||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SetDisplayState(DisplayState.PlayView);
|
||||
|
||||
// load the local player data and refresh the ui
|
||||
_localPlayerData = new LocalPlayerData();
|
||||
_localPlayerData.LoadFromTheWorld();
|
||||
|
||||
// register a callback to refresh the ui when the player signs in
|
||||
Backend.RegisterOnSignInCallback(_ =>
|
||||
{
|
||||
_localPlayerData.LoadFromTheWorld();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// called when the game object is enabled
|
||||
/// </summary>
|
||||
|
@ -70,4 +94,15 @@ private void OnDestroy()
|
|||
{
|
||||
Backend.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to show a menu based on the enum passed,
|
||||
/// and any other necessary actions
|
||||
/// </summary>
|
||||
/// <param name="newDisplayState">the game menu to show</param>
|
||||
public void SetDisplayState(DisplayState newDisplayState)
|
||||
{
|
||||
var currentDisplayState = St;
|
||||
switch (newDisplayState)
|
||||
}
|
||||
}
|
172
ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs
Normal file
172
ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs
Normal file
|
@ -0,0 +1,172 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
public class LocalPlayerData
|
||||
{
|
||||
/// <summary>
|
||||
/// last known email used
|
||||
/// </summary>
|
||||
public string LastKnownEmail = "";
|
||||
|
||||
/// <summary>
|
||||
/// last known username used
|
||||
/// </summary>
|
||||
public string LastKnownUsername = "Guest";
|
||||
|
||||
/// <summary>
|
||||
/// queue of the 10 most recent local scores
|
||||
/// </summary>
|
||||
public Queue<Score> RecentLocalScores = new(10);
|
||||
|
||||
/// <summary>
|
||||
/// queue of the 10 most recent online scores,
|
||||
/// used in user rating calculation and accuracy display stats
|
||||
/// </summary>
|
||||
public Queue<Score> RecentOnlineScores = new(10);
|
||||
|
||||
/// <summary>
|
||||
/// queue of the best online scores,
|
||||
/// used in user rating calculation and accuracy display stats
|
||||
/// </summary>
|
||||
public Queue<Score> BestOnlineScores = new(30);
|
||||
|
||||
/// <summary>
|
||||
/// loads player data from player prefs and database
|
||||
/// </summary>
|
||||
public void LoadFromTheWorld()
|
||||
{
|
||||
// load user data, possibly from the backend
|
||||
var possibleUser = GameManager.Instance.Backend.GetUser();
|
||||
var currentKnownEmail = string.Empty;
|
||||
var currentKnownUsername = string.Empty;
|
||||
if (possibleUser != null)
|
||||
{
|
||||
currentKnownEmail = possibleUser.Email;
|
||||
currentKnownUsername = GameManager.Instance.Backend.GetUsername();
|
||||
}
|
||||
var lastStoredEmail = PlayerPrefs.GetString("LastKnownEmail", "");
|
||||
var lastStoredUsername = PlayerPrefs.GetString("LastKnownUsername", "Guest");
|
||||
LastKnownEmail = string.IsNullOrEmpty(currentKnownEmail) ? lastStoredEmail : currentKnownEmail;
|
||||
LastKnownUsername = string.IsNullOrEmpty(currentKnownUsername) ? lastStoredUsername : currentKnownUsername;
|
||||
|
||||
// load local scores
|
||||
RecentLocalScores.Clear();
|
||||
for (var idx = 0; idx < 10; idx++)
|
||||
{
|
||||
var timestampRaw = PlayerPrefs.GetString($"RecentLocalScores_{idx}_Timestamp", "");
|
||||
if (timestampRaw == "") continue;
|
||||
|
||||
var timestamp = DateTime.TryParseExact(timestampRaw,
|
||||
"s",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var t)
|
||||
? t
|
||||
: DateTime.MinValue;
|
||||
|
||||
if (timestamp == DateTime.MinValue) continue;
|
||||
|
||||
var noOfRounds = PlayerPrefs.GetInt($"RecentLocalScores_{idx}_NoOfRounds", -1);
|
||||
var l = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", -1f);
|
||||
var c = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", -1f);
|
||||
var h = PlayerPrefs.GetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", -1f);
|
||||
|
||||
// if any of the values are invalid, don't add the score
|
||||
if (noOfRounds < 0 || l < 0 || c < 0 || h < 0) continue;
|
||||
|
||||
RegisterLocalScore(new Score(timestamp, noOfRounds, l, c, h));
|
||||
}
|
||||
|
||||
// load online scores
|
||||
RecentOnlineScores.Clear();
|
||||
GameManager.Instance.Backend.GetRecentScores((dtr, recentOnlineScores) =>
|
||||
{
|
||||
foreach (var onlineScore in recentOnlineScores)
|
||||
{
|
||||
if (RecentOnlineScores.Count > 10) RecentOnlineScores.Dequeue();
|
||||
RecentOnlineScores.Enqueue(onlineScore);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// registers a score to the player's local data
|
||||
/// </summary>
|
||||
/// <param name="score">the score to register</param>
|
||||
public void RegisterLocalScore(Score score)
|
||||
{
|
||||
if (RecentLocalScores.Count > 10) RecentLocalScores.Dequeue();
|
||||
RecentLocalScores.Enqueue(score);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// saves player data to player prefs
|
||||
/// </summary>
|
||||
public void SaveToTheWorld()
|
||||
{
|
||||
PlayerPrefs.SetString("LastKnownEmail", LastKnownEmail);
|
||||
PlayerPrefs.SetString("LastKnownUsername", LastKnownUsername);
|
||||
|
||||
var idx = 0;
|
||||
foreach (var score in RecentLocalScores)
|
||||
{
|
||||
PlayerPrefs.SetString($"RecentLocalScores_{idx}_Timestamp",
|
||||
score.Timestamp.ToString("s", CultureInfo.InvariantCulture));
|
||||
PlayerPrefs.SetInt($"RecentLocalScores_{idx}_NoOfRounds", score.NoOfRounds);
|
||||
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgLightnessAccuracy", score.AvgLightnessAccuracy);
|
||||
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgChromaAccuracy", score.AvgChromaAccuracy);
|
||||
PlayerPrefs.SetFloat($"RecentLocalScores_{idx}_AvgHueAccuracy", score.AvgHueAccuracy);
|
||||
idx++;
|
||||
}
|
||||
|
||||
// online scores are already saved in the backend
|
||||
}
|
||||
|
||||
public struct Score
|
||||
{
|
||||
/// <summary>
|
||||
/// timestamp of the score
|
||||
/// </summary>
|
||||
public DateTime Timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// number of rounds played (0-100)
|
||||
/// </summary>
|
||||
public int NoOfRounds;
|
||||
|
||||
/// <summary>
|
||||
/// average lightness accuracy across all rounds (0-100)
|
||||
/// </summary>
|
||||
public float AvgLightnessAccuracy;
|
||||
|
||||
/// <summary>
|
||||
/// average chroma accuracy across all rounds (0-100)
|
||||
/// </summary>
|
||||
public float AvgChromaAccuracy;
|
||||
|
||||
/// <summary>
|
||||
/// average hue accuracy across all rounds (0-100)
|
||||
/// </summary>
|
||||
public float AvgHueAccuracy;
|
||||
|
||||
/// <summary>
|
||||
/// constructor for the score struct
|
||||
/// </summary>
|
||||
/// <param name="timestamp">timestamp of the score</param>
|
||||
/// <param name="noOfRounds">number of rounds played (0-100)</param>
|
||||
/// <param name="l">average lightness accuracy across all rounds (0-100)</param>
|
||||
/// <param name="c">average chroma accuracy across all rounds (0-100)</param>
|
||||
/// <param name="h">average hue accuracy across all rounds (0-100)</param>
|
||||
public Score(DateTime timestamp = new(), int noOfRounds = 1, float l = 100.0f, float c = 100.0f,
|
||||
float h = 100.0f)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
NoOfRounds = noOfRounds;
|
||||
AvgLightnessAccuracy = l;
|
||||
AvgChromaAccuracy = c;
|
||||
AvgHueAccuracy = h;
|
||||
}
|
||||
}
|
||||
}
|
3
ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs.meta
Normal file
3
ColourMeOKGame/Assets/Scripts/LocalPlayerData.cs.meta
Normal file
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61389b17ec0645a6a0a9fe10bdaef72a
|
||||
timeCreated: 1731833904
|
18
ColourMeOKGame/Assets/Scripts/Playground.cs
Normal file
18
ColourMeOKGame/Assets/Scripts/Playground.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using UnityEngine;
|
||||
|
||||
public class Playground
|
||||
{
|
||||
private float _score;
|
||||
|
||||
private void SomethingSomethingScore()
|
||||
{
|
||||
var score = 0.0f;
|
||||
|
||||
_score = score;
|
||||
}
|
||||
|
||||
private void ShowTheScore()
|
||||
{
|
||||
Debug.Log(_score);
|
||||
}
|
||||
}
|
3
ColourMeOKGame/Assets/Scripts/Playground.cs.meta
Normal file
3
ColourMeOKGame/Assets/Scripts/Playground.cs.meta
Normal file
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 662d126b54a54d9ebe6a1dc716d199e3
|
||||
timeCreated: 1731830473
|
146
ColourMeOKGame/Assets/Scripts/SideViewUI.cs
Normal file
146
ColourMeOKGame/Assets/Scripts/SideViewUI.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
/// <summary>
|
||||
/// singleton for a single source of truth game state and flow management
|
||||
/// </summary>
|
||||
public class SideViewUI : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Button _accountButton;
|
||||
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Label _chromaAccuracyText;
|
||||
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Label _hueAccuracyText;
|
||||
|
||||
/// <summary>
|
||||
/// leaderboard button for showing the leaderboard
|
||||
/// </summary>
|
||||
private Button _leaderboardButton;
|
||||
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Label _lightnessAccuracyText;
|
||||
|
||||
/// <summary>
|
||||
/// play button for starting the game
|
||||
/// </summary>
|
||||
private Button _playButton;
|
||||
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Label _playerNameText;
|
||||
|
||||
/// <summary>
|
||||
/// settings button for showing the settings menu
|
||||
/// </summary>
|
||||
private Label _playerRatingText;
|
||||
|
||||
/// <summary>
|
||||
/// function to subscribe button events to their respective functions
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
var ui = GetComponent<UIDocument>().rootVisualElement;
|
||||
|
||||
_playButton = ui.Q<Button>("PlayButton");
|
||||
_playButton.clicked += OnPlayButtonClicked;
|
||||
|
||||
_leaderboardButton = ui.Q<Button>("LeaderboardButton");
|
||||
_leaderboardButton.clicked += OnLeaderboardButtonClicked;
|
||||
|
||||
_accountButton = ui.Q<Button>("AccountButton");
|
||||
_accountButton.clicked += OnAccountButtonClicked;
|
||||
|
||||
_lightnessAccuracyText = ui.Q<Label>("LightnessAccuracyText");
|
||||
_hueAccuracyText = ui.Q<Label>("HueAccuracyText");
|
||||
_chromaAccuracyText = ui.Q<Label>("ChromaAccuracyText");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to update the ui with the latest data
|
||||
/// </summary>
|
||||
/// <param name="localPlayerData"></param>
|
||||
private void RenderFromPlayerData(LocalPlayerData localPlayerData)
|
||||
{
|
||||
// calculate averages from both recent local scores and online scores
|
||||
var totalLightessAcc = 0f;
|
||||
var totalChromaAcc = 0f;
|
||||
var totalHueAcc = 0f;
|
||||
var totalRounds = 0;
|
||||
|
||||
// average out all the scores we have to get a stable-ish average
|
||||
|
||||
foreach (var localScore in localPlayerData.RecentLocalScores)
|
||||
{
|
||||
totalLightessAcc += localScore.AvgLightnessAccuracy;
|
||||
totalChromaAcc += localScore.AvgChromaAccuracy;
|
||||
totalHueAcc += localScore.AvgHueAccuracy;
|
||||
totalRounds += localScore.NoOfRounds;
|
||||
}
|
||||
|
||||
foreach (var onlineScore in localPlayerData.RecentOnlineScores)
|
||||
{
|
||||
totalLightessAcc += onlineScore.AvgLightnessAccuracy;
|
||||
totalChromaAcc += onlineScore.AvgChromaAccuracy;
|
||||
totalHueAcc += onlineScore.AvgHueAccuracy;
|
||||
totalRounds += onlineScore.NoOfRounds;
|
||||
}
|
||||
|
||||
foreach (var onlineScore in localPlayerData.BestOnlineScores)
|
||||
{
|
||||
totalLightessAcc += onlineScore.AvgLightnessAccuracy;
|
||||
totalChromaAcc += onlineScore.AvgChromaAccuracy;
|
||||
totalHueAcc += onlineScore.AvgHueAccuracy;
|
||||
totalRounds += onlineScore.NoOfRounds;
|
||||
}
|
||||
|
||||
// finally, set the labels
|
||||
_playerNameText.text = GameManager.Instance.Backend.GetUsername();
|
||||
_lightnessAccuracyText.text = $"{(totalLightessAcc / totalRounds):F}";
|
||||
_hueAccuracyText.text = $"{(totalHueAcc / totalRounds):F}";
|
||||
_chromaAccuracyText.text = $"{(totalChromaAcc / totalRounds):F}";
|
||||
|
||||
// and set the player rating, but after we get it from the backend
|
||||
// (god I LOVE async (I am LYING out of my teeth))
|
||||
GameManager.Instance.Backend.CalculateUserRating((dtr, rating) =>
|
||||
{
|
||||
if (dtr != Backend.DatabaseTransactionResult.Ok) return;
|
||||
_playerRatingText.text = $"{rating:F}";
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to show the play view
|
||||
/// </summary>
|
||||
private static void OnPlayButtonClicked()
|
||||
{
|
||||
GameManager.Instance.SetDisplayState(GameManager.DisplayState.PlayView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to show the leaderboard view
|
||||
/// </summary>
|
||||
private static void OnLeaderboardButtonClicked()
|
||||
{
|
||||
GameManager.Instance.SetDisplayState(GameManager.DisplayState.LeaderboardView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to show the account view
|
||||
/// </summary>
|
||||
private static void OnAccountButtonClicked()
|
||||
{
|
||||
GameManager.Instance.SetDisplayState(GameManager.DisplayState.AccountView);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 17a7c29fa9f16244682562d58604fdba
|
||||
guid: 6351b7620d84e2d43bc4f59c5f3f8b5c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
|
@ -100,6 +100,7 @@ #AccountFields TextField #unity-text-input {
|
|||
border-top-color: rgba(0, 0, 0, 0);
|
||||
border-bottom-color: rgba(0, 0, 0, 0);
|
||||
background-color: rgb(255, 232, 249);
|
||||
color: rgb(15, 13, 27);
|
||||
}
|
||||
|
||||
#AccountFields Button {
|
||||
|
|
Reference in a new issue