580 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
/// <summary>
/// Отвечает за отображение панели GameOver и анимацию кнопок
/// </summary>
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;
/// <summary>Приз, который можно забрать кнопкой "Забрать приз".</summary>
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<CanvasGroup>();
if (cg == null) cg = b.gameObject.AddComponent<CanvasGroup>();
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);
}
/// <summary>
/// Подготовка всех визуальных элементов: делаем начально всё с alpha=0 и смещен вниз,
/// при этом activeSelf не меняем.
/// </summary>
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<RectTransform>();
if (rt != null) replayButtonOriginalPos = rt.anchoredPosition;
}
if (continueButton != null)
{
var rt = continueButton.GetComponent<RectTransform>();
if (rt != null) continueButtonOriginalPos = rt.anchoredPosition;
}
if (menuButton != null)
{
var rt = menuButton.GetComponent<RectTransform>();
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<RectTransform>();
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<RectTransform>();
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<TMP_Text>();
if (txt != null)
txt.text = "Реванш!";
// Цена со скидкой — в отдельный текст
if (replayPriceText != null)
{
if (replayPrice < fullPrice)
replayPriceText.text = $"Реванш за <s>{fullPrice} р.</s> {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<TMP_Text>();
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()));
}
/// <summary>
/// Возвращает true только если обе кнопки (Реванш/Продолжить) скрыты — только кнопка "Меню".
/// </summary>
private bool IsOnlyMenuButtonVisible()
{
bool replayVisible = replayButton != null && replayButton.gameObject.activeSelf;
bool continueVisible = continueButton != null && continueButton.gameObject.activeSelf;
return !replayVisible && !continueVisible;
}
/// <summary>
/// Целевая позиция кнопки "Меню": при solo-режиме сдвигаем влево на menuButtonSoloOffsetX.
/// </summary>
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);
}
/// <summary>
/// Вызывается кнопкой "Забрать приз". Привязать в Inspector к claimPrizeButton.
/// </summary>
public void OnClaimPrizeClicked()
{
if (currentPendingPrize == null || gameManager == null) return;
Prize prize = currentPendingPrize;
currentPendingPrize = null;
// Ставим флаг — после таймера PrizeReminder вернуть в меню
var reminder = FindObjectOfType<PrizeReminderPanel>(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;
}
}