chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View File

@@ -0,0 +1,429 @@
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();
}
}