update map gen
This commit is contained in:
35
Assets/Scripts/Game/EloData.cs
Normal file
35
Assets/Scripts/Game/EloData.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Fusion;
|
||||
|
||||
namespace Hallucinate.Game
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct PlayerEloData
|
||||
{
|
||||
public int Rating;
|
||||
public int GamesPlayed;
|
||||
|
||||
public PlayerEloData(int rating, int gamesPlayed)
|
||||
{
|
||||
Rating = rating;
|
||||
GamesPlayed = gamesPlayed;
|
||||
}
|
||||
|
||||
public static PlayerEloData Default => new PlayerEloData(1000, 0);
|
||||
}
|
||||
|
||||
public struct EloResult : INetworkStruct
|
||||
{
|
||||
public int NewRatingA;
|
||||
public int NewRatingB;
|
||||
public int DeltaA;
|
||||
public int DeltaB;
|
||||
|
||||
public EloResult(int nA, int nB, int dA, int dB)
|
||||
{
|
||||
NewRatingA = nA;
|
||||
NewRatingB = nB;
|
||||
DeltaA = dA;
|
||||
DeltaB = dB;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Game/EloData.cs.meta
Normal file
2
Assets/Scripts/Game/EloData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7dc894771ad8df46831ee15ee34fe7d
|
||||
@@ -1,27 +1,43 @@
|
||||
using UnityEngine;
|
||||
using Fusion;
|
||||
|
||||
namespace Hallucinate.Game
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure logic for Elo rating calculations.
|
||||
/// Follows the 1v1 competitive formula with dynamic K-factor.
|
||||
/// </summary>
|
||||
public static class EloSystem
|
||||
{
|
||||
public const int RATING_FLOOR = 100;
|
||||
public const int PLACEMENT_GAMES = 30;
|
||||
|
||||
public static EloResult Calculate(
|
||||
int ratingA, int ratingB,
|
||||
int gamesPlayedA, int gamesPlayedB,
|
||||
float resultA) // 1=win, 0=lose, 0.5=draw
|
||||
{
|
||||
// 1. Expected Scores
|
||||
float eA = 1f / (1f + Mathf.Pow(10f, (ratingB - ratingA) / 400f));
|
||||
float eB = 1f - eA;
|
||||
|
||||
// 2. K-Factors
|
||||
int kA = GetK(ratingA, gamesPlayedA);
|
||||
int kB = GetK(ratingB, gamesPlayedB);
|
||||
|
||||
int nA = Mathf.Max(100, Mathf.RoundToInt(ratingA + kA * (resultA - eA)));
|
||||
int nB = Mathf.Max(100, Mathf.RoundToInt(ratingB + kB * ((1 - resultA) - (1 - eA))));
|
||||
// 3. New Ratings
|
||||
int nA = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingA + kA * (resultA - eA)));
|
||||
int nB = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingB + kB * ((1f - resultA) - eB)));
|
||||
|
||||
return new EloResult(nA, nB, nA - ratingA, nB - ratingB);
|
||||
}
|
||||
|
||||
private static int GetK(int r, int g) =>
|
||||
g < 30 ? 40 : r < 1200 ? 32 : r < 2000 ? 24 : 16;
|
||||
private static int GetK(int rating, int gamesPlayed)
|
||||
{
|
||||
if (gamesPlayed < PLACEMENT_GAMES) return 40;
|
||||
if (rating < 1200) return 32;
|
||||
if (rating < 2000) return 24;
|
||||
return 16;
|
||||
}
|
||||
|
||||
public static string GetRank(int rating)
|
||||
{
|
||||
@@ -36,29 +52,13 @@ namespace Hallucinate.Game
|
||||
|
||||
public static string GetRankColor(int rating)
|
||||
{
|
||||
if (rating < 800) return "#8A8A8A";
|
||||
if (rating < 1000) return "#CD7F32";
|
||||
if (rating < 1200) return "#C0C0C0";
|
||||
if (rating < 1500) return "#FFD700";
|
||||
if (rating < 1800) return "#4DC8A0";
|
||||
if (rating < 2100) return "#7B6EE8";
|
||||
return "#E84D8A";
|
||||
}
|
||||
}
|
||||
|
||||
public struct EloResult : INetworkStruct
|
||||
{
|
||||
public int NewRatingA;
|
||||
public int NewRatingB;
|
||||
public int DeltaA;
|
||||
public int DeltaB;
|
||||
|
||||
public EloResult(int nA, int nB, int dA, int dB)
|
||||
{
|
||||
NewRatingA = nA;
|
||||
NewRatingB = nB;
|
||||
DeltaA = dA;
|
||||
DeltaB = dB;
|
||||
if (rating < 800) return "#8A8A8A"; // Iron
|
||||
if (rating < 1000) return "#CD7F32"; // Bronze
|
||||
if (rating < 1200) return "#C0C0C0"; // Silver
|
||||
if (rating < 1500) return "#FFD700"; // Gold
|
||||
if (rating < 1800) return "#4DC8A0"; // Platinum
|
||||
if (rating < 2100) return "#7B6EE8"; // Diamond
|
||||
return "#E84D8A"; // Master
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
Assets/Scripts/Game/MatchEloManager.cs
Normal file
94
Assets/Scripts/Game/MatchEloManager.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Fusion;
|
||||
using Hallucinate.Game;
|
||||
using Hallucinate.UI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Hallucinate.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Orchestrates the Elo calculation and persistence on the Host.
|
||||
/// Broadcasts results to all clients.
|
||||
/// </summary>
|
||||
public class MatchEloManager : NetworkBehaviour
|
||||
{
|
||||
[Networked] public EloResult LastMatchResult { get; set; }
|
||||
[Networked] public bool IsCalculating { get; set; }
|
||||
|
||||
private Dictionary<PlayerRef, string> _playerUsernames = new Dictionary<PlayerRef, string>();
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
if (Object.HasStateAuthority)
|
||||
{
|
||||
// In a real scenario, you'd collect usernames as players join.
|
||||
// For now, we assume they are provided or stored in PlayerRef custom data.
|
||||
// Placeholder: Use a mock or collect from Session properties.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player's username when they join.
|
||||
/// </summary>
|
||||
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
|
||||
public void RPC_RegisterUsername(PlayerRef player, string username)
|
||||
{
|
||||
if (!_playerUsernames.ContainsKey(player))
|
||||
{
|
||||
_playerUsernames.Add(player, username);
|
||||
Debug.Log($"[EloManager] Registered {username} for {player}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by GameManager on Host when match ends.
|
||||
/// </summary>
|
||||
public async void ProcessMatchResult(PlayerRef winner, PlayerRef loser, bool isDraw = false)
|
||||
{
|
||||
if (!Object.HasStateAuthority) return;
|
||||
|
||||
IsCalculating = true;
|
||||
|
||||
string nameA = _playerUsernames.GetValueOrDefault(winner, "Unknown_A");
|
||||
string nameB = _playerUsernames.GetValueOrDefault(loser, "Unknown_B");
|
||||
|
||||
// 1. Fetch from Firebase
|
||||
var dataA = await FirebaseService.GetPlayerData(nameA);
|
||||
var dataB = await FirebaseService.GetPlayerData(nameB);
|
||||
|
||||
// 2. Calculate
|
||||
float resultA = isDraw ? 0.5f : 1.0f;
|
||||
var result = EloSystem.Calculate(dataA.Rating, dataB.Rating, dataA.GamesPlayed, dataB.GamesPlayed, resultA);
|
||||
|
||||
// 3. Update Data Objects
|
||||
dataA.Rating = result.NewRatingA;
|
||||
dataA.GamesPlayed++;
|
||||
|
||||
dataB.Rating = result.NewRatingB;
|
||||
dataB.GamesPlayed++;
|
||||
|
||||
// 4. Save to Firebase
|
||||
await Task.WhenAll(
|
||||
FirebaseService.SavePlayerData(nameA, dataA),
|
||||
FirebaseService.SavePlayerData(nameB, dataB)
|
||||
);
|
||||
|
||||
// 5. Broadcast
|
||||
LastMatchResult = result;
|
||||
IsCalculating = false;
|
||||
|
||||
Debug.Log($"[EloManager] Match Processed. Winner: {nameA} (+{result.DeltaA}), Loser: {nameB} ({result.DeltaB})");
|
||||
|
||||
// Send RPC to show UI
|
||||
RPC_NotifyClients(result);
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
|
||||
private void RPC_NotifyClients(EloResult result)
|
||||
{
|
||||
// This is where you'd trigger the Post-Match Rive/UI
|
||||
Debug.Log($"[Client] Received Elo Update: {result.DeltaA} / {result.DeltaB}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Game/MatchEloManager.cs.meta
Normal file
2
Assets/Scripts/Game/MatchEloManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1509b216eb9b7249bc7bb184f418a6f
|
||||
8
Assets/Scripts/GameSetup/Maze/Native.meta
Normal file
8
Assets/Scripts/GameSetup/Maze/Native.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05fdc25279e7ac148a44fde646c93546
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/Scripts/GameSetup/Maze/Native/NativeNoiseProvider.cs
Normal file
64
Assets/Scripts/GameSetup/Maze/Native/NativeNoiseProvider.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Hallucinate.GameSetup.Maze.Native
|
||||
{
|
||||
public class NativeNoiseProvider : IDisposable
|
||||
{
|
||||
private const string DLL_NAME = "BackroomsNoise";
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern IntPtr CreateNoiseGenerator(int seed, float frequency, int noiseType);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern float GetNoiseValue(IntPtr handle, float x, float z);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern void GetNoiseBuffer(IntPtr handle, float startX, float startZ, int width, int depth, float[] buffer);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern void DestroyNoiseGenerator(IntPtr handle);
|
||||
|
||||
private IntPtr _handle;
|
||||
public bool IsInitialized => _handle != IntPtr.Zero;
|
||||
|
||||
public NativeNoiseProvider(int seed, float frequency = 0.01f, int noiseType = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_handle = CreateNoiseGenerator(seed, frequency, noiseType);
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Debug.LogWarning($"Native library '{DLL_NAME}' not found. Ensure it is compiled and placed in Plugins folder.");
|
||||
}
|
||||
}
|
||||
|
||||
public float GetNoise(float x, float z)
|
||||
{
|
||||
if (!IsInitialized) return 0f;
|
||||
return GetNoiseValue(_handle, x, z);
|
||||
}
|
||||
|
||||
public void FillBuffer(float startX, float startZ, int width, int depth, float[] buffer)
|
||||
{
|
||||
if (!IsInitialized || buffer == null) return;
|
||||
GetNoiseBuffer(_handle, startX, startZ, width, depth, buffer);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
DestroyNoiseGenerator(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
~NativeNoiseProvider()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fd030227a1b87a4f8826f9b317fbf87
|
||||
@@ -18,18 +18,20 @@ public class GameManager : NetworkBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerGameOver() {
|
||||
[SerializeField] private Hallucinate.Network.MatchEloManager eloManager;
|
||||
|
||||
public void TriggerGameOver(PlayerRef winner, PlayerRef loser, bool isDraw = false) {
|
||||
if (!isGameOver) {
|
||||
// Mark the game as over
|
||||
isGameOver = true;
|
||||
|
||||
if (gameOverText != null) {
|
||||
// Display the Game Over text
|
||||
gameOverText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// Freeze the game by setting the time scale to 0
|
||||
Time.timeScale = 0;
|
||||
// Only Host processes Elo
|
||||
if (Runner.IsServer && eloManager != null) {
|
||||
eloManager.ProcessMatchResult(winner, loser, isDraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ Material:
|
||||
- _ZWrite: 0
|
||||
m_Colors:
|
||||
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
|
||||
- _Color: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
|
||||
- _Color: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
|
||||
- _Emission: {r: 0, g: 0, b: 0, a: 0}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _Flip: {r: 1, g: 1, b: 1, a: 1}
|
||||
@@ -123,6 +123,6 @@ Material:
|
||||
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
|
||||
- _SpecColor: {r: 0, g: 0, b: 0, a: 0}
|
||||
- _Specular: {r: 1, g: 1, b: 1, a: 0}
|
||||
- _TintColor: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
|
||||
- _TintColor: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
|
||||
@@ -53,10 +53,27 @@ namespace Hallucinate.UI
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RegisterUser(string username)
|
||||
public static async Task<Hallucinate.Game.PlayerEloData> GetPlayerData(string username)
|
||||
{
|
||||
string url = $"{BASE_URL}/{username}.json";
|
||||
string jsonData = "{\"created_at\": \"" + DateTime.Now.ToString() + "\"}";
|
||||
string url = $"{BASE_URL}/{username}/elo.json";
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
var operation = request.SendWebRequest();
|
||||
while (!operation.isDone) await Task.Yield();
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success || request.downloadHandler.text == "null")
|
||||
{
|
||||
return Hallucinate.Game.PlayerEloData.Default;
|
||||
}
|
||||
|
||||
return JsonUtility.FromJson<Hallucinate.Game.PlayerEloData>(request.downloadHandler.text);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> SavePlayerData(string username, Hallucinate.Game.PlayerEloData data)
|
||||
{
|
||||
string url = $"{BASE_URL}/{username}/elo.json";
|
||||
string jsonData = JsonUtility.ToJson(data);
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Put(url, jsonData))
|
||||
{
|
||||
|
||||
@@ -25,17 +25,17 @@ namespace Hallucinate.UI
|
||||
{
|
||||
Debug.Log($"<color=green>[Firebase] Username '{testUsername}' còn trống. Tiến hành đăng ký...</color>");
|
||||
|
||||
// Bước 2: Thử đăng ký user mới
|
||||
bool success = await FirebaseService.RegisterUser(testUsername);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Debug.Log("<color=green>[Firebase] Đăng ký thành công! Hãy kiểm tra trình duyệt (Firebase Console).</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[Firebase] Đăng ký thất bại. Kiểm tra link URL hoặc Internet.");
|
||||
}
|
||||
// // Bước 2: Thử đăng ký user mới
|
||||
// bool success = await FirebaseService.RegisterUser(testUsername);
|
||||
//
|
||||
// if (success)
|
||||
// {
|
||||
// Debug.Log("<color=green>[Firebase] Đăng ký thành công! Hãy kiểm tra trình duyệt (Firebase Console).</color>");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogError("[Firebase] Đăng ký thất bại. Kiểm tra link URL hoặc Internet.");
|
||||
// }
|
||||
}
|
||||
|
||||
Debug.Log("<color=cyan>--- Firebase Test Finished ---</color>");
|
||||
|
||||
@@ -69,25 +69,25 @@ namespace Hallucinate.UI
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2. Đăng ký user mới
|
||||
bool success = await FirebaseService.RegisterUser(username);
|
||||
if (success)
|
||||
{
|
||||
// 3. Lưu lại và đóng popup
|
||||
PlayerPrefs.SetString("Username", username);
|
||||
PlayerPrefs.Save();
|
||||
Debug.Log($"[Login] Registered as {username}");
|
||||
|
||||
// Thông báo cho UIManager biết đã login xong
|
||||
uiManager.OnLoginSuccess();
|
||||
await PlayTransitionOut();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowError("Connection error!");
|
||||
_confirmBtn.SetEnabled(true);
|
||||
_confirmBtn.text = "CONFIRM";
|
||||
}
|
||||
// // 2. Đăng ký user mới
|
||||
// bool success = await FirebaseService.RegisterUser(username);
|
||||
// if (success)
|
||||
// {
|
||||
// // 3. Lưu lại và đóng popup
|
||||
// PlayerPrefs.SetString("Username", username);
|
||||
// PlayerPrefs.Save();
|
||||
// Debug.Log($"[Login] Registered as {username}");
|
||||
//
|
||||
// // Thông báo cho UIManager biết đã login xong
|
||||
// uiManager.OnLoginSuccess();
|
||||
// await PlayTransitionOut();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// ShowError("Connection error!");
|
||||
// _confirmBtn.SetEnabled(true);
|
||||
// _confirmBtn.text = "CONFIRM";
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Hallucinate.Game;
|
||||
using Hallucinate.UI;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
{
|
||||
public class ProfileController : BaseUIController
|
||||
{
|
||||
private Label _username;
|
||||
private Label _rank;
|
||||
private Label _eloLabel;
|
||||
private ProgressBar _winRateBar;
|
||||
private Label _winRateText;
|
||||
private Button _logoutBtn;
|
||||
@@ -22,6 +26,7 @@ namespace Hallucinate.UI
|
||||
|
||||
_username = root.Q<Label>("Username");
|
||||
_rank = root.Q<Label>("Rank");
|
||||
_eloLabel = root.Q<Label>("EloLabel");
|
||||
_winRateBar = root.Q<ProgressBar>("WinRateBar");
|
||||
_winRateText = root.Q<Label>("WinRateText");
|
||||
_logoutBtn = root.Q<Button>("LogoutBtn");
|
||||
@@ -67,11 +72,11 @@ namespace Hallucinate.UI
|
||||
|
||||
public override async Task PlayTransitionIn()
|
||||
{
|
||||
LoadProfileData(); // Refresh data every time we show the profile
|
||||
await LoadProfileData(); // Refresh data every time we show the profile
|
||||
await base.PlayTransitionIn();
|
||||
}
|
||||
|
||||
private void LoadProfileData()
|
||||
private async Task LoadProfileData()
|
||||
{
|
||||
// Load saved username or fallback
|
||||
string savedName = PlayerPrefs.GetString("Username", "Unknown Player");
|
||||
@@ -81,10 +86,27 @@ namespace Hallucinate.UI
|
||||
_googleIdPlaceholder = PlayerPrefs.GetString("GoogleID", "NOT_LINKED");
|
||||
_avatarUrlPlaceholder = PlayerPrefs.GetString("AvatarURL", "");
|
||||
|
||||
// Mock progression data for now
|
||||
_rank.text = "DIAMOND II";
|
||||
_winRateBar.value = 72;
|
||||
_winRateText.text = "72%";
|
||||
// Fetch real Elo data
|
||||
var eloData = await FirebaseService.GetPlayerData(savedName);
|
||||
|
||||
if (_eloLabel != null) _eloLabel.text = eloData.Rating.ToString();
|
||||
|
||||
if (_rank != null)
|
||||
{
|
||||
_rank.text = EloSystem.GetRank(eloData.Rating).ToUpper();
|
||||
if (ColorUtility.TryParseHtmlString(EloSystem.GetRankColor(eloData.Rating), out Color rankColor))
|
||||
{
|
||||
_rank.style.color = rankColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Win Rate from GamesPlayed (requires adding wins to schema, but for now we use mock/partial)
|
||||
if (eloData.GamesPlayed > 0)
|
||||
{
|
||||
// Note: To show a real winrate, we'd need to store 'Wins' in PlayerEloData.
|
||||
// For now, keeping the mock or setting a placeholder.
|
||||
_winRateText.text = "CALCULATING...";
|
||||
}
|
||||
}
|
||||
|
||||
private async void Logout()
|
||||
|
||||
Reference in New Issue
Block a user