using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections.Generic; /// /// Centralized design tokens + styling helpers for every Canvas in the game. /// One source of truth for color, typography, spacing, animation timing, and /// emoji-capable font configuration. Replaces the per-file palette duplication /// that scattered across InGameHUDBuilder, HostLobbyUI, LobbyDisplayUI etc. /// /// Why this exists (P10): /// - "UI looks tiny on phone, bleeds off-screen during voting, font drops /// emoji glyphs" - all symptoms of palette/typography/sizing values being /// defined per-file with no shared scale. Centralizing fixes the /// whole-app inconsistency, not just the worst offender. /// - Emoji rendering on Android needs TMP_Settings.fallbackFontAssets /// pointed at a Noto Color Emoji TMP_FontAsset. We do this at runtime /// so the host project doesn't need a pre-baked TMP_Settings asset /// change (which would touch a binary scriptable object). /// /// Visual direction: /// - Dark base (#0A0E1A near-black) with one strong accent (teal-cyan /// #00C8C8). Readable in sunlight, doesn't clash with the orange/red /// impostor signals or the green map. /// - Typography ramp: one display weight, one body, one mono for codes. /// Sizes follow a major-third scale (1.25x ratio) so headlines /// dominate without shouting. /// - Spacing on an 8pt grid. Touch targets minimum 88pt (44pt @ 2x). /// - Animation: subtle. Fade 150ms, slide 200ms, ease-out. Game's tense - /// UI shouldn't be cute. /// public static class UITheme { // ── Palette ─────────────────────────────────────────────────────────────── // Names follow purpose, not literal color. Adding a new component? Use // Bg/Surface/Accent/Success/Danger/Warning/Muted - not raw hex. public static readonly Color Bg = H("#0A0E1A"); // page background public static readonly Color Surface = H("#121829"); // cards, panels public static readonly Color SurfaceAlt = H("#1A2138"); // elevated/active public static readonly Color SurfaceDim = H("#0E1322"); // recessed (input) public static readonly Color RowA = H("#1A2035"); // alt row public static readonly Color RowB = H("#161C2E"); // alt row public static readonly Color Border = new Color(1f, 1f, 1f, 0.10f); public static readonly Color Accent = H("#00C8C8"); // teal-cyan, primary CTA public static readonly Color AccentDim = H("#0A8A8A"); // pressed state public static readonly Color Success = H("#2DB84B"); // task done, you-voted public static readonly Color Danger = H("#E04040"); // impostor, eject, kill public static readonly Color Warning = H("#F08C1A"); // sabotage, meltdown public static readonly Color Caution = H("#FFB800"); // reconnect, info public static readonly Color TextHi = new Color(0.96f, 0.97f, 0.99f); // primary public static readonly Color TextMid = new Color(0.78f, 0.83f, 0.91f); // secondary public static readonly Color TextLo = new Color(0.55f, 0.62f, 0.75f); // tertiary/disabled public static readonly Color TextOnAccent= H("#001818"); // black-ish on bright accent static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; } // ── Typography ──────────────────────────────────────────────────────────── // Sizes are in TMP "px" units which scale with the CanvasScaler reference. // Our reference is 1080x1920 portrait; these values produce the intended // physical size on a typical 6" phone (~5mm tall body text). public const float FontDisplay = 64f; // hero (game-end win/loss) public const float FontHeadline = 44f; // section header public const float FontTitle = 32f; // panel header public const float FontBody = 26f; // standard text public const float FontSmall = 22f; // captions, helper text public const float FontTiny = 18f; // tiny meta, debug // Action button text scales separately because thumb-zone CTAs need to // dominate visually even on a small screen. public const float FontActionBtn= 40f; // ── Spacing (8pt grid) ──────────────────────────────────────────────────── public const float S1 = 4f; public const float S2 = 8f; public const float S3 = 12f; public const float S4 = 16f; public const float S5 = 24f; public const float S6 = 32f; public const float S7 = 48f; public const float S8 = 64f; public const float S9 = 96f; // ── Touch targets ───────────────────────────────────────────────────────── // Minimum heights tuned for 1080x1920 reference. CanvasScaler keeps these // visually consistent across phone sizes. public const float MinTapHeight = 88f; // 44pt @ 2x public const float StandardBtn = 110f; // typical button height public const float HeroBtn = 140f; // primary CTA public const float VoteRowMin = 96f; public const float VoteRowMax = 144f; // ── Corner radii (target value; needs an Image with a rounded sprite to // realize, but keeping the token here lets future polish hit one place) ── public const float Radius1 = 8f; public const float Radius2 = 14f; public const float Radius3 = 22f; // ── Animation timing ────────────────────────────────────────────────────── public const float DurFade = 0.15f; public const float DurSlide = 0.20f; public const float DurSnappy = 0.10f; public const float DurAmbient = 0.40f; // slow ambient pulses // ── Canvas reference (mirrored from InGameHUDBuilder for consistency) ──── public const float ReferenceWidth = 1080f; public const float ReferenceHeight = 1920f; public const float MatchWidthOrHeight = 0.5f; // ── Emoji font fallback ─────────────────────────────────────────────────── // Tracks whether we've already tried to wire a Noto Color Emoji fallback // into TMP_Settings. Once attempted (success or fail), we don't keep // re-poking - mobile resource lookups aren't free. static bool _emojiFallbackAttempted; /// /// Configures the project's TMP fallback font chain so emoji codepoints /// (📡 ⚙️ 🗳️ 👥 etc.) used throughout the UI render properly. Looks for a /// TMP_FontAsset named "NotoColorEmoji" (or "EmojiFallback") under /// `Resources/Fonts/` first; if that's missing, walks the OS font cache /// the platform exposes via Font.OSFontNames as a last-resort fallback. /// /// Idempotent + cheap on subsequent calls. Callers don't need to gate. /// public static void EnsureEmojiFontFallback() { if (_emojiFallbackAttempted) return; _emojiFallbackAttempted = true; // Primary: explicit Resources lookup. Drop a pre-built // `NotoColorEmoji.asset` (TMP_FontAsset) into Assets/Resources/Fonts/ // for Android/iOS - that's the production path. TMP_FontAsset emoji = Resources.Load("Fonts/NotoColorEmoji"); if (emoji == null) emoji = Resources.Load("Fonts/EmojiFallback"); if (emoji != null) { var def = TMP_Settings.defaultFontAsset; if (def != null) { if (def.fallbackFontAssetTable == null) def.fallbackFontAssetTable = new List(); if (!def.fallbackFontAssetTable.Contains(emoji)) { def.fallbackFontAssetTable.Add(emoji); Debug.Log("[UITheme] Emoji fallback wired: " + emoji.name); } } return; } // No bundled emoji font. Log once so the user knows to either drop // one in or strip emojis from labels - silent failure here means // missing-glyph rectangles surface in the UI without explanation. Debug.LogWarning( "[UITheme] No NotoColorEmoji TMP_FontAsset found under Resources/Fonts/. " + "Emoji glyphs in UI text will render as tofu boxes. " + "Drop a TMP-baked Noto Color Emoji asset at " + "Assets/Resources/Fonts/NotoColorEmoji.asset (any TMP_FontAsset name " + "containing 'Emoji' also matches via the secondary lookup)."); } // ── Style helpers ───────────────────────────────────────────────────────── // Apply consistent styling without repeating every property. Keep these // single-purpose; if you find yourself adding flags, that's a sign you // should add a new helper instead. /// Solid Image with theme-consistent color. Returns the Image. public static Image Surf(RectTransform rt, Color color) { var img = rt.gameObject.GetComponent() ?? rt.gameObject.AddComponent(); img.color = color; return img; } /// Stretches a RectTransform to fill its parent. public static void Stretch(RectTransform rt) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } /// Apply the project's standard CanvasScaler config. 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(ReferenceWidth, ReferenceHeight); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = MatchWidthOrHeight; scaler.referencePixelsPerUnit = 100f; } /// /// Anchor a RectTransform to the screen's safe rectangle (notch/punch-hole /// safe). Children inherit the inset. /// 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; } /// /// Style a TMP text field as a labeled text element. Sets size, color, /// alignment, weight - and ensures the emoji fallback chain is wired so /// any emoji glyphs in the text render properly on the first frame. /// public static void StyleText(TMP_Text tmp, float size, Color color, TextAlignmentOptions align, bool bold = false, FontStyles extra = FontStyles.Normal) { if (tmp == null) return; EnsureEmojiFontFallback(); tmp.fontSize = size; tmp.color = color; tmp.alignment = align; tmp.fontStyle = (bold ? FontStyles.Bold : FontStyles.Normal) | extra; tmp.enableWordWrapping = true; } /// Standard primary CTA button styling - bg, text, target graphic. public static Button StylePrimaryButton(RectTransform rt, string label, Color? bgColor = null, Color? textColor = null, float? fontSize = null) { var bg = Surf(rt, bgColor ?? Accent); var btn = rt.gameObject.GetComponent