Zabiju je 2. Epicky thriller od tvurce Zabiju je

This commit is contained in:
2026-04-26 14:58:39 +02:00
parent 700e6bfbfc
commit e0b808faed
10 changed files with 1114 additions and 465 deletions

View File

@@ -9,168 +9,208 @@ 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.
/// 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;
// 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)
// ── Canvas refs (wired by BindClientScene from Client.unity) ──────────
public Canvas ClientCreateJoinLobby;
public Canvas ClientInLobby;
public Canvas ClientLoadingScreen;
public Canvas ClientGameScreen; // parent of all HUD elements
public Canvas ClientGameScreen;
// 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;
// ── 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 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 TMP_Text _gameEndText;
private RectTransform _returnToLobbyBtn;
private TMP_Text _toastText;
private GameObject _toastGO;
// Runtime state
// ── Internal 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;
}
// 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; }
/// <summary>Called by Network subsystem when lobby player list changes.</summary>
public void NotifyLobbyChanged() => _lobbyDirty = true;
public bool IsCommsBlackout => _commsBlackout;
public bool IsPlayerDead => _isDead;
// ── Called from GameManager after Client.unity loads ──────────────────
// ── Scene binding ─────────────────────────────────────────────────────
public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
{
ClientCreateJoinLobby = createJoin;
ClientInLobby = inLobby;
ClientLoadingScreen = loading;
ClientGameScreen = game;
ClientInLobby = inLobby;
ClientLoadingScreen = loading;
ClientGameScreen = game;
EnsureCanvasReady(createJoin);
EnsureCanvasReady(inLobby);
EnsureCanvasReady(loading);
EnsureCanvasReady(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 (inLobby) inLobby.gameObject.SetActive(false);
if (loading) loading.gameObject.SetActive(false);
if (game) game.gameObject.SetActive(false);
if (game != null)
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)
{
_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);
_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);
}
// ── Main update (called every frame from GameManager.Update) ──────────
// ── Update (called every frame from GameManager.Update) ───────────────
public void UpdateLobbyUI()
{
var state = _gameClient.CurrentLobbyState;
if (state == null) return;
var lobbyState = _gameClient.CurrentLobbyState;
if (lobbyState == null) return;
// Update any LobbyDisplayUI listeners in the current scene
if (_lobbyDirty)
{
_lobbyDirty = false;
LobbyDisplayUI.RefreshAll(state);
LobbyDisplayUI.RefreshAll(lobbyState);
}
// Only do canvas switches if we are in Client.unity (canvases assigned)
if (ClientGameScreen == null) return;
switch (state.Phase)
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:
// GameEndPanel shown by HandleGameEnded
SetCanvases(false, false, false, true);
break;
}
TickToast();
}
// ── Game HUD tick ─────────────────────────────────────────────────────
private void UpdateGameHUD()
{
if (_roleText != null) _roleText.text = _gameClient.MyRole?.ToString() ?? "";
var s = _state;
if (s == null) return;
// Task list
// 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 t in _gameClient.MyTasks)
sb.AppendLine(t.Name);
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();
}
// Kill cooldown (managed by GameManager_Tasks via Update)
// Sabotage timer
// 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!";
_sabotageTimerText.text = remaining > 0 ? $"MELTDOWN: {remaining:F0}s" : "MELTDOWN!";
}
// Keep meeting voted-indicator rows fresh each frame
TickMeetingVoteIndicators();
}
// ── Helpers called by Network handlers ────────────────────────────────
// ── Kill cooldown helper (called from GameManager) ────────────────────
public void SetKillCooldownText(string text)
{
if (_killCooldownText != null)
{
_killCooldownText.text = text;
_killCooldownText.gameObject.SetActive(!string.IsNullOrEmpty(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)
@@ -179,6 +219,8 @@ namespace Subsystems
_taskProgressText.text = $"Tasks: {completed}/{total}";
}
// ── Action button ─────────────────────────────────────────────────────
public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null)
{
if (_actionButton == null) return;
@@ -191,15 +233,19 @@ namespace Subsystems
}
}
// ── Player state ──────────────────────────────────────────────────────
public void OnLocalPlayerDied()
{
_isDead = true;
if (_roleText != null) _roleText.text = "GHOST";
if (_state != null) _state.IsDead = true;
}
// ── Meeting ───────────────────────────────────────────────────────────
public void ShowMeetingAlert()
{
Debug.Log("Meeting called! Run to meeting point.");
ShowToast("Meeting called! Head to the meeting point.");
}
public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
@@ -207,99 +253,217 @@ namespace Subsystems
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!";
if (_meetingHeader != null)
_meetingHeader.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)
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)
{
var sb = new System.Text.StringBuilder();
foreach (var p in players)
sb.AppendLine($"{p.DisplayName} [{p.State}]");
playerList.text = sb.ToString();
// 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;
}
// Wire skip button if it exists
var skipBtn = _meetingPanel.transform.Find("SkipButton")?.GetComponent<Button>();
if (skipBtn != null)
string myId = _gameClient.ClientUuid;
bool canVote = !_isDead;
foreach (var player in players)
{
skipBtn.onClick.RemoveAllListeners();
skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null));
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()
{
var playerList = FindTMP(_meetingPanel?.transform, "MeetingPlayerList");
if (playerList != null)
playerList.text += "\n[Tap a name] then press VOTE — or press SKIP";
// no-op vote instructions are embedded in the row buttons
}
public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
{
if (_meetingPanel == null) return;
var resultText = FindTMP(_meetingPanel.transform, "VoteResult");
if (resultText != null)
if (_voteResultPanel != null) _voteResultPanel.SetActive(true);
if (_voteResultText != null)
{
if (payload.WasTie)
{
resultText.text = "TIE — nobody ejected.";
}
_voteResultText.text = "⚖ TIE — nobody ejected.";
else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
{
resultText.text = "Skip — nobody ejected.";
}
_voteResultText.text = "Nobody ejected (skip).";
else
{
var ejected = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
resultText.text = $"{ejected?.DisplayName ?? payload.EjectedPlayerId} ejected!";
var ej = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
_voteResultText.text = $"🚪 {ej?.DisplayName ?? payload.EjectedPlayerId} ejected!";
}
}
// Close panel after 5 seconds — use coroutine via GameManager
GameManager.Instance?.StartCoroutine(CloseMeetingPanelAfterDelay(5f));
// Auto-close meeting panel after 5 s
GameManager.Instance?.StartCoroutine(CloseMeetingAfterDelay(5f));
}
private System.Collections.IEnumerator CloseMeetingPanelAfterDelay(float delay)
private System.Collections.IEnumerator CloseMeetingAfterDelay(float delay)
{
yield return new UnityEngine.WaitForSeconds(delay);
if (_meetingPanel != null) _meetingPanel.SetActive(false);
if (_meetingPanel) _meetingPanel.SetActive(false);
if (_voteResultPanel) _voteResultPanel.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}";
}
// ── Sabotage ──────────────────────────────────────────────────────────
public void ShowSabotageTimer(DateTime deadline)
{
_sabotageMeltdownDeadline = deadline;
_sabotageTimerActive = true;
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(true);
_sabotageTimerActive = true;
if (_sabotagePanel) _sabotagePanel.SetActive(true);
if (_sabotageTimerText) _sabotageTimerText.gameObject.SetActive(true);
}
public void HideSabotageTimer()
{
_sabotageTimerActive = false;
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(false);
if (_sabotagePanel) _sabotagePanel.SetActive(false);
SetCommsBlackout(false);
}
public void SetCommsBlackout(bool active)
public void SetCommsBlackout(bool active) => _commsBlackout = active;
// ── Game end ──────────────────────────────────────────────────────────
public void ShowGameEndPanel(GameEndedPayload payload, string myUuid)
{
_commsBlackout = active;
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);
}
public bool IsCommsBlackout => _commsBlackout;
public bool IsPlayerDead => _isDead;
// ── Toast ─────────────────────────────────────────────────────────────
// ── Utilities ─────────────────────────────────────────────────────────
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)
{
@@ -314,14 +478,12 @@ namespace Subsystems
if (ClientGameScreen) ClientGameScreen.gameObject.SetActive(game);
}
// ── Utilities ─────────────────────────────────────────────────────────
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.
if (!canvas.enabled) canvas.enabled = true;
var t = canvas.transform;
if (t != null)
{
@@ -335,12 +497,34 @@ namespace Subsystems
{
if (root == null) return null;
foreach (var tmp in root.GetComponentsInChildren<TMP_Text>(true))
{
if (tmp != null && tmp.name == name)
return tmp;
}
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;
}
}
}