using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; using System.Collections.Generic; /// /// PreMatchIntroUI — "3 vs 3" matchup splash + countdown before gameplay. /// /// Flow: /// 1. Show team info (displayDuration seconds) /// 2. Countdown: 3 → 2 → 1 → FIGHT! /// 3. Fade out, set IsComplete = true /// 4. CashSystemManager waits for IsComplete before enabling combat /// /// Assign portrait Image slots in the Inspector. The script loads character /// sprites into those Image components by team index. /// /// Press Space (or skipButton) to skip intro → fast countdown then complete. /// /// Uses CanvasGroup to hide (NOT SetActive(false)) so the script stays alive. /// public class PreMatchIntroUI : MonoBehaviour { [Header("Panel")] [SerializeField] private GameObject rootPanel; [Header("Player Team (Left) — Assign portrait Image slots")] [SerializeField] private Image[] playerPortraits = new Image[3]; [SerializeField] private TextMeshProUGUI[] playerNames = new TextMeshProUGUI[3]; [Header("Enemy Team (Right) — Assign portrait Image slots")] [SerializeField] private Image[] enemyPortraits = new Image[3]; [SerializeField] private TextMeshProUGUI[] enemyNames = new TextMeshProUGUI[3]; [Header("Centre")] [SerializeField] private TextMeshProUGUI vsText; [Header("Countdown")] [Tooltip("Text element for countdown (3, 2, 1, FIGHT!)")] [SerializeField] private TextMeshProUGUI countdownText; [Header("Skip")] [Tooltip("Optional skip button — user can also press Space")] [SerializeField] private Button skipButton; [Tooltip("Text shown at bottom: 'Press Space to Skip'")] [SerializeField] private TextMeshProUGUI skipHintText; [Header("Game Mode Info (optional)")] [SerializeField] private TextMeshProUGUI modeTitle; [SerializeField] private TextMeshProUGUI objectiveText; [Header("Timing")] [SerializeField] private float displayDuration = 3.5f; [SerializeField] private float fadeInDuration = 0.4f; [SerializeField] private float fadeOutDuration = 0.5f; [SerializeField] private float countdownStepTime = 1f; [Header("Audio")] [SerializeField] private AudioClip introSFX; [SerializeField] private AudioClip countdownBeepSFX; [SerializeField] private AudioClip fightSFX; [SerializeField] private float sfxVolume = 1f; [Header("Colors")] [SerializeField] private Color playerTeamColor = new Color(0.13f, 0.59f, 0.95f); [SerializeField] private Color enemyTeamColor = new Color(0.96f, 0.26f, 0.21f); [SerializeField] private Color countdownColor = Color.white; [SerializeField] private Color fightColor = new Color(1f, 0.84f, 0f); // Gold // runtime private AudioSource _audio; private CanvasGroup _canvasGroup; private Coroutine _sequenceCoroutine; private bool _skipRequested; /// True once intro sequence (including countdown) is fully done. public bool IsComplete { get; private set; } private static PreMatchIntroUI _instance; public static PreMatchIntroUI Instance => _instance; /// /// Ensures a PreMatchIntroUI instance exists in the scene. /// Finds inactive instances first, or creates a minimal one on the existing Canvas. /// Call from CashSystemManager.Start() so the intro always works. /// public static void EnsureExists() { if (_instance != null) return; // 1. Try to find an inactive instance already in the scene PreMatchIntroUI[] all = Resources.FindObjectsOfTypeAll(); foreach (var p in all) { // Skip assets (prefabs) — only want scene objects if (p.gameObject.scene.name == null) continue; // Activate parents top-down first, then the object itself // (Awake only fires when the ENTIRE chain is active) List chain = new List(); Transform t = p.transform; while (t != null) { if (!t.gameObject.activeSelf) chain.Add(t.gameObject); t = t.parent; } // Activate top-down for (int i = chain.Count - 1; i >= 0; i--) chain[i].SetActive(true); if (_instance != null) { Debug.Log("[PreMatchIntroUI] Found existing inactive panel, activated it"); return; } } // 2. Create a minimal panel on an existing Canvas Canvas canvas = Object.FindObjectOfType(); if (canvas == null) { GameObject canvasObj = new GameObject("UICanvas"); canvas = canvasObj.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 100; canvasObj.AddComponent(); canvasObj.AddComponent(); } GameObject panelRoot = new GameObject("PreMatchIntroUI_Auto"); panelRoot.transform.SetParent(canvas.transform, false); // Stretch to fill canvas RectTransform rt = panelRoot.AddComponent(); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; // Background image (dark overlay) Image bg = panelRoot.AddComponent(); bg.color = new Color(0.05f, 0.05f, 0.1f, 0.85f); // Countdown text in centre GameObject textObj = new GameObject("CountdownText"); textObj.transform.SetParent(panelRoot.transform, false); RectTransform trt = textObj.AddComponent(); trt.anchorMin = new Vector2(0.3f, 0.3f); trt.anchorMax = new Vector2(0.7f, 0.7f); trt.offsetMin = Vector2.zero; trt.offsetMax = Vector2.zero; TextMeshProUGUI txt = textObj.AddComponent(); txt.text = ""; txt.fontSize = 120; txt.alignment = TextAlignmentOptions.Center; txt.color = Color.white; // VS text GameObject vsObj = new GameObject("VSText"); vsObj.transform.SetParent(panelRoot.transform, false); RectTransform vrt = vsObj.AddComponent(); vrt.anchorMin = new Vector2(0.35f, 0.55f); vrt.anchorMax = new Vector2(0.65f, 0.75f); vrt.offsetMin = Vector2.zero; vrt.offsetMax = Vector2.zero; TextMeshProUGUI vsTxt = vsObj.AddComponent(); vsTxt.text = "VS"; vsTxt.fontSize = 80; vsTxt.alignment = TextAlignmentOptions.Center; vsTxt.color = Color.yellow; PreMatchIntroUI intro = panelRoot.AddComponent(); intro.countdownText = txt; intro.vsText = vsTxt; Debug.Log("[PreMatchIntroUI] Auto-created minimal intro panel on Canvas"); } private void Awake() { _instance = this; if (rootPanel == null) rootPanel = gameObject; _audio = GetComponent(); if (_audio == null) _audio = gameObject.AddComponent(); _audio.playOnAwake = false; _audio.spatialBlend = 0f; _canvasGroup = rootPanel.GetComponent(); if (_canvasGroup == null) _canvasGroup = rootPanel.AddComponent(); // Hide via CanvasGroup — keep MonoBehaviour alive for Show() calls HideImmediate(); // Wire skip button if (skipButton != null) skipButton.onClick.AddListener(Skip); } private void Update() { // Space key skip if (!IsComplete && Input.GetKeyDown(KeyCode.Space)) _skipRequested = true; } // ─── Public API ────────────────────────────────────────── public void Show() { Debug.Log("[PreMatchIntroUI] Show() called"); IsComplete = false; _skipRequested = false; rootPanel.SetActive(true); _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = true; // Populate from SelectionOptions string[] playerTeam = SelectionOptions.Instance != null ? SelectionOptions.Instance.playerTeam : null; string[] enemyTeam = SelectionOptions.Instance != null ? SelectionOptions.Instance.enemyTeam : null; PopulateSide(playerTeam, playerPortraits, playerNames, playerTeamColor); PopulateSide(enemyTeam, enemyPortraits, enemyNames, enemyTeamColor); if (vsText != null) vsText.text = "VS"; // Game mode info if (modeTitle != null) modeTitle.text = "CASH SYSTEM"; if (objectiveText != null) objectiveText.text = "Collect cash and deposit it in your vault!"; // Skip hint if (skipHintText != null) skipHintText.text = "Press Space to Skip"; // Clear countdown at start if (countdownText != null) countdownText.text = ""; // Play intro sound if (introSFX != null) _audio.PlayOneShot(introSFX, sfxVolume); // Start the full intro sequence (fade in → display → countdown → fade out) if (_sequenceCoroutine != null) StopCoroutine(_sequenceCoroutine); _sequenceCoroutine = StartCoroutine(IntroSequence()); } /// Force hide — called if CashSystemManager needs to tear down early. public void Hide() { if (_sequenceCoroutine != null) StopCoroutine(_sequenceCoroutine); HideImmediate(); IsComplete = true; } /// Skip the intro — jumps to fast countdown then completes. public void Skip() { _skipRequested = true; } // ─── Intro Sequence ────────────────────────────────────── private IEnumerator IntroSequence() { // Phase 1: Fade in yield return StartCoroutine(FadeIn()); // Phase 2: Display team info for displayDuration (or until skip) float elapsed = 0f; while (elapsed < displayDuration && !_skipRequested) { elapsed += Time.unscaledDeltaTime; yield return null; } // Phase 3: Countdown (3, 2, 1, FIGHT!) // If skip was requested, do a fast countdown float stepTime = _skipRequested ? 0.25f : countdownStepTime; // Hide skip hint during countdown if (skipHintText != null) skipHintText.text = ""; string[] steps = { "3", "2", "1", "FIGHT!" }; foreach (string step in steps) { if (countdownText != null) { countdownText.text = step; countdownText.color = (step == "FIGHT!") ? fightColor : countdownColor; // Scale punch — make text pop countdownText.transform.localScale = Vector3.one * 1.5f; float scaleTimer = 0f; float scaleDuration = stepTime * 0.6f; while (scaleTimer < scaleDuration) { scaleTimer += Time.unscaledDeltaTime; float t = Mathf.Clamp01(scaleTimer / scaleDuration); countdownText.transform.localScale = Vector3.Lerp( Vector3.one * 1.5f, Vector3.one, t); yield return null; } countdownText.transform.localScale = Vector3.one; } // Fire event so other systems (like existing countdown UI) can react GameEvents.FireCountdownTick(step); // SFX if (step == "FIGHT!") { if (fightSFX != null) _audio.PlayOneShot(fightSFX, sfxVolume); } else { if (countdownBeepSFX != null) _audio.PlayOneShot(countdownBeepSFX, sfxVolume); } // If skipping again during countdown, accelerate further if (_skipRequested) stepTime = 0.15f; yield return new WaitForSecondsRealtime(Mathf.Max(0.05f, stepTime - (stepTime * 0.6f))); } // Brief hold on "FIGHT!" before fading yield return new WaitForSecondsRealtime(_skipRequested ? 0.2f : 0.8f); // Clear countdown text if (countdownText != null) countdownText.text = ""; GameEvents.FireCountdownTick(""); // Phase 4: Fade out yield return StartCoroutine(FadeOut()); // Done — CashSystemManager checks this to enable combat IsComplete = true; Debug.Log("[PreMatchIntroUI] Intro complete"); } // ─── Population ────────────────────────────────────────── private void PopulateSide(string[] names, Image[] portraits, TextMeshProUGUI[] labels, Color teamColor) { bool isPlayerSide = (portraits == playerPortraits); string sideLabel = isPlayerSide ? "Player" : "Enemy"; // Get portrait sprites stored from the selection screen (via SelectionOptions) Sprite[] teamSprites = null; if (SelectionOptions.Instance != null) teamSprites = isPlayerSide ? SelectionOptions.Instance.playerTeamSprites : SelectionOptions.Instance.enemyTeamSprites; Debug.Log($"[PreMatchIntroUI] PopulateSide({sideLabel}) — names: {(names != null ? string.Join(", ", names) : "null")}, sprites: {(teamSprites != null ? teamSprites.Length.ToString() : "null")}"); for (int i = 0; i < 3; i++) { bool hasChar = names != null && i < names.Length && !string.IsNullOrEmpty(names[i]); if (portraits != null && i < portraits.Length && portraits[i] != null) { if (hasChar) { // Use the sprite stored from the selection screen Sprite portrait = (teamSprites != null && i < teamSprites.Length) ? teamSprites[i] : null; if (portrait != null) { portraits[i].sprite = portrait; portraits[i].color = Color.white; portraits[i].preserveAspect = true; Debug.Log($"[PreMatchIntroUI] {sideLabel}[{i}] portrait set for {names[i]} ({portrait.name})"); } else { portraits[i].color = teamColor * 0.6f; Debug.LogWarning($"[PreMatchIntroUI] {sideLabel}[{i}] no portrait for {names[i]}, using team color"); } portraits[i].gameObject.SetActive(true); } else { portraits[i].gameObject.SetActive(false); } } if (labels != null && i < labels.Length && labels[i] != null) { labels[i].text = hasChar ? names[i].ToUpper() : ""; labels[i].color = teamColor; } } } // ─── Fade / Hide ───────────────────────────────────────── private void HideImmediate() { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } private IEnumerator FadeIn() { float elapsed = 0f; while (elapsed < fadeInDuration) { elapsed += Time.unscaledDeltaTime; _canvasGroup.alpha = Mathf.Clamp01(elapsed / fadeInDuration); yield return null; } _canvasGroup.alpha = 1f; } private IEnumerator FadeOut() { float elapsed = 0f; float startAlpha = _canvasGroup.alpha; while (elapsed < fadeOutDuration) { elapsed += Time.unscaledDeltaTime; _canvasGroup.alpha = Mathf.Lerp(startAlpha, 0f, elapsed / fadeOutDuration); yield return null; } HideImmediate(); } }