using System.Collections; using UnityEngine; using UnityEngine.UI; using TMPro; /// /// Отвечает за отображение панели GameOver и анимацию кнопок /// public class GameOverUI : MonoBehaviour { [SerializeField] private int playerId; [SerializeField] private GameManager gameManager; [Header("UI Elements")] [SerializeField] private Button replayButton; [SerializeField] private Button continueButton; [SerializeField] private Button menuButton; // Кнопка меню [SerializeField] private Button claimPrizeButton; // Кнопка "Забрать приз" (показывается вместо меню при наличии приза) [SerializeField] private GameOver gameOverStatusImage; // Надпись победы/поражения (GameOver на firstImage) [SerializeField] private GameOver panelBackground; // Фон панели (GameOver на самой панели) [SerializeField] private GameOver medalSwitcher; // Медальон (GameOver на secondImage) [SerializeField] private TMP_Text notEnoughMoneyText; [SerializeField] private TMP_Text revengeTimerText; // текст таймера реванша (60 сек) [SerializeField] private TMP_Text replayPriceText; // текст цены реванша со скидкой (отдельный) [Header("Animation Settings")] [SerializeField] private Image firstImage; // Картинка надписи победы/поражения [SerializeField] private Image secondImage; // Медальон [SerializeField] private float popDuration = 0.3f; // Длительность "попапа" в секундах [SerializeField] private float popScale = 1.5f; // Максимальный масштаб при "попапе" [SerializeField] private float delayBetweenElements = 0.15f; // Задержка между появлением элементов [SerializeField] private float buttonSlideDuration = 0.25f; // Длительность слайда кнопки [SerializeField] private float buttonSlideOffset = 500f; // Смещение по позиции (начальное) [SerializeField] private float pulseScale = 1.06f; // Насколько увеличивается надпись при пульсации [SerializeField] private float pulseDuration = 1.4f; // Длительность одного цикла пульсации [SerializeField] private float menuButtonSoloOffsetX = -100f; // Сдвиг кнопки "Меню" влево, когда нет второй кнопки (5-й бот) private Coroutine showAnimationCoroutine; private Coroutine pulseCoroutine; private Coroutine revengeTimerCoroutine; private Vector2 replayButtonOriginalPos; private Vector2 continueButtonOriginalPos; private Vector2 menuButtonOriginalPos; private bool positionsCached; private bool eventsSubscribed; // CanvasGroup для "прозрачности" кнопок (не меняем activeSelf) private CanvasGroup replayCg; private CanvasGroup continueCg; private CanvasGroup menuCg; // Для сохранения interactable в UpdateButtons между вызовами метода private bool lastIsBotGame; private bool lastIsWin; /// Приз, который можно забрать кнопкой "Забрать приз". private Prize currentPendingPrize; private void OnEnable() { CreditSystem.OnInitialize += Subscribe; if (!eventsSubscribed && (CreditSystem.onCoinAdded != null || CreditSystem.onCoinRemoved != null)) Subscribe(); EnsureCanvasGroups(); } private void OnDisable() { if (eventsSubscribed) { if (CreditSystem.onCoinAdded != null) CreditSystem.onCoinAdded.RemoveListener(HandleCoinsChanged); if (CreditSystem.onCoinRemoved != null) CreditSystem.onCoinRemoved.RemoveListener(HandleCoinsChanged); eventsSubscribed = false; } CreditSystem.OnInitialize -= Subscribe; } private void EnsureCanvasGroups() { replayCg = EnsureCanvasGroup(replayButton); continueCg = EnsureCanvasGroup(continueButton); menuCg = EnsureCanvasGroup(menuButton); } private static CanvasGroup EnsureCanvasGroup(Button b) { if (b == null) return null; var cg = b.GetComponent(); if (cg == null) cg = b.gameObject.AddComponent(); return cg; } private void Subscribe() { if (eventsSubscribed) return; if (CreditSystem.onCoinAdded != null) CreditSystem.onCoinAdded.AddListener(HandleCoinsChanged); if (CreditSystem.onCoinRemoved != null) CreditSystem.onCoinRemoved.AddListener(HandleCoinsChanged); eventsSubscribed = true; } private void HandleCoinsChanged(int playerID, int newAmount) { // Обновляем кнопки только если изменились монеты именно этого игрока или его оппонента int oponentId = playerId == 1 ? 2 : 1; if (playerID == playerId || playerID == oponentId) { UpdateButtons(); } } public void Show(bool isWin, bool isBotGame, Prize pendingPrize = null) { lastIsWin = isWin; lastIsBotGame = isBotGame; currentPendingPrize = pendingPrize; // Останавливаем старую анимацию если она ещё идёт if (showAnimationCoroutine != null) StopCoroutine(showAnimationCoroutine); if (pulseCoroutine != null) { StopCoroutine(pulseCoroutine); pulseCoroutine = null; } if (revengeTimerCoroutine != null) { StopCoroutine(revengeTimerCoroutine); revengeTimerCoroutine = null; } if (revengeTimerText != null) revengeTimerText.gameObject.SetActive(false); if (replayPriceText != null) replayPriceText.gameObject.SetActive(false); EnsureCanvasGroups(); // 1) Сразу определяем, какие кнопки видимые ApplyButtonVisibility(isWin, isBotGame, pendingPrize); // 2) Переключаем спрайты if (panelBackground != null) panelBackground.GameOverSwitch(isWin); if (gameOverStatusImage != null) gameOverStatusImage.GameOverSwitch(isWin); if (medalSwitcher != null) medalSwitcher.GameOverSwitch(isWin); // 3) Виндиста if (isBotGame && !isWin) VindistaController.Instance?.SetDiscountMode(true, playerId); UpdateButtons(); if (gameManager != null && gameManager.uiManager != null) gameManager.uiManager.OnPanelChanged?.Invoke(); PrepVisualsForAnimation(); int credits = CreditSystem.GetPlayerCredits(playerId); showAnimationCoroutine = StartCoroutine(ShowElementsSequence(isWin, isBotGame, credits)); // Таймер реванша — только при поражении в игре с ботом if (isBotGame && !isWin) revengeTimerCoroutine = StartCoroutine(RevengeTimerCoroutine()); } private void ApplyButtonVisibility(bool isWin, bool isBotGame, Prize pendingPrize = null) { bool isAtFinalBoss = gameManager != null && gameManager.currentBotLevel >= 4; // Победа над финальным боссом — только кнопка меню // Победа в PvE (не финал) — кнопка "Продолжить" // Всё остальное (поражение, PvP) — кнопка "Реванш" bool showContinueButton = isBotGame && isWin && !isAtFinalBoss; bool showReplayButton = !(isBotGame && isWin); // скрыт при любой победе в PvE bool showClaimPrize = pendingPrize != null; // кнопка "Забрать приз" вместо "Меню" if (replayButton != null) replayButton.gameObject.SetActive(showReplayButton); if (continueButton != null) continueButton.gameObject.SetActive(showContinueButton); if (menuButton != null) menuButton.gameObject.SetActive(!showClaimPrize); // скрываем "Меню" если есть приз if (claimPrizeButton != null) claimPrizeButton.gameObject.SetActive(showClaimPrize); // показываем "Забрать приз" UpdateNotEnoughMoneyText(false); } /// /// Подготовка всех визуальных элементов: делаем начально всё с alpha=0 и смещен вниз, /// при этом activeSelf не меняем. /// private void PrepVisualsForAnimation() { CacheButtonPositionsIfNeeded(); // Картинки: scale=0 if (firstImage != null) firstImage.transform.localScale = Vector3.zero; if (secondImage != null) secondImage.transform.localScale = Vector3.zero; // Кнопки: alpha=0 и сдвиг вниз (только если активны) PrepButtonVisual(replayButton, replayCg, replayButtonOriginalPos); PrepButtonVisual(continueButton, continueCg, continueButtonOriginalPos); // Анимируем либо menuButton, либо claimPrizeButton — тот что активен Button activeMenuBtn = (claimPrizeButton != null && claimPrizeButton.gameObject.activeSelf) ? claimPrizeButton : menuButton; var activeCg = EnsureCanvasGroup(activeMenuBtn); PrepButtonVisual(activeMenuBtn, activeCg, GetMenuButtonTargetPos()); } private void CacheButtonPositionsIfNeeded() { if (positionsCached) return; if (replayButton != null) { var rt = replayButton.GetComponent(); if (rt != null) replayButtonOriginalPos = rt.anchoredPosition; } if (continueButton != null) { var rt = continueButton.GetComponent(); if (rt != null) continueButtonOriginalPos = rt.anchoredPosition; } if (menuButton != null) { var rt = menuButton.GetComponent(); if (rt != null) menuButtonOriginalPos = rt.anchoredPosition; } positionsCached = true; } private void PrepButtonVisual(Button b, CanvasGroup cg, Vector2 original) { if (b == null || cg == null) return; if (!b.gameObject.activeSelf) return; cg.alpha = 0f; var rt = b.GetComponent(); if (rt != null) rt.anchoredPosition = new Vector2(original.x, original.y - buttonSlideOffset); } private IEnumerator ShowElementsSequence(bool isWin, bool isBotGame, int credits) { // 1. Первая картинка if (firstImage != null) { yield return StartCoroutine(PopInElement(firstImage.transform)); yield return new WaitForSeconds(delayBetweenElements); } // 2. Вторая картинка if (secondImage != null) { yield return StartCoroutine(PopInElement(secondImage.transform)); yield return new WaitForSeconds(delayBetweenElements); } // 3. Кнопки действия if (isBotGame) yield return StartCoroutine(ShowBotGameButtonsAnimated(isWin, credits)); else yield return StartCoroutine(ShowPvPGameButtonsAnimated(credits)); // 4. Меню yield return StartCoroutine(ShowMenuButtonAnimated()); // 5. Пульсация надписи победы/поражения if (firstImage != null) pulseCoroutine = StartCoroutine(PulseLoop(firstImage.transform)); } private IEnumerator PulseLoop(Transform element) { float half = pulseDuration * 0.5f; while (element != null) { // Плавно увеличиваем float elapsed = 0f; while (elapsed < half) { elapsed += Time.deltaTime; float t = Mathf.SmoothStep(0f, 1f, elapsed / half); element.localScale = Vector3.one * Mathf.Lerp(1f, pulseScale, t); yield return null; } // Плавно уменьшаем elapsed = 0f; while (elapsed < half) { elapsed += Time.deltaTime; float t = Mathf.SmoothStep(0f, 1f, elapsed / half); element.localScale = Vector3.one * Mathf.Lerp(pulseScale, 1f, t); yield return null; } } } private IEnumerator PopInElement(Transform element) { float elapsed = 0f; Vector3 targetScale = Vector3.one; while (elapsed < popDuration) { elapsed += Time.deltaTime; float progress = elapsed / popDuration; float scale; if (progress < 0.4f) { scale = Mathf.Lerp(0f, popScale, progress / 0.4f); } else { float returnProgress = (progress - 0.4f) / 0.6f; scale = Mathf.Lerp(popScale, 1f, returnProgress); } element.localScale = Vector3.one * scale; yield return null; } element.localScale = targetScale; } private IEnumerator SlideInButton(RectTransform rectTransform, Vector2 targetPosition) { Vector2 startPosition = rectTransform.anchoredPosition; float elapsed = 0f; while (elapsed < buttonSlideDuration) { elapsed += Time.deltaTime; float progress = elapsed / buttonSlideDuration; float easedProgress = 1f - Mathf.Pow(1f - progress, 3f); rectTransform.anchoredPosition = Vector2.Lerp(startPosition, targetPosition, easedProgress); yield return null; } rectTransform.anchoredPosition = targetPosition; } private IEnumerator FadeIn(CanvasGroup cg) { if (cg == null) yield break; float elapsed = 0f; while (elapsed < buttonSlideDuration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / buttonSlideDuration); cg.alpha = t; yield return null; } cg.alpha = 1f; } private IEnumerator SlideAndFadeIn(Button b, CanvasGroup cg, Vector2 targetPos) { if (b == null || cg == null) yield break; if (!b.gameObject.activeSelf) yield break; var rt = b.GetComponent(); if (rt == null) { cg.alpha = 1f; yield break; } // Одновременно: slide + fade Coroutine slide = StartCoroutine(SlideInButton(rt, targetPos)); Coroutine fade = StartCoroutine(FadeIn(cg)); yield return slide; yield return fade; } private IEnumerator ShowBotGameButtonsAnimated(bool isWin, int credits) { if (isWin) { // Continue: следующий уровень бесплатный (рефанд + оплата = 0), всегда доступна if (continueButton != null && continueButton.gameObject.activeSelf) { continueButton.interactable = true; yield return StartCoroutine(SlideAndFadeIn(continueButton, continueCg, continueButtonOriginalPos)); } } else { if (replayButton != null && replayButton.gameObject.activeSelf) { int fullPrice = CreditSystem.BotGamePrice; int replayPrice = CreditSystem.GetEffectiveBotPrice(isReplay: true); // В режиме бесплатной игры кнопка всегда доступна replayButton.interactable = CreditSystem.IsFreePlay || credits >= replayPrice; var txt = replayButton.GetComponentInChildren(); if (txt != null) txt.text = "Реванш!"; // Цена со скидкой — в отдельный текст if (replayPriceText != null) { if (replayPrice < fullPrice) replayPriceText.text = $"Реванш за {fullPrice} р. {replayPrice} р."; else replayPriceText.text = $"Реванш за {replayPrice} р."; replayPriceText.gameObject.SetActive(true); } yield return StartCoroutine(SlideAndFadeIn(replayButton, replayCg, replayButtonOriginalPos)); } } } private IEnumerator ShowPvPGameButtonsAnimated(int credits) { if (replayButton != null && replayButton.gameObject.activeSelf) { int oponentId = playerId == 1 ? 2 : 1; int oponentCredits = CreditSystem.GetPlayerCredits(oponentId); int price = CreditSystem.PlayerGamePrice; // В режиме бесплатной игры кнопка всегда доступна // Проверяем в сумме — игра возможна если вместе хватает на полную стоимость replayButton.interactable = CreditSystem.IsFreePlay || credits + oponentCredits >= price; var txt = replayButton.GetComponentInChildren(); if (txt != null) txt.text = "Еще раз!"; yield return StartCoroutine(SlideAndFadeIn(replayButton, replayCg, replayButtonOriginalPos)); } } private IEnumerator ShowMenuButtonAnimated() { // Анимируем активную кнопку — либо "Забрать приз", либо "Меню" Button activeBtn = (claimPrizeButton != null && claimPrizeButton.gameObject.activeSelf) ? claimPrizeButton : menuButton; var activeCg = EnsureCanvasGroup(activeBtn); if (activeBtn != null && activeBtn.gameObject.activeSelf) yield return StartCoroutine(SlideAndFadeIn(activeBtn, activeCg, GetMenuButtonTargetPos())); } /// /// Возвращает true только если обе кнопки (Реванш/Продолжить) скрыты — только кнопка "Меню". /// private bool IsOnlyMenuButtonVisible() { bool replayVisible = replayButton != null && replayButton.gameObject.activeSelf; bool continueVisible = continueButton != null && continueButton.gameObject.activeSelf; return !replayVisible && !continueVisible; } /// /// Целевая позиция кнопки "Меню": при solo-режиме сдвигаем влево на menuButtonSoloOffsetX. /// private Vector2 GetMenuButtonTargetPos() { if (IsOnlyMenuButtonVisible()) return new Vector2(menuButtonOriginalPos.x + menuButtonSoloOffsetX, menuButtonOriginalPos.y); return menuButtonOriginalPos; } public void UpdateButtons() { int credits = CreditSystem.GetPlayerCredits(playerId); int oponentId = playerId == 1 ? 2 : 1; int oponentCredits = CreditSystem.GetPlayerCredits(oponentId); // Continue: следующий уровень бесплатный — всегда доступна if (continueButton != null && continueButton.gameObject.activeSelf) continueButton.interactable = true; if (replayButton != null && replayButton.gameObject.activeSelf) { // В режиме бесплатной игры кнопка всегда доступна if (CreditSystem.IsFreePlay) { replayButton.interactable = true; } else if (lastIsBotGame) { int replayPrice = CreditSystem.GetEffectiveBotPrice(isReplay: true); replayButton.interactable = credits >= replayPrice; } else { int price = CreditSystem.PlayerGamePrice; replayButton.interactable = credits + oponentCredits >= price; } } // notEnoughMoneyText не используется — вместо него replayPriceText со скидкой UpdateNotEnoughMoneyText(false); } private void UpdateNotEnoughMoneyText(bool show) { if (notEnoughMoneyText != null) notEnoughMoneyText.gameObject.SetActive(show); } /// /// Вызывается кнопкой "Забрать приз". Привязать в Inspector к claimPrizeButton. /// public void OnClaimPrizeClicked() { if (currentPendingPrize == null || gameManager == null) return; Prize prize = currentPendingPrize; currentPendingPrize = null; // Ставим флаг — после таймера PrizeReminder вернуть в меню var reminder = FindObjectOfType(true); if (reminder != null) reminder.shouldReturnToMenu = true; if (prize.PrizeType == PrizeType.Special) { gameManager.uiManager.ShowOverlayAnimated("QRCode", 0f); return; } if (prize.PrizeType == PrizeType.Showcase) { gameManager.uiManager.ShowOverlayAnimated("PrizeSelection", 0f); return; } gameManager.uiManager.ShowOverlayAnimated("PrizeReminder", 0f); PrizeSystem.GivePrize(prize, playerId); } private IEnumerator RevengeTimerCoroutine() { float remaining = 60f; if (revengeTimerText != null) revengeTimerText.gameObject.SetActive(true); while (remaining > 0f) { if (revengeTimerText != null) revengeTimerText.text = Mathf.CeilToInt(remaining).ToString(); yield return new WaitForSeconds(1f); remaining -= 1f; } // Время вышло — скрываем таймер и уходим в меню if (revengeTimerText != null) revengeTimerText.gameObject.SetActive(false); if (menuButton != null) menuButton.onClick.Invoke(); revengeTimerCoroutine = null; } }