update map gen

This commit is contained in:
2026-06-09 22:46:32 +07:00
parent 9048435ac4
commit 1c8544d383
28 changed files with 834 additions and 442 deletions

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a7dc894771ad8df46831ee15ee34fe7d

View File

@@ -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
}
}
}

View 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}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b1509b216eb9b7249bc7bb184f418a6f