Files
GeoSusGame/Assets/GameManager/GameManager_UI.cs

531 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.UI;
using Subsystems;
using GeoSus.Client;
using System.Collections.Generic;
using System;
using TMPro;
namespace Subsystems
{
/// <summary>
/// Reads from GameManager_Network.State (the authoritative GameState) and drives
/// all in-game canvas panels. No business logic lives here.
/// </summary>
public class GameManager_UI
{
private GameClient _gameClient;
private GameState _state => GameManager.Instance?.networkSubsystem?.State;
// ── Canvas refs (wired by BindClientScene from Client.unity) ──────────
public Canvas ClientCreateJoinLobby;
public Canvas ClientInLobby;
public Canvas ClientLoadingScreen;
public Canvas ClientGameScreen;
// ── HUD element refs (resolved once in BindClientScene) ───────────────
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 TMP_Text _meetingHeader;
private Transform _meetingScrollContent;
private TMP_Text _meetingFallbackText;
private GameObject _voteResultPanel;
private TMP_Text _voteResultText;
private GameObject _gameEndPanel;
private TMP_Text _gameEndText;
private RectTransform _returnToLobbyBtn;
private TMP_Text _toastText;
private GameObject _toastGO;
// ── Internal state ────────────────────────────────────────────────────
private bool _isDead;
private bool _commsBlackout;
private DateTime _sabotageMeltdownDeadline;
private bool _sabotageTimerActive;
private volatile bool _lobbyDirty;
// Meeting vote-row references rebuilt each meeting
private readonly List<GameObject> _voteRows = new List<GameObject>();
private string _pendingVoteResultDisplay; // shown after voting
public GameManager_UI(GameClient gameClient) { _gameClient = gameClient; }
public void NotifyLobbyChanged() => _lobbyDirty = true;
public bool IsCommsBlackout => _commsBlackout;
public bool IsPlayerDead => _isDead;
// ── Scene binding ─────────────────────────────────────────────────────
public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
{
ClientCreateJoinLobby = createJoin;
ClientInLobby = inLobby;
ClientLoadingScreen = loading;
ClientGameScreen = game;
foreach (var c in new[] { createJoin, inLobby, loading, game })
EnsureCanvasReady(c);
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) return;
var t = game.transform;
_roleText = FindTMP(t, "Role");
_taskListText = FindTMP(t, "TaskList");
_taskProgressText = FindTMP(t, "TaskProgress");
_killCooldownText = FindTMP(t, "KillCooldown");
_sabotageTimerText = FindTMP(t, "SabotageTimer");
_gameEndText = FindTMP(t, "GameEndText");
_toastText = FindTMP(t, "Toast");
_meetingHeader = FindTMP(t, "MeetingHeader");
_meetingFallbackText = FindTMP(t, "MeetingPlayerList");
_voteResultText = FindTMP(t, "VoteResult");
_meetingScrollContent = FindTransform(t, "MeetingContent");
var actionGO = t.Find("ActionButton");
if (actionGO != null)
{
_actionButton = actionGO.GetComponent<Button>();
_actionButtonText = actionGO.GetComponentInChildren<TMP_Text>();
}
_sabotagePanel = t.Find("SabotagePanel")?.gameObject;
_meetingPanel = t.Find("MeetingPanel")?.gameObject;
_gameEndPanel = t.Find("GameEndPanel")?.gameObject;
_voteResultPanel = FindTransformGO(t, "VoteResultPanel");
_toastGO = FindTransformGO(t, "Toast");
var retBtn = FindTransform(t, "ReturnToLobbyButton");
if (retBtn != null) _returnToLobbyBtn = retBtn as RectTransform;
if (_meetingPanel) _meetingPanel.SetActive(false);
if (_gameEndPanel) _gameEndPanel.SetActive(false);
if (_voteResultPanel) _voteResultPanel.SetActive(false);
if (_toastGO) _toastGO.SetActive(false);
}
// ── Update (called every frame from GameManager.Update) ───────────────
public void UpdateLobbyUI()
{
var lobbyState = _gameClient.CurrentLobbyState;
if (lobbyState == null) return;
if (_lobbyDirty)
{
_lobbyDirty = false;
LobbyDisplayUI.RefreshAll(lobbyState);
}
if (ClientGameScreen == null) return;
switch (lobbyState.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:
SetCanvases(false, false, false, true);
break;
}
TickToast();
}
// ── Game HUD tick ─────────────────────────────────────────────────────
private void UpdateGameHUD()
{
var s = _state;
if (s == null) return;
// Role
if (_roleText != null)
{
string ghostSuffix = s.IsDead ? " (GHOST)" : "";
_roleText.text = $"{s.MyRole?.ToString() ?? "?"}{ghostSuffix}";
_roleText.color = s.MyRole == PlayerRole.Impostor ? new Color(0.9f,0.2f,0.2f) : new Color(0.2f,0.8f,1f);
}
// Task list with checkmarks
if (_taskListText != null)
{
var sb = new System.Text.StringBuilder();
foreach (var task in s.MyTasks)
{
bool done = s.MyCompletedTaskIds.Contains(task.TaskId);
string mark = done ? "<color=#2DB84B>✓</color>" : "○";
sb.AppendLine($"{mark} {task.Name}");
}
_taskListText.text = sb.ToString();
}
// Global task progress
if (_taskProgressText != null && s.TotalRequired > 0)
_taskProgressText.text = $"Tasks: {s.TotalCompleted}/{s.TotalRequired}";
// Kill cooldown
if (_killCooldownText != null)
{
bool show = s.KillCooldownRemaining > 0;
_killCooldownText.gameObject.SetActive(show);
if (show) _killCooldownText.text = $"Kill: {Mathf.CeilToInt(s.KillCooldownRemaining)}s";
}
// Sabotage banner
if (_sabotageTimerActive && _sabotageTimerText != null)
{
double remaining = (_sabotageMeltdownDeadline - DateTime.UtcNow).TotalSeconds;
_sabotageTimerText.text = remaining > 0 ? $"⚠ MELTDOWN: {remaining:F0}s" : "⚠ MELTDOWN!";
}
// Keep meeting voted-indicator rows fresh each frame
TickMeetingVoteIndicators();
}
// ── Kill cooldown helper (called from GameManager) ────────────────────
public void SetKillCooldownText(string text)
{
if (_killCooldownText == null) return;
bool show = !string.IsNullOrEmpty(text);
_killCooldownText.gameObject.SetActive(show);
if (show) _killCooldownText.text = text;
}
public void UpdateTaskProgress(int completed, int total)
{
if (_taskProgressText != null)
_taskProgressText.text = $"Tasks: {completed}/{total}";
}
// ── Action button ─────────────────────────────────────────────────────
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);
}
}
// ── Player state ──────────────────────────────────────────────────────
public void OnLocalPlayerDied()
{
_isDead = true;
if (_state != null) _state.IsDead = true;
}
// ── Meeting ───────────────────────────────────────────────────────────
public void ShowMeetingAlert()
{
ShowToast("⚠ Meeting called! Head to the meeting point.");
}
public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
{
if (_meetingPanel == null) return;
_meetingPanel.SetActive(true);
if (_meetingHeader != null)
_meetingHeader.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";
if (_voteResultPanel) _voteResultPanel.SetActive(false);
BuildMeetingVoteRows(players);
}
private void BuildMeetingVoteRows(List<PlayerInfo> players)
{
// Clear old rows
foreach (var r in _voteRows) if (r) UnityEngine.Object.Destroy(r);
_voteRows.Clear();
if (_meetingScrollContent == null || players == null)
{
// Fall back to text list
if (_meetingFallbackText != null)
{
_meetingFallbackText.gameObject.SetActive(true);
var sb = new System.Text.StringBuilder();
foreach (var p in players ?? new List<PlayerInfo>())
sb.AppendLine($"{p.DisplayName} [{p.State}]");
_meetingFallbackText.text = sb.ToString();
}
return;
}
string myId = _gameClient.ClientUuid;
bool canVote = !_isDead;
foreach (var player in players)
{
bool isMe = player.ClientUuid == myId;
bool isAlive = player.State == PlayerState.Alive;
var row = BuildVoteRow(player, isMe, isAlive, canVote && isAlive && !isMe);
row.transform.SetParent(_meetingScrollContent, false);
_voteRows.Add(row);
}
}
private GameObject BuildVoteRow(PlayerInfo player, bool isMe, bool isAlive, bool canVote)
{
const float ROW_H = 110f;
var go = new GameObject($"VoteRow_{player.ClientUuid}");
var rt = go.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(0, ROW_H);
var le = go.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = ROW_H;
var bg = go.AddComponent<Image>();
bg.color = isMe ? new Color(0.12f,0.18f,0.30f) : new Color(0.10f,0.12f,0.20f);
// Dead overlay
if (!isAlive)
{
bg.color = new Color(0.08f,0.08f,0.10f,0.7f);
}
// Name label
var namRT = MakeChild("Name", rt);
namRT.anchorMin = new Vector2(0,0); namRT.anchorMax = new Vector2(0.65f,1);
namRT.offsetMin = new Vector2(16,6); namRT.offsetMax = new Vector2(0,-6);
var namTmp = namRT.gameObject.AddComponent<TextMeshProUGUI>();
namTmp.text = (player.IsOwner ? "👑 " : "") + (player.DisplayName ?? "???");
namTmp.fontSize = 36;
namTmp.color = !isAlive ? Color.gray : (isMe ? Color.white : new Color(0.73f,0.8f,0.88f));
namTmp.fontStyle = isMe ? FontStyles.Bold : FontStyles.Normal;
namTmp.alignment = TextAlignmentOptions.MidlineLeft;
// Vote button
var voteBtnRT = MakeChild("VoteBtn", rt);
voteBtnRT.anchorMin = new Vector2(0.68f,0.10f); voteBtnRT.anchorMax = new Vector2(0.95f,0.90f);
var voteBg = voteBtnRT.gameObject.AddComponent<Image>();
voteBg.color = canVote ? new Color(0.2f,0.6f,1f) : new Color(0.2f,0.2f,0.2f,0.5f);
var voteBtn = voteBtnRT.gameObject.AddComponent<Button>();
voteBtn.targetGraphic = voteBg;
voteBtn.interactable = canVote;
string capturedId = player.ClientUuid;
voteBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(capturedId));
var voteTxtRT = MakeChild("Txt", voteBtnRT);
Stretch(voteTxtRT);
var voteTmp = voteTxtRT.gameObject.AddComponent<TextMeshProUGUI>();
voteTmp.text = isAlive ? "VOTE" : "DEAD";
voteTmp.fontSize = 28;
voteTmp.fontStyle = FontStyles.Bold;
voteTmp.color = Color.white;
voteTmp.alignment = TextAlignmentOptions.Center;
// Voted indicator (hidden by default; shown by TickMeetingVoteIndicators)
var votedRT = MakeChild("VotedTick", rt);
votedRT.anchorMin = new Vector2(0.95f,0.20f); votedRT.anchorMax = new Vector2(1f,0.80f);
var vtTmp = votedRT.gameObject.AddComponent<TextMeshProUGUI>();
vtTmp.text = "✓"; vtTmp.fontSize = 34;
vtTmp.color = new Color(0.18f,0.75f,0.30f); vtTmp.alignment = TextAlignmentOptions.Center;
votedRT.gameObject.SetActive(false);
return go;
}
private void TickMeetingVoteIndicators()
{
var s = _state;
if (s == null) return;
foreach (var row in _voteRows)
{
if (row == null) continue;
// Row name is "VoteRow_<uuid>"
string uuid = row.name.Replace("VoteRow_", "");
var tick = row.transform.Find("VotedTick")?.gameObject;
if (tick != null)
tick.SetActive(s.VotedPlayerIds.Contains(uuid));
}
}
public void AppendVoteInstruction()
{
// no-op vote instructions are embedded in the row buttons
}
public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
{
if (_voteResultPanel != null) _voteResultPanel.SetActive(true);
if (_voteResultText != null)
{
if (payload.WasTie)
_voteResultText.text = "⚖ TIE — nobody ejected.";
else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
_voteResultText.text = "Nobody ejected (skip).";
else
{
var ej = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
_voteResultText.text = $"🚪 {ej?.DisplayName ?? payload.EjectedPlayerId} ejected!";
}
}
// Auto-close meeting panel after 5 s
GameManager.Instance?.StartCoroutine(CloseMeetingAfterDelay(5f));
}
private System.Collections.IEnumerator CloseMeetingAfterDelay(float delay)
{
yield return new UnityEngine.WaitForSeconds(delay);
if (_meetingPanel) _meetingPanel.SetActive(false);
if (_voteResultPanel) _voteResultPanel.SetActive(false);
}
// ── Sabotage ──────────────────────────────────────────────────────────
public void ShowSabotageTimer(DateTime deadline)
{
_sabotageMeltdownDeadline = deadline;
_sabotageTimerActive = true;
if (_sabotagePanel) _sabotagePanel.SetActive(true);
if (_sabotageTimerText) _sabotageTimerText.gameObject.SetActive(true);
}
public void HideSabotageTimer()
{
_sabotageTimerActive = false;
if (_sabotagePanel) _sabotagePanel.SetActive(false);
SetCommsBlackout(false);
}
public void SetCommsBlackout(bool active) => _commsBlackout = active;
// ── Game end ──────────────────────────────────────────────────────────
public void ShowGameEndPanel(GameEndedPayload payload, string myUuid)
{
if (_gameEndPanel) _gameEndPanel.SetActive(true);
if (_gameEndText != null)
{
bool won = payload.Winners?.Contains(myUuid) ?? false;
string title = won ? "<color=#FFB800>🏆 VICTORY</color>" : "<color=#C43232>💔 DEFEAT</color>";
string faction = payload.WinningFaction == "Impostor" ? "Impostors win!" : "Crew wins!";
_gameEndText.text = $"{title}\n{faction}\n<size=38>{payload.Reason}</size>";
}
// Show "Return to Lobby" only for the host
if (_returnToLobbyBtn != null)
_returnToLobbyBtn.gameObject.SetActive(_gameClient.IsOwner);
}
// ── Toast ─────────────────────────────────────────────────────────────
public void ShowToast(string message)
{
if (_state != null) { _state.ToastMessage = message; _state.ToastExpiry = UnityEngine.Time.time + 4f; }
if (_toastGO == null) return;
_toastGO.SetActive(true);
if (_toastText != null) _toastText.text = message;
}
private void TickToast()
{
var s = _state;
if (_toastGO == null) return;
if (s != null && !string.IsNullOrEmpty(s.ToastMessage) && UnityEngine.Time.time < s.ToastExpiry)
{
_toastGO.SetActive(true);
if (_toastText != null) _toastText.text = s.ToastMessage;
}
else
{
_toastGO.SetActive(false);
}
}
// ── Canvas switching ──────────────────────────────────────────────────
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);
}
// ── Utilities ─────────────────────────────────────────────────────────
private static void EnsureCanvasReady(Canvas canvas)
{
if (canvas == null) return;
if (!canvas.enabled) canvas.enabled = true;
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;
}
private static Transform FindTransform(Transform root, string name)
{
if (root == null) return null;
foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
if (t.name == name) return t;
return null;
}
private static GameObject FindTransformGO(Transform root, string name)
=> FindTransform(root, name)?.gameObject;
private static RectTransform MakeChild(string name, RectTransform parent)
{
var go = new GameObject(name);
var rt = go.AddComponent<RectTransform>();
rt.SetParent(parent, false);
rt.localScale = Vector3.one;
return rt;
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
}
}
}