Files
rolld/game/Assets/Scripts/Stats/StatsTracker.cs
kerboul 103f8859d4 feat(Car): wire NWH Vehicle Physics 2 — scripts only, scene/prefabs still TODO
Code-only first pass. The local Player.prefab (ball) is still in the scene
until the user creates the PlayerCar / RemoteCar prefab variants in the Editor.

New scripts:
- VehicleLocalSetup: attaches name label + colored capsule marker to the local
  vehicle, registers its Rigidbody with NetworkManager for broadcast.
- RemoteVehicleSync: snapshot interpolation for remote vehicles. Disables the
  NWH VehicleController + every WheelController + every AudioSource on init.
  Makes the rigidbody kinematic, MovePosition-driven from network. Disables all
  non-trigger colliders during the 1.5s spawn grace window to avoid ejecting
  overlapping locals at connect.

Adapted scripts:
- NetworkManager: RegisterLocalVehicle(Transform, Rigidbody) replaces the
  FindFirstObjectByType<PlayerController>() lookup. Remote spawn now wires
  RemoteVehicleSync instead of RemotePlayerController.
- LobbyUI: OnConnected drives VehicleLocalSetup.SetupLocal; OnDisconnected
  toggles NWH.VehicleController instead of PlayerController.
- GameManager.SetPlayerActive: toggles VehicleController, not PlayerController.
- DebugNetworkUI: live position read from VehicleController.vehicleRigidbody.
- ChatUI: drops PlayerController.ResetInputs() (NWH polls Input System each
  frame; no manual reset needed when chat opens).
- StatsTracker: drops dead _pc field; Rigidbody still gets resolved via
  GetComponent on the host GameObject (will be the vehicle on Car).

Frontend (deployed earlier on master): build_ball replaces pretty_build assets.
2026-05-20 18:34:40 +02:00

177 lines
5.7 KiB
C#

using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// 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 float SEND_INTERVAL = 30f;
private const float MIN_SEND_INTERVAL = 6f; // juste au-dessus du rate-limit serveur (5s)
// Cumulative stats
private float _totalDistance;
private int _totalJumps;
private float _maxSpeed;
private int _bumpsGiven;
// Playtime
private float _sessionStart;
private float _playtimeSentSoFar; // how much playtime we already sent
// Tracking
private Vector3 _lastPos;
private bool _tracking;
private string _cachedName = "";
private float _lastSentTime = -999f;
private Rigidbody _rb;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
void Start()
{
_rb = GetComponent<Rigidbody>();
var nm = NetworkManager.Instance;
if (nm != null)
{
nm.OnConnected += OnConnected;
nm.OnDisconnected += OnDisconnected;
}
}
void OnDestroy()
{
var nm = NetworkManager.Instance;
if (nm != null)
{
nm.OnConnected -= OnConnected;
nm.OnDisconnected -= OnDisconnected;
}
}
void FixedUpdate()
{
if (!_tracking || _rb == null) return;
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 > _maxSpeed) _maxSpeed = speed;
}
// ─── Public hooks ─────────────────────────────────────────────────────
public void RegisterJump() => _totalJumps++;
public void RegisterBump() => _bumpsGiven++;
// ─── Connection events ────────────────────────────────────────────────
private void OnConnected()
{
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
_lastPos = transform.position;
_sessionStart = Time.time;
_tracking = true;
StartCoroutine(PeriodicSend());
}
private void OnDisconnected()
{
_tracking = false;
StopAllCoroutines();
SendStats(); // envoi final best-effort
}
// ─── Periodic send ────────────────────────────────────────────────────
private IEnumerator PeriodicSend()
{
while (_tracking)
{
yield return new WaitForSeconds(SEND_INTERVAL);
if (_tracking) SendStats();
}
}
// ─── HTTP send ────────────────────────────────────────────────────────
private void SendStats()
{
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)
{
float now = Time.time;
float sessionSecs = now - _sessionStart;
float playtimeToSend = sessionSecs - _playtimeSentSoFar;
_playtimeSentSoFar = sessionSecs;
var payload = new StatsPayload
{
name = playerName,
stats = new StatsData
{
totalDistance = _totalDistance,
totalJumps = _totalJumps,
maxSpeed = _maxSpeed,
bumpsGiven = _bumpsGiven,
totalPlaytime = playtimeToSend,
}
};
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] Sent for {playerName} — dist:{_totalDistance:F0}m spd:{_maxSpeed:F1}m/s jumps:{_totalJumps}");
}
// ─── 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 int bumpsGiven;
public float totalPlaytime;
}
}