using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections.Generic; /// /// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity). /// /// Call BuildNow() from GameManager.OnSceneLoaded BEFORE BindClientScene so GameManager_UI /// can locate named children. /// /// Named elements expected by GameManager_UI (Transform.Find / FindTMP): /// ActionButton – proxim action button /// SabotagePanel – top-of-screen sabotage banner /// SabotageTimer – TMP countdown text inside SabotagePanel /// MeetingPanel – full-screen voting overlay /// MeetingHeader – TMP title (event type) /// MeetingPhaseLabel – TMP sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL) /// MeetingPhaseProgressBar – Image filled, drains with countdown /// MeetingPhaseCountdown – TMP "0:24" countdown text /// MeetingPlayerList – TMP player list (text fallback) /// MyVoteIndicator – TMP "You voted for: X" strip /// SkipButton – skip-vote button /// VoteResultPanel – sub-panel shown after voting /// VoteResult – TMP result text /// GameEndPanel – full-screen end-of-game overlay /// GameEndText – TMP result text /// ReconnectOverlay – full-screen "reconnecting..." overlay /// ReconnectMessage – TMP "Reconnecting..." headline /// ReconnectSubtext – TMP secondary message /// KillCooldown – TMP kill-cooldown label /// TaskList – TMP task name list /// TaskProgress – TMP global task progress /// Toast – TMP toast notification /// public class InGameHUDBuilder : MonoBehaviour { // ── Palette ─────────────────────────────────────────────────────────────── // Aliases to UITheme so this file's existing local references keep working // without a wholesale rename pass. New code should call UITheme.* directly; // the C_* names stay as a transitional crutch for in-flight edits. static readonly Color C_BG = UITheme.Bg; static readonly Color C_BAR = UITheme.Surface; static readonly Color C_ACCENT = UITheme.Accent; static readonly Color C_GREEN = UITheme.Success; static readonly Color C_RED = UITheme.Danger; static readonly Color C_ORANGE = UITheme.Warning; static readonly Color C_YELLOW = UITheme.Caution; static readonly Color C_MUTED = UITheme.TextLo; static readonly Color C_ROW_A = UITheme.RowA; static readonly Color C_ROW_B = UITheme.RowB; private bool _built; // Reference resolution constants kept here as public surface so external // callers (other UI scripts that grew to use these) don't break. Forward // to UITheme so there's still one source of truth. public const float kReferenceWidth = UITheme.ReferenceWidth; public const float kReferenceHeight = UITheme.ReferenceHeight; public const float kMatchWidthHeight = UITheme.MatchWidthOrHeight; public void BuildNow() { if (!_built) { _built = true; Build(); } } void Start() { if (!_built) Build(); } void Build() { var rt = GetComponent(); if (rt == null) return; // Wire up emoji fallback before any TMP component renders. Idempotent // and cheap on subsequent calls. UITheme.EnsureEmojiFontFallback(); // Make sure the host canvas has a CanvasScaler with our reference // resolution. Without this, RectTransform offsets are interpreted as // raw pixels and the layout looks correct only on whichever device // the project was last opened against. ConfigureCanvasScaler(GetComponentInParent()); // Apply Screen.safeArea so iOS notches and Android punch-hole cameras // don't eat the top/bottom bar. We anchor the host RectTransform to // the safe rectangle so all child anchors inherit the inset. rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; ApplySafeArea(rt); BuildTopBar(rt); BuildTaskPanel(rt); BuildTaskProgress(rt); BuildBottomBar(rt); BuildActionButton(rt); BuildSabotagePanel(rt); BuildMeetingPanel(rt); BuildGameEndPanel(rt); BuildReconnectOverlay(rt); BuildSpectatePanel(rt); BuildToast(rt); } /// /// Apply the project's standard CanvasScaler config to a Canvas. Idempotent - /// adds the component if missing, otherwise updates settings in-place. /// public static void ConfigureCanvasScaler(Canvas canvas) { if (canvas == null) return; var scaler = canvas.GetComponent() ?? canvas.gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(kReferenceWidth, kReferenceHeight); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = kMatchWidthHeight; scaler.referencePixelsPerUnit = 100f; } /// /// Anchors the given RectTransform to the screen's safe rectangle, so all /// children inherit the inset. Called once at build time; the safe area /// rarely changes after the app launches and a full HUD rebuild on rotation /// would be the simpler way to handle that case. /// public static void ApplySafeArea(RectTransform rt) { if (rt == null) return; var safe = Screen.safeArea; var screenSize = new Vector2(Screen.width, Screen.height); if (screenSize.x <= 0 || screenSize.y <= 0) return; Vector2 aMin = safe.position; Vector2 aMax = safe.position + safe.size; aMin.x /= screenSize.x; aMin.y /= screenSize.y; aMax.x /= screenSize.x; aMax.y /= screenSize.y; rt.anchorMin = aMin; rt.anchorMax = aMax; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } // ── Top bar ─────────────────────────────────────────────────────────────── void BuildTopBar(RectTransform parent) { var bar = Child("_TopBar", parent); Anchor(bar, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-90f), Vector2.zero); bar.pivot = new Vector2(0.5f,1f); Img(bar, C_BAR); // Kill cooldown – right half, hidden by default var cd = Child("KillCooldown", bar); cd.anchorMin = new Vector2(0.5f, 0); cd.anchorMax = Vector2.one; cd.offsetMin = new Vector2(0, 6); cd.offsetMax = new Vector2(-12, -6); var cdTmp = cd.gameObject.AddComponent(); cdTmp.text = ""; cdTmp.fontSize = 32; cdTmp.color = C_ORANGE; cdTmp.fontStyle = FontStyles.Bold; cdTmp.alignment = TextAlignmentOptions.MidlineRight; cd.gameObject.SetActive(false); } // ── Task panel (right side) ─────────────────────────────────────────────── void BuildTaskPanel(RectTransform parent) { var panel = Child("_TaskPanel", parent); panel.anchorMin = new Vector2(1,0.35f); panel.anchorMax = new Vector2(1,0.88f); panel.pivot = new Vector2(1,0.5f); panel.sizeDelta = new Vector2(280,0); Img(panel, new Color(0.05f,0.06f,0.12f,0.85f)); var hdr = Child("_Hdr", panel); Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-44), Vector2.zero); hdr.pivot = new Vector2(0.5f,1f); Img(hdr, new Color(0.2f,0.6f,1f,0.5f)); TxtChild(hdr,"MY TASKS",26,Color.white,TextAlignmentOptions.Center,bold:true); var body = Child("TaskList", panel); body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one; body.offsetMin = new Vector2(8,8); body.offsetMax = new Vector2(-8,-48); var t = body.gameObject.AddComponent(); t.text = ""; t.fontSize = 22; t.color = Color.white; t.alignment = TextAlignmentOptions.TopLeft; t.enableWordWrapping = true; } // ── Task progress (above bottom bar) ───────────────────────────────────── void BuildTaskProgress(RectTransform parent) { var prog = Child("TaskProgress", parent); Anchor(prog, new Vector2(0,0), new Vector2(1,0), new Vector2(-20,120), new Vector2(20,160)); var t = prog.gameObject.AddComponent(); t.text = ""; t.fontSize = 28; t.color = Color.white; t.fontStyle = FontStyles.Bold; t.alignment = TextAlignmentOptions.Center; } // ── Bottom bar ──────────────────────────────────────────────────────────── void BuildBottomBar(RectTransform parent) { var bar = Child("_BottomBar", parent); Anchor(bar, Vector2.zero, new Vector2(1,0), Vector2.zero, new Vector2(0,110)); bar.pivot = new Vector2(0.5f,0); Img(bar, C_BAR); var recBtn = Child("_RecenterBtn", bar); recBtn.anchorMin = new Vector2(0.82f,0.08f); recBtn.anchorMax = new Vector2(0.98f,0.92f); var recBg = Img(recBtn, C_ACCENT); var recButton = recBtn.gameObject.AddComponent