using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using GeoSus.Client; namespace Subsystems { /// /// Round-robin task-to-minigame assignment, proximity detection, additive scene launch. /// public class GameManager_Tasks { private class TaskEntry { public GeoSus.Client.GameTask ServerTask; public string MinigameScene; public bool Completed; } private GameClient _gameClient; private string[] _minigameScenes; private MonoBehaviour _host; // GameManager MonoBehaviour for coroutines private List _tasks = new List(); private bool _minigameOpen; private string _loadedMinigameScene; private Camera _hostCameraSuspended; private GameObject _hostInGameHudHidden; // Proximity state (checked every frame in UpdateProximity) public GeoSus.Client.GameTask NearbyTask { get; private set; } // P13b: per-check distances pulled from the server-snapshotted lobby // settings (null-fallback to 5m matches the old hardcoded behavior). // Different actions use different fields so a host can tune e.g. a // long-range "spotter" task radius without also widening kill range. private const float ProximityRadiusFallback = 5f; public GameManager_Tasks(GameClient gameClient, string[] minigameScenes, MonoBehaviour host) { _gameClient = gameClient; _minigameScenes = minigameScenes ?? new string[0]; _host = host; } /// Called by Network subsystem when RoleAssigned fires. public void Initialize(List serverTasks) { _tasks.Clear(); if (_minigameScenes.Length == 0) return; for (int i = 0; i < serverTasks.Count; i++) { _tasks.Add(new TaskEntry { ServerTask = serverTasks[i], MinigameScene = _minigameScenes[i % _minigameScenes.Length], Completed = false }); } // Create map markers GameManager.Instance?.mapSubsystem?.CreateTaskMarkers(serverTasks); Debug.Log($"[Tasks] Initialized {_tasks.Count} tasks."); } /// Called every frame from GameManager.Update(). public void UpdateProximity() { if (_minigameOpen) return; // P13b: distances now come from the per-lobby settings snapshot // instead of one hardcoded 5m radius for everything. ?? fallback // matches the old behavior when running against an old server. var state = GameManager.Instance?.networkSubsystem?.State; var settings = state?.Settings; double taskDist = settings?.TaskStartDistanceM ?? ProximityRadiusFallback; double reportDist = settings?.ReportDistanceM ?? ProximityRadiusFallback; double emergencyDist = settings?.EmergencyMeetingCallRadiusM?? ProximityRadiusFallback; double killDist = settings?.KillDistanceM ?? ProximityRadiusFallback; NearbyTask = null; var myPos = _gameClient.MyPosition; if (myPos.Lat == 0 && myPos.Lon == 0) return; foreach (var entry in _tasks) { if (entry.Completed) continue; double dist = myPos.DistanceTo(entry.ServerTask.Location); if (dist <= taskDist) { NearbyTask = entry.ServerTask; break; } } // Drive the action button in UI var ui = GameManager.Instance?.uiSubsystem; if (ui == null || ui.IsPlayerDead) return; bool isImpostor = _gameClient.MyRole == GeoSus.Client.PlayerRole.Impostor; if (!isImpostor && NearbyTask != null) { ui.SetActionButton("USE", true, () => GameManager.Instance?.PerformAction()); return; } // Check body proximity if (!ui.IsCommsBlackout) { var body = _gameClient.FindNearbyBody(reportDist); if (body != null) { ui.SetActionButton("REPORT", true, () => GameManager.Instance?.PerformAction()); return; } // Emergency meeting proximity if (_gameClient.CurrentLobbyState?.MapData != null) { double dist = myPos.DistanceTo(_gameClient.CurrentLobbyState.MapData.Center); if (dist <= emergencyDist) { ui.SetActionButton("EMERGENCY", true, () => GameManager.Instance?.PerformAction()); return; } } } // Impostor kill if (isImpostor) { var target = _gameClient.FindNearbyPlayer(killDist); if (!string.IsNullOrEmpty(target)) { ui.SetActionButton("KILL", true, () => GameManager.Instance?.PerformAction()); // Hide sabotage menu while a kill is on offer (cleaner HUD). ui.SetSabotageMenuVisible(false); return; } } // Nothing nearby ui.SetActionButton("", false); // P13g: persistent sabotage menu for impostors when no proximity // action is on offer. Hidden when state isn't suitable - dead, // not-impostor, in meeting, sabotage already active, or comms // blackout (the impostor's own sabotage triggers a UI lock). bool inPlayingPhase = state != null && state.Phase == GeoSus.Client.GamePhase.Playing; bool sabotageActive = state?.ActiveSabotage != null; bool showSabMenu = isImpostor && !ui.IsPlayerDead && inPlayingPhase && !sabotageActive && !ui.IsCommsBlackout; ui.SetSabotageMenuVisible(showSabMenu); } /// Called externally (e.g., GameManager.PerformAction) to launch the nearby task. public void TriggerNearbyTask() { OnUsePressed(); } private void OnUsePressed() { if (NearbyTask == null || _minigameOpen) return; var entry = _tasks.Find(t => t.ServerTask.TaskId == NearbyTask.TaskId); if (entry != null) _host.StartCoroutine(LaunchMinigame(entry)); } private IEnumerator LaunchMinigame(TaskEntry entry) { _minigameOpen = true; Debug.Log($"[Tasks] Launching minigame '{entry.MinigameScene}' for task '{entry.ServerTask.Name}'"); // Validate that the scene name resolves to a build-included scene. // LoadSceneAsync silently returns null when the scene name doesn't // match (case-sensitive) or isn't in EditorBuildSettings, which // leaves the action button looking dead from the player's POV. if (string.IsNullOrEmpty(entry.MinigameScene) || !Application.CanStreamedLevelBeLoaded(entry.MinigameScene)) { Debug.LogError($"[Tasks] Minigame scene '{entry.MinigameScene}' is not loadable. " + $"Check the scene name (case-sensitive) and that it's enabled in Build Settings."); GameManager.Instance?.uiSubsystem?.ShowToast( $"Task scene missing: {entry.MinigameScene}"); _minigameOpen = false; yield break; } // Inform server that task started _gameClient.Send(new TaskStart { TaskId = entry.ServerTask.TaskId }); // Disable the host scene's main camera while the minigame is up. // With both cameras enabled the minigame's UI/3D content would // fight the host's map camera for screen space, and what gets // drawn depends on Camera.depth which isn't guaranteed across // scenes. Restored in FinishMinigame. _hostCameraSuspended = Camera.main; if (_hostCameraSuspended != null) _hostCameraSuspended.enabled = false; // Hide the persistent InGame HUD canvas (if present). It lives // in Client.unity and renders Screen Space - Overlay so it would // otherwise stack on top of the minigame's UI regardless of // which scene is active. SetActive(false) is reversible. _hostInGameHudHidden = GameObject.Find("InGame"); if (_hostInGameHudHidden != null && _hostInGameHudHidden.activeSelf) _hostInGameHudHidden.SetActive(false); else _hostInGameHudHidden = null; // nothing to restore var op = SceneManager.LoadSceneAsync(entry.MinigameScene, LoadSceneMode.Additive); if (op == null) { Debug.LogError($"[Tasks] LoadSceneAsync returned null for '{entry.MinigameScene}'."); GameManager.Instance?.uiSubsystem?.ShowToast( $"Task scene failed to load: {entry.MinigameScene}"); if (_hostCameraSuspended != null) { _hostCameraSuspended.enabled = true; _hostCameraSuspended = null; } _minigameOpen = false; yield break; } yield return op; _loadedMinigameScene = entry.MinigameScene; // CRITICAL: switch the active scene to the loaded minigame. // LoadSceneMode.Additive stacks scenes without changing which one // is "active" - and an inactive scene's RenderSettings, ambient // light, and skybox don't drive rendering. The host (Client.unity) // remains active and its lighting context still applies, which // is the root cause of "task opens to white screen": the // minigame's content loads but its visuals don't take over. // Without SetActiveScene, even minigames that ARE wired up // correctly render against the host's lighting and look broken. Scene scene = SceneManager.GetSceneByName(entry.MinigameScene); if (scene.IsValid()) SceneManager.SetActiveScene(scene); // Diagnostic: count cameras / canvases / lights in the loaded // scene. If the white screen persists after this fix, the // numbers tell us whether the scene is missing rendering bits // (camera=0, canvas=0) or if the issue is elsewhere. int camCount = 0, canvasCount = 0, lightCount = 0; foreach (var root in scene.GetRootGameObjects()) { camCount += root.GetComponentsInChildren(true).Length; canvasCount += root.GetComponentsInChildren(true).Length; lightCount += root.GetComponentsInChildren(true).Length; } Debug.Log($"[Tasks] Loaded '{entry.MinigameScene}': cameras={camCount}, " + $"canvases={canvasCount}, lights={lightCount}, " + $"activeScene={SceneManager.GetActiveScene().name}"); // Find the ITask component in the newly loaded scene ITask taskComponent = null; foreach (var root in scene.GetRootGameObjects()) { taskComponent = root.GetComponentInChildren(); if (taskComponent != null) break; } if (taskComponent == null) { Debug.LogWarning($"[Tasks] No ITask found in '{entry.MinigameScene}'. " + $"Either the minigame's controller script isn't attached to a GameObject in the scene, " + $"or the script doesn't implement ITask. Auto-completing."); yield return FinishMinigame(entry, true); yield break; } // Set task metadata taskComponent.TaskID = entry.ServerTask.TaskId; taskComponent.TaskName = entry.ServerTask.Name; taskComponent.TaskLocation = (entry.ServerTask.Location.Lat, entry.ServerTask.Location.Lon); bool done = false; taskComponent.Initialize(t => { done = true; }); // Wait for completion or exit yield return new WaitUntil(() => done); yield return FinishMinigame(entry, done); } private IEnumerator FinishMinigame(TaskEntry entry, bool completed) { if (completed) { entry.Completed = true; _gameClient.CompleteTask(entry.ServerTask.TaskId); Debug.Log($"[Tasks] Task '{entry.ServerTask.Name}' completed."); } else { Debug.Log($"[Tasks] Task '{entry.ServerTask.Name}' exited without completion."); } // Unload minigame scene. Switch the active scene back to the // host BEFORE the unload so we don't end up with no active // scene mid-frame (Unity will complain and lighting flickers). if (!string.IsNullOrEmpty(_loadedMinigameScene)) { var hostScene = SceneManager.GetSceneByName("Client"); if (hostScene.IsValid()) SceneManager.SetActiveScene(hostScene); var unload = SceneManager.UnloadSceneAsync(_loadedMinigameScene); yield return unload; _loadedMinigameScene = null; } // Re-enable the host camera that was suspended during the minigame. if (_hostCameraSuspended != null) { _hostCameraSuspended.enabled = true; _hostCameraSuspended = null; } // Re-show the InGame HUD canvas hidden at minigame entry. if (_hostInGameHudHidden != null) { _hostInGameHudHidden.SetActive(true); _hostInGameHudHidden = null; } _minigameOpen = false; } } }