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

@@ -2,29 +2,29 @@ C/C++ Structured Logl
j j
h/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/additional_project_files.txtC h/home/jracek/code/GeoSus/GeoSusGame/.utmp/RelWithDebInfo/3x4o6w3z/arm64-v8a/additional_project_files.txtC
A A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ¶çßËÜ3Å ½¿ÎÇÜ3i ?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  çÇËÎÜ3Å ½¿ÎÇÜ3i
g 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 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 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 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 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 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 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 Á ©ÀÎÇÜ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 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 t
r/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/CMakeLists.txt  ¶çßËÜ3 r/home/jracek/code/GeoSus/GeoSusGame/Library/Bee/Android/Prj/IL2CPP/Gradle/unityLibrary/src/main/cpp/CMakeLists.txt  èÇËÎÜ3
 
T ±Æ¿ÇÜ3„ T ±Æ¿ÇÜ3„

View File

@@ -197,7 +197,6 @@ public class GameManager : MonoBehaviour
} }
} }
// ── Kill cooldown ─────────────────────────────────────────────────────────
private float _killCooldownSeconds = 0f; private float _killCooldownSeconds = 0f;
private const float KillCooldownDuration = 20f; private const float KillCooldownDuration = 20f;
@@ -206,10 +205,16 @@ public class GameManager : MonoBehaviour
if (_killCooldownSeconds > 0) if (_killCooldownSeconds > 0)
{ {
_killCooldownSeconds -= Time.deltaTime; _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"); uiSubsystem?.SetKillCooldownText($"Kill: {Mathf.CeilToInt(_killCooldownSeconds)}s");
} }
else else
{ {
_killCooldownSeconds = 0f;
if (networkSubsystem?.State != null)
networkSubsystem.State.KillCooldownRemaining = 0;
uiSubsystem?.SetKillCooldownText(""); uiSubsystem?.SetKillCooldownText("");
} }
} }

View File

@@ -411,10 +411,14 @@ namespace Subsystems{
var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); var go = GameObject.CreatePrimitive(PrimitiveType.Sphere);
go.name = $"Task_{task.TaskId}"; go.name = $"Task_{task.TaskId}";
go.transform.parent = _mapCenterPoint.transform; go.transform.parent = _mapCenterPoint.transform;
go.transform.position = task.Location.ToLocalVector3(_centerPosition) + Vector3.up * 0.3f; go.transform.position = task.Location.ToLocalVector3(_centerPosition) + Vector3.up * 1f; // Raised
go.transform.localScale = Vector3.one * 0.5f; go.transform.localScale = Vector3.one * 8f; // Bigger
var mr = go.GetComponent<MeshRenderer>(); var mr = go.GetComponent<MeshRenderer>();
if (mr) mr.material.color = Color.yellow; if (mr) mr.material.color = Color.yellow;
var light = go.AddComponent<Light>();
light.color = Color.yellow;
light.intensity = 2;
light.range = 5;
_taskMarkers[task.TaskId] = go; _taskMarkers[task.TaskId] = go;
} }
} }

View File

@@ -14,9 +14,14 @@ namespace Subsystems
private const string _serverAddress = "geosus.honzuvkod.dev"; private const string _serverAddress = "geosus.honzuvkod.dev";
private const int _serverPort = 7777; private const int _serverPort = 7777;
private GameClient _gameClient; private GameClient _gameClient;
private GameManager _manager; // may be null for test clients private GameManager _manager;
private bool _pendingMapBuild; private bool _pendingMapBuild;
/// <summary>
/// Authoritative game state; written here, read by GameManager_UI.
/// </summary>
public GameState State { get; } = new GameState();
public GameManager_Network(GameClient gameClient, GameManager manager) public GameManager_Network(GameClient gameClient, GameManager manager)
{ {
_gameClient = gameClient; _gameClient = gameClient;
@@ -45,49 +50,40 @@ namespace Subsystems
} }
Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s..."); Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s...");
await Task.Delay(delayMs); await Task.Delay(delayMs);
delayMs = Mathf.Min(delayMs * 2, 30000); // exponential backoff, cap 30s delayMs = Mathf.Min(delayMs * 2, 30000);
} }
} }
public void RegisterEventHandlers() public void RegisterEventHandlers()
{ {
_gameClient.OnConnected += OnConnected; _gameClient.OnConnected += OnConnected;
_gameClient.OnDisconnected += OnDisconnected; _gameClient.OnDisconnected += OnDisconnected;
_gameClient.OnError += OnError; _gameClient.OnError += OnError;
_gameClient.OnMessage += OnMessage; _gameClient.OnMessage += OnMessage;
_gameClient.OnGameEvent += OnGameEvent; _gameClient.OnGameEvent += OnGameEvent;
} }
private void OnConnected() private void OnConnected() => Debug.Log("Successfully connected to the server.");
{ private void OnError(string e) => Debug.LogError($"Network error: {e}");
Debug.Log("Successfully connected to the server.");
}
private void OnDisconnected(string reason) private void OnDisconnected(string reason)
{ {
Debug.Log($"Disconnected: {reason}"); Debug.Log($"Disconnected: {reason}");
// Auto-reconnect unless the app is quitting
if (reason != "Disposed" && _manager != null) if (reason != "Disposed" && _manager != null)
_manager.StartCoroutine(ReconnectAfterDelay(3f)); _manager.StartCoroutine(ReconnectAfterDelay(3f));
} }
private System.Collections.IEnumerator ReconnectAfterDelay(float seconds) private IEnumerator ReconnectAfterDelay(float seconds)
{ {
yield return new UnityEngine.WaitForSeconds(seconds); yield return new UnityEngine.WaitForSeconds(seconds);
Debug.Log("Attempting to reconnect..."); Debug.Log("Attempting to reconnect...");
OpenConnection(); OpenConnection();
} }
private void OnError(string error)
{
Debug.LogError($"Network error: {error}");
}
private void OnMessage(Message message) private void OnMessage(Message message)
{ {
switch (message.Type) switch (message.Type)
{ {
case "GameEvent":
// handled via OnGameEvent
break;
case "CreateLobbyResponse": case "CreateLobbyResponse":
HandleCreateLobbyResponse(message as CreateLobbyResponse); HandleCreateLobbyResponse(message as CreateLobbyResponse);
break; break;
@@ -98,6 +94,7 @@ namespace Subsystems
HandlePositionBroadcast(message as PositionBroadcast); HandlePositionBroadcast(message as PositionBroadcast);
break; break;
case "Ack": case "Ack":
case "GameEvent":
break; break;
default: default:
Debug.Log("Received message of type: " + message.Type); Debug.Log("Received message of type: " + message.Type);
@@ -107,16 +104,19 @@ namespace Subsystems
private void OnGameEvent(GameEvent gameEvent) private void OnGameEvent(GameEvent gameEvent)
{ {
// Always sync player list from lobby state after any event
SyncPlayersFromLobby();
switch (gameEvent.EventType) switch (gameEvent.EventType)
{ {
case "PlayerJoined": case "PlayerJoined":
case "PlayerLeft": case "PlayerLeft":
case "HostChanged": case "HostChanged":
// SDK already updates CurrentLobbyState; just refresh UI
_manager?.uiSubsystem?.NotifyLobbyChanged(); _manager?.uiSubsystem?.NotifyLobbyChanged();
break; break;
case "GameStarting": case "GameStarting":
State.Phase = GamePhase.Loading;
HandleGameStarting(); HandleGameStarting();
break; break;
@@ -125,7 +125,7 @@ namespace Subsystems
break; break;
case "GameStarted": case "GameStarted":
HandleGameStarted(); State.Phase = GamePhase.Playing;
break; break;
case "RoleAssigned": case "RoleAssigned":
@@ -142,13 +142,17 @@ namespace Subsystems
case "BodyReported": case "BodyReported":
case "EmergencyMeetingCalled": case "EmergencyMeetingCalled":
HandleMeetingCalled(gameEvent); Toast("Meeting called! Head to the meeting point.");
break; break;
case "MeetingStarted": case "MeetingStarted":
HandleMeetingStarted(gameEvent); HandleMeetingStarted(gameEvent);
break; break;
case "PlayerVoted":
HandlePlayerVoted(gameEvent);
break;
case "VotingClosed": case "VotingClosed":
HandleVotingClosed(gameEvent); HandleVotingClosed(gameEvent);
break; break;
@@ -167,6 +171,7 @@ namespace Subsystems
case "SabotageRepaired": case "SabotageRepaired":
case "SabotageMeltdown": case "SabotageMeltdown":
State.ActiveSabotage = null;
_manager?.uiSubsystem?.HideSabotageTimer(); _manager?.uiSubsystem?.HideSabotageTimer();
_manager?.mapSubsystem?.ClearSabotageMarkers(); _manager?.mapSubsystem?.ClearSabotageMarkers();
break; break;
@@ -188,10 +193,8 @@ namespace Subsystems
if (message == null) return; if (message == null) return;
if (message.Success) if (message.Success)
{ {
Debug.Log($"Lobby created. Code: {message.JoinCode}, ID: {message.LobbyId}"); Debug.Log($"Lobby created. Code: {message.JoinCode}");
// Navigate to the create/waiting scene
SceneManager.LoadScene("create", LoadSceneMode.Single); SceneManager.LoadScene("create", LoadSceneMode.Single);
// Mark lobby UI dirty so LobbyDisplayUI refreshes once the scene is loaded
_manager?.uiSubsystem?.NotifyLobbyChanged(); _manager?.uiSubsystem?.NotifyLobbyChanged();
} }
else else
@@ -207,7 +210,6 @@ namespace Subsystems
{ {
Debug.Log($"Joined lobby: {message.LobbyId}"); Debug.Log($"Joined lobby: {message.LobbyId}");
SceneManager.LoadScene("join loading", LoadSceneMode.Single); SceneManager.LoadScene("join loading", LoadSceneMode.Single);
// Mark lobby UI dirty so LobbyDisplayUI refreshes once the scene is loaded
_manager?.uiSubsystem?.NotifyLobbyChanged(); _manager?.uiSubsystem?.NotifyLobbyChanged();
} }
else else
@@ -216,12 +218,24 @@ namespace Subsystems
} }
} }
// ── Game flow events ────────────────────────────────────────────────── // ── Game flow ─────────────────────────────────────────────────────────
private void HandleGameStarting() private void HandleGameStarting()
{ {
_pendingMapBuild = false; _pendingMapBuild = false;
// SDK sets Phase = Loading; load Client.unity // Reset per-game state
State.MyRole = null;
State.IsDead = false;
State.MyTasks = new List<GameTask>();
State.MyCompletedTaskIds = new HashSet<string>();
State.TotalCompleted = 0;
State.TotalRequired = 0;
State.ActiveMeeting = null;
State.LastVoteResult = null;
State.VotedPlayerIds = new HashSet<string>();
State.ActiveSabotage = null;
State.GameEndData = null;
State.KillCooldownRemaining = 0;
SceneManager.LoadScene("Client", LoadSceneMode.Single); SceneManager.LoadScene("Client", LoadSceneMode.Single);
} }
@@ -231,10 +245,6 @@ namespace Subsystems
TryBuildMapAndMarkers(); TryBuildMapAndMarkers();
} }
/// <summary>
/// Called from GameManager.OnSceneLoaded("Client") after scene objects are bound.
/// Ensures map construction still happens even if MapDataReady arrived earlier.
/// </summary>
public void OnClientSceneReady() public void OnClientSceneReady()
{ {
TryBuildMapAndMarkers(); TryBuildMapAndMarkers();
@@ -243,34 +253,40 @@ namespace Subsystems
private void TryBuildMapAndMarkers() private void TryBuildMapAndMarkers()
{ {
if (!_pendingMapBuild) return; if (!_pendingMapBuild) return;
if (_manager?.mapSubsystem == null) return; if (_manager?.mapSubsystem == null || !_manager.mapSubsystem.IsSceneReady) return;
if (!_manager.mapSubsystem.IsSceneReady) return;
if (_gameClient?.CurrentLobbyState?.MapData == null) return; if (_gameClient?.CurrentLobbyState?.MapData == null) return;
_manager.mapSubsystem.BuildMap(); _manager.mapSubsystem.BuildMap();
_manager.mapSubsystem.CreateTaskMarkers(_gameClient.MyTasks); _manager.mapSubsystem.CreateTaskMarkers(_gameClient.MyTasks);
_pendingMapBuild = false; _pendingMapBuild = false;
Debug.Log("[Network] Map built and task markers refreshed."); Debug.Log("[Network] Map built.");
}
private void HandleGameStarted()
{
Debug.Log("Game started");
// Phase is now Playing; GPS loop will start sending positions
} }
private void HandleRoleAssigned(GameEvent evt) private void HandleRoleAssigned(GameEvent evt)
{ {
var payload = evt.GetPayload<RoleAssignedPayload>(); var payload = evt.GetPayload<RoleAssignedPayload>();
if (payload == null || payload.ClientUuid != _gameClient.ClientUuid) return; 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<GameTask>();
State.MyCompletedTaskIds.Clear();
Debug.Log($"Role: {payload.Role}, Tasks: {State.MyTasks.Count}");
_manager?.taskSubsystem?.Initialize(State.MyTasks);
} }
private void HandleTaskCompleted(GameEvent evt) private void HandleTaskCompleted(GameEvent evt)
{ {
var payload = evt.GetPayload<TaskCompletedPayload>(); var payload = evt.GetPayload<TaskCompletedPayload>();
if (payload == null) return; 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?.uiSubsystem?.UpdateTaskProgress(payload.TotalCompleted, payload.TotalTasks);
_manager?.mapSubsystem?.RemoveTaskMarker(payload.TaskId); _manager?.mapSubsystem?.RemoveTaskMarker(payload.TaskId);
} }
@@ -279,28 +295,58 @@ namespace Subsystems
{ {
var payload = evt.GetPayload<PlayerKilledPayload>(); var payload = evt.GetPayload<PlayerKilledPayload>();
if (payload == null) return; 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?.mapSubsystem?.CreateBodyMarker(payload.BodyId, payload.Location);
{
_manager?.uiSubsystem?.ShowMeetingAlert(); 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) private void HandleMeetingStarted(GameEvent evt)
{ {
var payload = evt.GetPayload<MeetingStartedPayload>(); var payload = evt.GetPayload<MeetingStartedPayload>();
if (payload == null) return; if (payload == null) return;
_manager?.uiSubsystem?.ShowMeetingPanel(_gameClient.CurrentLobbyState?.Players, payload);
State.Phase = GamePhase.Meeting;
State.ActiveMeeting = payload;
State.VotedPlayerIds = new HashSet<string>();
State.LastVoteResult = null;
SyncPlayersFromLobby();
_manager?.uiSubsystem?.ShowMeetingPanel(State.Players, payload);
}
private void HandlePlayerVoted(GameEvent evt)
{
var payload = evt.GetPayload<PlayerVotedPayload>();
if (payload == null) return;
State.VotedPlayerIds.Add(payload.VoterId);
} }
private void HandleVotingClosed(GameEvent evt) private void HandleVotingClosed(GameEvent evt)
{ {
var payload = evt.GetPayload<VotingClosedPayload>(); var payload = evt.GetPayload<VotingClosedPayload>();
if (payload == null) return; 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(); _manager?.mapSubsystem?.ClearBodyMarkers();
} }
@@ -308,11 +354,16 @@ namespace Subsystems
{ {
var payload = evt.GetPayload<GameEndedPayload>(); var payload = evt.GetPayload<GameEndedPayload>();
if (payload == null) return; if (payload == null) return;
State.Phase = GamePhase.Ended;
State.GameEndData = payload;
_manager?.uiSubsystem?.ShowGameEndPanel(payload, _gameClient.ClientUuid); _manager?.uiSubsystem?.ShowGameEndPanel(payload, _gameClient.ClientUuid);
} }
private void HandleReturnedToLobby() private void HandleReturnedToLobby()
{ {
State.Phase = GamePhase.Lobby;
if (_gameClient.IsOwner) if (_gameClient.IsOwner)
SceneManager.LoadScene("create", LoadSceneMode.Single); SceneManager.LoadScene("create", LoadSceneMode.Single);
else else
@@ -323,6 +374,9 @@ namespace Subsystems
{ {
var payload = evt.GetPayload<SabotageStartedPayload>(); var payload = evt.GetPayload<SabotageStartedPayload>();
if (payload == null) return; if (payload == null) return;
State.ActiveSabotage = payload;
_manager?.mapSubsystem?.CreateSabotageMarkers(payload.RepairStations); _manager?.mapSubsystem?.CreateSabotageMarkers(payload.RepairStations);
if (payload.Type == SabotageType.CriticalMeltdown && payload.Deadline.HasValue) if (payload.Type == SabotageType.CriticalMeltdown && payload.Deadline.HasValue)
_manager?.uiSubsystem?.ShowSabotageTimer(payload.Deadline.Value); _manager?.uiSubsystem?.ShowSabotageTimer(payload.Deadline.Value);
@@ -336,6 +390,21 @@ namespace Subsystems
_manager?.mapSubsystem?.UpdatePlayerAvatars(_gameClient.PlayerPositions, _gameClient.ClientUuid); _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 ────────────────────────────────────────────────────── // ── Send helpers ──────────────────────────────────────────────────────
public void CreateLobby(double lat, double lon, double radius = 500, int impostorCount = 1, int taskCount = 5) 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() public void LeaveLobby()
{ {
_gameClient.LeaveLobby(); _gameClient.LeaveLobby();
State.Phase = GamePhase.Lobby;
SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single); SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single);
} }

View File

@@ -176,12 +176,10 @@ namespace Subsystems
taskComponent.TaskLocation = (entry.ServerTask.Location.Lat, entry.ServerTask.Location.Lon); taskComponent.TaskLocation = (entry.ServerTask.Location.Lat, entry.ServerTask.Location.Lon);
bool done = false; bool done = false;
bool exited = false;
taskComponent.Initialize(t => { done = true; }); taskComponent.Initialize(t => { done = true; });
taskComponent.ExitTask(t => { exited = true; });
// Wait for completion or exit // Wait for completion or exit
yield return new WaitUntil(() => done || exited); yield return new WaitUntil(() => done);
yield return FinishMinigame(entry, done); yield return FinishMinigame(entry, done);
} }

View File

@@ -9,168 +9,208 @@ using TMPro;
namespace Subsystems namespace Subsystems
{ {
/// <summary> /// <summary>
/// Manages UI for the GameManager. Canvas references are only valid in Client.unity; /// Reads from GameManager_Network.State (the authoritative GameState) and drives
/// Art-menu scenes use their own lightweight UI scripts that read from GameManager.Instance. /// all in-game canvas panels. No business logic lives here.
/// </summary> /// </summary>
public class GameManager_UI public class GameManager_UI
{ {
private GameClient _gameClient; private GameClient _gameClient;
private GameState _state => GameManager.Instance?.networkSubsystem?.State;
// Set by GameManager after Client.unity loads (called from GameManager.OnSceneLoaded) // ── Canvas refs (wired by BindClientScene from Client.unity) ──────────
public Canvas ClientCreateJoinLobby; // fallback join-code canvas in Client.unity public Canvas ClientCreateJoinLobby;
public Canvas ClientInLobby; // InLobby canvas in Client.unity (unused now, kept compat) public Canvas ClientInLobby;
public Canvas ClientLoadingScreen; public Canvas ClientLoadingScreen;
public Canvas ClientGameScreen; // parent of all HUD elements public Canvas ClientGameScreen;
// HUD elements (children of ClientGameScreen, resolved at runtime) // ── HUD element refs (resolved once in BindClientScene) ───────────────
private TMP_Text _roleText; private TMP_Text _roleText;
private TMP_Text _taskListText; private TMP_Text _taskListText;
private TMP_Text _taskProgressText; private TMP_Text _taskProgressText;
private Button _actionButton; private Button _actionButton;
private TMP_Text _actionButtonText; private TMP_Text _actionButtonText;
private TMP_Text _killCooldownText; private TMP_Text _killCooldownText;
private GameObject _sabotagePanel; private GameObject _sabotagePanel;
private TMP_Text _sabotageTimerText; private TMP_Text _sabotageTimerText;
private GameObject _meetingPanel; 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 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 _isDead;
private bool _commsBlackout; private bool _commsBlackout;
private DateTime _sabotageMeltdownDeadline; private DateTime _sabotageMeltdownDeadline;
private bool _sabotageTimerActive; private bool _sabotageTimerActive;
// Lobby-changed flag — set from network thread, consumed in Update
private volatile bool _lobbyDirty; private volatile bool _lobbyDirty;
public GameManager_UI(GameClient gameClient) // Meeting vote-row references rebuilt each meeting
{ private readonly List<GameObject> _voteRows = new List<GameObject>();
_gameClient = gameClient; 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 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) public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
{ {
ClientCreateJoinLobby = createJoin; ClientCreateJoinLobby = createJoin;
ClientInLobby = inLobby; ClientInLobby = inLobby;
ClientLoadingScreen = loading; ClientLoadingScreen = loading;
ClientGameScreen = game; ClientGameScreen = game;
EnsureCanvasReady(createJoin); foreach (var c in new[] { createJoin, inLobby, loading, game })
EnsureCanvasReady(inLobby); EnsureCanvasReady(c);
EnsureCanvasReady(loading);
EnsureCanvasReady(game);
if (createJoin) createJoin.gameObject.SetActive(false); if (createJoin) createJoin.gameObject.SetActive(false);
if (inLobby) inLobby.gameObject.SetActive(false); if (inLobby) inLobby.gameObject.SetActive(false);
if (loading) loading.gameObject.SetActive(false); if (loading) loading.gameObject.SetActive(false);
if (game) game.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"); _actionButton = actionGO.GetComponent<Button>();
_taskListText = FindTMP(game.transform, "TaskList"); _actionButtonText = actionGO.GetComponentInChildren<TMP_Text>();
_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);
} }
_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() public void UpdateLobbyUI()
{ {
var state = _gameClient.CurrentLobbyState; var lobbyState = _gameClient.CurrentLobbyState;
if (state == null) return; if (lobbyState == null) return;
// Update any LobbyDisplayUI listeners in the current scene
if (_lobbyDirty) if (_lobbyDirty)
{ {
_lobbyDirty = false; _lobbyDirty = false;
LobbyDisplayUI.RefreshAll(state); LobbyDisplayUI.RefreshAll(lobbyState);
} }
// Only do canvas switches if we are in Client.unity (canvases assigned)
if (ClientGameScreen == null) return; if (ClientGameScreen == null) return;
switch (state.Phase) switch (lobbyState.Phase)
{ {
case GamePhase.Loading: case GamePhase.Loading:
SetCanvases(false, false, true, false); SetCanvases(false, false, true, false);
break; break;
case GamePhase.Lobby: case GamePhase.Lobby:
SetCanvases(false, true, false, false); SetCanvases(false, true, false, false);
break; break;
case GamePhase.Playing: case GamePhase.Playing:
case GamePhase.Meeting: case GamePhase.Meeting:
case GamePhase.Voting: case GamePhase.Voting:
SetCanvases(false, false, false, true); SetCanvases(false, false, false, true);
UpdateGameHUD(); UpdateGameHUD();
break; break;
case GamePhase.Ended: case GamePhase.Ended:
// GameEndPanel shown by HandleGameEnded SetCanvases(false, false, false, true);
break; break;
} }
TickToast();
} }
// ── Game HUD tick ─────────────────────────────────────────────────────
private void UpdateGameHUD() 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) if (_taskListText != null)
{ {
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
foreach (var t in _gameClient.MyTasks) foreach (var task in s.MyTasks)
sb.AppendLine(t.Name); {
bool done = s.MyCompletedTaskIds.Contains(task.TaskId);
string mark = done ? "<color=#2DB84B>✓</color>" : "○";
sb.AppendLine($"{mark} {task.Name}");
}
_taskListText.text = sb.ToString(); _taskListText.text = sb.ToString();
} }
// Kill cooldown (managed by GameManager_Tasks via Update) // Global task progress
// Sabotage timer 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) if (_sabotageTimerActive && _sabotageTimerText != null)
{ {
double remaining = (_sabotageMeltdownDeadline - DateTime.UtcNow).TotalSeconds; 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) public void SetKillCooldownText(string text)
{ {
if (_killCooldownText != null) if (_killCooldownText == null) return;
{ bool show = !string.IsNullOrEmpty(text);
_killCooldownText.text = text; _killCooldownText.gameObject.SetActive(show);
_killCooldownText.gameObject.SetActive(!string.IsNullOrEmpty(text)); if (show) _killCooldownText.text = text;
}
} }
public void UpdateTaskProgress(int completed, int total) public void UpdateTaskProgress(int completed, int total)
@@ -179,6 +219,8 @@ namespace Subsystems
_taskProgressText.text = $"Tasks: {completed}/{total}"; _taskProgressText.text = $"Tasks: {completed}/{total}";
} }
// ── Action button ─────────────────────────────────────────────────────
public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null) public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null)
{ {
if (_actionButton == null) return; if (_actionButton == null) return;
@@ -191,15 +233,19 @@ namespace Subsystems
} }
} }
// ── Player state ──────────────────────────────────────────────────────
public void OnLocalPlayerDied() public void OnLocalPlayerDied()
{ {
_isDead = true; _isDead = true;
if (_roleText != null) _roleText.text = "GHOST"; if (_state != null) _state.IsDead = true;
} }
// ── Meeting ───────────────────────────────────────────────────────────
public void ShowMeetingAlert() 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) public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
@@ -207,99 +253,217 @@ namespace Subsystems
if (_meetingPanel == null) return; if (_meetingPanel == null) return;
_meetingPanel.SetActive(true); _meetingPanel.SetActive(true);
var header = FindTMP(_meetingPanel.transform, "MeetingHeader"); if (_meetingHeader != null)
if (header != null) _meetingHeader.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";
header.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";
// Build simple text list of players — full vote buttons need prefabs in Unity Editor if (_voteResultPanel) _voteResultPanel.SetActive(false);
var playerList = FindTMP(_meetingPanel.transform, "MeetingPlayerList");
if (playerList != null && players != null) 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(); // Fall back to text list
foreach (var p in players) if (_meetingFallbackText != null)
sb.AppendLine($"{p.DisplayName} [{p.State}]"); {
playerList.text = sb.ToString(); _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 string myId = _gameClient.ClientUuid;
var skipBtn = _meetingPanel.transform.Find("SkipButton")?.GetComponent<Button>(); bool canVote = !_isDead;
if (skipBtn != null)
foreach (var player in players)
{ {
skipBtn.onClick.RemoveAllListeners(); bool isMe = player.ClientUuid == myId;
skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null)); 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() public void AppendVoteInstruction()
{ {
var playerList = FindTMP(_meetingPanel?.transform, "MeetingPlayerList"); // no-op vote instructions are embedded in the row buttons
if (playerList != null)
playerList.text += "\n[Tap a name] then press VOTE — or press SKIP";
} }
public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players) public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
{ {
if (_meetingPanel == null) return; if (_voteResultPanel != null) _voteResultPanel.SetActive(true);
if (_voteResultText != null)
var resultText = FindTMP(_meetingPanel.transform, "VoteResult");
if (resultText != null)
{ {
if (payload.WasTie) if (payload.WasTie)
{ _voteResultText.text = "⚖ TIE — nobody ejected.";
resultText.text = "TIE — nobody ejected.";
}
else if (string.IsNullOrEmpty(payload.EjectedPlayerId)) else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
{ _voteResultText.text = "Nobody ejected (skip).";
resultText.text = "Skip — nobody ejected.";
}
else else
{ {
var ejected = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId); var ej = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
resultText.text = $"{ejected?.DisplayName ?? payload.EjectedPlayerId} ejected!"; _voteResultText.text = $"🚪 {ej?.DisplayName ?? payload.EjectedPlayerId} ejected!";
} }
} }
// Close panel after 5 seconds — use coroutine via GameManager // Auto-close meeting panel after 5 s
GameManager.Instance?.StartCoroutine(CloseMeetingPanelAfterDelay(5f)); 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); 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) // ── Sabotage ──────────────────────────────────────────────────────────
{
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) public void ShowSabotageTimer(DateTime deadline)
{ {
_sabotageMeltdownDeadline = deadline; _sabotageMeltdownDeadline = deadline;
_sabotageTimerActive = true; _sabotageTimerActive = true;
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(true); if (_sabotagePanel) _sabotagePanel.SetActive(true);
if (_sabotageTimerText) _sabotageTimerText.gameObject.SetActive(true);
} }
public void HideSabotageTimer() public void HideSabotageTimer()
{ {
_sabotageTimerActive = false; _sabotageTimerActive = false;
if (_sabotageTimerText != null) _sabotageTimerText.gameObject.SetActive(false); if (_sabotagePanel) _sabotagePanel.SetActive(false);
SetCommsBlackout(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; // ── Toast ─────────────────────────────────────────────────────────────
public bool IsPlayerDead => _isDead;
// ── 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) private void SetCanvases(bool createJoin, bool inLobby, bool loading, bool game)
{ {
@@ -314,14 +478,12 @@ namespace Subsystems
if (ClientGameScreen) ClientGameScreen.gameObject.SetActive(game); if (ClientGameScreen) ClientGameScreen.gameObject.SetActive(game);
} }
// ── Utilities ─────────────────────────────────────────────────────────
private static void EnsureCanvasReady(Canvas canvas) private static void EnsureCanvasReady(Canvas canvas)
{ {
if (canvas == null) return; if (canvas == null) return;
if (!canvas.enabled) canvas.enabled = true;
if (!canvas.enabled)
canvas.enabled = true;
// Some scene canvases are saved with zero scale, which makes UI invisible.
var t = canvas.transform; var t = canvas.transform;
if (t != null) if (t != null)
{ {
@@ -335,12 +497,34 @@ namespace Subsystems
{ {
if (root == null) return null; if (root == null) return null;
foreach (var tmp in root.GetComponentsInChildren<TMP_Text>(true)) 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; 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;
}
} }
} }

View File

@@ -0,0 +1,42 @@
using GeoSus.Client;
using System.Collections.Generic;
/// <summary>
/// Single source of truth for all in-game state on the client.
/// Updated exclusively by GameManager_Network; read by GameManager_UI.
/// </summary>
public class GameState
{
// ── Phase / Role ──────────────────────────────────────────────────────────
public GamePhase Phase { get; set; } = GamePhase.Lobby;
public PlayerRole? MyRole { get; set; }
public bool IsDead { get; set; }
// ── Tasks ─────────────────────────────────────────────────────────────────
public List<GameTask> MyTasks { get; set; } = new List<GameTask>();
public HashSet<string> MyCompletedTaskIds { get; set; } = new HashSet<string>();
public int TotalCompleted { get; set; }
public int TotalRequired { get; set; }
// ── Players ───────────────────────────────────────────────────────────────
public List<PlayerInfo> Players { get; set; } = new List<PlayerInfo>();
// ── Meeting ───────────────────────────────────────────────────────────────
public MeetingStartedPayload ActiveMeeting { get; set; }
public VotingClosedPayload LastVoteResult { get; set; }
public HashSet<string> VotedPlayerIds { get; set; } = new HashSet<string>();
// ── Sabotage ──────────────────────────────────────────────────────────────
public SabotageStartedPayload ActiveSabotage { get; set; }
// ── End game ──────────────────────────────────────────────────────────────
public GameEndedPayload GameEndData { get; set; }
// ── Kill cooldown (tracked by GameManager, reflected here for UI) ─────────
public float KillCooldownRemaining { get; set; }
// ── Notification (toast) ─────────────────────────────────────────────────
public string ToastMessage { get; set; }
public float ToastExpiry { get; set; } // UnityEngine.Time.time
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82b3963a05498b68baf483476d0d81f4

View File

@@ -3,63 +3,397 @@ using UnityEngine.UI;
using TMPro; using TMPro;
/// <summary> /// <summary>
/// Attach to a manager GameObject in host lobby.unity. /// Attach to any manager GameObject in the create-lobby scene.
/// Reads radius from the "radius" slider/input and triggers CreateLobby. /// On Start it nukes all Art placeholder children from the Canvas and builds
/// Also wires the "vytvořit" button. /// a complete mobile-portrait lobby-configuration screen entirely in code.
/// </summary> /// </summary>
public class HostLobbyUI : MonoBehaviour public class HostLobbyUI : MonoBehaviour
{ {
[Header("Optional refs (auto-found by name if null)")] // ── Colours ───────────────────────────────────────────────────────────────
public Slider radiusSlider; static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; }
public TMP_InputField radiusInput; static readonly Color C_BG = H("#0D0F1A");
public Button createButton; static readonly Color C_HDR = H("#141927");
static readonly Color C_CARD = H("#111525");
static readonly Color C_BORDER = H("#1E2540");
static readonly Color C_ACCENT = H("#3399FF");
static readonly Color C_GREEN = H("#2DB84B");
static readonly Color C_MUTED = new Color(0.47f, 0.53f, 0.67f);
static readonly Color C_WHITE = Color.white;
static readonly Color C_INPUT = H("#0A0D1A");
// ── Live values ───────────────────────────────────────────────────────────
private float _radius = 500f;
private int _impostors = 1;
private int _tasks = 5;
private string _playerName = "";
// ── UI refs ───────────────────────────────────────────────────────────────
private TMP_Text _radiusValueLabel;
private TMP_Text _impostorValueLabel;
private TMP_Text _taskValueLabel;
private TMP_Text _statusText;
void Start() void Start()
{ {
if (radiusSlider == null) // Pre-populate from GameManager defaults
var gm = GameManager.Instance;
if (gm != null)
{ {
var go = GameObject.Find("radius"); _radius = (float)gm.pendingRadius;
if (go != null) radiusSlider = go.GetComponent<Slider>(); _impostors = gm.pendingImpostorCount;
} _tasks = gm.pendingTaskCount;
if (radiusInput == null) _playerName = gm.displayName ?? "";
{
var go = GameObject.Find("radius");
if (go != null) radiusInput = go.GetComponent<TMP_InputField>();
} }
if (createButton == null) var canvasGO = GameObject.Find("Canvas");
if (canvasGO == null)
{ {
// Try all name variants used by the Art team Debug.LogError("[HostLobbyUI] No Canvas found!");
var go = GameObject.Find("stvo\u0159it") // stvořit return;
?? GameObject.Find("stvorit")
?? GameObject.Find("vytvo\u0159it") // vytvořit
?? GameObject.Find("vytvorit");
if (go != null)
{
createButton = go.GetComponent<Button>();
// Disable the Art team's direct scene-changer so only our
// wired OnCreateClicked fires (navigation is handled by
// HandleCreateLobbyResponse after the server confirms).
var sceneChanger = go.GetComponent<CudlikZmenaSceny>();
if (sceneChanger != null) sceneChanger.enabled = false;
}
} }
if (createButton != null) // Nuke all Art placeholder children
createButton.onClick.AddListener(OnCreateClicked); var kill = new System.Collections.Generic.List<GameObject>();
foreach (Transform child in canvasGO.transform)
kill.Add(child.gameObject);
foreach (var go in kill)
DestroyImmediate(go);
// Disable scene-changer components that bypass our logic
foreach (var sc in canvasGO.GetComponentsInChildren<CudlikZmenaSceny>(true))
sc.enabled = false;
Build(canvasGO.GetComponent<RectTransform>() ?? canvasGO.AddComponent<RectTransform>());
} }
private void OnCreateClicked() // ── Builder ───────────────────────────────────────────────────────────────
void Build(RectTransform root)
{
// Full-screen background
var bg = MakeRT("BG", root);
Stretch(bg);
bg.gameObject.AddComponent<CanvasRenderer>();
Img(bg, C_BG);
// ── Header bar ───────────────────────────────────────────────────────
var hdr = MakeRT("Header", root);
Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-80), new Vector2(0,0));
Img(hdr, C_HDR);
var hdrTxt = Txt("Create Lobby", hdr, 28, C_WHITE, TextAlignmentOptions.Center, bold: true);
Stretch(hdrTxt.rectTransform);
// ── Scroll body ───────────────────────────────────────────────────────
var scroll = MakeRT("Scroll", root);
Anchor(scroll, new Vector2(0,0), new Vector2(1,1), new Vector2(0,80), new Vector2(0,-80));
var sf = scroll.gameObject.AddComponent<ScrollRect>();
sf.horizontal = false;
Img(scroll, new Color(0,0,0,0));
var content = MakeRT("Content", scroll);
content.anchorMin = new Vector2(0,1);
content.anchorMax = new Vector2(1,1);
content.pivot = new Vector2(0.5f, 1);
content.offsetMin = Vector2.zero;
content.offsetMax = Vector2.zero;
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
vlg.padding = new RectOffset(16, 16, 12, 12);
vlg.spacing = 12;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sf.content = content;
sf.viewport = scroll;
// ── Player name card ─────────────────────────────────────────────────
AddSectionLabel("PLAYER NAME", content);
var nameCard = AddCard(content, 70);
var nameInput = MakeInputField("Your name", nameCard, _playerName);
Stretch(nameInput.GetComponent<RectTransform>());
nameInput.onEndEdit.AddListener(v =>
{
_playerName = v.Trim();
var gm2 = GameManager.Instance;
if (gm2 != null) gm2.displayName = _playerName;
PlayerPrefs.SetString("PlayerName", _playerName);
});
// ── Radius card ───────────────────────────────────────────────────────
AddSectionLabel("PLAY AREA RADIUS", content);
var radCard = AddCard(content, 110);
var radRT = radCard.GetComponent<RectTransform>();
var radLbl = MakeRT("RadLbl", radRT);
Anchor(radLbl, new Vector2(0,1), new Vector2(1,1), new Vector2(12,-40), new Vector2(-12,-4));
var rt = radLbl.gameObject;
var radTmp = rt.AddComponent<TextMeshProUGUI>();
radTmp.text = RadiusLabel(_radius);
radTmp.fontSize = 20; radTmp.color = C_ACCENT;
radTmp.alignment = TextAlignmentOptions.Center;
_radiusValueLabel = radTmp;
var slider = MakeRT("RadSlider", radRT).gameObject.AddComponent<Slider>();
var srt = slider.GetComponent<RectTransform>();
Anchor(srt, new Vector2(0,0), new Vector2(1,0), new Vector2(16, 14), new Vector2(-16, 44));
slider.minValue = 100; slider.maxValue = 2000; slider.value = _radius;
BuildSliderVisuals(slider, C_ACCENT);
slider.onValueChanged.AddListener(v =>
{
_radius = v;
_radiusValueLabel.text = RadiusLabel(v);
var gm2 = GameManager.Instance;
if (gm2 != null) gm2.pendingRadius = v;
});
// ── Impostor count card ───────────────────────────────────────────────
AddSectionLabel("IMPOSTORS", content);
var impCard = AddCard(content, 80);
BuildStepper(impCard.GetComponent<RectTransform>(), ref _impostors, 1, 4,
v => { var gm2 = GameManager.Instance; if (gm2 != null) gm2.pendingImpostorCount = v; },
out _impostorValueLabel);
// ── Task count card ───────────────────────────────────────────────────
AddSectionLabel("TASKS PER PLAYER", content);
var taskCard = AddCard(content, 80);
BuildStepper(taskCard.GetComponent<RectTransform>(), ref _tasks, 1, 15,
v => { var gm2 = GameManager.Instance; if (gm2 != null) gm2.pendingTaskCount = v; },
out _taskValueLabel);
// ── Status text ───────────────────────────────────────────────────────
var statusCard = AddCard(content, 40);
_statusText = Txt("", statusCard.GetComponent<RectTransform>(), 16, C_MUTED, TextAlignmentOptions.Center);
Stretch(_statusText.rectTransform);
// ── Footer create button ──────────────────────────────────────────────
var footer = MakeRT("Footer", root);
Anchor(footer, new Vector2(0,0), new Vector2(1,0), new Vector2(0,0), new Vector2(0,80));
Img(footer, C_HDR);
var btnRT = MakeRT("CreateBtn", footer);
Anchor(btnRT, new Vector2(0.1f,0.15f), new Vector2(0.9f,0.85f), Vector2.zero, Vector2.zero);
var btn = btnRT.gameObject.AddComponent<Button>();
var btnImg = btnRT.gameObject.AddComponent<Image>();
btnImg.color = C_GREEN;
btn.targetGraphic = btnImg;
var cb = btn.colors; cb.pressedColor = C_GREEN * 0.7f; btn.colors = cb;
var btnTxt = Txt("CREATE LOBBY", btnRT, 22, C_WHITE, TextAlignmentOptions.Center, bold: true);
Stretch(btnTxt.rectTransform);
btn.onClick.AddListener(OnCreateClicked);
// ── Back button ───────────────────────────────────────────────────────
var backRT = MakeRT("BackBtn", root);
Anchor(backRT, new Vector2(0,1), new Vector2(0,1), new Vector2(8,-72), new Vector2(72,-8));
var backBtn = backRT.gameObject.AddComponent<Button>();
var backImg = backRT.gameObject.AddComponent<Image>();
backImg.color = new Color(1,1,1,0);
backBtn.targetGraphic = backImg;
var backTxt = Txt("←", backRT, 28, C_MUTED, TextAlignmentOptions.Center);
Stretch(backTxt.rectTransform);
backBtn.onClick.AddListener(() => UnityEngine.SceneManagement.SceneManager.LoadScene("main menu asi idk lol"));
}
// ── Actions ───────────────────────────────────────────────────────────────
void OnCreateClicked()
{ {
var gm = GameManager.Instance; var gm = GameManager.Instance;
if (gm == null) return; if (gm == null) { SetStatus("GameManager not found!", Color.red); return; }
// Read radius from slider or input field if (string.IsNullOrWhiteSpace(_playerName))
if (radiusSlider != null) {
gm.pendingRadius = radiusSlider.value; SetStatus("Enter a player name first.", Color.yellow);
else if (radiusInput != null && float.TryParse(radiusInput.text, out float r)) return;
gm.pendingRadius = r; }
gm.pendingRadius = _radius;
gm.pendingImpostorCount = _impostors;
gm.pendingTaskCount = _tasks;
gm.displayName = _playerName;
SetStatus("Connecting…", C_MUTED);
gm.CreateLobbyButton(); gm.CreateLobbyButton();
} }
void SetStatus(string msg, Color col)
{
if (_statusText == null) return;
_statusText.text = msg;
_statusText.color = col;
}
// ── UI helpers ────────────────────────────────────────────────────────────
static string RadiusLabel(float v) => $"{Mathf.RoundToInt(v)} m";
void BuildStepper(RectTransform parent, ref int val, int min, int max,
System.Action<int> onChange, out TMP_Text label)
{
int captured = val; // local copy for closures
// minus
var minusRT = MakeRT("Minus", parent);
Anchor(minusRT, new Vector2(0,0), new Vector2(0,1), new Vector2(8,8), new Vector2(72,-8));
var minusBtn = minusRT.gameObject.AddComponent<Button>();
var minusImg = minusRT.gameObject.AddComponent<Image>(); minusImg.color = C_BORDER;
minusBtn.targetGraphic = minusImg;
var minusTxt = Txt("", minusRT, 28, C_WHITE, TextAlignmentOptions.Center);
Stretch(minusTxt.rectTransform);
// label
var lblRT = MakeRT("Val", parent);
Stretch(lblRT);
var lbl = Txt(captured.ToString(), lblRT, 26, C_WHITE, TextAlignmentOptions.Center, bold: true);
label = lbl;
// plus
var plusRT = MakeRT("Plus", parent);
Anchor(plusRT, new Vector2(1,0), new Vector2(1,1), new Vector2(-72,8), new Vector2(-8,-8));
var plusBtn = plusRT.gameObject.AddComponent<Button>();
var plusImg = plusRT.gameObject.AddComponent<Image>(); plusImg.color = C_ACCENT;
plusBtn.targetGraphic = plusImg;
var plusTxt = Txt("+", plusRT, 28, C_WHITE, TextAlignmentOptions.Center);
Stretch(plusTxt.rectTransform);
minusBtn.onClick.AddListener(() =>
{
captured = Mathf.Max(min, captured - 1);
lbl.text = captured.ToString();
onChange?.Invoke(captured);
});
plusBtn.onClick.AddListener(() =>
{
captured = Mathf.Min(max, captured + 1);
lbl.text = captured.ToString();
onChange?.Invoke(captured);
});
// write back initial
val = captured;
}
static void BuildSliderVisuals(Slider s, Color fillColor)
{
// Background track
var bgRT = MakeRT("Background", s.GetComponent<RectTransform>());
Stretch(bgRT);
bgRT.gameObject.AddComponent<CanvasRenderer>();
var bgImg = bgRT.gameObject.AddComponent<Image>(); bgImg.color = new Color(0.15f,0.18f,0.28f);
// Fill area
var fillArea = MakeRT("Fill Area", s.GetComponent<RectTransform>());
Anchor(fillArea, new Vector2(0,0.25f), new Vector2(1,0.75f), new Vector2(5,0), new Vector2(-5,0));
var fillRT = MakeRT("Fill", fillArea);
fillRT.anchorMin = Vector2.zero; fillRT.anchorMax = Vector2.one;
fillRT.offsetMin = Vector2.zero; fillRT.offsetMax = Vector2.zero;
fillRT.gameObject.AddComponent<CanvasRenderer>();
var fillImg = fillRT.gameObject.AddComponent<Image>(); fillImg.color = fillColor;
s.fillRect = fillRT;
// Handle area
var handleArea = MakeRT("Handle Slide Area", s.GetComponent<RectTransform>());
Stretch(handleArea);
var handleRT = MakeRT("Handle", handleArea);
handleRT.sizeDelta = new Vector2(28, 28);
handleRT.anchorMin = new Vector2(0, 0.5f); handleRT.anchorMax = new Vector2(0, 0.5f);
handleRT.anchoredPosition = Vector2.zero;
handleRT.gameObject.AddComponent<CanvasRenderer>();
var hImg = handleRT.gameObject.AddComponent<Image>(); hImg.color = Color.white;
s.handleRect = handleRT;
s.targetGraphic = hImg;
}
static void AddSectionLabel(string text, RectTransform parent)
{
var rt = MakeRT("Lbl_" + text, parent);
var le = rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = 28;
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = text; tmp.fontSize = 13; tmp.color = new Color(0.47f,0.53f,0.67f);
tmp.fontStyle = FontStyles.Bold; tmp.alignment = TextAlignmentOptions.Left;
}
static RectTransform AddCard(RectTransform parent, float height)
{
var rt = MakeRT("Card", parent);
var le = rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = height;
var img = rt.gameObject.AddComponent<Image>(); img.color = H("#111525");
// border via outline
var outline = rt.gameObject.AddComponent<Outline>();
outline.effectColor = H("#1E2540");
outline.effectDistance = new Vector2(1, -1);
return rt;
}
static TMP_InputField MakeInputField(string placeholder, RectTransform parent, string initialValue)
{
var go = new GameObject("InputField");
var rt = go.AddComponent<RectTransform>();
rt.SetParent(parent, false);
rt.localScale = Vector3.one;
var img = go.AddComponent<Image>(); img.color = H("#0A0D1A");
var field = go.AddComponent<TMP_InputField>();
var textArea = MakeRT("Text Area", rt);
Stretch(textArea);
textArea.offsetMin = new Vector2(10, 4);
textArea.offsetMax = new Vector2(-10, -4);
var areaImg = textArea.gameObject.AddComponent<Image>(); areaImg.color = new Color(0,0,0,0);
var mask = textArea.gameObject.AddComponent<RectMask2D>();
var phRT = MakeRT("Placeholder", textArea);
Stretch(phRT);
var phTmp = phRT.gameObject.AddComponent<TextMeshProUGUI>();
phTmp.text = placeholder; phTmp.fontSize = 20;
phTmp.color = new Color(0.47f, 0.53f, 0.67f);
phTmp.alignment = TextAlignmentOptions.MidlineLeft;
var txtRT = MakeRT("Text", textArea);
Stretch(txtRT);
var txtTmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
txtTmp.fontSize = 20; txtTmp.color = Color.white;
txtTmp.alignment = TextAlignmentOptions.MidlineLeft;
field.textViewport = textArea;
field.textComponent = txtTmp;
field.placeholder = phTmp;
field.text = initialValue;
field.targetGraphic = img;
return field;
}
static RectTransform MakeRT(string name, RectTransform parent)
{
var go = new GameObject(name);
var rt = go.AddComponent<RectTransform>();
rt.SetParent(parent, false);
rt.localScale = Vector3.one;
return rt;
}
static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
}
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 offMin, Vector2 offMax)
{
rt.anchorMin = aMin; rt.anchorMax = aMax;
rt.offsetMin = offMin; rt.offsetMax = offMax;
}
static Image Img(RectTransform rt, Color c)
{
rt.gameObject.AddComponent<CanvasRenderer>();
var img = rt.gameObject.AddComponent<Image>(); img.color = c; return img;
}
static TextMeshProUGUI Txt(string text, RectTransform parent, float size, Color color,
TextAlignmentOptions align, bool bold = false)
{
var rt = MakeRT("T_" + text, parent);
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align;
if (bold) tmp.fontStyle = FontStyles.Bold;
return tmp;
}
} }

View File

@@ -1,276 +1,291 @@
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using TMPro; using TMPro;
using System.Collections.Generic;
/// <summary> /// <summary>
/// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity). /// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity).
/// ///
/// Call BuildNow() explicitly from GameManager.OnSceneLoaded BEFORE BindClientScene(), /// Call BuildNow() from GameManager.OnSceneLoaded BEFORE BindClientScene so GameManager_UI
/// so that GameManager_UI can find the newly created elements by name. /// can locate named children.
/// ///
/// Named GameObjects created as direct children of InGame canvas (required by Transform.Find): /// Named elements expected by GameManager_UI (Transform.Find / FindTMP):
/// ActionButton — Button + TMP child; shown/hidden by GameManager_Tasks.UpdateProximity() /// ActionButton proxim action button
/// SabotagePanel — warning banner at top (contains "SabotageTimer" TMP_Text) /// SabotagePanel top-of-screen sabotage banner
/// • MeetingPanel — voting/meeting overlay (populated by GameManager_UI.ShowMeetingPanel) /// SabotageTimer TMP countdown text inside SabotagePanel
/// • GameEndPanel — end-of-game overlay (contains "GameEndText" TMP_Text) /// MeetingPanel full-screen voting overlay
/// /// MeetingHeader TMP title
/// Named TMP_Text descendants (found by GameManager_UI.FindTMP — any depth): /// MeetingPlayerList TMP player list (text fallback)
/// • KillCooldown — shown to impostors during kill cooldown /// SkipButton skip-vote button
/// • TaskList — crewmate task names /// VoteResultPanel sub-panel shown after voting
/// • TaskProgress — global task completion "X/Y tasks" /// VoteResult TMP result text
/// • SabotageTimer — countdown inside SabotagePanel /// GameEndPanel full-screen end-of-game overlay
/// GameEndText — win/lose result text inside GameEndPanel /// GameEndText TMP result text
/// • (Role already exists in the scene) /// KillCooldown TMP kill-cooldown label
/// /// TaskList TMP task name list
/// Additional elements managed by this script: /// TaskProgress TMP global task progress
/// • RecenterBtn — calls MapCameraController.Instance.Recenter() /// Toast TMP toast notification
/// </summary> /// </summary>
public class InGameHUDBuilder : MonoBehaviour public class InGameHUDBuilder : MonoBehaviour
{ {
// ── Color palette ───────────────────────────────────────────────────────── // ── Palette ───────────────────────────────────────────────────────────────
private static readonly Color C_BG = new Color(0.05f, 0.06f, 0.12f, 0.80f); static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; }
private static readonly Color C_BAR_BG = new Color(0.03f, 0.04f, 0.08f, 0.90f); static readonly Color C_BG = H("#0D0F1A");
private static readonly Color C_ACCENT = new Color(0.20f, 0.60f, 1.00f, 1.00f); static readonly Color C_BAR = H("#141927");
private static readonly Color C_GREEN = new Color(0.18f, 0.75f, 0.30f, 1.00f); static readonly Color C_ACCENT = H("#3399FF");
private static readonly Color C_RED = new Color(0.76f, 0.19f, 0.19f, 1.00f); static readonly Color C_GREEN = H("#2DB84B");
private static readonly Color C_ORANGE = new Color(0.95f, 0.55f, 0.10f, 1.00f); static readonly Color C_RED = H("#C43232");
static readonly Color C_ORANGE = H("#F08C1A");
static readonly Color C_YELLOW = H("#FFB800");
static readonly Color C_MUTED = new Color(0.47f, 0.53f, 0.67f);
static readonly Color C_ROW_A = H("#1A2035");
static readonly Color C_ROW_B = H("#161C2E");
private bool _built; private bool _built;
// ── Entry points ────────────────────────────────────────────────────────── public void BuildNow() { if (!_built) { _built = true; Build(); } }
void Start() { if (!_built) Build(); }
/// <summary>Called from GameManager.OnSceneLoaded before BindClientScene.</summary>
public void BuildNow()
{
if (_built) return;
_built = true;
Build();
}
void Start()
{
if (!_built) Build(); // safety fallback
}
// ── Build ─────────────────────────────────────────────────────────────────
void Build() void Build()
{ {
var rt = GetComponent<RectTransform>(); var rt = GetComponent<RectTransform>();
if (rt == null) return; if (rt == null) return;
// ── Top bar: role is already in scene, add kill-cooldown ─────────────
BuildTopBar(rt); BuildTopBar(rt);
// ── Right task panel ──────────────────────────────────────────────────
BuildTaskPanel(rt); BuildTaskPanel(rt);
// ── Task progress (above bottom bar) ─────────────────────────────────
BuildTaskProgress(rt); BuildTaskProgress(rt);
// ── Bottom bar: action button + recenter ──────────────────────────────
BuildBottomBar(rt); BuildBottomBar(rt);
// ── Action button (DIRECT child — Transform.Find requirement) ─────────
BuildActionButton(rt); BuildActionButton(rt);
// ── Sabotage panel (DIRECT child) ─────────────────────────────────────
BuildSabotagePanel(rt); BuildSabotagePanel(rt);
// ── Meeting panel (DIRECT child) ──────────────────────────────────────
BuildMeetingPanel(rt); BuildMeetingPanel(rt);
// ── Game-end panel (DIRECT child) ─────────────────────────────────────
BuildGameEndPanel(rt); BuildGameEndPanel(rt);
BuildToast(rt);
} }
// ── Section builders ────────────────────────────────────────────────────── // ── Top bar ───────────────────────────────────────────────────────────────
void BuildTopBar(RectTransform parent) void BuildTopBar(RectTransform parent)
{ {
// Thin semi-transparent header at very top var bar = Child("_TopBar", parent);
var bar = AddChild("_TopBar", parent); Anchor(bar, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-90f), Vector2.zero);
Anchor(bar, new Vector2(0f, 1f), new Vector2(1f, 1f)); bar.pivot = new Vector2(0.5f,1f);
bar.sizeDelta = new Vector2(0f, 90f); Img(bar, C_BAR);
bar.anchoredPosition = new Vector2(0f, 0f);
bar.pivot = new Vector2(0.5f, 1f);
AddImage(bar.gameObject, C_BAR_BG);
// Kill cooldown (right side) — starts hidden // Kill cooldown right half, hidden by default
var cd = AddChild("KillCooldown", bar); var cd = Child("KillCooldown", bar);
Anchor(cd, new Vector2(0.5f, 0f), new Vector2(1f, 1f)); cd.anchorMin = new Vector2(0.5f, 0); cd.anchorMax = Vector2.one;
cd.offsetMin = new Vector2(0f, 6f); cd.offsetMin = new Vector2(0, 6); cd.offsetMax = new Vector2(-12, -6);
cd.offsetMax = new Vector2(-12f, -6f);
var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>(); var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
cdTmp.text = ""; cdTmp.text = ""; cdTmp.fontSize = 32; cdTmp.color = C_ORANGE;
cdTmp.fontSize = 32; cdTmp.fontStyle = FontStyles.Bold; cdTmp.alignment = TextAlignmentOptions.MidlineRight;
cdTmp.color = C_ORANGE;
cdTmp.fontStyle = FontStyles.Bold;
cdTmp.alignment = TextAlignmentOptions.MidlineRight;
cd.gameObject.SetActive(false); cd.gameObject.SetActive(false);
} }
// ── Task panel (right side) ───────────────────────────────────────────────
void BuildTaskPanel(RectTransform parent) void BuildTaskPanel(RectTransform parent)
{ {
// Right-side floating panel (always visible during game) var panel = Child("_TaskPanel", parent);
var panel = AddChild("_TaskPanel", parent); panel.anchorMin = new Vector2(1,0.35f); panel.anchorMax = new Vector2(1,0.88f);
Anchor(panel, new Vector2(1f, 0.35f), new Vector2(1f, 0.88f)); panel.pivot = new Vector2(1,0.5f); panel.sizeDelta = new Vector2(280,0);
panel.pivot = new Vector2(1f, 0.5f); Img(panel, new Color(0.05f,0.06f,0.12f,0.85f));
panel.sizeDelta = new Vector2(280f, 0f);
panel.anchoredPosition = Vector2.zero;
AddImage(panel.gameObject, C_BG);
// "MY TASKS" header var hdr = Child("_Hdr", panel);
var hdr = AddChild("_Header", panel); Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-44), Vector2.zero);
Anchor(hdr, new Vector2(0f, 1f), new Vector2(1f, 1f)); hdr.pivot = new Vector2(0.5f,1f);
hdr.pivot = new Vector2(0.5f, 1f); Img(hdr, new Color(0.2f,0.6f,1f,0.5f));
hdr.sizeDelta = new Vector2(0f, 44f); TxtChild(hdr,"MY TASKS",26,Color.white,TextAlignmentOptions.Center,bold:true);
hdr.anchoredPosition = Vector2.zero;
AddImage(hdr.gameObject, C_ACCENT * new Color(1, 1, 1, 0.6f));
var hdrTmp = AddTextChild(hdr, "_HeaderTxt", "MY TASKS", 26, FontStyles.Bold, TextAlignmentOptions.Center);
hdrTmp.color = Color.white;
// Task list body var body = Child("TaskList", panel);
var body = AddChild("TaskList", panel); body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one;
Anchor(body, new Vector2(0f, 0f), new Vector2(1f, 1f)); body.offsetMin = new Vector2(8,8); body.offsetMax = new Vector2(-8,-48);
body.offsetMin = new Vector2(8f, 8f); var t = body.gameObject.AddComponent<TextMeshProUGUI>();
body.offsetMax = new Vector2(-8f, -48f); t.text = ""; t.fontSize = 22; t.color = Color.white; t.alignment = TextAlignmentOptions.TopLeft;
var taskTmp = body.gameObject.AddComponent<TextMeshProUGUI>(); t.enableWordWrapping = true;
taskTmp.text = "";
taskTmp.fontSize = 22;
taskTmp.color = Color.white;
taskTmp.alignment = TextAlignmentOptions.TopLeft;
} }
// ── Task progress (above bottom bar) ─────────────────────────────────────
void BuildTaskProgress(RectTransform parent) void BuildTaskProgress(RectTransform parent)
{ {
var prog = AddChild("TaskProgress", parent); var prog = Child("TaskProgress", parent);
Anchor(prog, new Vector2(0f, 0f), new Vector2(1f, 0f)); Anchor(prog, new Vector2(0,0), new Vector2(1,0), new Vector2(-20,120), new Vector2(20,160));
prog.pivot = new Vector2(0.5f, 0f); var t = prog.gameObject.AddComponent<TextMeshProUGUI>();
prog.sizeDelta = new Vector2(-20f, 40f); t.text = ""; t.fontSize = 28; t.color = Color.white;
prog.anchoredPosition = new Vector2(0f, 120f); // above bottom bar t.fontStyle = FontStyles.Bold; t.alignment = TextAlignmentOptions.Center;
var tmp = prog.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = "";
tmp.fontSize = 28;
tmp.color = Color.white;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center;
} }
// ── Bottom bar ────────────────────────────────────────────────────────────
void BuildBottomBar(RectTransform parent) void BuildBottomBar(RectTransform parent)
{ {
var bar = AddChild("_BottomBar", parent); var bar = Child("_BottomBar", parent);
Anchor(bar, new Vector2(0f, 0f), new Vector2(1f, 0f)); Anchor(bar, Vector2.zero, new Vector2(1,0), Vector2.zero, new Vector2(0,110));
bar.pivot = new Vector2(0.5f, 0f); bar.pivot = new Vector2(0.5f,0);
bar.sizeDelta = new Vector2(0f, 110f); Img(bar, C_BAR);
bar.anchoredPosition = Vector2.zero;
AddImage(bar.gameObject, C_BAR_BG);
// Recenter button (bottom-right of bar) var recBtn = Child("_RecenterBtn", bar);
var recBtn = AddChild("_RecenterBtn", bar); recBtn.anchorMin = new Vector2(0.82f,0.08f); recBtn.anchorMax = new Vector2(0.98f,0.92f);
Anchor(recBtn, new Vector2(0.82f, 0.08f), new Vector2(0.98f, 0.92f)); var recBg = Img(recBtn, C_ACCENT);
var recBg = AddImage(recBtn.gameObject, C_ACCENT);
var recButton = recBtn.gameObject.AddComponent<Button>(); var recButton = recBtn.gameObject.AddComponent<Button>();
var recColors = recButton.colors;
recColors.pressedColor = new Color(0.1f, 0.4f, 0.8f);
recButton.colors = recColors;
recButton.targetGraphic = recBg; recButton.targetGraphic = recBg;
recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter()); recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter());
var recTxt = AddTextChild(recBtn, "_RecTxt", "⊙", 42, FontStyles.Bold, TextAlignmentOptions.Center); TxtChild(recBtn,"⊙",42,Color.white,TextAlignmentOptions.Center,bold:true);
recTxt.color = Color.white;
} }
// ── Action button (DIRECT child so Transform.Find works) ─────────────────
void BuildActionButton(RectTransform parent) void BuildActionButton(RectTransform parent)
{ {
// MUST be a DIRECT child so Transform.Find("ActionButton") works var btn = Child("ActionButton", parent);
var btn = AddChild("ActionButton", parent); Anchor(btn, new Vector2(0.15f,0), new Vector2(0.80f,0), new Vector2(0,12), new Vector2(0,102));
Anchor(btn, new Vector2(0.15f, 0f), new Vector2(0.80f, 0f)); btn.pivot = new Vector2(0.5f,0);
btn.pivot = new Vector2(0.5f, 0f); var bg = Img(btn, C_GREEN);
btn.sizeDelta = new Vector2(0f, 90f);
btn.anchoredPosition = new Vector2(0f, 12f);
var bg = AddImage(btn.gameObject, C_GREEN);
var button = btn.gameObject.AddComponent<Button>(); var button = btn.gameObject.AddComponent<Button>();
var colors = button.colors;
colors.normalColor = C_GREEN;
colors.pressedColor = new Color(0.12f, 0.55f, 0.22f);
button.colors = colors;
button.targetGraphic = bg; button.targetGraphic = bg;
// TMP child named "Text" so GetComponentInChildren<TMP_Text> finds it var txtRt = Child("Text", btn);
var txtRt = AddChild("Text", btn);
Stretch(txtRt); Stretch(txtRt);
var tmp = txtRt.gameObject.AddComponent<TextMeshProUGUI>(); var tmp = txtRt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = "ACTION"; tmp.text = "ACTION"; tmp.fontSize = 44; tmp.fontStyle = FontStyles.Bold;
tmp.fontSize = 44; tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;
tmp.fontStyle = FontStyles.Bold;
tmp.color = Color.white;
tmp.alignment = TextAlignmentOptions.Center;
btn.gameObject.SetActive(false); // hidden until proximity detected btn.gameObject.SetActive(false);
} }
// ── Sabotage panel (top strip) ────────────────────────────────────────────
void BuildSabotagePanel(RectTransform parent) void BuildSabotagePanel(RectTransform parent)
{ {
// DIRECT child var panel = Child("SabotagePanel", parent);
var panel = AddChild("SabotagePanel", parent); panel.anchorMin = new Vector2(0,1); panel.anchorMax = new Vector2(1,1);
Anchor(panel, new Vector2(0f, 0.88f), new Vector2(1f, 1f)); panel.pivot = new Vector2(0.5f,1f); panel.sizeDelta = new Vector2(0,80);
panel.offsetMin = new Vector2(0f, -10f); Img(panel, new Color(0.76f,0.19f,0.19f,0.92f));
panel.offsetMax = new Vector2(0f, -80f);
AddImage(panel.gameObject, C_RED * new Color(1, 1, 1, 0.88f));
var timer = AddChild("SabotageTimer", panel); var timer = Child("SabotageTimer", panel);
Stretch(timer); Stretch(timer);
var tmp = timer.gameObject.AddComponent<TextMeshProUGUI>(); var t = timer.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = "SABOTAGE!"; t.text = "SABOTAGE!"; t.fontSize = 48; t.fontStyle = FontStyles.Bold;
tmp.fontSize = 48; t.color = Color.white; t.alignment = TextAlignmentOptions.Center;
tmp.fontStyle = FontStyles.Bold;
tmp.color = Color.white;
tmp.alignment = TextAlignmentOptions.Center;
panel.gameObject.SetActive(false); panel.gameObject.SetActive(false);
} }
// ── Meeting panel (full screen overlay) ───────────────────────────────────
void BuildMeetingPanel(RectTransform parent) void BuildMeetingPanel(RectTransform parent)
{ {
// DIRECT child — populated by GameManager_UI.ShowMeetingPanel at runtime var panel = Child("MeetingPanel", parent);
var panel = AddChild("MeetingPanel", parent); Stretch(panel);
Anchor(panel, new Vector2(0.05f, 0.10f), new Vector2(0.95f, 0.90f)); Img(panel, new Color(0.04f,0.05f,0.14f,0.97f));
AddImage(panel.gameObject, new Color(0.04f, 0.05f, 0.14f, 0.96f));
var title = AddChild("_MeetingTitle", panel); // Header
Anchor(title, new Vector2(0f, 0.85f), new Vector2(1f, 1f)); var hdr = Child("MeetingHeader", panel);
var titleTmp = title.gameObject.AddComponent<TextMeshProUGUI>(); Anchor(hdr, new Vector2(0,0.86f), new Vector2(1,1), Vector2.zero, Vector2.zero);
titleTmp.text = "EMERGENCY MEETING"; var hdrTmp = hdr.gameObject.AddComponent<TextMeshProUGUI>();
titleTmp.fontSize = 44; hdrTmp.text = "EMERGENCY MEETING"; hdrTmp.fontSize = 52;
titleTmp.fontStyle = FontStyles.Bold; hdrTmp.fontStyle = FontStyles.Bold; hdrTmp.color = C_ORANGE;
titleTmp.color = C_ORANGE; hdrTmp.alignment = TextAlignmentOptions.Center;
titleTmp.alignment = TextAlignmentOptions.Center;
// Scrollable player vote list
var scrollArea = Child("_MeetingScroll", panel);
Anchor(scrollArea, new Vector2(0,0.22f), new Vector2(1,0.86f), Vector2.zero, Vector2.zero);
BuildMeetingScroll(scrollArea);
// Text fallback (hidden by default; shown if scroll build fails)
var fallback = Child("MeetingPlayerList", panel);
Anchor(fallback, new Vector2(0,0.22f), new Vector2(1,0.86f), new Vector2(8,0), new Vector2(-8,0));
var fallbackTmp = fallback.gameObject.AddComponent<TextMeshProUGUI>();
fallbackTmp.text = ""; fallbackTmp.fontSize = 28; fallbackTmp.color = Color.white;
fallbackTmp.alignment = TextAlignmentOptions.TopLeft;
fallback.gameObject.SetActive(false); // hidden scroll list used instead
// Skip button
var skip = Child("SkipButton", panel);
Anchor(skip, new Vector2(0.05f,0.04f), new Vector2(0.95f,0.18f), Vector2.zero, Vector2.zero);
var skipBg = Img(skip, C_MUTED);
var skipBtn = skip.gameObject.AddComponent<Button>();
skipBtn.targetGraphic = skipBg;
skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null));
TxtChild(skip, "⏭ SKIP", 36, Color.white, TextAlignmentOptions.Center, bold: true);
// Vote-result sub-panel (hidden until voting closes)
var resultPanel = Child("VoteResultPanel", panel);
Anchor(resultPanel, new Vector2(0,0.04f), new Vector2(1,0.22f), Vector2.zero, Vector2.zero);
Img(resultPanel, new Color(0.05f,0.05f,0.15f,0.95f));
var resultText = Child("VoteResult", resultPanel);
Stretch(resultText);
var rtTmp = resultText.gameObject.AddComponent<TextMeshProUGUI>();
rtTmp.text = ""; rtTmp.fontSize = 34; rtTmp.color = C_YELLOW;
rtTmp.fontStyle = FontStyles.Bold; rtTmp.alignment = TextAlignmentOptions.Center;
resultPanel.gameObject.SetActive(false);
panel.gameObject.SetActive(false); panel.gameObject.SetActive(false);
} }
void BuildMeetingScroll(RectTransform rt)
{
var sr = rt.gameObject.AddComponent<ScrollRect>();
var vp = Child("Viewport", rt);
Stretch(vp);
vp.gameObject.AddComponent<RectMask2D>();
var content = Child("MeetingContent", vp);
content.anchorMin = new Vector2(0,1); content.anchorMax = new Vector2(1,1);
content.pivot = new Vector2(0.5f,1);
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
vlg.childControlWidth = true; vlg.childControlHeight = false;
vlg.childForceExpandWidth = true; vlg.spacing = 4;
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sr.viewport = vp; sr.content = content;
sr.horizontal = false; sr.vertical = true; sr.scrollSensitivity = 60;
}
// ── Game-end panel ────────────────────────────────────────────────────────
void BuildGameEndPanel(RectTransform parent) void BuildGameEndPanel(RectTransform parent)
{ {
// DIRECT child, full-screen overlay var panel = Child("GameEndPanel", parent);
var panel = AddChild("GameEndPanel", parent);
Stretch(panel); Stretch(panel);
AddImage(panel.gameObject, new Color(0f, 0f, 0f, 0.85f)); Img(panel, new Color(0,0,0,0.90f));
var txt = AddChild("GameEndText", panel); // Result text (upper half)
Stretch(txt); var txt = Child("GameEndText", panel);
Anchor(txt, new Vector2(0,0.4f), new Vector2(1,0.9f), Vector2.zero, Vector2.zero);
var tmp = txt.gameObject.AddComponent<TextMeshProUGUI>(); var tmp = txt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = ""; tmp.text = ""; tmp.fontSize = 72; tmp.fontStyle = FontStyles.Bold;
tmp.fontSize = 72; tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;
tmp.fontStyle = FontStyles.Bold;
tmp.color = Color.white; // "Return to lobby" button owner only (GameManager_UI shows/hides it)
tmp.alignment = TextAlignmentOptions.Center; var retBtn = Child("ReturnToLobbyButton", panel);
Anchor(retBtn, new Vector2(0.15f,0.22f), new Vector2(0.85f,0.36f), Vector2.zero, Vector2.zero);
var retBg = Img(retBtn, C_GREEN);
var retButton = retBtn.gameObject.AddComponent<Button>();
retButton.targetGraphic = retBg;
retButton.onClick.AddListener(() => GameManager.Instance?.gameClient?.ReturnToLobby());
TxtChild(retBtn, "▶ RETURN TO LOBBY", 38, Color.white, TextAlignmentOptions.Center, bold: true);
retBtn.gameObject.SetActive(false); // shown only for host
// "Leave" button
var leaveBtn = Child("LeaveGameButton", panel);
Anchor(leaveBtn, new Vector2(0.3f,0.08f), new Vector2(0.7f,0.20f), Vector2.zero, Vector2.zero);
var leaveBg = Img(leaveBtn, C_RED);
var leaveButton = leaveBtn.gameObject.AddComponent<Button>();
leaveButton.targetGraphic = leaveBg;
leaveButton.onClick.AddListener(() => GameManager.Instance?.LeaveLobbyButton());
TxtChild(leaveBtn, "✕ LEAVE", 34, Color.white, TextAlignmentOptions.Center, bold: true);
panel.gameObject.SetActive(false); panel.gameObject.SetActive(false);
} }
// ── Toast notification ────────────────────────────────────────────────────
void BuildToast(RectTransform parent)
{
var toast = Child("Toast", parent);
Anchor(toast, new Vector2(0.05f,0.88f), new Vector2(0.95f,0.94f), Vector2.zero, Vector2.zero);
Img(toast, new Color(0.1f,0.1f,0.2f,0.92f));
TxtChild(toast, "", 30, C_YELLOW, TextAlignmentOptions.Center, bold: true);
toast.gameObject.SetActive(false);
}
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
RectTransform AddChild(string name, RectTransform parent) RectTransform Child(string name, RectTransform parent)
{ {
var go = new GameObject(name); var go = new GameObject(name);
var rt = go.AddComponent<RectTransform>(); var rt = go.AddComponent<RectTransform>();
@@ -279,40 +294,35 @@ public class InGameHUDBuilder : MonoBehaviour
return rt; return rt;
} }
Image AddImage(GameObject go, Color color) Image Img(RectTransform rt, Color c)
{ {
var img = go.AddComponent<Image>(); var img = rt.gameObject.AddComponent<Image>();
img.color = color; img.color = c;
return img; return img;
} }
TextMeshProUGUI AddTextChild(RectTransform parent, string name, string text,
float size, FontStyles style, TextAlignmentOptions align)
{
var rt = AddChild(name, parent);
Stretch(rt);
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = size;
tmp.fontStyle = style;
tmp.alignment = align;
tmp.color = Color.white;
return tmp;
}
void Anchor(RectTransform rt, Vector2 min, Vector2 max)
{
rt.anchorMin = min;
rt.anchorMax = max;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
void Stretch(RectTransform rt) void Stretch(RectTransform rt)
{ {
rt.anchorMin = Vector2.zero; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.offsetMin = Vector2.zero; }
rt.offsetMax = Vector2.zero;
// min/max by anchor + absolute offset corners
void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 offsetMin, Vector2 offsetMax)
{
rt.anchorMin = aMin; rt.anchorMax = aMax;
rt.offsetMin = offsetMin; rt.offsetMax = offsetMax;
}
TextMeshProUGUI TxtChild(RectTransform parent, string text, float size, Color color,
TextAlignmentOptions align, bool bold = false)
{
var rt = Child("Txt", parent);
Stretch(rt);
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align;
if (bold) tmp.fontStyle = FontStyles.Bold;
return tmp;
} }
} }