347 lines
13 KiB
C#
347 lines
13 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using Subsystems;
|
|
using GeoSus.Client;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
using TMPro;
|
|
|
|
namespace Subsystems
|
|
{
|
|
/// <summary>
|
|
/// Manages UI for the GameManager. Canvas references are only valid in Client.unity;
|
|
/// Art-menu scenes use their own lightweight UI scripts that read from GameManager.Instance.
|
|
/// </summary>
|
|
public class GameManager_UI
|
|
{
|
|
private GameClient _gameClient;
|
|
|
|
// Set by GameManager after Client.unity loads (called from GameManager.OnSceneLoaded)
|
|
public Canvas ClientCreateJoinLobby; // fallback join-code canvas in Client.unity
|
|
public Canvas ClientInLobby; // InLobby canvas in Client.unity (unused now, kept compat)
|
|
public Canvas ClientLoadingScreen;
|
|
public Canvas ClientGameScreen; // parent of all HUD elements
|
|
|
|
// HUD elements (children of ClientGameScreen, resolved at runtime)
|
|
private TMP_Text _roleText;
|
|
private TMP_Text _taskListText;
|
|
private TMP_Text _taskProgressText;
|
|
private Button _actionButton;
|
|
private TMP_Text _actionButtonText;
|
|
private TMP_Text _killCooldownText;
|
|
private GameObject _sabotagePanel;
|
|
private TMP_Text _sabotageTimerText;
|
|
private GameObject _meetingPanel;
|
|
private GameObject _gameEndPanel;
|
|
private TMP_Text _gameEndText;
|
|
|
|
// Runtime state
|
|
private bool _isDead;
|
|
private bool _commsBlackout;
|
|
private DateTime _sabotageMeltdownDeadline;
|
|
private bool _sabotageTimerActive;
|
|
|
|
// Lobby-changed flag — set from network thread, consumed in Update
|
|
private volatile bool _lobbyDirty;
|
|
|
|
public GameManager_UI(GameClient gameClient)
|
|
{
|
|
_gameClient = gameClient;
|
|
}
|
|
|
|
/// <summary>Called by Network subsystem when lobby player list changes.</summary>
|
|
public void NotifyLobbyChanged() => _lobbyDirty = true;
|
|
|
|
// ── Called from GameManager after Client.unity loads ──────────────────
|
|
|
|
public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
|
|
{
|
|
ClientCreateJoinLobby = createJoin;
|
|
ClientInLobby = inLobby;
|
|
ClientLoadingScreen = loading;
|
|
ClientGameScreen = game;
|
|
|
|
EnsureCanvasReady(createJoin);
|
|
EnsureCanvasReady(inLobby);
|
|
EnsureCanvasReady(loading);
|
|
EnsureCanvasReady(game);
|
|
|
|
if (createJoin) createJoin.gameObject.SetActive(false);
|
|
if (inLobby) inLobby.gameObject.SetActive(false);
|
|
if (loading) loading.gameObject.SetActive(false);
|
|
if (game) game.gameObject.SetActive(false);
|
|
|
|
if (game != null)
|
|
{
|
|
_roleText = FindTMP(game.transform, "Role");
|
|
_taskListText = FindTMP(game.transform, "TaskList");
|
|
_taskProgressText = FindTMP(game.transform, "TaskProgress");
|
|
_killCooldownText = FindTMP(game.transform, "KillCooldown");
|
|
_sabotageTimerText = FindTMP(game.transform, "SabotageTimer");
|
|
_gameEndText = FindTMP(game.transform, "GameEndText");
|
|
|
|
var actionGO = game.transform.Find("ActionButton");
|
|
if (actionGO != null)
|
|
{
|
|
_actionButton = actionGO.GetComponent<Button>();
|
|
_actionButtonText = actionGO.GetComponentInChildren<TMP_Text>();
|
|
}
|
|
|
|
var sabGO = game.transform.Find("SabotagePanel");
|
|
_sabotagePanel = sabGO?.gameObject;
|
|
|
|
var meetGO = game.transform.Find("MeetingPanel");
|
|
_meetingPanel = meetGO?.gameObject;
|
|
if (_meetingPanel) _meetingPanel.SetActive(false);
|
|
|
|
var endGO = game.transform.Find("GameEndPanel");
|
|
_gameEndPanel = endGO?.gameObject;
|
|
if (_gameEndPanel) _gameEndPanel.SetActive(false);
|
|
}
|
|
}
|
|
|
|
// ── Main update (called every frame from GameManager.Update) ──────────
|
|
|
|
public void UpdateLobbyUI()
|
|
{
|
|
var state = _gameClient.CurrentLobbyState;
|
|
if (state == null) return;
|
|
|
|
// Update any LobbyDisplayUI listeners in the current scene
|
|
if (_lobbyDirty)
|
|
{
|
|
_lobbyDirty = false;
|
|
LobbyDisplayUI.RefreshAll(state);
|
|
}
|
|
|
|
// Only do canvas switches if we are in Client.unity (canvases assigned)
|
|
if (ClientGameScreen == null) return;
|
|
|
|
switch (state.Phase)
|
|
{
|
|
case GamePhase.Loading:
|
|
SetCanvases(false, false, true, false);
|
|
break;
|
|
|
|
case GamePhase.Lobby:
|
|
SetCanvases(false, true, false, false);
|
|
break;
|
|
|
|
case GamePhase.Playing:
|
|
case GamePhase.Meeting:
|
|
case GamePhase.Voting:
|
|
SetCanvases(false, false, false, true);
|
|
UpdateGameHUD();
|
|
break;
|
|
|
|
case GamePhase.Ended:
|
|
// GameEndPanel shown by HandleGameEnded
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UpdateGameHUD()
|
|
{
|
|
if (_roleText != null) _roleText.text = _gameClient.MyRole?.ToString() ?? "";
|
|
|
|
// Task list
|
|
if (_taskListText != null)
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
foreach (var t in _gameClient.MyTasks)
|
|
sb.AppendLine(t.Name);
|
|
_taskListText.text = sb.ToString();
|
|
}
|
|
|
|
// Kill cooldown (managed by GameManager_Tasks via Update)
|
|
// Sabotage timer
|
|
if (_sabotageTimerActive && _sabotageTimerText != null)
|
|
{
|
|
double remaining = (_sabotageMeltdownDeadline - DateTime.UtcNow).TotalSeconds;
|
|
_sabotageTimerText.text = remaining > 0 ? $"MELTDOWN: {remaining:F0}s" : "MELTDOWN!";
|
|
}
|
|
}
|
|
|
|
// ── Helpers called by Network handlers ────────────────────────────────
|
|
|
|
public void SetKillCooldownText(string text)
|
|
{
|
|
if (_killCooldownText != null)
|
|
{
|
|
_killCooldownText.text = text;
|
|
_killCooldownText.gameObject.SetActive(!string.IsNullOrEmpty(text));
|
|
}
|
|
}
|
|
|
|
public void UpdateTaskProgress(int completed, int total)
|
|
{
|
|
if (_taskProgressText != null)
|
|
_taskProgressText.text = $"Tasks: {completed}/{total}";
|
|
}
|
|
|
|
public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null)
|
|
{
|
|
if (_actionButton == null) return;
|
|
_actionButton.gameObject.SetActive(visible);
|
|
if (_actionButtonText != null) _actionButtonText.text = label;
|
|
if (onClick != null)
|
|
{
|
|
_actionButton.onClick.RemoveAllListeners();
|
|
_actionButton.onClick.AddListener(onClick);
|
|
}
|
|
}
|
|
|
|
public void OnLocalPlayerDied()
|
|
{
|
|
_isDead = true;
|
|
if (_roleText != null) _roleText.text = "GHOST";
|
|
}
|
|
|
|
public void ShowMeetingAlert()
|
|
{
|
|
Debug.Log("Meeting called! Run to meeting point.");
|
|
}
|
|
|
|
public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
|
|
{
|
|
if (_meetingPanel == null) return;
|
|
_meetingPanel.SetActive(true);
|
|
|
|
var header = FindTMP(_meetingPanel.transform, "MeetingHeader");
|
|
if (header != null)
|
|
header.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";
|
|
|
|
// Build simple text list of players — full vote buttons need prefabs in Unity Editor
|
|
var playerList = FindTMP(_meetingPanel.transform, "MeetingPlayerList");
|
|
if (playerList != null && players != null)
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
foreach (var p in players)
|
|
sb.AppendLine($"{p.DisplayName} [{p.State}]");
|
|
playerList.text = sb.ToString();
|
|
}
|
|
|
|
// Wire skip button if it exists
|
|
var skipBtn = _meetingPanel.transform.Find("SkipButton")?.GetComponent<Button>();
|
|
if (skipBtn != null)
|
|
{
|
|
skipBtn.onClick.RemoveAllListeners();
|
|
skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null));
|
|
}
|
|
}
|
|
|
|
public void AppendVoteInstruction()
|
|
{
|
|
var playerList = FindTMP(_meetingPanel?.transform, "MeetingPlayerList");
|
|
if (playerList != null)
|
|
playerList.text += "\n[Tap a name] then press VOTE — or press SKIP";
|
|
}
|
|
|
|
public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
|
|
{
|
|
if (_meetingPanel == null) return;
|
|
|
|
var resultText = FindTMP(_meetingPanel.transform, "VoteResult");
|
|
if (resultText != null)
|
|
{
|
|
if (payload.WasTie)
|
|
{
|
|
resultText.text = "TIE — nobody ejected.";
|
|
}
|
|
else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
|
|
{
|
|
resultText.text = "Skip — nobody ejected.";
|
|
}
|
|
else
|
|
{
|
|
var ejected = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
|
|
resultText.text = $"{ejected?.DisplayName ?? payload.EjectedPlayerId} ejected!";
|
|
}
|
|
}
|
|
|
|
// Close panel after 5 seconds — use coroutine via GameManager
|
|
GameManager.Instance?.StartCoroutine(CloseMeetingPanelAfterDelay(5f));
|
|
}
|
|
|
|
private System.Collections.IEnumerator CloseMeetingPanelAfterDelay(float delay)
|
|
{
|
|
yield return new UnityEngine.WaitForSeconds(delay);
|
|
if (_meetingPanel != null) _meetingPanel.SetActive(false);
|
|
}
|
|
|
|
public void ShowGameEndPanel(GameEndedPayload payload, string myUuid)
|
|
{
|
|
if (_gameEndPanel != null) _gameEndPanel.SetActive(true);
|
|
bool won = payload.Winners != null && payload.Winners.Contains(myUuid);
|
|
if (_gameEndText != null)
|
|
_gameEndText.text = $"{(won ? "VICTORY" : "DEFEAT")}\n{payload.WinningFaction} wins!\n{payload.Reason}";
|
|
}
|
|
|
|
public void ShowSabotageTimer(DateTime deadline)
|
|
{
|
|
_sabotageMeltdownDeadline = deadline;
|
|
_sabotageTimerActive = true;
|
|
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(true);
|
|
}
|
|
|
|
public void HideSabotageTimer()
|
|
{
|
|
_sabotageTimerActive = false;
|
|
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(false);
|
|
SetCommsBlackout(false);
|
|
}
|
|
|
|
public void SetCommsBlackout(bool active)
|
|
{
|
|
_commsBlackout = active;
|
|
}
|
|
|
|
public bool IsCommsBlackout => _commsBlackout;
|
|
public bool IsPlayerDead => _isDead;
|
|
|
|
// ── Utilities ─────────────────────────────────────────────────────────
|
|
|
|
private void SetCanvases(bool createJoin, bool inLobby, bool loading, bool game)
|
|
{
|
|
EnsureCanvasReady(ClientCreateJoinLobby);
|
|
EnsureCanvasReady(ClientInLobby);
|
|
EnsureCanvasReady(ClientLoadingScreen);
|
|
EnsureCanvasReady(ClientGameScreen);
|
|
|
|
if (ClientCreateJoinLobby) ClientCreateJoinLobby.gameObject.SetActive(createJoin);
|
|
if (ClientInLobby) ClientInLobby.gameObject.SetActive(inLobby);
|
|
if (ClientLoadingScreen) ClientLoadingScreen.gameObject.SetActive(loading);
|
|
if (ClientGameScreen) ClientGameScreen.gameObject.SetActive(game);
|
|
}
|
|
|
|
private static void EnsureCanvasReady(Canvas canvas)
|
|
{
|
|
if (canvas == null) return;
|
|
|
|
if (!canvas.enabled)
|
|
canvas.enabled = true;
|
|
|
|
// Some scene canvases are saved with zero scale, which makes UI invisible.
|
|
var t = canvas.transform;
|
|
if (t != null)
|
|
{
|
|
var s = t.localScale;
|
|
if (Mathf.Abs(s.x) < 0.001f || Mathf.Abs(s.y) < 0.001f || Mathf.Abs(s.z) < 0.001f)
|
|
t.localScale = Vector3.one;
|
|
}
|
|
}
|
|
|
|
private static TMP_Text FindTMP(Transform root, string name)
|
|
{
|
|
if (root == null) return null;
|
|
foreach (var tmp in root.GetComponentsInChildren<TMP_Text>(true))
|
|
{
|
|
if (tmp != null && tmp.name == name)
|
|
return tmp;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|