474 lines
18 KiB
C#
Raw 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 GameOver gameOverStatusImage; // Надпись победы/поражения (GameOver на firstImage)
[SerializeField] private GameOver panelBackground; // Фон панели (GameOver на самой панели)
[SerializeField] private GameOver medalSwitcher; // Медальон (GameOver на secondImage)
[SerializeField] private TMP_Text notEnoughMoneyText;
[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 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 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)
{
lastIsWin = isWin;
lastIsBotGame = isBotGame;
// Останавливаем старую анимацию если она ещё идёт
if (showAnimationCoroutine != null)
{
StopCoroutine(showAnimationCoroutine);
}
if (pulseCoroutine != null)
{
StopCoroutine(pulseCoroutine);
pulseCoroutine = null;
}
EnsureCanvasGroups();
// 1) Сразу определяем, какие кнопки видимые (чтобы список не менялся)
ApplyButtonVisibility(isWin, isBotGame);
// 2) Переключаем спрайты: фон панели, надпись, медальон (GameOver на secondImage)
if (panelBackground != null)
panelBackground.GameOverSwitch(isWin);
if (gameOverStatusImage != null)
gameOverStatusImage.GameOverSwitch(isWin);
if (medalSwitcher != null)
medalSwitcher.GameOverSwitch(isWin);
// 3) Сразу обновляем interactable (зависит от монеток)
UpdateButtons();
// 4) Чтобы сработал метод, что нужно кнопки обновлять
if (gameManager != null && gameManager.uiManager != null)
gameManager.uiManager.OnPanelChanged?.Invoke();
// 5) Готовим всё визуально перед стартом анимации и запускаем анимацию
PrepVisualsForAnimation();
int credits = CreditSystem.GetPlayerCredits(playerId);
showAnimationCoroutine = StartCoroutine(ShowElementsSequence(isWin, isBotGame, credits));
}
private void ApplyButtonVisibility(bool isWin, bool isBotGame)
{
// На финальном боссе (уровень 4) после победы кнопки "Продолжить" нет — только "Меню"
bool isAtFinalBoss = gameManager != null && gameManager.currentBotLevel >= 4;
// - Если победа в PvE и это не финальный босс: continueButton
// - Если поражение в PvE, PvP, или финальный босс побеждён: replayButton
// - Menu: всегда
bool showContinueButton = isBotGame && isWin && !isAtFinalBoss;
bool showReplayButton = !showContinueButton;
if (replayButton != null) replayButton.gameObject.SetActive(showReplayButton);
if (continueButton != null) continueButton.gameObject.SetActive(showContinueButton);
if (menuButton != null) menuButton.gameObject.SetActive(true);
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);
PrepButtonVisual(menuButton, menuCg, 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 replayPrice = CreditSystem.GetEffectiveBotPrice(isReplay: true);
replayButton.interactable = credits >= replayPrice;
var txt = replayButton.GetComponentInChildren<TMP_Text>();
if (txt != null) txt.text = "Реванш!";
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 = credits >= price * 2 || (credits >= price && oponentCredits >= price);
var txt = replayButton.GetComponentInChildren<TMP_Text>();
if (txt != null) txt.text = "Еще раз!";
yield return StartCoroutine(SlideAndFadeIn(replayButton, replayCg, replayButtonOriginalPos));
}
}
private IEnumerator ShowMenuButtonAnimated()
{
if (menuButton != null && menuButton.gameObject.activeSelf)
{
// Кнопка меню всегда интерактивна
yield return StartCoroutine(SlideAndFadeIn(menuButton, menuCg, GetMenuButtonTargetPos()));
}
}
/// <summary>
/// Возвращает true, когда только кнопка "Меню" видима (5-й бот, победа — нет Реванша/Продолжить).
/// </summary>
private bool IsOnlyMenuButtonVisible()
{
bool isAtFinalBoss = gameManager != null && gameManager.currentBotLevel >= 4;
return lastIsBotGame && lastIsWin && isAtFinalBoss;
}
/// <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 (lastIsBotGame)
{
int replayPrice = CreditSystem.GetEffectiveBotPrice(isReplay: true);
replayButton.interactable = credits >= replayPrice;
}
else
{
int price = CreditSystem.PlayerGamePrice;
replayButton.interactable = credits >= price * 2 || (credits >= price && oponentCredits >= price);
}
}
bool showNotEnoughMoney = !lastIsWin &&
replayButton != null &&
replayButton.gameObject.activeSelf &&
!replayButton.interactable;
UpdateNotEnoughMoneyText(showNotEnoughMoney);
}
private void UpdateNotEnoughMoneyText(bool show)
{
if (notEnoughMoneyText != null)
notEnoughMoneyText.gameObject.SetActive(show);
}
}