GeoSus
This commit is contained in:
@@ -13,14 +13,21 @@ using System.Collections.Generic;
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -29,20 +36,29 @@ using System.Collections.Generic;
|
||||
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");
|
||||
// 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(); }
|
||||
|
||||
@@ -51,6 +67,25 @@ public class InGameHUDBuilder : MonoBehaviour
|
||||
var rt = GetComponent<RectTransform>();
|
||||
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<Canvas>());
|
||||
|
||||
// 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);
|
||||
@@ -59,9 +94,51 @@ public class InGameHUDBuilder : MonoBehaviour
|
||||
BuildSabotagePanel(rt);
|
||||
BuildMeetingPanel(rt);
|
||||
BuildGameEndPanel(rt);
|
||||
BuildReconnectOverlay(rt);
|
||||
BuildSpectatePanel(rt);
|
||||
BuildToast(rt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the project's standard CanvasScaler config to a Canvas. Idempotent -
|
||||
/// adds the component if missing, otherwise updates settings in-place.
|
||||
/// </summary>
|
||||
public static void ConfigureCanvasScaler(Canvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
var scaler = canvas.GetComponent<CanvasScaler>()
|
||||
?? canvas.gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(kReferenceWidth, kReferenceHeight);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = kMatchWidthHeight;
|
||||
scaler.referencePixelsPerUnit = 100f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
@@ -166,45 +243,93 @@ public class InGameHUDBuilder : MonoBehaviour
|
||||
}
|
||||
|
||||
// ── Meeting panel (full screen overlay) ───────────────────────────────────
|
||||
//
|
||||
// Layout (vertical, top to bottom):
|
||||
// 0.90 - 1.00 MeetingHeader - "EMERGENCY MEETING" / "BODY REPORTED"
|
||||
// 0.83 - 0.90 MeetingPhaseLabel - "ARRIVAL" / "DISCUSSION" / "VOTING" / "RESULTS"
|
||||
// 0.79 - 0.83 MeetingPhaseProgressBar - thin fill bar that drains as countdown runs
|
||||
// 0.74 - 0.79 MeetingPhaseCountdown - "0:24" countdown text
|
||||
// 0.18 - 0.74 _MeetingScroll - vote rows (or VoteResultPanel when resolved)
|
||||
// 0.14 - 0.18 MyVoteIndicator - "You voted for: X" or "Voting hasn't started"
|
||||
// 0.04 - 0.14 SkipButton - skip vote (will move into the row list in P2.7)
|
||||
void BuildMeetingPanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("MeetingPanel", parent);
|
||||
Stretch(panel);
|
||||
Img(panel, new Color(0.04f,0.05f,0.14f,0.97f));
|
||||
|
||||
// Header
|
||||
// Title
|
||||
var hdr = Child("MeetingHeader", panel);
|
||||
Anchor(hdr, new Vector2(0,0.86f), new Vector2(1,1), Vector2.zero, Vector2.zero);
|
||||
Anchor(hdr, new Vector2(0,0.90f), 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;
|
||||
|
||||
// Sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL)
|
||||
var phaseLbl = Child("MeetingPhaseLabel", panel);
|
||||
Anchor(phaseLbl, new Vector2(0,0.83f), new Vector2(1,0.90f), Vector2.zero, Vector2.zero);
|
||||
var phaseLblTmp = phaseLbl.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
phaseLblTmp.text = ""; phaseLblTmp.fontSize = 32;
|
||||
phaseLblTmp.fontStyle = FontStyles.Bold; phaseLblTmp.color = C_ACCENT;
|
||||
phaseLblTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Phase progress bar (drains as countdown elapses)
|
||||
var progBg = Child("MeetingPhaseProgressBg", panel);
|
||||
Anchor(progBg, new Vector2(0.10f,0.79f), new Vector2(0.90f,0.83f), Vector2.zero, Vector2.zero);
|
||||
Img(progBg, new Color(0.10f,0.13f,0.22f,1f));
|
||||
var progFill = Child("MeetingPhaseProgressBar", progBg);
|
||||
progFill.anchorMin = new Vector2(0,0); progFill.anchorMax = new Vector2(1,1);
|
||||
progFill.offsetMin = Vector2.zero; progFill.offsetMax = Vector2.zero;
|
||||
var fillImg = progFill.gameObject.AddComponent<Image>();
|
||||
fillImg.color = C_ACCENT;
|
||||
fillImg.type = Image.Type.Filled;
|
||||
fillImg.fillMethod = Image.FillMethod.Horizontal;
|
||||
fillImg.fillAmount = 0f;
|
||||
|
||||
// Countdown text under the bar
|
||||
var cd = Child("MeetingPhaseCountdown", panel);
|
||||
Anchor(cd, new Vector2(0,0.74f), new Vector2(1,0.79f), Vector2.zero, Vector2.zero);
|
||||
var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
cdTmp.text = ""; cdTmp.fontSize = 28;
|
||||
cdTmp.color = new Color(0.8f,0.85f,0.95f);
|
||||
cdTmp.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);
|
||||
Anchor(scrollArea, new Vector2(0,0.18f), new Vector2(1,0.74f), 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));
|
||||
Anchor(fallback, new Vector2(0,0.18f), new Vector2(1,0.74f), 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
|
||||
fallback.gameObject.SetActive(false); // hidden - scroll list used instead
|
||||
|
||||
// Skip button
|
||||
// "Your vote: X" indicator strip
|
||||
var myVote = Child("MyVoteIndicator", panel);
|
||||
Anchor(myVote, new Vector2(0.05f,0.14f), new Vector2(0.95f,0.18f), Vector2.zero, Vector2.zero);
|
||||
var myVoteTmp = myVote.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
myVoteTmp.text = ""; myVoteTmp.fontSize = 26;
|
||||
myVoteTmp.color = new Color(0.73f,0.8f,0.88f);
|
||||
myVoteTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Skip button (will be merged into the vote list as a row in P2.7)
|
||||
var skip = Child("SkipButton", panel);
|
||||
Anchor(skip, new Vector2(0.05f,0.04f), new Vector2(0.95f,0.18f), Vector2.zero, Vector2.zero);
|
||||
Anchor(skip, new Vector2(0.05f,0.04f), new Vector2(0.95f,0.14f), 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)
|
||||
// Vote-result sub-panel - now sized to *replace* the scroll area when
|
||||
// results arrive, instead of squeezing into the bottom strip alongside
|
||||
// skip/my-vote (which caused the previous overlap).
|
||||
var resultPanel = Child("VoteResultPanel", panel);
|
||||
Anchor(resultPanel, new Vector2(0,0.04f), new Vector2(1,0.22f), Vector2.zero, Vector2.zero);
|
||||
Anchor(resultPanel, new Vector2(0,0.18f), new Vector2(1,0.74f), Vector2.zero, Vector2.zero);
|
||||
Img(resultPanel, new Color(0.05f,0.05f,0.15f,0.95f));
|
||||
var resultText = Child("VoteResult", resultPanel);
|
||||
Stretch(resultText);
|
||||
@@ -273,6 +398,58 @@ public class InGameHUDBuilder : MonoBehaviour
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Reconnect overlay (visible while the socket is dropping/reconnecting) ─
|
||||
void BuildReconnectOverlay(RectTransform parent)
|
||||
{
|
||||
var panel = Child("ReconnectOverlay", parent);
|
||||
Stretch(panel);
|
||||
Img(panel, new Color(0.04f,0.05f,0.14f,0.90f));
|
||||
|
||||
var msg = Child("ReconnectMessage", panel);
|
||||
Anchor(msg, new Vector2(0,0.45f), new Vector2(1,0.65f), Vector2.zero, Vector2.zero);
|
||||
var msgTmp = msg.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
msgTmp.text = "Reconnecting..."; msgTmp.fontSize = 56;
|
||||
msgTmp.fontStyle = FontStyles.Bold; msgTmp.color = C_YELLOW;
|
||||
msgTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var sub = Child("ReconnectSubtext", panel);
|
||||
Anchor(sub, new Vector2(0,0.35f), new Vector2(1,0.45f), Vector2.zero, Vector2.zero);
|
||||
var subTmp = sub.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
subTmp.text = "Server keeps your slot for up to 60 seconds.";
|
||||
subTmp.fontSize = 28; subTmp.color = new Color(0.73f,0.8f,0.88f);
|
||||
subTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Spectate panel (visible after death; dim banner so the player still
|
||||
// sees the live map but understands they're spectating) ────────────────
|
||||
void BuildSpectatePanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("SpectatePanel", parent);
|
||||
// Top strip only - we don't want to occlude the map. Just enough to
|
||||
// communicate "you're dead, watching live."
|
||||
panel.anchorMin = new Vector2(0, 1); panel.anchorMax = new Vector2(1, 1);
|
||||
panel.pivot = new Vector2(0.5f, 1f); panel.sizeDelta = new Vector2(0, 96);
|
||||
Img(panel, new Color(0f, 0f, 0f, 0.65f));
|
||||
|
||||
var label = Child("SpectateLabel", panel);
|
||||
Stretch(label);
|
||||
var t = label.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
UITheme.StyleText(t, UITheme.FontTitle, UITheme.TextHi,
|
||||
TextAlignmentOptions.Center, bold: true);
|
||||
t.text = "👻 YOU ARE DEAD - SPECTATING";
|
||||
|
||||
var sub = Child("SpectateSub", panel);
|
||||
Anchor(sub, new Vector2(0, 0), new Vector2(1, 0.45f), Vector2.zero, Vector2.zero);
|
||||
var st = sub.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
UITheme.StyleText(st, UITheme.FontSmall, UITheme.TextMid,
|
||||
TextAlignmentOptions.Center);
|
||||
st.text = "Crew can finish tasks as ghosts. Impostors cannot kill you.";
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Toast notification ────────────────────────────────────────────────────
|
||||
void BuildToast(RectTransform parent)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user