430 lines
16 KiB
C#
430 lines
16 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>True once intro sequence (including countdown) is fully done.</summary>
|
|
public bool IsComplete { get; private set; }
|
|
|
|
private static PreMatchIntroUI _instance;
|
|
public static PreMatchIntroUI Instance => _instance;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static void EnsureExists()
|
|
{
|
|
if (_instance != null) return;
|
|
|
|
// 1. Try to find an inactive instance already in the scene
|
|
PreMatchIntroUI[] all = Resources.FindObjectsOfTypeAll<PreMatchIntroUI>();
|
|
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<GameObject> chain = new List<GameObject>();
|
|
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<Canvas>();
|
|
if (canvas == null)
|
|
{
|
|
GameObject canvasObj = new GameObject("UICanvas");
|
|
canvas = canvasObj.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = 100;
|
|
canvasObj.AddComponent<UnityEngine.UI.CanvasScaler>();
|
|
canvasObj.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
|
}
|
|
|
|
GameObject panelRoot = new GameObject("PreMatchIntroUI_Auto");
|
|
panelRoot.transform.SetParent(canvas.transform, false);
|
|
|
|
// Stretch to fill canvas
|
|
RectTransform rt = panelRoot.AddComponent<RectTransform>();
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
|
|
// Background image (dark overlay)
|
|
Image bg = panelRoot.AddComponent<Image>();
|
|
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<RectTransform>();
|
|
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<TextMeshProUGUI>();
|
|
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<RectTransform>();
|
|
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<TextMeshProUGUI>();
|
|
vsTxt.text = "VS";
|
|
vsTxt.fontSize = 80;
|
|
vsTxt.alignment = TextAlignmentOptions.Center;
|
|
vsTxt.color = Color.yellow;
|
|
|
|
PreMatchIntroUI intro = panelRoot.AddComponent<PreMatchIntroUI>();
|
|
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<AudioSource>();
|
|
if (_audio == null) _audio = gameObject.AddComponent<AudioSource>();
|
|
_audio.playOnAwake = false;
|
|
_audio.spatialBlend = 0f;
|
|
|
|
_canvasGroup = rootPanel.GetComponent<CanvasGroup>();
|
|
if (_canvasGroup == null) _canvasGroup = rootPanel.AddComponent<CanvasGroup>();
|
|
|
|
// 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());
|
|
}
|
|
|
|
/// <summary>Force hide — called if CashSystemManager needs to tear down early.</summary>
|
|
public void Hide()
|
|
{
|
|
if (_sequenceCoroutine != null) StopCoroutine(_sequenceCoroutine);
|
|
HideImmediate();
|
|
IsComplete = true;
|
|
}
|
|
|
|
/// <summary>Skip the intro — jumps to fast countdown then completes.</summary>
|
|
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();
|
|
}
|
|
}
|