diff --git a/game/Assets/Scripts/GameSetup.cs b/game/Assets/Scripts/GameSetup.cs index 1447ad6..f0985cc 100644 --- a/game/Assets/Scripts/GameSetup.cs +++ b/game/Assets/Scripts/GameSetup.cs @@ -1,154 +1,13 @@ using UnityEngine; /// -/// Global game setup applied at startup: -/// - Application.runInBackground (physics continues on ALT-TAB) -/// - Tall invisible arena barriers (prevent ball escape) -/// - Visual enhancements: obstacle colors, floor tint, lighting contrast -/// Attach to a persistent GameObject (e.g. NetworkManager). +/// Global startup settings. Scene geometry and materials are set directly in the Editor. /// public class GameSetup : MonoBehaviour { - [Header("Arena Boundaries")] - public float arenaHalfSize = 45f; - public float barrierHeight = 50f; - public float barrierThickness = 1f; - - [Header("Visuals")] - public bool enhanceVisuals = true; - void Awake() { - // --- Keep physics and network running on focus loss --- Application.runInBackground = true; Application.targetFrameRate = 60; - - // Barriers removed — respawn system handles falls (Y < -10) - } - - void Start() - { - if (enhanceVisuals) - EnhanceVisuals(); - } - - // --- Barrier creation --- - - private void CreateBarrier(string name, Vector3 position, Vector3 size) - { - var go = new GameObject(name); - go.transform.position = position; - var col = go.AddComponent(); - col.size = size; - // No Renderer = invisible. Static collider = immovable wall. - } - - // --- Visual enhancements --- - - private void EnhanceVisuals() - { - TintFloor(); - ColorObstacles(); - ColorWallsAndGrids(); - EnhanceLighting(); - } - - private void TintFloor() - { - var plane = GameObject.Find("Plane"); - if (plane == null) return; - var rend = plane.GetComponent(); - if (rend == null) return; - - var mat = new Material(rend.sharedMaterial); - // Soft blue-gray instead of flat white - Color floorColor = new Color(0.70f, 0.74f, 0.82f, 1f); - SetMatColor(mat, floorColor); - rend.material = mat; - } - - private void ColorObstacles() - { - Color[] palette = - { - new Color(0.42f, 0.55f, 0.75f), // Steel blue - new Color(0.60f, 0.45f, 0.68f), // Muted purple - new Color(0.48f, 0.68f, 0.55f), // Sage green - new Color(0.74f, 0.52f, 0.42f), // Warm terracotta - new Color(0.68f, 0.65f, 0.44f), // Sandy gold - new Color(0.44f, 0.62f, 0.72f), // Slate teal - }; - - for (int i = 1; i <= 18; i++) - { - var obs = GameObject.Find($"Obs_{i}"); - if (obs == null) continue; - var rend = obs.GetComponent(); - if (rend == null) continue; - - var mat = new Material(rend.sharedMaterial); - SetMatColor(mat, palette[i % palette.Length]); - rend.material = mat; - } - } - - private void ColorWallsAndGrids() - { - Color wallColor = new Color(0.50f, 0.54f, 0.62f); - foreach (string name in new[] { "Wall_North", "Wall_South", "Wall_East", "Wall_West" }) - { - var wall = GameObject.Find(name); - if (wall == null) continue; - var rend = wall.GetComponent(); - if (rend == null) continue; - var mat = new Material(rend.sharedMaterial); - SetMatColor(mat, wallColor); - rend.material = mat; - } - - Color gridColor = new Color(0.58f, 0.61f, 0.68f); - for (int i = 1; i <= 4; i++) - { - foreach (string dir in new[] { "NS", "EW" }) - { - var grid = GameObject.Find($"Grid_{dir}_{i}"); - if (grid == null) continue; - var rend = grid.GetComponent(); - if (rend == null) continue; - var mat = new Material(rend.sharedMaterial); - SetMatColor(mat, gridColor); - rend.material = mat; - } - } - } - - private void EnhanceLighting() - { - // Directional light: warm white, stronger, soft shadows - var lights = FindObjectsByType(FindObjectsSortMode.None); - foreach (var light in lights) - { - if (light.type == LightType.Directional) - { - light.color = new Color(1f, 0.96f, 0.90f); // Warm white - light.intensity = 1.6f; - light.shadows = LightShadows.Soft; - light.shadowStrength = 0.75f; - } - } - - // Ambient: cool tint for contrast with warm direct light - RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat; - RenderSettings.ambientLight = new Color(0.32f, 0.36f, 0.48f); - } - - // --- Utility --- - - private static void SetMatColor(Material mat, Color color) - { - if (mat.HasProperty("_BaseColor")) - mat.SetColor("_BaseColor", color); - if (mat.HasProperty("_Color")) - mat.color = color; } } diff --git a/game/Assets/Scripts/Network/DebugNetworkUI.cs b/game/Assets/Scripts/Network/DebugNetworkUI.cs index c547dad..aac97be 100644 --- a/game/Assets/Scripts/Network/DebugNetworkUI.cs +++ b/game/Assets/Scripts/Network/DebugNetworkUI.cs @@ -67,11 +67,11 @@ public class DebugNetworkUI : MonoBehaviour string name = !string.IsNullOrEmpty(nm.LocalPlayerName) ? nm.LocalPlayerName : "\u2014"; string room = !string.IsNullOrEmpty(nm.RoomId) ? nm.RoomId[..Mathf.Min(8, nm.RoomId.Length)] : "\u2014"; string sess = !string.IsNullOrEmpty(nm.LocalSessionId) ? nm.LocalSessionId[..Mathf.Min(6, nm.LocalSessionId.Length)] : "\u2014"; - info = $" {dot} {name} | Room {room} | Sess {sess} | {nm.PlayerCount}P | {nm.serverURL} | {_currentFps:F0} FPS"; + info = $" {dot} {name} | Room {room} | Sess {sess} | {nm.PlayerCount}P | {"wss://rolld.io:2567"} | {_currentFps:F0} FPS"; } else { - info = $" {dot} {nm.ConnectionStatus} | {nm.serverURL} | {_currentFps:F0} FPS"; + info = $" {dot} {nm.ConnectionStatus} | {"wss://rolld.io:2567"} | {_currentFps:F0} FPS"; } GUI.Label(new Rect(0, 0, Screen.width, h), info, ImGuiSkin.HudLabel); @@ -93,7 +93,7 @@ public class DebugNetworkUI : MonoBehaviour GUIStyle statusStyle = nm.IsConnected ? ImGuiSkin.StatusGreen : ImGuiSkin.StatusRed; GUILayout.Label($"\u25CF {nm.ConnectionStatus}", statusStyle); - ImGuiSkin.DrawField("Server", nm.serverURL); + ImGuiSkin.DrawField("Server", "wss://rolld.io:2567"); ImGuiSkin.DrawField("Room ID", string.IsNullOrEmpty(nm.RoomId) ? "\u2014" : nm.RoomId); ImGuiSkin.DrawField("Session", string.IsNullOrEmpty(nm.LocalSessionId) ? "\u2014" : nm.LocalSessionId); ImGuiSkin.DrawField("Players", nm.PlayerCount.ToString()); diff --git a/game/Assets/Scripts/Network/LobbyUI.cs b/game/Assets/Scripts/Network/LobbyUI.cs index a4e8545..549be34 100644 --- a/game/Assets/Scripts/Network/LobbyUI.cs +++ b/game/Assets/Scripts/Network/LobbyUI.cs @@ -327,7 +327,7 @@ public class LobbyUI : MonoBehaviour if (_isConnecting && !NetworkManager.Instance.IsConnected) { _isConnecting = false; - _statusMessage = "Erreur : Timeout de connexion. Vérifiez que le serveur est lancé."; + _statusMessage = "Erreur : Impossible de joindre rolld.io. Réessayez dans quelques instants."; if (!string.IsNullOrEmpty(NetworkManager.Instance.LastError)) { _statusMessage += $"\n{NetworkManager.Instance.LastError}"; diff --git a/game/Assets/Scripts/Network/NetworkManager.cs b/game/Assets/Scripts/Network/NetworkManager.cs index 6614474..5d0e80f 100644 --- a/game/Assets/Scripts/Network/NetworkManager.cs +++ b/game/Assets/Scripts/Network/NetworkManager.cs @@ -12,9 +12,7 @@ public class NetworkManager : MonoBehaviour { public static NetworkManager Instance { get; private set; } - [Header("Connection")] - [Tooltip("Colyseus server endpoint (overridden by frontend via SetServerURL)")] - public string serverURL = "ws://localhost:2567"; + private const string serverURL = "wss://rolld.io:2567"; [Header("Prefab")] [Tooltip("Prefab for remote players (must have RemotePlayerController)")] @@ -94,13 +92,6 @@ public class NetworkManager : MonoBehaviour } } - /// Called from frontend JS via SendMessage to override the server URL. - public void SetServerURL(string url) - { - serverURL = url; - Debug.Log($"[Network] Server URL set to: {url}"); - } - public NetworkPlayer GetLocalPlayerState() { if (_room == null || _room.State.players == null || string.IsNullOrEmpty(LocalSessionId)) return null; diff --git a/rolld_backend/game/src/rooms/ArenaRoom.js b/rolld_backend/game/src/rooms/ArenaRoom.js index 2f90731..ce5eb7d 100644 --- a/rolld_backend/game/src/rooms/ArenaRoom.js +++ b/rolld_backend/game/src/rooms/ArenaRoom.js @@ -1,19 +1,35 @@ const { Room } = require("@colyseus/core"); const { GameState, Player } = require("../schema/GameState"); +const ROUND_MODES = ["race", "survival", "teams"]; +const LOBBY_TIMEOUT = 30; // seconds before auto-start +const COUNTDOWN_DURATION = 3; +const ROUND_END_DURATION = 5; +const RACE_TIMEOUT = 180; // 3 min +const SURVIVAL_START_DELAY = 20; // seconds before deathzone rises +const SURVIVAL_RISE_RATE = 0.3; // units/sec +const SURVIVAL_MAX_Y = 15; +const TEAMS_DURATION = 90; +const QUALIFY_RATIO = 0.6; // top 60% qualify in race + class ArenaRoom extends Room { maxClients = 20; onCreate(options) { this.setState(new GameState()); - this.setPatchRate(16); // ~62.5 Hz state broadcast - console.log(`[ArenaRoom] Room ${this.roomId} created (patchRate=16ms ~62Hz)`); + this.setPatchRate(16); // ~62.5 Hz + + this._phaseTimer = null; + this._survivalInterval = null; + this._teamInterval = null; + this._lobbyTimer = null; + this._inZonePlayers = new Set(); // sessionIds currently in zone + + console.log(`[ArenaRoom] Room ${this.roomId} created`); - // Handle position updates from clients this.onMessage("position", (client, data) => { const player = this.state.players.get(client.sessionId); - if (!player) return; - + if (!player || player.isEliminated) return; player.x = data.x ?? player.x; player.y = data.y ?? player.y; player.z = data.z ?? player.z; @@ -30,90 +46,378 @@ class ArenaRoom extends Room { player.t = Date.now(); }); - // Handle chat messages (optional, for future) - this.onMessage("chat", (client, data) => { - this.broadcast("chat", { - sender: client.sessionId, - name: this.state.players.get(client.sessionId)?.name || "???", - message: data.message, - }); + this.onMessage("ready", (client) => { + const player = this.state.players.get(client.sessionId); + if (!player || this.state.phase !== "lobby") return; + player.isReady = true; + console.log(`[ArenaRoom] ${client.sessionId} ready`); + this._checkAllReady(); + }); + + this.onMessage("checkpointReached", (client, data) => { + if (this.state.phase !== "playing" || this.state.gameMode !== "race") return; + const player = this.state.players.get(client.sessionId); + if (!player || player.isEliminated || player.isQualified) return; + const expected = player.checkpointIndex; + if (data.index !== expected) return; // must hit in order + player.checkpointIndex = data.index + 1; + // The last checkpoint (index 4 = finish) qualifies the player + // CheckpointSystem sends index after increment, so finish = totalCheckpoints + const TOTAL_CHECKPOINTS = 5; + if (player.checkpointIndex >= TOTAL_CHECKPOINTS) { + this._qualifyPlayer(client.sessionId, "finish"); + } + }); + + this.onMessage("deathZoneHit", (client) => { + if (this.state.phase !== "playing" || this.state.gameMode !== "survival") return; + this._eliminatePlayer(client.sessionId, "deathzone"); + }); + + this.onMessage("inZone", (client, data) => { + if (this.state.phase !== "playing" || this.state.gameMode !== "teams") return; + const player = this.state.players.get(client.sessionId); + if (!player || player.isEliminated) return; + if (data.inZone) { + this._inZonePlayers.add(client.sessionId); + } else { + this._inZonePlayers.delete(client.sessionId); + } }); } onJoin(client, options) { - console.log(`[ArenaRoom] ${client.sessionId} joined (name: ${options.name || "anonymous"})`); - + console.log(`[ArenaRoom] ${client.sessionId} joined (${options.name || "anonymous"})`); const player = new Player(); player.name = options.name || "Joueur"; player.colorR = options.colorR ?? 1; player.colorG = options.colorG ?? 0.4; player.colorB = options.colorB ?? 0.2; - - // Find a spawn position away from other players - const spawnPos = this._findSpawnPosition(); - player.x = spawnPos.x; - player.y = spawnPos.y; - player.z = spawnPos.z; + const spawn = this._findSpawnPosition(); + player.x = spawn.x; + player.y = spawn.y; + player.z = spawn.z; player.t = Date.now(); - this.state.players.set(client.sessionId, player); + this._updatePlayersAlive(); + + // Auto-start lobby timer on first player + if (this.state.players.size === 1 && this.state.phase === "lobby") { + this._startLobbyTimer(); + } } onLeave(client, consented) { - console.log(`[ArenaRoom] ${client.sessionId} left (consented: ${consented})`); + console.log(`[ArenaRoom] ${client.sessionId} left`); + this._inZonePlayers.delete(client.sessionId); this.state.players.delete(client.sessionId); + this._updatePlayersAlive(); + if (this.state.phase === "playing") { + this._checkRoundEndCondition(); + } } onDispose() { + this._clearAllTimers(); console.log(`[ArenaRoom] Room ${this.roomId} disposed`); } - /** - * Find a spawn position elevated and away from existing players. - * Tries up to 10 random positions, picks the one farthest from others. - * Falls back to random if no good spot found. - */ - _findSpawnPosition() { - const MIN_DIST = 3.0; - const SPAWN_Y = 5; // elevated spawn — ball drops naturally - const RANGE = 20; - let bestPos = { x: 0, y: SPAWN_Y, z: 0 }; - let bestMinDist = 0; + // ─── Phase transitions ────────────────────────────────────────────── - const existingPositions = []; - this.state.players.forEach((p) => { - existingPositions.push({ x: p.x, z: p.z }); + _startLobbyTimer() { + if (this._lobbyTimer) return; + this._lobbyTimer = setTimeout(() => this._startCountdown(), LOBBY_TIMEOUT * 1000); + console.log(`[ArenaRoom] Lobby timer started (${LOBBY_TIMEOUT}s)`); + } + + _checkAllReady() { + if (this.state.players.size < 2) return; + let allReady = true; + this.state.players.forEach((p) => { if (!p.isReady) allReady = false; }); + if (allReady) { + clearTimeout(this._lobbyTimer); + this._lobbyTimer = null; + this._startCountdown(); + } + } + + _startCountdown() { + if (this.state.phase !== "lobby") return; + this.state.phase = "countdown"; + this.state.countdown = COUNTDOWN_DURATION; + console.log(`[ArenaRoom] Countdown started`); + + const tick = () => { + this.state.countdown -= 1; + if (this.state.countdown <= 0) { + this._startPlaying(); + } else { + this._phaseTimer = setTimeout(tick, 1000); + } + }; + this._phaseTimer = setTimeout(tick, 1000); + } + + _startPlaying() { + const modeIndex = (this.state.roundNumber - 1) % ROUND_MODES.length; + this.state.gameMode = ROUND_MODES[modeIndex]; + this.state.phase = "playing"; + this.state.countdown = 0; + + // Reset player state for new round + let teamToggle = 0; + this.state.players.forEach((p, id) => { + p.isEliminated = false; + p.isQualified = false; + p.isReady = false; + p.checkpointIndex = 0; + if (this.state.gameMode === "teams") { + p.team = (teamToggle++ % 2 === 0) ? 1 : 2; + } else { + p.team = 0; + } }); - // If no existing players, just random - if (existingPositions.length === 0) { - return { - x: (Math.random() - 0.5) * RANGE, - y: SPAWN_Y, - z: (Math.random() - 0.5) * RANGE, - }; + this.state.deathZoneY = -50; + this.state.teamScoreRed = 0; + this.state.teamScoreBlue = 0; + this._inZonePlayers.clear(); + this._updatePlayersAlive(); + + this.broadcast("roundStart", { + round: this.state.roundNumber, + mode: this.state.gameMode, + totalRounds: this.state.totalRounds, + }); + + console.log(`[ArenaRoom] Round ${this.state.roundNumber} started (mode: ${this.state.gameMode})`); + + if (this.state.gameMode === "race") { + this._phaseTimer = setTimeout(() => this._endRaceTimeout(), RACE_TIMEOUT * 1000); + } else if (this.state.gameMode === "survival") { + this._phaseTimer = setTimeout(() => this._startSurvivalRise(), SURVIVAL_START_DELAY * 1000); + } else if (this.state.gameMode === "teams") { + this._startTeamsScoring(); + this._phaseTimer = setTimeout(() => this._endTeamsRound(), TEAMS_DURATION * 1000); + } + } + + _endRound() { + if (this.state.phase !== "playing") return; + this._clearAllTimers(); + this.state.phase = "roundEnd"; + this.broadcast("roundEnd", { round: this.state.roundNumber }); + console.log(`[ArenaRoom] Round ${this.state.roundNumber} ended`); + + // Check if all rounds done + if (this.state.roundNumber >= this.state.totalRounds) { + this._phaseTimer = setTimeout(() => this._endGame(), ROUND_END_DURATION * 1000); + } else { + this._phaseTimer = setTimeout(() => this._nextRound(), ROUND_END_DURATION * 1000); + } + } + + _nextRound() { + this.state.roundNumber += 1; + this.state.phase = "lobby"; + this.state.playersAlive = 0; + this.state.players.forEach((p) => { + if (!p.isEliminated) { + p.isReady = false; + const spawn = this._findSpawnPosition(); + p.x = spawn.x; p.y = spawn.y; p.z = spawn.z; + } + }); + this._updatePlayersAlive(); + this._lobbyTimer = null; + this._startLobbyTimer(); + console.log(`[ArenaRoom] Lobby for round ${this.state.roundNumber}`); + } + + _endGame() { + this.state.phase = "gameEnd"; + // Find winner: last qualified player, or player with most checkpoints + let winner = ""; + let best = -1; + this.state.players.forEach((p) => { + const score = p.isQualified ? 1000 : p.checkpointIndex; + if (score > best) { best = score; winner = p.name; } + }); + this.state.winnerName = winner; + this.broadcast("gameEnd", { winner }); + console.log(`[ArenaRoom] Game over — winner: ${winner}`); + } + + // ─── Race mode ────────────────────────────────────────────────────── + + _endRaceTimeout() { + // Eliminate anyone who hasn't qualified + this.state.players.forEach((p, id) => { + if (!p.isQualified && !p.isEliminated) { + this._eliminatePlayer(id, "timeout"); + } + }); + this._endRound(); + } + + // ─── Survival mode ────────────────────────────────────────────────── + + _startSurvivalRise() { + console.log(`[ArenaRoom] DeathZone starts rising`); + this._survivalInterval = setInterval(() => { + this.state.deathZoneY += SURVIVAL_RISE_RATE * (16 / 1000); + if (this.state.deathZoneY > SURVIVAL_MAX_Y) { + this.state.deathZoneY = SURVIVAL_MAX_Y; + } + }, 16); + } + + // ─── Teams mode ───────────────────────────────────────────────────── + + _startTeamsScoring() { + this._teamInterval = setInterval(() => { + let redInZone = 0; + let blueInZone = 0; + this._inZonePlayers.forEach((id) => { + const p = this.state.players.get(id); + if (!p || p.isEliminated) return; + if (p.team === 1) redInZone++; + else if (p.team === 2) blueInZone++; + }); + if (redInZone > blueInZone) this.state.teamScoreRed = Math.min(this.state.teamScoreRed + 1, 32767); + else if (blueInZone > redInZone) this.state.teamScoreBlue = Math.min(this.state.teamScoreBlue + 1, 32767); + }, 1000); + } + + _endTeamsRound() { + // Eliminate losing team + const redWins = this.state.teamScoreRed >= this.state.teamScoreBlue; + const losingTeam = redWins ? 2 : 1; + this.state.players.forEach((p, id) => { + if (p.team === losingTeam && !p.isEliminated) { + this._eliminatePlayer(id, "teams_lost"); + } else if (!p.isEliminated) { + this._qualifyPlayer(id, "teams_won"); + } + }); + this._endRound(); + } + + // ─── Elimination helpers ───────────────────────────────────────────── + + _eliminatePlayer(sessionId, reason) { + const player = this.state.players.get(sessionId); + if (!player || player.isEliminated || player.isQualified) return; + player.isEliminated = true; + this._updatePlayersAlive(); + this.broadcast("eliminated", { sessionId, name: player.name, reason }); + console.log(`[ArenaRoom] ${player.name} (${sessionId}) eliminated: ${reason}`); + this._checkRoundEndCondition(); + } + + _qualifyPlayer(sessionId, reason) { + const player = this.state.players.get(sessionId); + if (!player || player.isQualified || player.isEliminated) return; + player.isQualified = true; + this._updatePlayersAlive(); + this.broadcast("qualified", { sessionId, name: player.name }); + console.log(`[ArenaRoom] ${player.name} (${sessionId}) qualified: ${reason}`); + + if (this.state.gameMode === "race") { + const aliveCount = this._getAliveCount(); + const totalActive = this._getActiveCount(); + const qualifiedCount = this._getQualifiedCount(); + // Eliminate once qualify_ratio reached + const toQualify = Math.ceil(totalActive * QUALIFY_RATIO); + if (qualifiedCount >= toQualify) { + this.state.players.forEach((p, id) => { + if (!p.isQualified && !p.isEliminated) { + this._eliminatePlayer(id, "too_slow"); + } + }); + this._endRound(); + } + } else if (this.state.gameMode === "survival") { + // In survival: only 1 qualifies (last one), rest get eliminated by zone + this._checkRoundEndCondition(); + } + } + + _checkRoundEndCondition() { + if (this.state.phase !== "playing") return; + const alive = this._getAliveCount(); + const qualified = this._getQualifiedCount(); + const total = this._getActiveCount(); + + if (this.state.gameMode === "survival") { + if (alive <= 1) { + // Qualify the last survivor + this.state.players.forEach((p, id) => { + if (!p.isEliminated && !p.isQualified) { + this._qualifyPlayer(id, "last_survivor"); + } + }); + this._endRound(); + } + } else if (alive === 0 || alive + qualified >= total) { + this._endRound(); + } + } + + _getAliveCount() { + let n = 0; + this.state.players.forEach((p) => { if (!p.isEliminated && !p.isQualified) n++; }); + return n; + } + + _getQualifiedCount() { + let n = 0; + this.state.players.forEach((p) => { if (p.isQualified) n++; }); + return n; + } + + _getActiveCount() { + return this.state.players.size; + } + + _updatePlayersAlive() { + this.state.playersAlive = this._getAliveCount(); + } + + _clearAllTimers() { + if (this._phaseTimer) { clearTimeout(this._phaseTimer); this._phaseTimer = null; } + if (this._lobbyTimer) { clearTimeout(this._lobbyTimer); this._lobbyTimer = null; } + if (this._survivalInterval) { clearInterval(this._survivalInterval); this._survivalInterval = null; } + if (this._teamInterval) { clearInterval(this._teamInterval); this._teamInterval = null; } + } + + // ─── Spawn helper ──────────────────────────────────────────────────── + + _findSpawnPosition() { + const MIN_DIST = 3.0; + const SPAWN_Y = 5; + const RANGE = 20; + const existing = []; + this.state.players.forEach((p) => existing.push({ x: p.x, z: p.z })); + + if (existing.length === 0) { + return { x: (Math.random() - 0.5) * RANGE, y: SPAWN_Y, z: (Math.random() - 0.5) * RANGE }; } - for (let attempt = 0; attempt < 10; attempt++) { + let best = { x: 0, y: SPAWN_Y, z: 0 }; + let bestDist = 0; + for (let i = 0; i < 10; i++) { const cx = (Math.random() - 0.5) * RANGE; const cz = (Math.random() - 0.5) * RANGE; - let minDist = Infinity; - for (const p of existingPositions) { - const dx = cx - p.x; - const dz = cz - p.z; - const d = Math.sqrt(dx * dx + dz * dz); - if (d < minDist) minDist = d; - } - if (minDist >= MIN_DIST) { - return { x: cx, y: SPAWN_Y, z: cz }; - } - if (minDist > bestMinDist) { - bestMinDist = minDist; - bestPos = { x: cx, y: SPAWN_Y, z: cz }; + let minD = Infinity; + for (const p of existing) { + const d = Math.sqrt((cx - p.x) ** 2 + (cz - p.z) ** 2); + if (d < minD) minD = d; } + if (minD >= MIN_DIST) return { x: cx, y: SPAWN_Y, z: cz }; + if (minD > bestDist) { bestDist = minD; best = { x: cx, y: SPAWN_Y, z: cz }; } } - - return bestPos; + return best; } } diff --git a/rolld_backend/game/src/schema/GameState.js b/rolld_backend/game/src/schema/GameState.js index f25be27..a8e610f 100644 --- a/rolld_backend/game/src/schema/GameState.js +++ b/rolld_backend/game/src/schema/GameState.js @@ -21,6 +21,12 @@ class Player extends Schema { this.avx = 0; this.avy = 0; this.avz = 0; + // Game state + this.isEliminated = false; + this.isQualified = false; + this.isReady = false; + this.team = 0; + this.checkpointIndex = 0; } } @@ -43,17 +49,42 @@ defineTypes(Player, { avx: "float32", avy: "float32", avz: "float32", + isEliminated: "boolean", + isQualified: "boolean", + isReady: "boolean", + team: "int8", + checkpointIndex: "int8", }); class GameState extends Schema { constructor() { super(); this.players = new MapSchema(); + this.phase = "lobby"; + this.countdown = 0; + this.roundNumber = 1; + this.totalRounds = 3; + this.playersAlive = 0; + this.gameMode = "race"; + this.deathZoneY = -50; + this.teamScoreRed = 0; + this.teamScoreBlue = 0; + this.winnerName = ""; } } defineTypes(GameState, { players: { map: Player }, + phase: "string", + countdown: "float32", + roundNumber: "int8", + totalRounds: "int8", + playersAlive: "int8", + gameMode: "string", + deathZoneY: "float32", + teamScoreRed: "int16", + teamScoreBlue: "int16", + winnerName: "string", }); module.exports = { GameState, Player };