using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using MegaKoop.Game; #if STEAMWORKSNET using Steamworks; #endif namespace MegaKoop.Game.Networking { /// /// Handles transitioning from the lobby into the character scene and spawning player avatars. /// Lives alongside the Steam services object so it survives scene loads. /// [DefaultExecutionOrder(-500)] public class LobbyGameSceneCoordinator : MonoBehaviour { [SerializeField] private string characterSceneName = "CharacterScene"; [SerializeField] private float spawnRadius = 3f; [SerializeField] private float minimumSpawnSpacing = 2.5f; private readonly List pendingPlayers = new(); private string localSteamId = string.Empty; private bool loadPending; private bool hasSpawned; private void Awake() { DontDestroyOnLoad(gameObject); SceneManager.sceneLoaded += HandleSceneLoaded; } private void OnDestroy() { SceneManager.sceneLoaded -= HandleSceneLoaded; } /// /// Captures the current lobby membership and moves everyone into the character scene. /// public void BeginGame(MegaKoop.Steam.SteamLobbyService steam) { if (steam == null) { Debug.LogWarning("[LobbyGameSceneCoordinator] SteamLobbyService missing; cannot begin game."); return; } CapturePlayers(steam); if (!string.Equals(SceneManager.GetActiveScene().name, characterSceneName, StringComparison.OrdinalIgnoreCase)) { loadPending = true; hasSpawned = false; SceneManager.LoadScene(characterSceneName); } else { // Already in the target scene (e.g., during hot reload). loadPending = false; hasSpawned = false; SpawnPlayersInScene(SceneManager.GetActiveScene()); } } private void CapturePlayers(MegaKoop.Steam.SteamLobbyService steam) { pendingPlayers.Clear(); localSteamId = steam.LocalSteamIdString ?? string.Empty; var members = steam.GetMembers(); if (members != null) { foreach (var member in members) { pendingPlayers.Add(new LobbyPlayerInfo { SteamId = member.steamId, DisplayName = string.IsNullOrWhiteSpace(member.name) ? "Player" : member.name, IsLocal = member.steamId == localSteamId }); } } // Ensure the local player is represented even if lobby data is empty (e.g., offline stub). if (!pendingPlayers.Exists(p => p.IsLocal)) { pendingPlayers.Add(new LobbyPlayerInfo { SteamId = localSteamId, DisplayName = GetFallbackLocalName(), IsLocal = true }); } pendingPlayers.Sort((a, b) => string.CompareOrdinal(a.SteamId ?? string.Empty, b.SteamId ?? string.Empty)); } private void HandleSceneLoaded(Scene scene, LoadSceneMode mode) { if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase)) { return; } if (!loadPending && hasSpawned) { return; } loadPending = false; hasSpawned = false; SpawnPlayersInScene(scene); } private void SpawnPlayersInScene(Scene scene) { if (hasSpawned) { return; } var template = FindWizardTemplate(scene); if (template == null) { Debug.LogWarning("[LobbyGameSceneCoordinator] Wizard template not found in CharacterScene; cannot spawn players."); return; } hasSpawned = true; Vector3 basePosition = template.transform.position; Quaternion baseRotation = template.transform.rotation; Transform parent = template.transform.parent; // Hide template so only spawned copies are visible. template.SetActive(false); int total = Mathf.Max(1, pendingPlayers.Count); for (int i = 0; i < total; i++) { LobbyPlayerInfo info = i < pendingPlayers.Count ? pendingPlayers[i] : new LobbyPlayerInfo { DisplayName = $"Player {i + 1}", IsLocal = i == 0 }; Vector3 spawnPosition = CalculateSpawnPosition(basePosition, i, total); var clone = Instantiate(template, spawnPosition, baseRotation, parent); clone.name = BuildPlayerName(info, i); ConfigureCloneForPlayer(clone, info, i); clone.SetActive(true); } } private static GameObject FindWizardTemplate(Scene scene) { foreach (var root in scene.GetRootGameObjects()) { if (root.name.Equals("Wizard", StringComparison.OrdinalIgnoreCase)) { return root; } var controller = root.GetComponentInChildren(true); if (controller != null) { return controller.gameObject; } } return null; } private Vector3 CalculateSpawnPosition(Vector3 center, int index, int total) { if (total <= 1) { return center; } float radiusForRing = Mathf.Max(spawnRadius, minimumSpawnSpacing) * Mathf.Ceil((index + 1) / 6f); float angle = (Mathf.PI * 2f / Mathf.Max(1, Mathf.Min(total, 6))) * (index % 6); Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radiusForRing; return center + offset; } private void ConfigureCloneForPlayer(GameObject clone, LobbyPlayerInfo info, int index) { // Ensure network identity is deterministic across clients. var identity = clone.GetComponent() ?? clone.AddComponent(); identity.SetNetworkId(index + 1); var bridge = clone.GetComponent() ?? clone.AddComponent(); bridge.AssignOwner(ParseSteamId(info.SteamId), info.IsLocal); var inputSender = clone.GetComponent() ?? clone.AddComponent(); inputSender.enabled = info.IsLocal; // Local player keeps the camera and audio; remote avatars are visual only. var cameras = clone.GetComponentsInChildren(true); foreach (var camera in cameras) { camera.enabled = info.IsLocal; if (!info.IsLocal) { camera.gameObject.SetActive(false); } } var listeners = clone.GetComponentsInChildren(true); foreach (var listener in listeners) { listener.enabled = info.IsLocal; if (!info.IsLocal) { listener.gameObject.SetActive(false); } } var thirdPersonCamera = clone.GetComponentInChildren(true); if (thirdPersonCamera != null) { thirdPersonCamera.enabled = info.IsLocal; } } private static string BuildPlayerName(LobbyPlayerInfo info, int index) { string baseName = !string.IsNullOrWhiteSpace(info.DisplayName) ? info.DisplayName : $"Player {index + 1}"; return $"Wizard_{baseName}"; } private static string GetFallbackLocalName() { #if STEAMWORKSNET if (SteamBootstrap.IsInitialized) { return SteamFriends.GetPersonaName(); } #endif return "You"; } private static ulong ParseSteamId(string steamId) { if (string.IsNullOrEmpty(steamId)) { return 0; } if (ulong.TryParse(steamId, out var value)) { return value; } return 0; } [Serializable] private struct LobbyPlayerInfo { public string SteamId; public string DisplayName; public bool IsLocal; } } }