feat: free-roam mode + fix multiplayer sync + remote player polish

Backend (ArenaRoom.js):
- Strip race state machine (lobby/countdown/playing/round/qualify). Persistent
  "playing" phase, no rounds, no checkpoints. Free-roam multi.
- Spawn lowered to y=1.5 (was 5) + MIN_DIST raised to 5 (was 3) to avoid
  ejecting overlapping players at connect.
- Schema kept intact (handshake-safe); deprecated fields default-valued.
- npm run schema:gen wired (anti-drift codegen).

Unity client:
- C# schema generated by schema-codegen into RolldSchema namespace
  (Generated/GameState.cs, Generated/Player.cs). NetworkSchema.cs removed —
  handshake no longer scans global namespace.
- NetworkManager: typed Room<GameState>, callbacks rebound, seeds players
  already in room on join.
- RemotePlayerController:
  * Post-spawn 1.5s grace window (BumpReady) — local PlayerController.HandleBump
    ignores remotes during grace.
  * Solid SphereCollider disabled during grace, re-enabled afterwards — fixes
    the kinematic-vs-dynamic eject when a new client spawns inside someone.
  * NPCBall prefab material switched from invisible-in-URP Default-Material to
    BallShader.shadergraph.
  * TrailRenderer added, tinted with player's chosen color.
  * Name label distance-scales (1x-8x) so pseudos remain readable far away.
- GameHUD: OnGUI emptied — race UI (rounds, mode, timer, playersAlive) gone.
- GameCanvas.jsx: BUILD_PREFIX/VERSION bumped for cache-bust.

Frontend WebGL build (pretty_build): final build with all the above.
This commit is contained in:
2026-05-20 12:25:48 +02:00
parent ec05fb8ddd
commit 32becc12f9
22 changed files with 288 additions and 453 deletions

View File

@@ -101,108 +101,7 @@ public class GameHUD : MonoBehaviour
void OnGUI()
{
if (_phase == "lobby" && !_localRaceActive) return;
ImGuiSkin.EnsureReady();
var nm = NetworkManager.Instance;
// ── Countdown (center, large) ─────────────────────────────────────
if (_phase == "countdown" && _countdown > 0f)
{
float scale = 1f + _countdownPulse * 0.4f;
float fontSize = 96f * scale;
var countStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = Mathf.RoundToInt(fontSize),
fontStyle = FontStyle.Bold,
};
countStyle.normal.textColor = new Color(1f, 0.85f, 0.1f, 1f);
GUI.Label(new Rect(0, Screen.height * 0.3f, Screen.width, 120f),
Mathf.CeilToInt(_countdown).ToString(), countStyle);
// "Préparez-vous !" label below
var subStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 22,
fontStyle = FontStyle.Bold,
};
subStyle.normal.textColor = new Color(1f, 1f, 1f, 0.8f);
string modeLabel = _gameMode switch {
"race" => "COURSE",
"survival" => "SURVIVAL",
"teams" => "ÉQUIPES",
_ => _gameMode.ToUpper()
};
GUI.Label(new Rect(0, Screen.height * 0.3f + 110f, Screen.width, 36f),
$"— {modeLabel} —", subStyle);
return;
}
// ── Top-left: Round & Mode ─────────────────────────────────────────
float panelX = 12f;
float panelY = 12f;
float panelW = 220f;
float panelH = 70f;
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
GUI.DrawTexture(new Rect(panelX, panelY, panelW, panelH), _bgTex);
GUI.color = Color.white;
var roundStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleLeft,
fontSize = 14,
fontStyle = FontStyle.Bold,
};
roundStyle.normal.textColor = new Color(1f, 0.85f, 0.1f);
GUI.Label(new Rect(panelX + 8f, panelY + 4f, panelW - 16f, 28f),
$"ROUND {_roundNumber} / {_totalRounds}", roundStyle);
var modeStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, fontSize = 12 };
modeStyle.normal.textColor = new Color(0.7f, 0.7f, 0.85f);
string modeFull = _gameMode switch {
"race" => "COURSE", "survival" => "SURVIVAL", "teams" => "ÉQUIPES", _ => _gameMode.ToUpper()
};
GUI.Label(new Rect(panelX + 8f, panelY + 32f, panelW - 16f, 24f), modeFull, modeStyle);
// ── Top-right: Players alive ──────────────────────────────────────
float prX = Screen.width - 180f;
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
GUI.DrawTexture(new Rect(prX, panelY, 168f, panelH), _bgTex);
GUI.color = Color.white;
var aliveStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 28,
fontStyle = FontStyle.Bold,
};
aliveStyle.normal.textColor = new Color(0.3f, 1f, 0.5f);
GUI.Label(new Rect(prX, panelY + 2f, 168f, 40f), $"{_cachedPlayersAlive}", aliveStyle);
var aliveLabel = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
aliveLabel.normal.textColor = new Color(0.6f, 0.6f, 0.7f);
GUI.Label(new Rect(prX, panelY + 40f, 168f, 22f), "joueurs en jeu", aliveLabel);
// ── Round timer (top center) ──────────────────────────────────────
float displayTimer = _timerRunning ? _roundTimer : (_localRaceActive ? _localRaceTimer : -1f);
if (displayTimer >= 0f)
{
int mins = Mathf.FloorToInt(displayTimer / 60f);
int secs = Mathf.FloorToInt(displayTimer % 60f);
var timerStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 18,
fontStyle = FontStyle.Bold,
};
timerStyle.normal.textColor = new Color(0.85f, 0.85f, 0.9f, 0.9f);
GUI.Label(new Rect(Screen.width * 0.5f - 60f, panelY, 120f, 40f),
$"{mins:00}:{secs:00}", timerStyle);
}
// Race UI disabled — free-roam mode has no rounds/countdown/timer.
}
// Static accessors for cross-script use