using UnityEngine; /// /// Singleton. Manages the dialogue UI box. /// Attach to any persistent GameObject in the scene (e.g. a "Managers" object), /// or drop it on the Player — it will survive fine either way. /// /// Controls: /// E or Space or Enter — advance to next page / close /// Escape — close immediately /// /// While dialogue is open: /// - Cursor is unlocked and visible /// - FirstPersonController mouse look is blocked (via IsOpen flag) /// public class DialogueManager : MonoBehaviour { // ── Singleton ───────────────────────────────────────────────────── public static DialogueManager Instance { get; private set; } // ── Inspector ───────────────────────────────────────────────────── [Header("Box Layout")] [Tooltip("Height of the dialogue box as a fraction of screen height.")] public float boxHeightFraction = 0.22f; public float boxMargin = 24f; public float innerPad = 18f; [Header("Colours — Cruelty Squad palette")] public Color colBoxBg = new Color(0.04f, 0.06f, 0.04f, 0.94f); public Color colBoxBorder = new Color(0.18f, 0.75f, 0.22f, 0.90f); public Color colNameBg = new Color(0.10f, 0.28f, 0.10f, 1.00f); public Color colNameText = new Color(0.25f, 1.00f, 0.30f, 1.00f); public Color colBodyText = new Color(0.78f, 0.90f, 0.78f, 1.00f); public Color colHintText = new Color(0.35f, 0.55f, 0.35f, 0.85f); public Color colPageDots = new Color(0.28f, 0.65f, 0.30f, 0.80f); [Header("Text")] public int nameFontSize = 13; public int bodyFontSize = 14; public int hintFontSize = 10; [Header("Typewriter Effect")] public bool useTypewriter = true; public float charsPerSecond = 40f; // ── Public state ────────────────────────────────────────────────── public bool IsOpen { get; private set; } // ── Private ─────────────────────────────────────────────────────── private DialogueLine[] _lines; private int _lineIndex; private int _pageIndex; // Typewriter private float _charTimer; private int _visibleChars; private bool _pageComplete; private Texture2D _white; // ── Current text shortcuts ───────────────────────────────────────── private string CurrentSpeaker => (_lines != null && _lineIndex < _lines.Length) ? _lines[_lineIndex].speakerName : ""; private string CurrentPage => (_lines != null && _lineIndex < _lines.Length && _pageIndex < _lines[_lineIndex].pages.Length) ? _lines[_lineIndex].pages[_pageIndex] : ""; private int TotalPages => (_lines != null && _lineIndex < _lines.Length) ? _lines[_lineIndex].pages.Length : 1; // ───────────────────────────────────────────────────────────────── void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } void Start() { _white = Texture2D.whiteTexture; } // ───────────────────────────────────────────────────────────────── public void StartDialogue(DialogueLine[] lines) { if (lines == null || lines.Length == 0) return; _lines = lines; _lineIndex = 0; _pageIndex = 0; IsOpen = true; BeginPage(); Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } void BeginPage() { _charTimer = 0f; _visibleChars = useTypewriter ? 0 : int.MaxValue; _pageComplete = !useTypewriter; } // ───────────────────────────────────────────────────────────────── void Update() { if (!IsOpen) return; // Advance typewriter if (useTypewriter && !_pageComplete) { _charTimer += Time.deltaTime; _visibleChars = Mathf.FloorToInt(_charTimer * charsPerSecond); if (_visibleChars >= CurrentPage.Length) { _visibleChars = CurrentPage.Length; _pageComplete = true; } } // Advance or close on E / Space / Return bool advance = Input.GetKeyDown(KeyCode.E) || Input.GetKeyDown(KeyCode.Space) || Input.GetKeyDown(KeyCode.Return); bool cancel = Input.GetKeyDown(KeyCode.Escape); if (cancel) { CloseDialogue(); return; } if (advance) { // If typewriter is still running, skip to end of page first if (useTypewriter && !_pageComplete) { _visibleChars = CurrentPage.Length; _pageComplete = true; return; } // Try next page in current line _pageIndex++; if (_pageIndex < TotalPages) { BeginPage(); return; } // Try next line _lineIndex++; _pageIndex = 0; if (_lineIndex < _lines.Length) { BeginPage(); return; } // All done CloseDialogue(); } } void CloseDialogue() { IsOpen = false; _lines = null; Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } // ───────────────────────────────────────────────────────────────── void OnGUI() { if (!IsOpen || _lines == null) return; float sw = Screen.width; float sh = Screen.height; float bh = sh * boxHeightFraction; float bw = sw - boxMargin * 2f; float by = sh - bh - boxMargin; float bx = boxMargin; // ── Outer border ───────────────────────────────────────────── float border = 2f; DrawTex(new Rect(bx - border, by - border, bw + border * 2f, bh + border * 2f), colBoxBorder); // ── Main background ─────────────────────────────────────────── DrawTex(new Rect(bx, by, bw, bh), colBoxBg); // ── Left accent stripe ──────────────────────────────────────── DrawTex(new Rect(bx, by, 4f, bh), colBoxBorder); float cx = bx + innerPad + 6f; // content X (after the stripe + pad) float cy = by + innerPad; float cw = bw - innerPad * 2f - 6f; // ── Speaker name bar ────────────────────────────────────────── string speaker = CurrentSpeaker; float nameH = 0f; if (!string.IsNullOrWhiteSpace(speaker)) { GUIStyle nameStyle = new GUIStyle(); nameStyle.fontSize = nameFontSize; nameStyle.fontStyle = FontStyle.Bold; nameStyle.normal.textColor = colNameText; nameStyle.alignment = TextAnchor.MiddleLeft; Vector2 ns = nameStyle.CalcSize(new GUIContent(speaker)); nameH = ns.y + 6f; float namePadX = 10f; DrawTex(new Rect(cx - 2f, cy, ns.x + namePadX * 2f + 4f, nameH), colNameBg); GUI.Label(new Rect(cx + namePadX, cy + 3f, ns.x + 4f, ns.y), speaker, nameStyle); cy += nameH + 8f; } // ── Body text (typewriter) ──────────────────────────────────── string fullText = CurrentPage; string visibleText = useTypewriter ? fullText.Substring(0, Mathf.Min(_visibleChars, fullText.Length)) : fullText; GUIStyle bodyStyle = new GUIStyle(); bodyStyle.fontSize = bodyFontSize; bodyStyle.fontStyle = FontStyle.Normal; bodyStyle.normal.textColor = colBodyText; bodyStyle.wordWrap = true; bodyStyle.richText = true; float bodyH = bh - innerPad * 2f - nameH - 8f - 20f; // leave room for hint GUI.Label(new Rect(cx, cy, cw, bodyH), visibleText, bodyStyle); // ── Page indicator dots ─────────────────────────────────────── int total = TotalPages; if (total > 1) { float dotR = 4f; float dotGap = dotR * 2f + 4f; float dotsW = total * dotGap; float dotX = bx + bw * 0.5f - dotsW * 0.5f; float dotY = by + bh - innerPad - dotR; for (int i = 0; i < total; i++) { Color dc = (i == _pageIndex) ? colBoxBorder : colPageDots; DrawTex(new Rect(dotX + i * dotGap, dotY - dotR, dotR * 2f, dotR * 2f), dc); } } // ── Advance hint ────────────────────────────────────────────── string hintStr = _pageComplete ? (_lineIndex >= _lines.Length - 1 && _pageIndex >= TotalPages - 1 ? "[ E ] Close" : "[ E ] Next") : "[ E ] Skip"; GUIStyle hintStyle = new GUIStyle(); hintStyle.fontSize = hintFontSize; hintStyle.fontStyle = FontStyle.Bold; hintStyle.normal.textColor = colHintText; hintStyle.alignment = TextAnchor.LowerRight; GUI.Label(new Rect(bx, by, bw - innerPad, bh - 6f), hintStr, hintStyle); } // ───────────────────────────────────────────────────────────────── void DrawTex(Rect r, Color c) { Color prev = GUI.color; GUI.color = c; GUI.DrawTexture(r, _white); GUI.color = prev; } }