using UnityEngine; using GeoSus.Client; using Subsystems; using System.Collections; using System; using TMPro; using UnityEngine.SceneManagement; public class GameManager : MonoBehaviour { // Singleton public static GameManager Instance { get; private set; } [Header("Subsystems")] public GameManager_Network networkSubsystem; public GameManager_UI uiSubsystem; public GameManager_Map mapSubsystem; public GameManager_Input inputSubsystem; public GameManager_Tasks taskSubsystem; public GameClient gameClient; [Header("Player Info")] public string displayName; [Header("Scene Management")] [SerializeField] public string firstMenuScene = "main menu asi idk lol"; [Header("UI Elements (Client.unity)")] // Canvas names in Client.unity — found at runtime in OnSceneLoaded private const string CanvasNameJoinCreate = "LobbySelector"; private const string CanvasNameInLobby = "InLobby"; private const string CanvasNameLoading = "LoadingScreen"; private const string CanvasNameGame = "InGame"; [Header("Map")] // MapCenterPoint and Player are in Client.unity — wired at runtime in OnSceneLoaded. // buildingSettings/pathwaySettings/areaSettings must be assigned in SampleScene Inspector. public BuildingSettings buildingSettings; public PathwaySettings pathwaySettings; public AreaSettings areaSettings; [Header("Lobby Settings")] public double pendingRadius = 500; public int pendingImpostorCount = 1; public int pendingTaskCount = 5; /// /// P13b/c: full settings overrides accumulated by HostLobbyUI before the /// host taps "Create". Null = host didn't change anything beyond the three /// flat fields above; server falls through to its current defaults for /// every field. Each field is independently nullable so the host can /// opt into changing only what they care about. /// public GameSettingsOverrides pendingSettings; [Header("Task Minigames (round-robin)")] // Names MUST match the scene file names in Assets/Scenes (case-sensitive) // and each one MUST be enabled in EditorBuildSettings, or LoadSceneAsync // will silently fail and the task button will appear dead. [SerializeField] public string[] minigameScenes = { "MiniGame-Kabely V10", "MiniGame-insertkeys", "MiniGame-FlappyBird", "MiniGame-ThrowInHole", "MiniGame-Satelit" }; [Header("Debug")] public bool testMode = false; private GameClient _secondClient; private GameClient _thirdClient; private GameManager_Network _secondNetwork; private GameManager_Network _thirdNetwork; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } void Start() { if (string.IsNullOrEmpty(displayName)) displayName = PlayerPrefs.GetString("PlayerName", GenerateUsername()); gameClient = new GameClient(GenerateUUID(), displayName); networkSubsystem = new GameManager_Network(gameClient, this); mapSubsystem = new GameManager_Map(gameClient, null, buildingSettings, pathwaySettings, areaSettings); uiSubsystem = new GameManager_UI(gameClient); inputSubsystem = new GameManager_Input(gameClient, null, testMode); taskSubsystem = new GameManager_Tasks(gameClient, minigameScenes, this); if (testMode) { _secondClient = new GameClient(GenerateUUID(), GenerateUsername()); _secondNetwork = new GameManager_Network(_secondClient, null); _thirdClient = new GameClient(GenerateUUID(), GenerateUsername()); _thirdNetwork = new GameManager_Network(_thirdClient, null); _secondNetwork.OpenConnection(); _thirdNetwork.OpenConnection(); } networkSubsystem.OpenConnection(); // Start GPS immediately at app launch. Acquiring a fix on a cold // device can take 5-30 seconds; if we wait until CreateLobby is // pressed, the lobby will be seeded with bad coords. Starting here // means the user's normal navigation through the menus gives the // GPS subsystem time to settle. inputSubsystem?.EnsureGPSStarted(); // Load main menu after GameManager is ready if (!string.IsNullOrEmpty(firstMenuScene)) SceneManager.LoadScene(firstMenuScene, LoadSceneMode.Single); } private void Update() { // Tick the SDK dispatcher so callbacks fire on main thread gameClient?.Update(); if (testMode) { _secondClient?.Update(); _thirdClient?.Update(); } if (gameClient?.CurrentLobbyState != null) { uiSubsystem?.UpdateLobbyUI(); taskSubsystem?.UpdateProximity(); } if (gameClient?.MyRole == PlayerRole.Impostor) UpdateKillCooldown(); inputSubsystem?.positionCheck(); } void OnEnable() { SceneManager.sceneLoaded += OnSceneLoaded; } void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; } /// /// After Client.unity loads, re-bind all canvas/HUD references because /// those GameObjects don't exist in the Art menu scenes. /// private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (scene.name == "Client") { var roots = scene.GetRootGameObjects(); // Find a root or deep GameObject by name in the loaded scene GameObject FindGO(string n) { foreach (var go in roots) { if (go.name == n) return go; var found = go.transform.Find(n); if (found != null) return found.gameObject; } return null; } Canvas FindCanvas(string n) { var go = FindGO(n); return go != null ? go.GetComponent() : null; } // ── Build HUD BEFORE BindClientScene so FindTMP/Find can locate new elements ── var inGameGO = FindGO("InGame"); if (inGameGO != null) { var builder = inGameGO.GetComponent() ?? inGameGO.AddComponent(); builder.BuildNow(); } // ── Wire canvases (after HUD is built) ── // Apply our standard CanvasScaler (1080x1920 reference, match=0.5) // to every canvas in the scene before binding so layouts scale // identically across phones and tablets without per-device tweaks. var cJoin = FindCanvas(CanvasNameJoinCreate); var cLobby = FindCanvas(CanvasNameInLobby); var cLoad = FindCanvas(CanvasNameLoading); var cGame = FindCanvas(CanvasNameGame); InGameHUDBuilder.ConfigureCanvasScaler(cJoin); InGameHUDBuilder.ConfigureCanvasScaler(cLobby); InGameHUDBuilder.ConfigureCanvasScaler(cLoad); InGameHUDBuilder.ConfigureCanvasScaler(cGame); uiSubsystem?.BindClientScene(cJoin, cLobby, cLoad, cGame); // ── Wire map center point and player capsule ── var mapCenter = FindGO("MapCenterPoint"); var player = FindGO("Capsule"); mapSubsystem?.SetMapCenterPoint(mapCenter); inputSubsystem?.SetPlayerObject(player); // ── Attach camera controller to Main Camera ── var mainCamGO = FindGO("Main Camera"); if (mainCamGO != null) { var camCtrl = mainCamGO.GetComponent() ?? mainCamGO.AddComponent(); camCtrl.SetTarget(player); } // If MapDataReady arrived before Client scene finished loading, // this will build the map now that scene references are valid. networkSubsystem?.OnClientSceneReady(); } else if (scene.name == "create" || scene.name == "join loading") { // Lobby scene just loaded — ensure LobbyDisplayUI refreshes once // its Start() has run and registered itself (happens before Update). uiSubsystem?.NotifyLobbyChanged(); } } private float _killCooldownSeconds = 0f; private const float KillCooldownDuration = 20f; private void UpdateKillCooldown() { if (_killCooldownSeconds > 0) { _killCooldownSeconds -= Time.deltaTime; // Mirror into GameState so UI reads from the single source of truth if (networkSubsystem?.State != null) networkSubsystem.State.KillCooldownRemaining = _killCooldownSeconds; uiSubsystem?.SetKillCooldownText($"Kill: {Mathf.CeilToInt(_killCooldownSeconds)}s"); } else { _killCooldownSeconds = 0f; if (networkSubsystem?.State != null) networkSubsystem.State.KillCooldownRemaining = 0; uiSubsystem?.SetKillCooldownText(""); } } /// /// Called by the ActionButton. Routes to kill / report / emergency / use-task /// depending on current proximity state. /// public void PerformAction() { if (uiSubsystem == null || uiSubsystem.IsPlayerDead) return; bool isImpostor = gameClient?.MyRole == PlayerRole.Impostor; // P13b: pull per-lobby distances from the server-snapshotted settings // instead of hardcoding 5m for every check. ?? fallback keeps the // pre-P13b behavior on old server builds that don't ship settings. var settings = networkSubsystem?.State?.Settings; double reportDist = settings?.ReportDistanceM ?? 5.0; double emergencyDist = settings?.EmergencyMeetingCallRadiusM ?? 5.0; double killDist = settings?.KillDistanceM ?? 5.0; // 1. Nearby task → USE var nearbyTask = taskSubsystem?.NearbyTask; if (nearbyTask != null && !isImpostor) { taskSubsystem.TriggerNearbyTask(); return; } // 2. Nearby body → REPORT if (!uiSubsystem.IsCommsBlackout) { var nearbyBody = gameClient?.FindNearbyBody(reportDist); if (nearbyBody != null) { gameClient.ReportBody(nearbyBody.BodyId); return; } // 3. Near map centre → EMERGENCY if (gameClient?.CurrentLobbyState?.MapData != null) { double distToCenter = gameClient.MyPosition.DistanceTo(gameClient.CurrentLobbyState.MapData.Center); if (distToCenter <= emergencyDist) { gameClient.CallEmergencyMeeting(); return; } } } // 4. Impostor kill if (isImpostor && _killCooldownSeconds <= 0) { var targetUuid = gameClient?.FindNearbyPlayer(killDist); if (!string.IsNullOrEmpty(targetUuid)) { gameClient.Kill(targetUuid); _killCooldownSeconds = KillCooldownDuration; } } } /// Called by Impostor sabotage buttons. public void StartSabotage(int typeIndex) { gameClient?.Send(new GeoSus.Client.StartSabotage { SabotageType = (SabotageType)typeIndex }); } /// Called by the meeting vote buttons. Pass null to skip. public void CastVote(string targetUuid) { gameClient?.Vote(targetUuid); } protected string GenerateUUID() { return System.Guid.NewGuid().ToString(); } protected string GenerateUsername() { return "Player" + UnityEngine.Random.Range(1000, 9999).ToString(); } // Called by HostLobbyUI public void CreateLobbyButton() { // Refuse to create a lobby without a real GPS fix. The previous // behavior of silently using a hardcoded Czechia fallback meant the // game always started at the same place no matter where the host was, // and the player capsule would spawn miles away in coordinate space // because they're at their real GPS while the map was built around // the fallback. Both bugs share this single gate. if (inputSubsystem?.LastKnownPosition == null) { // testMode bypasses the GPS gate entirely so debug runs still work. if (!testMode) { Debug.LogWarning("[GameManager] CreateLobby blocked: no GPS fix yet. " + "Make sure location permission is granted and you have signal."); uiSubsystem?.ShowToast("Waiting for GPS fix... grant location permission and try again."); inputSubsystem?.EnsureGPSStarted(); return; } } var pos = inputSubsystem?.LastKnownPosition; double lat = pos?.Lat ?? 0; double lon = pos?.Lon ?? 0; networkSubsystem.CreateLobby(lat, lon, pendingRadius, pendingImpostorCount, pendingTaskCount, pendingSettings); if (testMode) StartCoroutine(ConnectTestClients()); } // Called by JoinLobbyUI with the code from the input field public void JoinLobbyButton(string code) { if (!string.IsNullOrEmpty(code)) networkSubsystem.JoinLobby(code); else Debug.LogWarning("Join code is empty!"); } public void LeaveLobbyButton() { networkSubsystem.LeaveLobby(); } public void StartGameButton() { networkSubsystem.StartGame(); } void OnApplicationQuit() { gameClient?.Disconnect(); _secondClient?.Disconnect(); _thirdClient?.Disconnect(); } IEnumerator ConnectTestClients() { // Wait until host lobby code exists float wait = 0f; while ((gameClient?.CurrentLobbyState == null || string.IsNullOrEmpty(gameClient.CurrentLobbyState.JoinCode)) && wait < 20f) { wait += 0.25f; yield return new WaitForSeconds(0.25f); } var joinCode = gameClient?.CurrentLobbyState?.JoinCode; if (string.IsNullOrEmpty(joinCode)) { Debug.LogWarning("[TestMode] Could not join test clients: join code not available."); yield break; } // Wait until helper clients are connected and handshake-complete wait = 0f; while (((_secondClient == null || !_secondClient.IsReady) || (_thirdClient == null || !_thirdClient.IsReady)) && wait < 20f) { wait += 0.25f; yield return new WaitForSeconds(0.25f); } if (_secondClient == null || _thirdClient == null || !_secondClient.IsReady || !_thirdClient.IsReady) { Debug.LogWarning("[TestMode] Helper clients are not ready, skipping auto-join."); yield break; } _secondNetwork?.JoinLobby(joinCode); _thirdNetwork?.JoinLobby(joinCode); Debug.Log($"[TestMode] Helper clients joined lobby with code {joinCode}."); } }