feat: stats + chat + frontend pages (Stats, Chat, NavBar)
Backend: - StatsManager.js: JSON persistence, leaderboard, rate-limit 1/5s - ChatManager.js: 200-msg buffer, JSON persistence - index.js: routes GET/POST /stats, /chat/history, /chat/send (Zod validation) - ArenaRoom.js: chat handler broadcasts to room + persists via ChatManager Unity: - StatsTracker.cs: distance, maxSpeed, jumps, bumps, checkpoints, raceTime tracking - ChatUI.cs: F3 toggle, bottom-right panel, polling 3s, unread badge - NetworkManager.cs: SendChatMessage() + OnMessage<ChatUI.ChatMessage>(chat) - CheckpointSystem.cs: RegisterCheckpoint/Finish hooks - PlayerController.cs: RegisterJump/Bump hooks, physics rebalance, billboard fix - GameHUD.cs: LocalRaceTimer, SetTotalRounds, OnRoundStart signature fix - GameManager.cs: spectator cam reconnect fix Frontend: - NavBar.jsx: fixed top nav, Accueil/Stats/Chat/Jouer - App.jsx: page state (home/play/stats/chat) + NavBar - StatsPage.jsx: 6-tab leaderboard, auto-refresh 30s - ChatPage.jsx: polling 3s, localStorage name, Enter to send Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -376,9 +376,9 @@ public class PlayerController : MonoBehaviour
|
||||
{
|
||||
if (context.started)
|
||||
{
|
||||
// Touche appuyée
|
||||
isJumpPressed = true;
|
||||
jumpPressTime = 0f;
|
||||
StatsTracker.Instance?.RegisterJump();
|
||||
Debug.Log("Jump Started");
|
||||
}
|
||||
else if (context.performed)
|
||||
@@ -516,6 +516,7 @@ public class PlayerController : MonoBehaviour
|
||||
return;
|
||||
|
||||
_lastBumpTime[id] = Time.time;
|
||||
StatsTracker.Instance?.RegisterBump();
|
||||
|
||||
// Repulsion direction: from remote toward local player
|
||||
Vector3 dir = (transform.position - other.transform.position).normalized;
|
||||
@@ -606,6 +607,13 @@ public class PlayerController : MonoBehaviour
|
||||
fontStyle = FontStyle.Bold
|
||||
};
|
||||
labelStyle.normal.textColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.9f);
|
||||
// Outline: draw 4× in black at ±1px, then once in white
|
||||
var shadowStyle = new GUIStyle(labelStyle);
|
||||
shadowStyle.normal.textColor = new Color(0f, 0f, 0f, _gaugeDisplayAlpha * 0.55f);
|
||||
GUI.Label(new Rect(x + 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||
GUI.Label(new Rect(x - 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||
GUI.Label(new Rect(x + 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||
GUI.Label(new Rect(x - 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||
GUI.Label(new Rect(x, y - 26f, barWidth, 24f), "JUMP POWER", labelStyle);
|
||||
|
||||
// Ensure textures
|
||||
|
||||
@@ -142,7 +142,6 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
case GamePhase.Lobby:
|
||||
SetPlayerActive(NetworkManager.Instance?.IsConnected ?? false);
|
||||
SetSpectatorActive(false);
|
||||
gameHUD?.SetPhase("lobby");
|
||||
break;
|
||||
|
||||
|
||||
@@ -171,6 +171,10 @@ public class NetworkManager : MonoBehaviour
|
||||
Debug.Log($"[Network] Game over — Winner: {msg.winner}");
|
||||
OnGameEnd?.Invoke(msg.winner);
|
||||
});
|
||||
_room.OnMessage<ChatUI.ChatMessage>("chat", msg =>
|
||||
{
|
||||
ChatUI.Instance?.ReceiveChatMessage(msg);
|
||||
});
|
||||
|
||||
_room.OnLeave += OnRoomLeave;
|
||||
OnConnected?.Invoke();
|
||||
@@ -206,6 +210,12 @@ public class NetworkManager : MonoBehaviour
|
||||
await _room.Send("checkpointReached", new { index });
|
||||
}
|
||||
|
||||
public async void SendChatMessage(string text)
|
||||
{
|
||||
if (_room != null && IsConnected)
|
||||
await _room.Send("chat", new { text });
|
||||
}
|
||||
|
||||
// ─── State Callbacks ─────────────────────────────────────────────────
|
||||
|
||||
private void _OnPhaseChanged(string phase)
|
||||
|
||||
@@ -67,6 +67,7 @@ public class CheckpointSystem : MonoBehaviour
|
||||
}
|
||||
|
||||
_localCheckpointIndex++;
|
||||
StatsTracker.Instance?.RegisterCheckpoint();
|
||||
NetworkManager.Instance?.SendCheckpoint(_localCheckpointIndex);
|
||||
|
||||
Debug.Log($"[Checkpoint] Reached {_localCheckpointIndex}/{checkpoints.Length}");
|
||||
@@ -79,6 +80,7 @@ public class CheckpointSystem : MonoBehaviour
|
||||
if (_localCheckpointIndex >= checkpoints.Length)
|
||||
{
|
||||
_finished = true;
|
||||
StatsTracker.Instance?.RegisterFinish(GameHUD.Instance != null ? GameHUD.Instance.LocalRaceTimer : 0f);
|
||||
Debug.Log("[Checkpoint] FINISH LINE reached!");
|
||||
}
|
||||
else
|
||||
|
||||
229
game/Assets/Scripts/Stats/StatsTracker.cs
Normal file
229
game/Assets/Scripts/Stats/StatsTracker.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks per-session and per-round player statistics and uploads them to the game server.
|
||||
/// All HTTP calls use UnityWebRequest coroutines (WebGL-safe, no async/await).
|
||||
/// </summary>
|
||||
public class StatsTracker : MonoBehaviour
|
||||
{
|
||||
public static StatsTracker Instance { get; private set; }
|
||||
|
||||
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||
|
||||
// Cumulative session stats (accumulate across rounds)
|
||||
private float _totalDistance;
|
||||
private int _totalJumps;
|
||||
private float _maxSpeed;
|
||||
private float _bestRaceTime; // 0 = not set
|
||||
private int _racesPlayed;
|
||||
private int _qualifications;
|
||||
private int _eliminations;
|
||||
private int _checkpointsTotal;
|
||||
private int _bumpsGiven;
|
||||
private float _totalPlaytime;
|
||||
|
||||
// Per-round deltas (reset after each send)
|
||||
private float _roundDistance;
|
||||
private float _roundMaxSpeed;
|
||||
private float _sessionStart;
|
||||
|
||||
private Vector3 _lastPos;
|
||||
private bool _trackingActive;
|
||||
private PlayerController _pc;
|
||||
private Rigidbody _rb;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
_sessionStart = Time.time;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
_pc = GetComponent<PlayerController>();
|
||||
_rb = GetComponent<Rigidbody>();
|
||||
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
nm.OnRoundStart += OnRoundStart;
|
||||
nm.OnRoundEnd += OnRoundEnd;
|
||||
nm.OnQualified += OnQualified;
|
||||
nm.OnEliminated += OnEliminated;
|
||||
nm.OnConnected += OnConnected;
|
||||
nm.OnDisconnected += OnDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
nm.OnRoundStart -= OnRoundStart;
|
||||
nm.OnRoundEnd -= OnRoundEnd;
|
||||
nm.OnQualified -= OnQualified;
|
||||
nm.OnEliminated -= OnEliminated;
|
||||
nm.OnConnected -= OnConnected;
|
||||
nm.OnDisconnected -= OnDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!_trackingActive || _rb == null || _pc == null || !_pc.enabled) return;
|
||||
|
||||
Vector3 pos = transform.position;
|
||||
float delta = Vector3.Distance(pos, _lastPos);
|
||||
if (delta < 20f) // sanity cap against teleports
|
||||
{
|
||||
_roundDistance += delta;
|
||||
_totalDistance += delta;
|
||||
}
|
||||
_lastPos = pos;
|
||||
|
||||
float speed = _rb.linearVelocity.magnitude;
|
||||
if (speed > _roundMaxSpeed) _roundMaxSpeed = speed;
|
||||
if (speed > _maxSpeed) _maxSpeed = speed;
|
||||
}
|
||||
|
||||
// ─── Public hooks ────────────────────────────────────────────────────
|
||||
|
||||
public void RegisterJump()
|
||||
{
|
||||
_totalJumps++;
|
||||
}
|
||||
|
||||
public void RegisterBump()
|
||||
{
|
||||
_bumpsGiven++;
|
||||
}
|
||||
|
||||
public void RegisterCheckpoint()
|
||||
{
|
||||
_checkpointsTotal++;
|
||||
}
|
||||
|
||||
public void RegisterFinish(float raceTime)
|
||||
{
|
||||
if (raceTime <= 0f) return;
|
||||
if (_bestRaceTime <= 0f || raceTime < _bestRaceTime)
|
||||
_bestRaceTime = raceTime;
|
||||
}
|
||||
|
||||
// ─── Event handlers ──────────────────────────────────────────────────
|
||||
|
||||
private void OnConnected()
|
||||
{
|
||||
_lastPos = transform.position;
|
||||
_trackingActive = true;
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
_trackingActive = false;
|
||||
_totalPlaytime += Time.time - _sessionStart;
|
||||
SendStats(); // best-effort on disconnect
|
||||
}
|
||||
|
||||
private void OnRoundStart(int round, string mode, int totalRounds)
|
||||
{
|
||||
_racesPlayed++;
|
||||
_roundDistance = 0f;
|
||||
_roundMaxSpeed = 0f;
|
||||
_lastPos = transform.position;
|
||||
_trackingActive = true;
|
||||
}
|
||||
|
||||
private void OnRoundEnd(int round)
|
||||
{
|
||||
_trackingActive = false;
|
||||
SendStats();
|
||||
_roundDistance = 0f;
|
||||
_roundMaxSpeed = 0f;
|
||||
}
|
||||
|
||||
private void OnQualified(string sessionId)
|
||||
{
|
||||
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
||||
_qualifications++;
|
||||
}
|
||||
|
||||
private void OnEliminated(string sessionId, string reason)
|
||||
{
|
||||
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
||||
_eliminations++;
|
||||
}
|
||||
|
||||
// ─── HTTP send ───────────────────────────────────────────────────────
|
||||
|
||||
private void SendStats()
|
||||
{
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm == null || string.IsNullOrEmpty(nm.LocalPlayerName)) return;
|
||||
StartCoroutine(DoSendStats(nm.LocalPlayerName));
|
||||
}
|
||||
|
||||
private IEnumerator DoSendStats(string playerName)
|
||||
{
|
||||
_totalPlaytime += Time.time - _sessionStart;
|
||||
_sessionStart = Time.time;
|
||||
|
||||
var payload = new StatsPayload
|
||||
{
|
||||
name = playerName,
|
||||
stats = new StatsData
|
||||
{
|
||||
totalDistance = _totalDistance,
|
||||
totalJumps = _totalJumps,
|
||||
maxSpeed = _maxSpeed,
|
||||
bestRaceTime = _bestRaceTime > 0f ? _bestRaceTime : 0f,
|
||||
racesPlayed = _racesPlayed,
|
||||
qualifications = _qualifications,
|
||||
eliminations = _eliminations,
|
||||
checkpointsTotal = _checkpointsTotal,
|
||||
bumpsGiven = _bumpsGiven,
|
||||
totalPlaytime = _totalPlaytime,
|
||||
}
|
||||
};
|
||||
|
||||
string json = JsonUtility.ToJson(payload);
|
||||
byte[] body = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
using var req = new UnityWebRequest($"{SERVER_URL}/stats/update", "POST");
|
||||
req.uploadHandler = new UploadHandlerRaw(body);
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
req.SetRequestHeader("Content-Type", "application/json");
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
Debug.LogWarning($"[Stats] Upload failed: {req.error}");
|
||||
else
|
||||
Debug.Log($"[Stats] Uploaded for {playerName}");
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────
|
||||
|
||||
[System.Serializable]
|
||||
private class StatsPayload { public string name; public StatsData stats; }
|
||||
|
||||
[System.Serializable]
|
||||
private class StatsData
|
||||
{
|
||||
public float totalDistance;
|
||||
public int totalJumps;
|
||||
public float maxSpeed;
|
||||
public float bestRaceTime;
|
||||
public int racesPlayed;
|
||||
public int qualifications;
|
||||
public int eliminations;
|
||||
public int checkpointsTotal;
|
||||
public int bumpsGiven;
|
||||
public float totalPlaytime;
|
||||
}
|
||||
}
|
||||
259
game/Assets/Scripts/UI/ChatUI.cs
Normal file
259
game/Assets/Scripts/UI/ChatUI.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// General chat panel. Toggle with F3.
|
||||
/// Polls GET /chat/history every 3s and sends via POST /chat/send (or Colyseus if connected).
|
||||
/// Uses ImGuiSkin for visual consistency.
|
||||
/// </summary>
|
||||
public class ChatUI : MonoBehaviour
|
||||
{
|
||||
public static ChatUI Instance { get; private set; }
|
||||
public static bool IsVisible { get; private set; }
|
||||
|
||||
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||
private const float POLL_INTERVAL = 3f;
|
||||
private const int MAX_DISPLAY = 50;
|
||||
|
||||
private bool _visible;
|
||||
private string _inputText = "";
|
||||
private Vector2 _scrollPos;
|
||||
private float _pollTimer;
|
||||
private long _lastTimestamp;
|
||||
private bool _autoScroll = true;
|
||||
|
||||
private readonly List<ChatMessage> _messages = new();
|
||||
private int _unreadCount;
|
||||
|
||||
// Cached textures for badge
|
||||
private static Texture2D _badgeTex;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
// Initial load
|
||||
StartCoroutine(DoPoll());
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Keyboard.current != null && Keyboard.current[Key.F3].wasPressedThisFrame)
|
||||
Toggle();
|
||||
|
||||
if (_visible)
|
||||
{
|
||||
_pollTimer += Time.deltaTime;
|
||||
if (_pollTimer >= POLL_INTERVAL) { _pollTimer = 0f; StartCoroutine(DoPoll()); }
|
||||
|
||||
// Send on Enter (only when chat input has text)
|
||||
if (!string.IsNullOrWhiteSpace(_inputText) &&
|
||||
Keyboard.current != null && Keyboard.current[Key.Enter].wasPressedThisFrame)
|
||||
{
|
||||
TrySend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Toggle()
|
||||
{
|
||||
_visible = !_visible;
|
||||
IsVisible = _visible;
|
||||
|
||||
if (_visible)
|
||||
{
|
||||
_unreadCount = 0;
|
||||
_autoScroll = true;
|
||||
_pollTimer = POLL_INTERVAL; // poll immediately
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only re-lock if no other UI is open
|
||||
if (!KeyBindingUI.IsVisible)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!_visible)
|
||||
{
|
||||
DrawBadge();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiSkin.EnsureReady();
|
||||
|
||||
// Panel bottom-right, doesn't obstruct the center
|
||||
float w = 460f;
|
||||
float h = 440f;
|
||||
float x = Screen.width - w - 12f;
|
||||
float y = Screen.height - h - 12f;
|
||||
|
||||
ImGuiSkin.BeginWindowAt(x, y, w, h, "CHAT GÉNÉRAL");
|
||||
|
||||
// ── Message history ───────────────────────────────────────────
|
||||
float listH = h - 130f;
|
||||
_scrollPos = GUILayout.BeginScrollView(_scrollPos, ImGuiSkin.ScrollView, GUILayout.Height(listH));
|
||||
|
||||
foreach (var msg in _messages)
|
||||
{
|
||||
var ts = System.DateTimeOffset.FromUnixTimeMilliseconds(msg.timestamp).ToLocalTime();
|
||||
string timeStr = ts.ToString("HH:mm");
|
||||
|
||||
var timeStyle = new GUIStyle(ImGuiSkin.LabelDim) { fontSize = 10, fixedWidth = 36f };
|
||||
var nameStyle = new GUIStyle(ImGuiSkin.LabelBold);
|
||||
nameStyle.normal.textColor = ImGuiSkin.ColAccent;
|
||||
var textStyle = new GUIStyle(ImGuiSkin.LabelRich);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(timeStr, timeStyle);
|
||||
GUILayout.Label(msg.name + " :", nameStyle, GUILayout.Width(100f));
|
||||
GUILayout.Label(msg.text, textStyle);
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.Space(1f);
|
||||
}
|
||||
|
||||
if (_autoScroll) _scrollPos.y = float.MaxValue;
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
ImGuiSkin.Separator();
|
||||
GUILayout.Space(4f);
|
||||
|
||||
// ── Input row ────────────────────────────────────────────────
|
||||
GUILayout.BeginHorizontal();
|
||||
GUI.SetNextControlName("ChatInput");
|
||||
_inputText = GUILayout.TextField(_inputText, 200, ImGuiSkin.TextField, GUILayout.Height(28f));
|
||||
|
||||
bool canSend = !string.IsNullOrWhiteSpace(_inputText) && PlayerName.Length > 0;
|
||||
GUI.enabled = canSend;
|
||||
if (GUILayout.Button("Envoyer", ImGuiSkin.Button, GUILayout.Width(80f), GUILayout.Height(28f)))
|
||||
TrySend();
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label("F3 — Ouvrir / Fermer · Entrée — Envoyer", ImGuiSkin.Footer);
|
||||
|
||||
ImGuiSkin.EndWindow();
|
||||
|
||||
// Auto-focus input field
|
||||
GUI.FocusControl("ChatInput");
|
||||
}
|
||||
|
||||
private void DrawBadge()
|
||||
{
|
||||
if (_unreadCount <= 0) return;
|
||||
if (_badgeTex == null)
|
||||
{
|
||||
_badgeTex = new Texture2D(1, 1);
|
||||
_badgeTex.SetPixel(0, 0, Color.white);
|
||||
_badgeTex.Apply();
|
||||
}
|
||||
float bx = Screen.width - 68f;
|
||||
float by = Screen.height - 32f;
|
||||
GUI.color = new Color(0.9f, 0.2f, 0.2f, 0.9f);
|
||||
GUI.DrawTexture(new Rect(bx, by, 56f, 22f), _badgeTex);
|
||||
GUI.color = Color.white;
|
||||
var s = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11, fontStyle = FontStyle.Bold };
|
||||
s.normal.textColor = Color.white;
|
||||
GUI.Label(new Rect(bx, by, 56f, 22f), $"💬 {_unreadCount}", s);
|
||||
}
|
||||
|
||||
// ─── Send ────────────────────────────────────────────────────────────
|
||||
|
||||
private string PlayerName => NetworkManager.Instance?.LocalPlayerName ?? "";
|
||||
|
||||
private void TrySend()
|
||||
{
|
||||
string text = _inputText.Trim();
|
||||
if (string.IsNullOrEmpty(text) || PlayerName.Length == 0) return;
|
||||
_inputText = "";
|
||||
_autoScroll = true;
|
||||
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null && nm.IsConnected)
|
||||
{
|
||||
// Fast path: through Colyseus (room broadcasts it back to all players AND saves to ChatManager)
|
||||
nm.SendChatMessage(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: direct HTTP (for frontend-only visitors or disconnected state)
|
||||
StartCoroutine(DoSend(PlayerName, text));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HTTP polling ─────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator DoPoll()
|
||||
{
|
||||
string url = $"{SERVER_URL}/chat/history?since={_lastTimestamp}";
|
||||
using var req = UnityWebRequest.Get(url);
|
||||
yield return req.SendWebRequest();
|
||||
if (req.result != UnityWebRequest.Result.Success) yield break;
|
||||
|
||||
var wrapper = JsonUtility.FromJson<MessageListWrapper>($"{{\"items\":{req.downloadHandler.text}}}");
|
||||
if (wrapper?.items == null) yield break;
|
||||
|
||||
int added = 0;
|
||||
foreach (var msg in wrapper.items)
|
||||
{
|
||||
if (msg.timestamp > _lastTimestamp)
|
||||
{
|
||||
_messages.Add(msg);
|
||||
_lastTimestamp = msg.timestamp;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
if (_messages.Count > MAX_DISPLAY)
|
||||
_messages.RemoveRange(0, _messages.Count - MAX_DISPLAY);
|
||||
|
||||
if (added > 0 && !_visible)
|
||||
_unreadCount += added;
|
||||
}
|
||||
|
||||
private IEnumerator DoSend(string name, string text)
|
||||
{
|
||||
var payload = new SendPayload { name = name, text = text };
|
||||
string json = JsonUtility.ToJson(payload);
|
||||
byte[] body = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
using var req = new UnityWebRequest($"{SERVER_URL}/chat/send", "POST");
|
||||
req.uploadHandler = new UploadHandlerRaw(body);
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
req.SetRequestHeader("Content-Type", "application/json");
|
||||
yield return req.SendWebRequest();
|
||||
}
|
||||
|
||||
// Called by NetworkManager when a "chat" message arrives via Colyseus
|
||||
public void ReceiveChatMessage(ChatMessage msg)
|
||||
{
|
||||
if (_messages.Count >= MAX_DISPLAY)
|
||||
_messages.RemoveAt(0);
|
||||
_messages.Add(msg);
|
||||
if (msg.timestamp > _lastTimestamp) _lastTimestamp = msg.timestamp;
|
||||
if (!_visible) _unreadCount++;
|
||||
_autoScroll = true;
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────
|
||||
|
||||
[System.Serializable]
|
||||
public class ChatMessage { public int id; public long timestamp; public string name; public string text; }
|
||||
|
||||
[System.Serializable]
|
||||
private class SendPayload { public string name; public string text; }
|
||||
|
||||
[System.Serializable]
|
||||
private class MessageListWrapper { public List<ChatMessage> items; }
|
||||
}
|
||||
@@ -89,6 +89,8 @@ public class GameHUD : MonoBehaviour
|
||||
_countdownPulse = Mathf.Max(0f, _countdownPulse - Time.deltaTime * 3f);
|
||||
}
|
||||
|
||||
public float LocalRaceTimer => _localRaceTimer;
|
||||
|
||||
public void SetPhase(string phase) => _phase = phase;
|
||||
public void SetCountdown(float v) => _countdown = v;
|
||||
public void SetRoundInfo(int round, string mode) { _roundNumber = round; _gameMode = mode; }
|
||||
|
||||
Reference in New Issue
Block a user