feat: nouveau build WebGL last_build + fixes stats et schema Colyseus

- Unity build last_build remplace build_mai
- NetworkSchema.cs: correction types sbyte pour int8 (fix OverflowException Colyseus)
- StatsTracker: envoi periodique toutes les 30s, plus de dependance aux round events
- StatsTracker: cooldown client 6s pour respecter le rate-limit serveur
- StatsPage: correction row.value au lieu de row[activeTab]
- StatsPage: suppression onglet Courses (racesPlayed)
- Backend index.js: logging POST /stats/update
- Scene Tutorial: mise a jour, suppression assets obsoletes (TutorialInfo, physicMaterials)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 00:12:14 +02:00
parent cf7d73ba08
commit aa27725c4e
44 changed files with 14881 additions and 9226 deletions

View File

@@ -1,11 +1,21 @@
using Colyseus.Schema;
// Generated from @colyseus/schema 4.0.15 — DO NOT EDIT MANUALLY
// Class names kept as NetworkPlayer/NetworkState to match existing codebase references.
using Colyseus.Schema;
#if UNITY_5_3_OR_NEWER
using UnityEngine.Scripting;
#endif
// Must match server-side defineTypes field order exactly
public partial class NetworkPlayer : Schema
{
[Type(0, "float32")] public float x = 0;
[Type(1, "float32")] public float y = 5;
[Type(2, "float32")] public float z = 0;
#if UNITY_5_3_OR_NEWER
[Preserve]
#endif
public NetworkPlayer() { }
[Type(0, "float32")] public float x = 0;
[Type(1, "float32")] public float y = 5;
[Type(2, "float32")] public float z = 0;
[Type(3, "float32")] public float vx = 0;
[Type(4, "float32")] public float vy = 0;
[Type(5, "float32")] public float vz = 0;
@@ -13,31 +23,35 @@ public partial class NetworkPlayer : Schema
[Type(7, "float32")] public float ry = 0;
[Type(8, "float32")] public float rz = 0;
[Type(9, "float32")] public float rw = 1;
[Type(10, "float64")] public double t = 0;
[Type(11, "string")] public string name = "";
[Type(10, "float64")] public double t = 0;
[Type(11, "string")] public string name = "";
[Type(12, "float32")] public float colorR = 1;
[Type(13, "float32")] public float colorG = 1;
[Type(14, "float32")] public float colorB = 1;
[Type(15, "float32")] public float avx = 0;
[Type(16, "float32")] public float avy = 0;
[Type(17, "float32")] public float avz = 0;
// Game state — order must match server defineTypes exactly
[Type(18, "boolean")] public bool isEliminated = false;
[Type(19, "boolean")] public bool isQualified = false;
[Type(20, "boolean")] public bool isReady = false;
[Type(21, "int8")] public int checkpointIndex = 0;
[Type(18, "boolean")] public bool isEliminated = false;
[Type(19, "boolean")] public bool isQualified = false;
[Type(20, "boolean")] public bool isReady = false;
[Type(21, "int8")] public sbyte checkpointIndex = 0;
}
public partial class NetworkState : Schema
{
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
public MapSchema<NetworkPlayer> players;
#if UNITY_5_3_OR_NEWER
[Preserve]
#endif
public NetworkState() { }
[Type(1, "string")] public string phase = "lobby";
[Type(2, "float32")] public float countdown = 0;
[Type(3, "int8")] public int roundNumber = 1;
[Type(4, "int8")] public int totalRounds = 4;
[Type(5, "int8")] public int playersAlive = 0;
[Type(6, "string")] public string gameMode = "race";
[Type(7, "string")] public string winnerName = "";
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
public MapSchema<NetworkPlayer> players = null;
[Type(1, "string")] public string phase = "lobby";
[Type(2, "float32")] public float countdown = 0;
[Type(3, "int8")] public sbyte roundNumber = 1;
[Type(4, "int8")] public sbyte totalRounds = 3;
[Type(5, "int8")] public sbyte playersAlive = 0;
[Type(6, "string")] public string gameMode = "race";
[Type(7, "string")] public string winnerName = "";
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5bf5e078a2ee9ed4fa95eacab5753f3a

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6d1f3d6aaca8e97498f40d827f7c5216

View File

@@ -4,41 +4,40 @@ 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).
/// Tracks player statistics and uploads them to the game server every 30s + on disconnect.
/// No dependency on round events — works even if Colyseus callbacks are broken.
/// </summary>
public class StatsTracker : MonoBehaviour
{
public static StatsTracker Instance { get; private set; }
private const string SERVER_URL = "https://game.rolld.kerboul.me";
private const string SERVER_URL = "https://game.rolld.kerboul.me";
private const float SEND_INTERVAL = 30f;
private const float MIN_SEND_INTERVAL = 6f; // juste au-dessus du rate-limit serveur (5s)
// Cumulative session stats (accumulate across rounds)
// Cumulative stats
private float _totalDistance;
private int _totalJumps;
private float _maxSpeed;
private int _racesPlayed;
private int _qualifications;
private int _eliminations;
private int _bumpsGiven;
private float _totalPlaytime;
// Per-round deltas (reset after each send)
private float _roundDistance;
private float _roundMaxSpeed;
// Playtime
private float _sessionStart;
private float _playtimeSentSoFar; // how much playtime we already sent
// Tracking
private Vector3 _lastPos;
private bool _trackingActive;
private bool _tracking;
private string _cachedName = "";
private float _lastSentTime = -999f;
private PlayerController _pc;
private Rigidbody _rb;
private Rigidbody _rb;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
_sessionStart = Time.time;
}
void Start()
@@ -49,11 +48,7 @@ public class StatsTracker : MonoBehaviour
var nm = NetworkManager.Instance;
if (nm != null)
{
nm.OnRoundStart += OnRoundStart;
nm.OnRoundEnd += OnRoundEnd;
nm.OnQualified += OnQualified;
nm.OnEliminated += OnEliminated;
nm.OnConnected += OnConnected;
nm.OnConnected += OnConnected;
nm.OnDisconnected += OnDisconnected;
}
}
@@ -63,123 +58,90 @@ public class StatsTracker : MonoBehaviour
var nm = NetworkManager.Instance;
if (nm != null)
{
nm.OnRoundStart -= OnRoundStart;
nm.OnRoundEnd -= OnRoundEnd;
nm.OnQualified -= OnQualified;
nm.OnEliminated -= OnEliminated;
nm.OnConnected -= OnConnected;
nm.OnConnected -= OnConnected;
nm.OnDisconnected -= OnDisconnected;
}
}
void FixedUpdate()
{
if (!_trackingActive || _rb == null || _pc == null || !_pc.enabled) return;
if (!_tracking || _rb == null) return;
Vector3 pos = transform.position;
float delta = Vector3.Distance(pos, _lastPos);
if (delta < 20f) // sanity cap against teleports
{
_roundDistance += delta;
_totalDistance += delta;
}
Vector3 pos = transform.position;
float delta = Vector3.Distance(pos, _lastPos);
if (delta < 20f) // filtre téléportations
_totalDistance += delta;
_lastPos = pos;
float speed = _rb.linearVelocity.magnitude;
if (speed > _roundMaxSpeed) _roundMaxSpeed = speed;
if (speed > _maxSpeed) _maxSpeed = speed;
if (speed > _maxSpeed) _maxSpeed = speed;
}
// ─── Public hooks ────────────────────────────────────────────────────
// ─── Public hooks ────────────────────────────────────────────────────
public void RegisterJump()
{
_totalJumps++;
}
public void RegisterJump() => _totalJumps++;
public void RegisterBump() => _bumpsGiven++;
public void RegisterBump()
{
_bumpsGiven++;
}
// ─── Event handlers ──────────────────────────────────────────────────
// ─── Connection events ────────────────────────────────────────────────
private void OnConnected()
{
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
_lastPos = transform.position;
_trackingActive = true;
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
_lastPos = transform.position;
_sessionStart = Time.time;
_tracking = true;
StartCoroutine(PeriodicSend());
}
private void OnDisconnected()
{
_trackingActive = false;
_totalPlaytime += Time.time - _sessionStart;
SendStats(); // best-effort on disconnect
_tracking = false;
StopAllCoroutines();
SendStats(); // envoi final best-effort
}
private void OnRoundStart(int round, string mode, int totalRounds)
// ─── Periodic send ────────────────────────────────────────────────────
private IEnumerator PeriodicSend()
{
_racesPlayed++;
_roundDistance = 0f;
_roundMaxSpeed = 0f;
_lastPos = transform.position;
_trackingActive = true;
while (_tracking)
{
yield return new WaitForSeconds(SEND_INTERVAL);
if (_tracking) SendStats();
}
}
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 ───────────────────────────────────────────────────────
// ─── HTTP send ────────────────────────────────────────────────────────
private void SendStats()
{
// Prefer live name, fall back to cached (useful on disconnect where name is cleared)
if (Time.time - _lastSentTime < MIN_SEND_INTERVAL) return;
var nm = NetworkManager.Instance;
string name = (nm != null && !string.IsNullOrEmpty(nm.LocalPlayerName))
? nm.LocalPlayerName
: _cachedName;
if (string.IsNullOrEmpty(name)) return;
_lastSentTime = Time.time;
StartCoroutine(DoSendStats(name));
}
private IEnumerator DoSendStats(string playerName)
{
_totalPlaytime += Time.time - _sessionStart;
_sessionStart = Time.time;
float now = Time.time;
float sessionSecs = now - _sessionStart;
float playtimeToSend = sessionSecs - _playtimeSentSoFar;
_playtimeSentSoFar = sessionSecs;
var payload = new StatsPayload
{
name = playerName,
name = playerName,
stats = new StatsData
{
totalDistance = _totalDistance,
totalJumps = _totalJumps,
maxSpeed = _maxSpeed,
racesPlayed = _racesPlayed,
qualifications = _qualifications,
eliminations = _eliminations,
bumpsGiven = _bumpsGiven,
totalPlaytime = _totalPlaytime,
totalDistance = _totalDistance,
totalJumps = _totalJumps,
maxSpeed = _maxSpeed,
bumpsGiven = _bumpsGiven,
totalPlaytime = playtimeToSend,
}
};
@@ -196,7 +158,7 @@ public class StatsTracker : MonoBehaviour
if (req.result != UnityWebRequest.Result.Success)
Debug.LogWarning($"[Stats] Upload failed: {req.error}");
else
Debug.Log($"[Stats] Uploaded for {playerName}");
Debug.Log($"[Stats] Sent for {playerName} — dist:{_totalDistance:F0}m spd:{_maxSpeed:F1}m/s jumps:{_totalJumps}");
}
// ─── DTOs ─────────────────────────────────────────────────────────────
@@ -210,9 +172,6 @@ public class StatsTracker : MonoBehaviour
public float totalDistance;
public int totalJumps;
public float maxSpeed;
public int racesPlayed;
public int qualifications;
public int eliminations;
public int bumpsGiven;
public float totalPlaytime;
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 51e21afb9dba1904bb425ac1fae825cb