275 lines
No EOL
11 KiB
C#
275 lines
No EOL
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using UnityEngine;
|
|
|
|
public class LocalPlayerData
|
|
{
|
|
/// <summary>
|
|
/// maximum number of the best online scores to keep track of
|
|
/// </summary>
|
|
public const int MaxBestOnlineScores = 10;
|
|
|
|
/// <summary>
|
|
/// maximum number of recent local scores to keep track of
|
|
/// </summary>
|
|
public const int MaxRecentLocalScores = 10;
|
|
|
|
/// <summary>
|
|
/// queue of the best online scores,
|
|
/// used in user rating calculation and accuracy display stats
|
|
/// </summary>
|
|
public Queue<Score> BestOnlineScores = new(20);
|
|
|
|
/// <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>
|
|
/// loads player data from player prefs and database
|
|
/// </summary>
|
|
public void LoadFromTheWorld(Action<LocalPlayerData> callback)
|
|
{
|
|
// 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, Math.Max(1, noOfRounds), Math.Clamp(l, 0f, 100f),
|
|
Math.Clamp(c, 0f, 100f), Math.Clamp(h, 0f, 100f)));
|
|
}
|
|
|
|
// load online scores
|
|
RecentOnlineScores.Clear();
|
|
GameManager.Instance.Backend.GetRecentScores((_, recentOnlineScores) =>
|
|
{
|
|
foreach (var onlineScore in recentOnlineScores)
|
|
{
|
|
if (RecentOnlineScores.Count > 10) RecentOnlineScores.Dequeue();
|
|
RecentOnlineScores.Enqueue(onlineScore);
|
|
}
|
|
|
|
Debug.Log(
|
|
$"loaded lpdata from the world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
|
|
|
|
callback(this);
|
|
});
|
|
}
|
|
|
|
/// <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++;
|
|
}
|
|
|
|
Debug.Log("saved lpdata to playerprefs"); // online scores are already saved in the backend
|
|
}
|
|
|
|
/// <summary>
|
|
/// registers a score to the player's local data
|
|
/// </summary>
|
|
/// <param name="score">the score to register</param>
|
|
public void RegisterLocalScore(Score score)
|
|
{
|
|
while (RecentLocalScores.Count >= MaxRecentLocalScores) RecentLocalScores.Dequeue();
|
|
RecentLocalScores.Enqueue(score);
|
|
}
|
|
|
|
/// <summary>
|
|
/// calculates the user rating based on whatever local data is available
|
|
/// </summary>
|
|
/// <returns>the user rating (0-100f)</returns>
|
|
public float CalculateUserRating()
|
|
{
|
|
// user rating is like CHUNITHM's rating system
|
|
// where best + recent scores are averaged out
|
|
// in this case 20 best scores, and 10 recent scores are used
|
|
|
|
// ensure the scores don't exceed their arbitrary limits
|
|
while (RecentOnlineScores.Count > MaxRecentLocalScores) RecentOnlineScores.Dequeue();
|
|
while (RecentLocalScores.Count > MaxRecentLocalScores) RecentLocalScores.Dequeue();
|
|
while (BestOnlineScores.Count > MaxBestOnlineScores) BestOnlineScores.Dequeue();
|
|
|
|
// if online scores are available, use them
|
|
var recentScores = RecentOnlineScores.Count > 0 ? RecentOnlineScores : RecentLocalScores;
|
|
var bestScores = BestOnlineScores;
|
|
|
|
var scores = 0;
|
|
var totalRating = 0d;
|
|
|
|
foreach (var score in recentScores)
|
|
{
|
|
scores++;
|
|
var dL = score.AvgLightnessAccuracy;
|
|
var dC = score.AvgChromaAccuracy;
|
|
var dH = score.AvgHueAccuracy;
|
|
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
|
|
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy
|
|
+ score.AvgHueAccuracy * score.AvgHueAccuracy);
|
|
totalRating = (dL + dC + dH + dE) / 4d;
|
|
}
|
|
|
|
foreach (var score in bestScores)
|
|
{
|
|
scores++;
|
|
var dL = score.AvgLightnessAccuracy;
|
|
var dC = score.AvgChromaAccuracy;
|
|
var dH = score.AvgHueAccuracy;
|
|
var dE = Math.Sqrt(score.AvgLightnessAccuracy * score.AvgLightnessAccuracy
|
|
+ score.AvgChromaAccuracy * score.AvgChromaAccuracy
|
|
+ score.AvgHueAccuracy * score.AvgHueAccuracy);
|
|
totalRating = (dL + dC + dH + dE) / 4d;
|
|
}
|
|
|
|
return Math.Clamp((float)(totalRating / scores), 0f, 100f);
|
|
}
|
|
|
|
public struct Score
|
|
{
|
|
/// <summary>
|
|
/// timestamp of the score
|
|
/// </summary>
|
|
public DateTime Timestamp;
|
|
|
|
/// <summary>
|
|
/// number of rounds played (0-100)
|
|
/// </summary>
|
|
public readonly int NoOfRounds;
|
|
|
|
/// <summary>
|
|
/// average lightness accuracy across all rounds (0-100)
|
|
/// </summary>
|
|
public readonly float AvgLightnessAccuracy;
|
|
|
|
/// <summary>
|
|
/// average chroma accuracy across all rounds (0-100)
|
|
/// </summary>
|
|
public readonly float AvgChromaAccuracy;
|
|
|
|
/// <summary>
|
|
/// average hue accuracy across all rounds (0-100)
|
|
/// </summary>
|
|
public readonly 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// dict-based constructor for the score struct
|
|
/// </summary>
|
|
/// <param name="data">dictionary of the score data</param>
|
|
public Score(Dictionary<string, object> data)
|
|
{
|
|
// try to safely construct the score from a backend-provided dictionary
|
|
// for each value, if it's not found, or not a valid value, set it to a default value
|
|
Timestamp = data.ContainsKey("timestamp") && data["timestamp"] is long t
|
|
? DateTimeOffset.FromUnixTimeSeconds(t).DateTime
|
|
: DateTime.MinValue;
|
|
NoOfRounds = data.ContainsKey("noOfRounds") && data["noOfRounds"] is int n ? n : 1;
|
|
AvgLightnessAccuracy = data.ContainsKey("avgLightnessAccuracy") && data["avgLightnessAccuracy"] is float l
|
|
? l
|
|
: 100.0f;
|
|
AvgChromaAccuracy = data.ContainsKey("avgChromaAccuracy") && data["avgChromaAccuracy"] is float c
|
|
? c
|
|
: 100.0f;
|
|
AvgHueAccuracy = data.ContainsKey("avgHueAccuracy") && data["avgHueAccuracy"] is float h ? h : 100.0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// converts the score struct to a dictionary safe for the backend
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public Dictionary<string, object> ToDictionary()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "timestamp", new DateTimeOffset(Timestamp).ToUnixTimeSeconds() },
|
|
{ "noOfRounds", NoOfRounds },
|
|
{ "avgLightnessAccuracy", AvgLightnessAccuracy },
|
|
{ "avgChromaAccuracy", AvgChromaAccuracy },
|
|
{ "avgHueAccuracy", AvgHueAccuracy }
|
|
};
|
|
}
|
|
}
|
|
} |