From 53fd617abedc35231a94a863689822bd9468a8d8 Mon Sep 17 00:00:00 2001 From: Thibault Pouch Date: Fri, 13 Mar 2026 19:28:19 +0100 Subject: [PATCH] feat: add RetroMainMenuUI script and associated meta file for main menu functionality --- Assets/Code/Scripts/UI.meta | 8 + Assets/Code/Scripts/UI/RetroMainMenuUI.cs | 363 ++++++++++++++++++ .../Code/Scripts/UI/RetroMainMenuUI.cs.meta | 2 + 3 files changed, 373 insertions(+) create mode 100644 Assets/Code/Scripts/UI.meta create mode 100644 Assets/Code/Scripts/UI/RetroMainMenuUI.cs create mode 100644 Assets/Code/Scripts/UI/RetroMainMenuUI.cs.meta diff --git a/Assets/Code/Scripts/UI.meta b/Assets/Code/Scripts/UI.meta new file mode 100644 index 0000000..a85f6a0 --- /dev/null +++ b/Assets/Code/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e88664529cd503644b2b92f055895969 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Code/Scripts/UI/RetroMainMenuUI.cs b/Assets/Code/Scripts/UI/RetroMainMenuUI.cs new file mode 100644 index 0000000..7350d09 --- /dev/null +++ b/Assets/Code/Scripts/UI/RetroMainMenuUI.cs @@ -0,0 +1,363 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using TMPro; +#if ENABLE_INPUT_SYSTEM +using UnityEngine.InputSystem.UI; +#endif + +public class RetroMainMenuUI : MonoBehaviour +{ + private Canvas m_MenuCanvas; + private bool m_MenuActive; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Bootstrap() + { + if (Object.FindFirstObjectByType() != null) + { + return; + } + + GameObject root = new("RetroMainMenuUI"); + root.AddComponent(); + } + + private void Awake() + { + m_MenuActive = true; + Time.timeScale = 0f; + ApplyMenuCursorState(); + + BuildMenu(); + EnsureEventSystem(); + } + + private void LateUpdate() + { + if (!m_MenuActive) + { + return; + } + + // Some gameplay scripts lock the cursor during Start/Update. + // Force menu cursor state while the menu is active. + ApplyMenuCursorState(); + } + + private void BuildMenu() + { + Color bgColor = HexToColor("001e26"); + Color panelColor = HexToColor("517567"); + Color titleColor = HexToColor("f3d58d"); + Color TextNormalColor = HexToColor("eb9843"); + Color textWarningColor = HexToColor("c12204"); + Color shadowColor = HexToColor("520805"); + + GameObject canvasObject = new("MainMenuCanvas"); + Canvas canvas = canvasObject.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.sortingOrder = 10000; + m_MenuCanvas = canvas; + + CanvasScaler scaler = canvasObject.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920f, 1080f); + scaler.matchWidthOrHeight = 0.5f; + + canvasObject.AddComponent(); + + // Background + GameObject background = CreateImage("Background", canvasObject.transform, bgColor); + StretchToFull(background.GetComponent()); + + // Decorative horizontal lines (scanline aesthetic) + CreateLine("TopLine", background.transform, new Rect(0, -60, 0, 4), panelColor, AnchorPreset.TopStretch); + CreateLine("BotLine", background.transform, new Rect(0, 60, 0, 4), panelColor, AnchorPreset.BottomStretch); + + // --- LEFT PANEL --- + GameObject leftPanel = new GameObject("LeftPanel", typeof(RectTransform)); + leftPanel.transform.SetParent(canvasObject.transform, false); + RectTransform leftRect = leftPanel.GetComponent(); + leftRect.anchorMin = new Vector2(0.08f, 0.1f); + leftRect.anchorMax = new Vector2(0.45f, 0.9f); + leftRect.offsetMin = Vector2.zero; + leftRect.offsetMax = Vector2.zero; + + // Title + TextMeshProUGUI titleText = CreateTMP("Title", leftPanel.transform, "HEADLESS HAZARD", titleColor, 72, TextAlignmentOptions.BottomLeft); + RectTransform titleRect = titleText.GetComponent(); + titleRect.anchorMin = new Vector2(0f, 0.85f); + titleRect.anchorMax = new Vector2(1f, 1f); + titleRect.offsetMin = Vector2.zero; + titleRect.offsetMax = Vector2.zero; + titleText.fontStyle = FontStyles.Bold; + + // Title Shadow + TextMeshProUGUI titleShadow = CreateTMP("TitleShadow", leftPanel.transform, "HEADLESS HAZARD", shadowColor, 72, TextAlignmentOptions.BottomLeft); + RectTransform shadowRect = titleShadow.GetComponent(); + shadowRect.anchorMin = new Vector2(0f, 0.85f); + shadowRect.anchorMax = new Vector2(1f, 1f); + shadowRect.offsetMin = new Vector2(4f, -4f); // apply drop shadow offset + shadowRect.offsetMax = new Vector2(4f, -4f); + titleShadow.fontStyle = FontStyles.Bold; + titleShadow.transform.SetSiblingIndex(0); // push behind title + + // Subtitle / Decorative Status + TextMeshProUGUI subText = CreateTMP("Subtitle", leftPanel.transform, "SYSTEM_BOOT // OS.ACTIVE_ ", panelColor, 20, TextAlignmentOptions.TopLeft); + RectTransform subRect = subText.GetComponent(); + subRect.anchorMin = new Vector2(0f, 0.80f); + subRect.anchorMax = new Vector2(1f, 0.85f); + subRect.offsetMin = Vector2.zero; + subRect.offsetMax = Vector2.zero; + + // Button Group + GameObject buttonGroup = new("ButtonGroup", typeof(RectTransform), typeof(VerticalLayoutGroup)); + buttonGroup.transform.SetParent(leftPanel.transform, false); + RectTransform groupRect = buttonGroup.GetComponent(); + groupRect.anchorMin = new Vector2(0f, 0f); + groupRect.anchorMax = new Vector2(1f, 0.65f); + groupRect.offsetMin = Vector2.zero; + groupRect.offsetMax = Vector2.zero; + + VerticalLayoutGroup layout = buttonGroup.GetComponent(); + layout.childAlignment = TextAnchor.UpperLeft; + layout.spacing = 16f; + layout.childControlWidth = true; + layout.childControlHeight = false; + + CreateTextButton(buttonGroup.transform, "> INITIALIZE_PLAY", TextNormalColor, titleColor, () => + { + Debug.Log("Play clicked."); + OnPlayClicked(); + }); + + CreateTextButton(buttonGroup.transform, "> CONFIGURE_PARAMS", TextNormalColor, titleColor, () => + { + Debug.Log("Options clicked."); + }); + + CreateTextButton(buttonGroup.transform, "> TERMINATE_PROCESS", TextNormalColor, textWarningColor, () => + { + Debug.Log("Quit clicked."); + Application.Quit(); + }); + + + // --- RIGHT PANEL (Level Info) --- + GameObject rightPanel = new GameObject("RightPanel", typeof(RectTransform)); + rightPanel.transform.SetParent(canvasObject.transform, false); + RectTransform rightRect = rightPanel.GetComponent(); + rightRect.anchorMin = new Vector2(0.55f, 0.4f); + rightRect.anchorMax = new Vector2(0.92f, 0.82f); + rightRect.offsetMin = Vector2.zero; + rightRect.offsetMax = Vector2.zero; + + // Right side Border lines + CreateLine("R_Top", rightPanel.transform, new Rect(0, 0, 0, 2), panelColor, AnchorPreset.TopStretch); + CreateLine("R_Bot", rightPanel.transform, new Rect(0, 0, 0, 2), panelColor, AnchorPreset.BottomStretch); + CreateLine("R_Left", rightPanel.transform, new Rect(0, 0, 2, 0), panelColor, AnchorPreset.LeftStretch); + CreateLine("R_Right", rightPanel.transform, new Rect(0, 0, 2, 0), panelColor, AnchorPreset.RightStretch); + + // Right Panel Headers + TextMeshProUGUI headerText = CreateTMP("LevelHeader", rightPanel.transform, "CURRENT_SECTOR", panelColor, 24, TextAlignmentOptions.TopLeft); + headerText.GetComponent().anchorMin = new Vector2(0f, 1f); + headerText.GetComponent().anchorMax = new Vector2(1f, 1f); + headerText.GetComponent().anchoredPosition = new Vector2(20f, -20f); + + // Big Level Text + TextMeshProUGUI levelText = CreateTMP("LevelNumber", rightPanel.transform, "LEVEL 01", textWarningColor, 140, TextAlignmentOptions.Center); + StretchToFull(levelText.GetComponent()); + levelText.fontStyle = FontStyles.Bold; + + // Decorative status + TextMeshProUGUI statusText = CreateTMP("LevelStatus", rightPanel.transform, "[ STATUS: OPTIMAL ]", panelColor, 24, TextAlignmentOptions.BottomRight); + statusText.GetComponent().anchorMin = new Vector2(0f, 0f); + statusText.GetComponent().anchorMax = new Vector2(1f, 0f); + statusText.GetComponent().anchoredPosition = new Vector2(-20f, 20f); + } + + private void OnPlayClicked() + { + m_MenuActive = false; + Time.timeScale = 1f; + Cursor.lockState = CursorLockMode.Locked; + Cursor.visible = false; + + if (m_MenuCanvas != null) + { + Destroy(m_MenuCanvas.gameObject); + } + + Destroy(gameObject); + } + + private static GameObject CreateTextButton( + Transform parent, + string label, + Color normalColor, + Color highlightColor, + UnityEngine.Events.UnityAction clickAction) + { + GameObject buttonObject = new(label, typeof(RectTransform), typeof(TextMeshProUGUI), typeof(Button)); + buttonObject.transform.SetParent(parent, false); + + RectTransform rect = buttonObject.GetComponent(); + rect.sizeDelta = new Vector2(0f, 60f); // Height 60, width auto-controlled by LayoutGroup + + TextMeshProUGUI text = buttonObject.GetComponent(); + text.text = label; + text.fontSize = 38; + text.alignment = TextAlignmentOptions.Left; + text.color = Color.white; // Button tint applies on top of white + text.textWrappingMode = TextWrappingModes.NoWrap; + + Button button = buttonObject.GetComponent