Files
GeoSusGame/Assets/Scripts/InGameHUDBuilder.cs

329 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}