using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
///
/// singleton for a single source of truth game state and flow management
///
public class GameManager : MonoBehaviour
{
///
/// singleton pattern: define instance field for accessing the singleton elsewhere
///
public static GameManager Instance;
///
/// ui manager object for handling ui state and flow
///
public UIManager ui;
///
/// list of callbacks to call when the local player data changes
///
private readonly List> _onLocalPlayerDataChangeCallbacks = new();
///
/// the local player data object for storing player data
///
private LocalPlayerData _data;
///
/// backend object for handling communication with the firebase backend
///
public Backend Backend;
///
/// gameplay object for handling game loop
///
public Gameplay Gameplay;
///
/// read-only property for accessing the local player data outside this class
///
public LocalPlayerData Data => _data;
///
/// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
///
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);
}
}
///
/// start modifying state
///
private void Start()
{
Debug.Log("GameManager starts here");
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
}
///
/// initialise variables and ui elements
///
private void OnEnable()
{
ui = UIManager.Instance;
// load the local player data and refresh the ui
_data = new LocalPlayerData();
Backend = new Backend();
Backend.Initialise(status =>
{
Debug.Log("initialised backend, setting connection status text");
ui.UI.Q("ConnectionStatusText").text = status switch
{
Backend.FirebaseConnectionStatus.Connected => "Status: Connected",
Backend.FirebaseConnectionStatus.Updating => "Status: Updating... (Retrying in a bit!)",
Backend.FirebaseConnectionStatus.NotConnected => "Status: Disconnected",
Backend.FirebaseConnectionStatus.UpdateRequired =>
"Status: Disconnected (Device Component Update Required)",
Backend.FirebaseConnectionStatus.ExternalError => "Status: Disconnected (External/Device Error)",
Backend.FirebaseConnectionStatus.InternalError => "Status: Disconnected (Internal Error)",
_ => "Status: Disconnected (unknown fcs state, this is unreachable and a bug)"
};
if (status == Backend.FirebaseConnectionStatus.Connected) return;
// if we're not connected, hide any online 'features'
ui.UI.Q("LeaderboardButton").style.display = DisplayStyle.None;
ui.UI.Q("AccountButton").style.display = DisplayStyle.None;
});
// register a callback to refresh the ui when the player signs in
Backend.RegisterOnSignInCallback(_ =>
{
Debug.Log("sign in callback, refreshing GameManager-controlled SideView UI");
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
});
Backend.RegisterOnConnectionStatusChangedCallback(status =>
{
Debug.Log($"post-fcStatus change, deciding to show/hide buttons based on new status: {status}");
ui.UI.Q("LeaderboardButton").style.display =
status == Backend.FirebaseConnectionStatus.Connected
? DisplayStyle.Flex
: DisplayStyle.None;
ui.UI.Q("AccountButton").style.display =
status == Backend.FirebaseConnectionStatus.Connected
? DisplayStyle.Flex
: DisplayStyle.None;
});
Gameplay = new Gameplay(ui.UI);
}
///
/// called when the application is quitting, saves the local player data
///
private void OnApplicationQuit()
{
Debug.Log("running deferred cleanup/save functions");
Backend.Cleanup();
_data.SaveToTheWorld();
}
///
/// function to register a callback to be called when the local player data changes
///
/// callback function that takes a LocalPlayerData object
public void RegisterOnLocalPlayerDataChangeCallback(Action callback)
{
_onLocalPlayerDataChangeCallbacks.Add(callback);
Debug.Log($"registering LocalPlayerDataChangeCallback ({_onLocalPlayerDataChangeCallbacks.Count})");
}
///
/// function to fire all local player data change callbacks
///
public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
{
Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
foreach (var callback in _onLocalPlayerDataChangeCallbacks)
try
{
callback.Invoke(data);
}
catch (Exception e)
{
Debug.LogError($"error invoking LocalPlayerDataChangeCallback: {e.Message}");
}
}
public void SignalGameEnd(List playedRounds)
{
Debug.Log("signalling game end");
// calculate historical averages
var historicalLightnessAcc = 0f;
var historicalChromaAcc = 0f;
var historicalHueAcc = 0f;
foreach (var localScore in _data.RecentLocalScores)
{
historicalLightnessAcc += localScore.AvgLightnessAccuracy;
historicalChromaAcc += localScore.AvgChromaAccuracy;
historicalHueAcc += localScore.AvgHueAccuracy;
}
foreach (var onlineScore in _data.RecentOnlineScores)
{
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy;
}
foreach (var onlineScore in _data.BestOnlineScores)
{
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
historicalHueAcc += onlineScore.AvgHueAccuracy;
}
// calculate round averages
var roundLightnessAcc = 0d;
var roundChromaAcc = 0d;
var roundHueAcc = 0d;
var roundPerceivedAcc = 0d;
var maxDistance = Colorimetry.CalculateDistance(Color.black, Color.white);
foreach (var distance in playedRounds.Select(round =>
Colorimetry.CalculateDistance(round.TemplateColour, round.ResponseColour)))
{
roundLightnessAcc += distance.dL / maxDistance.dL;
roundChromaAcc += distance.dC / maxDistance.dC;
roundHueAcc += distance.dH / maxDistance.dH;
roundPerceivedAcc += distance.dE / maxDistance.dE;
}
roundLightnessAcc /= playedRounds.Count;
roundChromaAcc /= playedRounds.Count;
roundHueAcc /= playedRounds.Count;
roundPerceivedAcc /= playedRounds.Count;
var roundAcc = (roundLightnessAcc + roundChromaAcc + roundHueAcc) / 3;
// make comparison texts
var lAccDeltaText = (roundLightnessAcc > historicalLightnessAcc ? "+" : "-") +
Math.Abs(roundLightnessAcc - historicalLightnessAcc).ToString("P");
var cAccDeltaText = (roundChromaAcc > historicalChromaAcc ? "+" : "-") +
Math.Abs(roundChromaAcc - historicalChromaAcc).ToString("P");
var hAccDeltaText = (roundHueAcc > historicalHueAcc ? "+" : "-") +
Math.Abs(roundHueAcc - historicalHueAcc).ToString("P");
var score = new LocalPlayerData.Score(DateTime.Now,
playedRounds.Count,
(float)roundLightnessAcc,
(float)roundChromaAcc,
(float)roundHueAcc,
(float)roundPerceivedAcc);
_data.RegisterLocalScore(score);
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
Backend.SubmitScore(score,
submitRes =>
{
if (submitRes != Backend.DatabaseTransactionResult.Ok)
{
Debug.Log("couldn't submit score");
TransitionToResultsView(_data.CalculateUserRating());
return;
}
Backend.CalculateUserRating((urcRes, userRating) =>
{
if (urcRes != Backend.DatabaseTransactionResult.Ok)
{
Debug.Log("couldn't calculate user rating");
TransitionToResultsView(_data.CalculateUserRating());
FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
return;
}
Backend.UpdateUserRating(updateRes =>
{
if (updateRes != Backend.DatabaseTransactionResult.Ok)
{
Debug.Log("calculated user rating but couldn't update it");
TransitionToResultsView(userRating);
return;
}
TransitionToResultsView(userRating);
});
});
});
return;
void TransitionToResultsView(float rating)
{
var ratingText = rating >= 0 ? $"\nYour rating is {rating}" : "\nYour rating could not be calculated.";
// build the result text and show the results view
ui.UI.Q("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,",
$"you were {roundAcc} accurate.", "",
$"Lightness was {roundLightnessAcc}% accurate. ({lAccDeltaText} from your average)",
$"Chroma was {roundChromaAcc}% accurate. ({cAccDeltaText} from your average)",
$"Hue was {roundHueAcc}% accurate. ({hAccDeltaText} from your average)") + ratingText;
ui.SetDisplayState(UIManager.DisplayState.ResultsView);
}
}
}