From e0b808faed842b95123729f877549ba6afaa058c Mon Sep 17 00:00:00 2001 From: Jan Racek Date: Sun, 26 Apr 2026 14:58:39 +0200 Subject: [PATCH] Zabiju je 2. Epicky thriller od tvurce Zabiju je --- .../arm64-v8a/configure_fingerprint.bin | 26 +- Assets/GameManager/GameManager.cs | 7 +- Assets/GameManager/GameManager_Map.cs | 8 +- Assets/GameManager/GameManager_Network.cs | 174 +++++-- Assets/GameManager/GameManager_Tasks.cs | 4 +- Assets/GameManager/GameManager_UI.cs | 472 ++++++++++++------ Assets/Scripts/GameState.cs | 42 ++ Assets/Scripts/GameState.cs.meta | 2 + Assets/Scripts/HostLobbyUI.cs | 412 +++++++++++++-- Assets/Scripts/InGameHUDBuilder.cs | 432 ++++++++-------- 10 files changed, 1114 insertions(+), 465 deletions(-) create mode 100644 Assets/Scripts/GameState.cs create mode 100644 Assets/Scripts/GameState.cs.meta diff --git a/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/configure_fingerprint.bin b/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/configure_fingerprint.bin index f660e87..724898a 100644 --- a/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/configure_fingerprint.bin +++ b/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/configure_fingerprint.bin @@ -2,29 +2,29 @@ C/C++ Structured Logl j h/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/additional_project_files.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ΆηίΛά3Ε ½ΏΞΗά3i +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ηΗΛΞά3Ε ½ΏΞΗά3i g -e/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/android_gradle_build.json  ΆηίΛά3Ώ ΟΏΞΗά3n +e/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/android_gradle_build.json  ηΗΛΞά3Ώ ΟΏΞΗά3n l -j/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/android_gradle_build_mini.json  ΆηίΛά3σ ”ΐΞΗά3[ +j/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/android_gradle_build_mini.json  ηΗΛΞά3σ ”ΐΞΗά3[ Y -W/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build.ninja  ΆηίΛά3Š€ ΏΎΞΗά3_ +W/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build.ninja  ηΗΛΞά3Š€ ΏΎΞΗά3_ ] -[/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build.ninja.txt  ΆηίΛά3d +[/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build.ninja.txt  ηΗΛΞά3d b -`/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build_file_index.txt  ΆηίΛά3ς ͺΐΞΗά3e +`/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/build_file_index.txt  ηΗΛΞά3ς ͺΐΞΗά3e c -a/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/compile_commands.json  ΆηίΛά3ΒR ΏΎΞΗά3i +a/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/compile_commands.json  ηΗΛΞά3ΒR ΏΎΞΗά3i g -e/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/compile_commands.json.bin  ΆηίΛά3 Ž ΏΎΞΗά3o +e/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/compile_commands.json.bin  θΗΛΞά3 Ž ΏΎΞΗά3o m -k/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/metadata_generation_command.txt  ΆηίΛά3 +k/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/metadata_generation_command.txt  θΗΛΞά3 Α ©ΐΞΗά3b ` -^/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/prefab_config.json  ΆηίΛά3 Θ ͺΐΞΗά3g +^/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/prefab_config.json  θΗΛΞά3 Θ ͺΐΞΗά3g e -c/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/symbol_folder_index.txt  ΆηίΛά3 ” ͺΐΞΗά3v +c/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/symbol_folder_index.txt  θΗΛΞά3 ” ͺΐΞΗά3v t -r/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/CMakeLists.txt  ΆηίΛά3  T ±ΖΏΗά3„ +r/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/CMakeLists.txt  θΗΛΞά3  T ±ΖΏΗά3„  -/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/GameActivity/CMakeLists.txt  ΆηίΛά3ϋ °ΖΏΗά3 \ No newline at end of file +/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/GameActivity/CMakeLists.txt  θΗΛΞά3ϋ °ΖΏΗά3 \ No newline at end of file diff --git a/Assets/GameManager/GameManager.cs b/Assets/GameManager/GameManager.cs index 00d9637..2479887 100644 --- a/Assets/GameManager/GameManager.cs +++ b/Assets/GameManager/GameManager.cs @@ -197,7 +197,6 @@ public class GameManager : MonoBehaviour } } - // ── Kill cooldown ───────────────────────────────────────────────────────── private float _killCooldownSeconds = 0f; private const float KillCooldownDuration = 20f; @@ -206,10 +205,16 @@ public class GameManager : MonoBehaviour 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(""); } } diff --git a/Assets/GameManager/GameManager_Map.cs b/Assets/GameManager/GameManager_Map.cs index 12a4aa2..40a36e1 100644 --- a/Assets/GameManager/GameManager_Map.cs +++ b/Assets/GameManager/GameManager_Map.cs @@ -411,10 +411,14 @@ namespace Subsystems{ var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); go.name = $"Task_{task.TaskId}"; go.transform.parent = _mapCenterPoint.transform; - go.transform.position = task.Location.ToLocalVector3(_centerPosition) + Vector3.up * 0.3f; - go.transform.localScale = Vector3.one * 0.5f; + go.transform.position = task.Location.ToLocalVector3(_centerPosition) + Vector3.up * 1f; // Raised + go.transform.localScale = Vector3.one * 8f; // Bigger var mr = go.GetComponent(); if (mr) mr.material.color = Color.yellow; + var light = go.AddComponent(); + light.color = Color.yellow; + light.intensity = 2; + light.range = 5; _taskMarkers[task.TaskId] = go; } } diff --git a/Assets/GameManager/GameManager_Network.cs b/Assets/GameManager/GameManager_Network.cs index 6eebf9b..cbf27a2 100644 --- a/Assets/GameManager/GameManager_Network.cs +++ b/Assets/GameManager/GameManager_Network.cs @@ -14,9 +14,14 @@ namespace Subsystems private const string _serverAddress = "geosus.honzuvkod.dev"; private const int _serverPort = 7777; private GameClient _gameClient; - private GameManager _manager; // may be null for test clients + private GameManager _manager; private bool _pendingMapBuild; + /// + /// Authoritative game state; written here, read by GameManager_UI. + /// + public GameState State { get; } = new GameState(); + public GameManager_Network(GameClient gameClient, GameManager manager) { _gameClient = gameClient; @@ -45,49 +50,40 @@ namespace Subsystems } Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s..."); await Task.Delay(delayMs); - delayMs = Mathf.Min(delayMs * 2, 30000); // exponential backoff, cap 30s + delayMs = Mathf.Min(delayMs * 2, 30000); } } public void RegisterEventHandlers() { - _gameClient.OnConnected += OnConnected; + _gameClient.OnConnected += OnConnected; _gameClient.OnDisconnected += OnDisconnected; - _gameClient.OnError += OnError; - _gameClient.OnMessage += OnMessage; - _gameClient.OnGameEvent += OnGameEvent; + _gameClient.OnError += OnError; + _gameClient.OnMessage += OnMessage; + _gameClient.OnGameEvent += OnGameEvent; } - private void OnConnected() - { - Debug.Log("Successfully connected to the server."); - } + private void OnConnected() => Debug.Log("Successfully connected to the server."); + private void OnError(string e) => Debug.LogError($"Network error: {e}"); + private void OnDisconnected(string reason) { Debug.Log($"Disconnected: {reason}"); - // Auto-reconnect unless the app is quitting if (reason != "Disposed" && _manager != null) _manager.StartCoroutine(ReconnectAfterDelay(3f)); } - private System.Collections.IEnumerator ReconnectAfterDelay(float seconds) + private IEnumerator ReconnectAfterDelay(float seconds) { yield return new UnityEngine.WaitForSeconds(seconds); Debug.Log("Attempting to reconnect..."); OpenConnection(); } - private void OnError(string error) - { - Debug.LogError($"Network error: {error}"); - } private void OnMessage(Message message) { switch (message.Type) { - case "GameEvent": - // handled via OnGameEvent - break; case "CreateLobbyResponse": HandleCreateLobbyResponse(message as CreateLobbyResponse); break; @@ -98,6 +94,7 @@ namespace Subsystems HandlePositionBroadcast(message as PositionBroadcast); break; case "Ack": + case "GameEvent": break; default: Debug.Log("Received message of type: " + message.Type); @@ -107,16 +104,19 @@ namespace Subsystems private void OnGameEvent(GameEvent gameEvent) { + // Always sync player list from lobby state after any event + SyncPlayersFromLobby(); + switch (gameEvent.EventType) { case "PlayerJoined": case "PlayerLeft": case "HostChanged": - // SDK already updates CurrentLobbyState; just refresh UI _manager?.uiSubsystem?.NotifyLobbyChanged(); break; case "GameStarting": + State.Phase = GamePhase.Loading; HandleGameStarting(); break; @@ -125,7 +125,7 @@ namespace Subsystems break; case "GameStarted": - HandleGameStarted(); + State.Phase = GamePhase.Playing; break; case "RoleAssigned": @@ -142,13 +142,17 @@ namespace Subsystems case "BodyReported": case "EmergencyMeetingCalled": - HandleMeetingCalled(gameEvent); + Toast("Meeting called! Head to the meeting point."); break; case "MeetingStarted": HandleMeetingStarted(gameEvent); break; + case "PlayerVoted": + HandlePlayerVoted(gameEvent); + break; + case "VotingClosed": HandleVotingClosed(gameEvent); break; @@ -167,6 +171,7 @@ namespace Subsystems case "SabotageRepaired": case "SabotageMeltdown": + State.ActiveSabotage = null; _manager?.uiSubsystem?.HideSabotageTimer(); _manager?.mapSubsystem?.ClearSabotageMarkers(); break; @@ -188,10 +193,8 @@ namespace Subsystems if (message == null) return; if (message.Success) { - Debug.Log($"Lobby created. Code: {message.JoinCode}, ID: {message.LobbyId}"); - // Navigate to the create/waiting scene + Debug.Log($"Lobby created. Code: {message.JoinCode}"); SceneManager.LoadScene("create", LoadSceneMode.Single); - // Mark lobby UI dirty so LobbyDisplayUI refreshes once the scene is loaded _manager?.uiSubsystem?.NotifyLobbyChanged(); } else @@ -207,7 +210,6 @@ namespace Subsystems { Debug.Log($"Joined lobby: {message.LobbyId}"); SceneManager.LoadScene("join loading", LoadSceneMode.Single); - // Mark lobby UI dirty so LobbyDisplayUI refreshes once the scene is loaded _manager?.uiSubsystem?.NotifyLobbyChanged(); } else @@ -216,12 +218,24 @@ namespace Subsystems } } - // ── Game flow events ────────────────────────────────────────────────── + // ── Game flow ───────────────────────────────────────────────────────── private void HandleGameStarting() { _pendingMapBuild = false; - // SDK sets Phase = Loading; load Client.unity + // Reset per-game state + State.MyRole = null; + State.IsDead = false; + State.MyTasks = new List(); + State.MyCompletedTaskIds = new HashSet(); + State.TotalCompleted = 0; + State.TotalRequired = 0; + State.ActiveMeeting = null; + State.LastVoteResult = null; + State.VotedPlayerIds = new HashSet(); + State.ActiveSabotage = null; + State.GameEndData = null; + State.KillCooldownRemaining = 0; SceneManager.LoadScene("Client", LoadSceneMode.Single); } @@ -231,10 +245,6 @@ namespace Subsystems TryBuildMapAndMarkers(); } - /// - /// Called from GameManager.OnSceneLoaded("Client") after scene objects are bound. - /// Ensures map construction still happens even if MapDataReady arrived earlier. - /// public void OnClientSceneReady() { TryBuildMapAndMarkers(); @@ -243,34 +253,40 @@ namespace Subsystems private void TryBuildMapAndMarkers() { if (!_pendingMapBuild) return; - if (_manager?.mapSubsystem == null) return; - if (!_manager.mapSubsystem.IsSceneReady) return; + if (_manager?.mapSubsystem == null || !_manager.mapSubsystem.IsSceneReady) return; if (_gameClient?.CurrentLobbyState?.MapData == null) return; _manager.mapSubsystem.BuildMap(); _manager.mapSubsystem.CreateTaskMarkers(_gameClient.MyTasks); _pendingMapBuild = false; - Debug.Log("[Network] Map built and task markers refreshed."); - } - - private void HandleGameStarted() - { - Debug.Log("Game started"); - // Phase is now Playing; GPS loop will start sending positions + Debug.Log("[Network] Map built."); } private void HandleRoleAssigned(GameEvent evt) { var payload = evt.GetPayload(); if (payload == null || payload.ClientUuid != _gameClient.ClientUuid) return; - Debug.Log($"Role: {payload.Role}, Tasks: {payload.Tasks?.Count ?? 0}"); - _manager?.taskSubsystem?.Initialize(_gameClient.MyTasks); + + State.MyRole = payload.Role; + State.MyTasks = payload.Tasks ?? new List(); + State.MyCompletedTaskIds.Clear(); + + Debug.Log($"Role: {payload.Role}, Tasks: {State.MyTasks.Count}"); + _manager?.taskSubsystem?.Initialize(State.MyTasks); } private void HandleTaskCompleted(GameEvent evt) { var payload = evt.GetPayload(); if (payload == null) return; + + // Track if it's our task + if (payload.ClientUuid == _gameClient.ClientUuid) + State.MyCompletedTaskIds.Add(payload.TaskId); + + State.TotalCompleted = payload.TotalCompleted; + State.TotalRequired = payload.TotalTasks; + _manager?.uiSubsystem?.UpdateTaskProgress(payload.TotalCompleted, payload.TotalTasks); _manager?.mapSubsystem?.RemoveTaskMarker(payload.TaskId); } @@ -279,28 +295,58 @@ namespace Subsystems { var payload = evt.GetPayload(); if (payload == null) return; - _manager?.mapSubsystem?.CreateBodyMarker(payload.BodyId, payload.Location); - if (payload.VictimId == _gameClient.ClientUuid) - _manager?.uiSubsystem?.OnLocalPlayerDied(); - } - private void HandleMeetingCalled(GameEvent evt) - { - _manager?.uiSubsystem?.ShowMeetingAlert(); + _manager?.mapSubsystem?.CreateBodyMarker(payload.BodyId, payload.Location); + + if (payload.VictimId == _gameClient.ClientUuid) + { + State.IsDead = true; + _manager?.uiSubsystem?.OnLocalPlayerDied(); + } + + // Update player state in our list + var p = State.Players.Find(x => x.ClientUuid == payload.VictimId); + if (p != null) p.State = PlayerState.Dead; } private void HandleMeetingStarted(GameEvent evt) { var payload = evt.GetPayload(); if (payload == null) return; - _manager?.uiSubsystem?.ShowMeetingPanel(_gameClient.CurrentLobbyState?.Players, payload); + + State.Phase = GamePhase.Meeting; + State.ActiveMeeting = payload; + State.VotedPlayerIds = new HashSet(); + State.LastVoteResult = null; + + SyncPlayersFromLobby(); + _manager?.uiSubsystem?.ShowMeetingPanel(State.Players, payload); + } + + private void HandlePlayerVoted(GameEvent evt) + { + var payload = evt.GetPayload(); + if (payload == null) return; + State.VotedPlayerIds.Add(payload.VoterId); } private void HandleVotingClosed(GameEvent evt) { var payload = evt.GetPayload(); if (payload == null) return; - _manager?.uiSubsystem?.ShowVoteResult(payload, _gameClient.CurrentLobbyState?.Players); + + State.Phase = GamePhase.Playing; + State.ActiveMeeting = null; + State.LastVoteResult = payload; + + // Mark ejected player dead in our list + if (!string.IsNullOrEmpty(payload.EjectedPlayerId)) + { + var p = State.Players.Find(x => x.ClientUuid == payload.EjectedPlayerId); + if (p != null) p.State = PlayerState.Dead; + } + + _manager?.uiSubsystem?.ShowVoteResult(payload, State.Players); _manager?.mapSubsystem?.ClearBodyMarkers(); } @@ -308,11 +354,16 @@ namespace Subsystems { var payload = evt.GetPayload(); if (payload == null) return; + + State.Phase = GamePhase.Ended; + State.GameEndData = payload; + _manager?.uiSubsystem?.ShowGameEndPanel(payload, _gameClient.ClientUuid); } private void HandleReturnedToLobby() { + State.Phase = GamePhase.Lobby; if (_gameClient.IsOwner) SceneManager.LoadScene("create", LoadSceneMode.Single); else @@ -323,6 +374,9 @@ namespace Subsystems { var payload = evt.GetPayload(); if (payload == null) return; + + State.ActiveSabotage = payload; + _manager?.mapSubsystem?.CreateSabotageMarkers(payload.RepairStations); if (payload.Type == SabotageType.CriticalMeltdown && payload.Deadline.HasValue) _manager?.uiSubsystem?.ShowSabotageTimer(payload.Deadline.Value); @@ -336,6 +390,21 @@ namespace Subsystems _manager?.mapSubsystem?.UpdatePlayerAvatars(_gameClient.PlayerPositions, _gameClient.ClientUuid); } + // ── Helpers ─────────────────────────────────────────────────────────── + + private void SyncPlayersFromLobby() + { + var lobby = _gameClient.CurrentLobbyState; + if (lobby?.Players != null) + State.Players = lobby.Players; + } + + private void Toast(string message) + { + State.ToastMessage = message; + State.ToastExpiry = UnityEngine.Time.time + 4f; + } + // ── Send helpers ────────────────────────────────────────────────────── public void CreateLobby(double lat, double lon, double radius = 500, int impostorCount = 1, int taskCount = 5) @@ -352,6 +421,7 @@ namespace Subsystems public void LeaveLobby() { _gameClient.LeaveLobby(); + State.Phase = GamePhase.Lobby; SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single); } diff --git a/Assets/GameManager/GameManager_Tasks.cs b/Assets/GameManager/GameManager_Tasks.cs index 11725fa..bbab151 100644 --- a/Assets/GameManager/GameManager_Tasks.cs +++ b/Assets/GameManager/GameManager_Tasks.cs @@ -176,12 +176,10 @@ namespace Subsystems taskComponent.TaskLocation = (entry.ServerTask.Location.Lat, entry.ServerTask.Location.Lon); bool done = false; - bool exited = false; taskComponent.Initialize(t => { done = true; }); - taskComponent.ExitTask(t => { exited = true; }); // Wait for completion or exit - yield return new WaitUntil(() => done || exited); + yield return new WaitUntil(() => done); yield return FinishMinigame(entry, done); } diff --git a/Assets/GameManager/GameManager_UI.cs b/Assets/GameManager/GameManager_UI.cs index c3337f3..a28c9fb 100644 --- a/Assets/GameManager/GameManager_UI.cs +++ b/Assets/GameManager/GameManager_UI.cs @@ -9,168 +9,208 @@ using TMPro; namespace Subsystems { /// - /// 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. /// 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 _voteRows = new List(); + private string _pendingVoteResultDisplay; // shown after voting + + public GameManager_UI(GameClient gameClient) { _gameClient = gameClient; } - /// Called by Network subsystem when lobby player list changes. 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