// Assets/_Project/Scripts/Gameplay/LobbyService.cs
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Net;
namespace TD.Gameplay
{
///
/// Scene singleton that owns lobby-wide actions: Start Match (host),
/// Return to Lobby (after a match), and the "host disconnected" broadcast.
/// Lives in the Lobby scene with a NetworkObject; spawns when the
/// scene loads.
///
///
/// What lives here vs. on PlayerMatchState. Per-player state
/// (race, ready, slot, display name) lives on
/// because it carries across scene transitions with the player connection.
/// Lobby-wide ACTIONS (Start Match, Return to Lobby) live here because they
/// are scoped to the lobby scene and don't need to persist.
///
/// Start Match flow.
///
/// - Host clicks Start in the lobby UI.
/// - UI calls ; server validates:
/// every PlayerMatchState has a race and IsReady.
/// - Server calls NetworkBootstrap.LoadSceneAsHost(SceneNames.Match).
/// NGO replicates the scene load to every client.
/// - Match scene loads; spawns there; gameplay
/// begins as usual.
///
///
/// Return to Lobby flow. Triggered by the match-end overlay's
/// "Retry" button. Server calls ;
/// resets on every player; loads
/// the Lobby scene. Race picks are preserved so players don't have to
/// re-select.
///
/// Host-leaves behavior (Option A). When the host quits, NGO
/// shuts down the connection for every client. Clients detect the disconnect
/// in (subscribed to
/// NetworkManager.OnClientDisconnectCallback on the local client)
/// and load the MainMenu scene locally. See roadmap §1.7-Future Steam Lobby
/// Migration for the Option C upgrade (lobby persists, new host elected)
/// once Steam SDK is in.
///
[RequireComponent(typeof(NetworkObject))]
public class LobbyService : NetworkBehaviour
{
// ----- Singleton --------------------------------------------------
public static LobbyService Instance { get; private set; }
// ----- Lifecycle --------------------------------------------------
public override void OnNetworkSpawn()
{
if (Instance != null && Instance != this)
{
Debug.LogError("[LobbyService] Duplicate instance detected. " +
"Only one LobbyService should exist per scene.");
return;
}
Instance = this;
// Entering the lobby always starts everyone un-ready. Race picks
// are preserved from the previous lobby visit, but ready state
// resets so each match requires explicit re-readying.
if (IsServer)
ResetAllReady();
}
public override void OnNetworkDespawn()
{
if (Instance == this) Instance = null;
}
// ----- Public RPC API (client → server) --------------------------
///
/// Host-only request to begin the match. Validates that every connected
/// player has picked a race and is ready, then transitions every peer
/// to the Match scene via NGO scene management.
///
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void RequestStartMatchRpc(RpcParams rpcParams = default)
{
// Only the host's local client may start a match. We compare the
// sender's clientId to the server's clientId (which == the host's
// local clientId in host mode).
if (rpcParams.Receive.SenderClientId != NetworkManager.Singleton.LocalClientId)
{
Debug.LogWarning("[LobbyService] Non-host client attempted to start the match. Ignored.");
return;
}
if (!AreAllPlayersReady(out string reason))
{
Debug.Log($"[LobbyService] Cannot start match: {reason}");
return;
}
NetworkBootstrap.LoadSceneAsHost(SceneNames.Match);
}
///
/// Anyone can request a return to the lobby (typically the host clicks
/// "Retry" after a match). Server-side: resets ready state on every
/// player and triggers the scene transition. Race picks are preserved.
///
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void RequestReturnToLobbyRpc()
{
// We don't gate this on host-only currently — any client retrying is
// semantically equivalent (the host needs to be willing to keep
// running). When dedicated server / Steam migration lands this
// becomes host-only.
ResetAllReady();
NetworkBootstrap.LoadSceneAsHost(SceneNames.Lobby);
}
// ----- Public queries --------------------------------------------
///
/// True if every connected player has picked a race AND marked themselves
/// ready. Returns the reason as when false
/// (for debug logs / future UI tooltips on the disabled Start button).
///
public static bool AreAllPlayersReady(out string reason)
{
int count = 0;
foreach (var pms in PlayerMatchState.AllPlayers)
{
count++;
if (pms.RaceSelection == RaceId.None)
{
reason = $"Slot {pms.Slot} hasn't picked a race.";
return false;
}
if (!pms.IsReady)
{
reason = $"Slot {pms.Slot} is not ready.";
return false;
}
}
if (count == 0)
{
reason = "No players connected.";
return false;
}
reason = string.Empty;
return true;
}
// ----- Server helpers --------------------------------------------
private static void ResetAllReady()
{
foreach (var pms in PlayerMatchState.AllPlayers)
pms.SetReady(false);
}
}
}