329 lines
16 KiB
C#
329 lines
16 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
using System.Collections.Generic;
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// MeetingPlayerList – TMP player list (text fallback)
|
||
/// 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
|
||
/// KillCooldown – TMP kill-cooldown label
|
||
/// TaskList – TMP task name list
|
||
/// TaskProgress – TMP global task progress
|
||
/// Toast – TMP toast notification
|
||
/// </summary>
|
||
public class InGameHUDBuilder : MonoBehaviour
|
||
{
|
||
// ── Palette ───────────────────────────────────────────────────────────────
|
||
static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; }
|
||
static readonly Color C_BG = H("#0D0F1A");
|
||
static readonly Color C_BAR = H("#141927");
|
||
static readonly Color C_ACCENT = H("#3399FF");
|
||
static readonly Color C_GREEN = H("#2DB84B");
|
||
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;
|
||
|
||
public void BuildNow() { if (!_built) { _built = true; Build(); } }
|
||
void Start() { if (!_built) Build(); }
|
||
|
||
void Build()
|
||
{
|
||
var rt = GetComponent<RectTransform>();
|
||
if (rt == null) return;
|
||
|
||
BuildTopBar(rt);
|
||
BuildTaskPanel(rt);
|
||
BuildTaskProgress(rt);
|
||
BuildBottomBar(rt);
|
||
BuildActionButton(rt);
|
||
BuildSabotagePanel(rt);
|
||
BuildMeetingPanel(rt);
|
||
BuildGameEndPanel(rt);
|
||
BuildToast(rt);
|
||
}
|
||
|
||
// ── 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<TextMeshProUGUI>();
|
||
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<TextMeshProUGUI>();
|
||
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<TextMeshProUGUI>();
|
||
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<Button>();
|
||
recButton.targetGraphic = recBg;
|
||
recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter());
|
||
TxtChild(recBtn,"⊙",42,Color.white,TextAlignmentOptions.Center,bold:true);
|
||
}
|
||
|
||
// ── Action button (DIRECT child so Transform.Find works) ─────────────────
|
||
void BuildActionButton(RectTransform parent)
|
||
{
|
||
var btn = Child("ActionButton", parent);
|
||
Anchor(btn, new Vector2(0.15f,0), new Vector2(0.80f,0), new Vector2(0,12), new Vector2(0,102));
|
||
btn.pivot = new Vector2(0.5f,0);
|
||
var bg = Img(btn, C_GREEN);
|
||
var button = btn.gameObject.AddComponent<Button>();
|
||
button.targetGraphic = bg;
|
||
|
||
var txtRt = Child("Text", btn);
|
||
Stretch(txtRt);
|
||
var tmp = txtRt.gameObject.AddComponent<TextMeshProUGUI>();
|
||
tmp.text = "ACTION"; tmp.fontSize = 44; tmp.fontStyle = FontStyles.Bold;
|
||
tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;
|
||
|
||
btn.gameObject.SetActive(false);
|
||
}
|
||
|
||
// ── Sabotage panel (top strip) ────────────────────────────────────────────
|
||
void BuildSabotagePanel(RectTransform parent)
|
||
{
|
||
var panel = Child("SabotagePanel", parent);
|
||
panel.anchorMin = new Vector2(0,1); panel.anchorMax = new Vector2(1,1);
|
||
panel.pivot = new Vector2(0.5f,1f); panel.sizeDelta = new Vector2(0,80);
|
||
Img(panel, new Color(0.76f,0.19f,0.19f,0.92f));
|
||
|
||
var timer = Child("SabotageTimer", panel);
|
||
Stretch(timer);
|
||
var t = timer.gameObject.AddComponent<TextMeshProUGUI>();
|
||
t.text = "SABOTAGE!"; t.fontSize = 48; t.fontStyle = FontStyles.Bold;
|
||
t.color = Color.white; t.alignment = TextAlignmentOptions.Center;
|
||
|
||
panel.gameObject.SetActive(false);
|
||
}
|
||
|
||
// ── Meeting panel (full screen overlay) ───────────────────────────────────
|
||
void BuildMeetingPanel(RectTransform parent)
|
||
{
|
||
var panel = Child("MeetingPanel", parent);
|
||
Stretch(panel);
|
||
Img(panel, new Color(0.04f,0.05f,0.14f,0.97f));
|
||
|
||
// Header
|
||
var hdr = Child("MeetingHeader", panel);
|
||
Anchor(hdr, new Vector2(0,0.86f), new Vector2(1,1), Vector2.zero, Vector2.zero);
|
||
var hdrTmp = hdr.gameObject.AddComponent<TextMeshProUGUI>();
|
||
hdrTmp.text = "EMERGENCY MEETING"; hdrTmp.fontSize = 52;
|
||
hdrTmp.fontStyle = FontStyles.Bold; hdrTmp.color = C_ORANGE;
|
||
hdrTmp.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);
|
||
}
|
||
|
||
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)
|
||
{
|
||
var panel = Child("GameEndPanel", parent);
|
||
Stretch(panel);
|
||
Img(panel, new Color(0,0,0,0.90f));
|
||
|
||
// Result text (upper half)
|
||
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>();
|
||
tmp.text = ""; tmp.fontSize = 72; tmp.fontStyle = FontStyles.Bold;
|
||
tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;
|
||
|
||
// "Return to lobby" button – owner only (GameManager_UI shows/hides it)
|
||
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);
|
||
}
|
||
|
||
// ── 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 ───────────────────────────────────────────────────────────────
|
||
|
||
RectTransform Child(string name, RectTransform parent)
|
||
{
|
||
var go = new GameObject(name);
|
||
var rt = go.AddComponent<RectTransform>();
|
||
rt.SetParent(parent, false);
|
||
rt.localScale = Vector3.one;
|
||
return rt;
|
||
}
|
||
|
||
Image Img(RectTransform rt, Color c)
|
||
{
|
||
var img = rt.gameObject.AddComponent<Image>();
|
||
img.color = c;
|
||
return img;
|
||
}
|
||
|
||
void Stretch(RectTransform rt)
|
||
{
|
||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||
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;
|
||
}
|
||
}
|
||
|