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
});
}
}
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);
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)
{
var controller = clone.GetComponent();
if (controller != null)
{
controller.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;
if (info.IsLocal)
{
thirdPersonCamera.SetTarget(clone.transform);
}
}
}
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";
}
[Serializable]
private struct LobbyPlayerInfo
{
public string SteamId;
public string DisplayName;
public bool IsLocal;
}
}
}