Files
DeviantMobile-Rohan/Assets/Scripts/CashSystem/CashSystemUI.cs

664 lines
25 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// CashSystemUI — Event-driven HUD for Cash System mode.
/// Subscribes to GameEvents; never polls via FindObjectOfType.
/// Includes: timer, vault bars, carried cash, kill feed, combo popup, vault attack warning, countdown.
/// </summary>
public class CashSystemUI : MonoBehaviour
{
[Header("Timer Display")]
[SerializeField] private TextMeshProUGUI timerText;
[Header("Vault Displays")]
[SerializeField] private TextMeshProUGUI playerVaultText;
[SerializeField] private TextMeshProUGUI enemyVaultText;
[SerializeField] private Slider playerVaultBar;
[SerializeField] private Slider enemyVaultBar;
[Header("Carried Cash Display")]
[SerializeField] private TextMeshProUGUI carriedCashText;
[SerializeField] private GameObject carriedCashPanel;
[Header("Character Switch Indicator")]
[SerializeField] private TextMeshProUGUI currentCharacterText;
[SerializeField] private Image[] teamCharacterIcons;
[Header("Steal Progress")]
[SerializeField] private GameObject stealProgressPanel;
[SerializeField] private Slider stealProgressBar;
[SerializeField] private TextMeshProUGUI stealProgressText;
[Header("Steal Dial (optional)")]
[Tooltip("Optional sprite to use for the steal progress radial dial. If left empty the code will try to copy the import UI's dial sprite at runtime.")]
[SerializeField] private Sprite stealDialSprite;
[Header("Countdown Display")]
[SerializeField] private TextMeshProUGUI countdownText;
[SerializeField] private GameObject countdownPanel;
[Header("Kill Feed")]
[SerializeField] private TextMeshProUGUI killFeedText;
[SerializeField] private int maxFeedEntries = 5;
[Header("Combo Popup")]
[SerializeField] private TextMeshProUGUI comboText;
[SerializeField] private GameObject comboPanel;
[Header("Popup Message")]
[SerializeField] private TextMeshProUGUI popupText;
[SerializeField] private GameObject popupPanel;
[Header("Pickup Screen Effect")]
[Tooltip("Full-screen flash image (stretch to fill, starts transparent)")]
[SerializeField] private Image pickupFlashImage;
[Tooltip("Color of the screen flash when picking up cash")]
[SerializeField] private Color pickupFlashColor = new Color(1f, 0.84f, 0f, 0.35f); // Gold
[Tooltip("How long the flash lasts")]
[SerializeField] private float pickupFlashDuration = 0.3f;
[Tooltip("Text popup showing collected amount (e.g. '+$100')")]
[SerializeField] private TextMeshProUGUI pickupAmountText;
[SerializeField] private GameObject pickupAmountPanel;
[Header("Win Target")]
[SerializeField] private float targetCash = 2000f;
// ─── Runtime ─────────────────────────────────────────────
private readonly List<string> _feedEntries = new List<string>();
private Coroutine _comboFade;
private Coroutine _popupFade;
private Coroutine _pickupFlashCoroutine;
private float _originalTimerFontSize = -1f;
private bool _dirtyVault;
// Cached score values (set by events, read in LateUpdate for dirty-flag refresh)
private float _playerScore, _enemyScore;
// Cached carrier reference — invalidated on character switch
private CashCarrier _cachedCarrier;
private GameObject _cachedCarrierCharacter;
private bool _stealWasActive;
// CanvasGroup for intro delay — hidden until intro panel finishes
private CanvasGroup _canvasGroup;
// ─── Lifecycle ───────────────────────────────────────────
private void Start()
{
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
gameObject.SetActive(false);
return;
}
// Set up CanvasGroup so we can hide during intro without disabling the script
_canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null) _canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Hide the HUD during the intro panel — CashSystemManager will show it after intro completes
_canvasGroup.alpha = 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
if (playerVaultBar != null) playerVaultBar.maxValue = targetCash;
if (enemyVaultBar != null) enemyVaultBar.maxValue = targetCash;
if (stealProgressPanel != null) stealProgressPanel.SetActive(false);
ConfigureStealDial();
if (comboPanel != null) comboPanel.SetActive(false);
if (popupPanel != null) popupPanel.SetActive(false);
// Explicitly hide carried cash panel and clear text on start
HideCarried();
if (carriedCashText != null) carriedCashText.text = "";
}
/// <summary>Show the HUD (called by CashSystemManager after intro completes).</summary>
public void ShowHUD()
{
if (_canvasGroup == null) return;
_canvasGroup.alpha = 1f;
_canvasGroup.blocksRaycasts = true;
_canvasGroup.interactable = true;
Debug.Log("[CashSystemUI] HUD shown");
}
/// <summary>
/// Configure the steal progress dial to match the import UI's dial if possible.
/// Tries in this order:
/// 1) If `stealDialSprite` is assigned in inspector, use that.
/// 2) If an ImportUIPanel exists in the scene (created by CashBoxImportController), copy its ProgressDial sprite.
/// 3) If we have a Slider with a fill Rect, apply the sprite as a radial Image.
/// </summary>
private void ConfigureStealDial()
{
if (stealProgressBar == null) return;
Sprite spriteToUse = stealDialSprite;
// Try to find the runtime ImportUIPanel created by CashBoxImportController
if (spriteToUse == null)
{
var importPanel = GameObject.Find("ImportUIPanel");
if (importPanel != null)
{
var dial = importPanel.transform.Find("ProgressDial");
if (dial != null)
{
var img = dial.GetComponent<Image>();
if (img != null) spriteToUse = img.sprite;
}
}
}
if (spriteToUse == null) return;
// Apply sprite to the Slider's fill image if available
var fillRect = stealProgressBar.fillRect;
if (fillRect != null)
{
var fillImg = fillRect.GetComponent<Image>();
if (fillImg != null)
{
fillImg.sprite = spriteToUse;
fillImg.type = Image.Type.Filled;
fillImg.fillMethod = Image.FillMethod.Radial360;
fillImg.fillOrigin = (int)Image.Origin360.Top;
fillImg.fillClockwise = true;
fillImg.preserveAspect = true;
}
}
// COPY ADDITIONAL VISUAL SETTINGS FROM IMPORT UI
var importPanelObj = GameObject.Find("ImportUIPanel");
if (importPanelObj != null)
{
// Copy panel background color if stealProgressPanel has an Image
var importImage = importPanelObj.GetComponent<Image>();
if (importImage != null && stealProgressPanel != null)
{
var stealPanelImg = stealProgressPanel.GetComponent<Image>();
if (stealPanelImg != null)
{
stealPanelImg.color = importImage.color;
}
}
// Copy font/size/color from Import's AmountText to our stealProgressText
var amountTf = importPanelObj.transform.Find("AmountText");
if (amountTf != null && stealProgressText != null)
{
var importAmountTmp = amountTf.GetComponent<TextMeshProUGUI>();
if (importAmountTmp != null)
{
// Copy common TMP settings
stealProgressText.font = importAmountTmp.font;
stealProgressText.fontSize = importAmountTmp.fontSize;
stealProgressText.color = importAmountTmp.color;
stealProgressText.fontStyle = importAmountTmp.fontStyle;
stealProgressText.enableAutoSizing = importAmountTmp.enableAutoSizing;
stealProgressText.outlineWidth = importAmountTmp.outlineWidth;
stealProgressText.outlineColor = importAmountTmp.outlineColor;
stealProgressText.alignment = importAmountTmp.alignment;
}
}
// Also copy countdown/status text style if present (optional)
var countdownTf = importPanelObj.transform.Find("CountdownText");
if (countdownTf != null && timerText != null)
{
var importCountdownTmp = countdownTf.GetComponent<TextMeshProUGUI>();
if (importCountdownTmp != null)
{
timerText.font = importCountdownTmp.font;
timerText.fontSize = importCountdownTmp.fontSize;
timerText.color = importCountdownTmp.color;
}
}
}
}
private void OnEnable()
{
GameEvents.OnTimerUpdated += OnTimerUpdated;
GameEvents.OnScoreChanged += OnScoreChanged;
GameEvents.OnBagPickedUp += OnBagPickedUp;
GameEvents.OnCashDeposited += OnCashDeposited;
GameEvents.OnKillFeedEntry += OnKillFeedEntry;
GameEvents.OnComboMultiplier += OnComboMultiplier;
GameEvents.OnVaultUnderAttack += OnVaultUnderAttack;
GameEvents.OnVaultDefended += OnVaultDefended;
GameEvents.OnCountdownTick += OnCountdownTick;
GameEvents.OnPopupMessage += OnPopupMessage;
GameEvents.OnCharacterSwitched += OnCharacterSwitched;
GameEvents.OnCarrierKilled += OnCarrierKilled;
GameEvents.OnMatchEnd += OnMatchEnd;
GameEvents.OnBagDropped += OnBagDropped;
}
private void OnDisable()
{
GameEvents.OnTimerUpdated -= OnTimerUpdated;
GameEvents.OnScoreChanged -= OnScoreChanged;
GameEvents.OnBagPickedUp -= OnBagPickedUp;
GameEvents.OnCashDeposited -= OnCashDeposited;
GameEvents.OnKillFeedEntry -= OnKillFeedEntry;
GameEvents.OnComboMultiplier -= OnComboMultiplier;
GameEvents.OnVaultUnderAttack -= OnVaultUnderAttack;
GameEvents.OnVaultDefended -= OnVaultDefended;
GameEvents.OnCountdownTick -= OnCountdownTick;
GameEvents.OnPopupMessage -= OnPopupMessage;
GameEvents.OnCharacterSwitched -= OnCharacterSwitched;
GameEvents.OnCarrierKilled -= OnCarrierKilled;
GameEvents.OnMatchEnd -= OnMatchEnd;
GameEvents.OnBagDropped -= OnBagDropped;
}
private void LateUpdate()
{
// Dirty-flag: only refresh vault bars when data changed
if (_dirtyVault)
{
_dirtyVault = false;
RefreshVaultBars();
}
// Steal progress: check every frame only while active, otherwise throttle to ~6Hz
if (_stealWasActive || Time.frameCount % 10 == 0)
{
UpdateStealProgress();
}
// Carried cash: event-driven via OnBagPickedUp/OnBagDropped/OnCashDeposited/OnCarrierKilled
// Only poll at ~10Hz as a safety net for edge cases
if (Time.frameCount % 6 == 0)
{
RefreshCarriedCash();
}
}
// ─── Event Handlers ──────────────────────────────────────
private void OnTimerUpdated(float remaining)
{
if (timerText == null) return;
int m = Mathf.FloorToInt(remaining / 60);
int s = Mathf.FloorToInt(remaining % 60);
timerText.text = $"{m:00}:{s:00}";
timerText.color = remaining <= 30f
? Color.Lerp(Color.red, Color.white, Mathf.PingPong(Time.time * 2, 1))
: Color.white;
}
private void OnScoreChanged(string teamTag, float total)
{
if (teamTag == "Player") _playerScore = total;
else _enemyScore = total;
_dirtyVault = true;
}
private void OnBagPickedUp(CashBag bag, CashCarrier carrier)
{
// Update carrier display immediately
RefreshCarriedCash();
// Only show screen effects for the player-controlled character
CashSystemManager mgr = CashSystemManager.Instance;
if (mgr == null) return;
GameObject controlled = mgr.CurrentControlledCharacter;
if (controlled == null || carrier == null) return;
if (carrier.gameObject != controlled) return;
// Screen flash effect
ShowPickupFlash();
// Floating "+$X" amount text
float amount = bag != null ? bag.CashValue : 0f;
ShowPickupAmount(amount);
}
private void OnBagDropped(CashBag bag, Vector3 pos)
{
RefreshCarriedCash();
}
private void OnCashDeposited(string teamTag, float value, int combo)
{
_dirtyVault = true;
RefreshCarriedCash();
}
private void OnKillFeedEntry(string killer, string action, string victim)
{
string entry = $"<b>{killer}</b> {action} <b>{victim}</b>";
_feedEntries.Add(entry);
if (_feedEntries.Count > maxFeedEntries)
_feedEntries.RemoveAt(0);
if (killFeedText != null)
killFeedText.text = string.Join("\n", _feedEntries);
}
private void OnComboMultiplier(int multiplier)
{
if (comboText == null) return;
comboText.text = $"x{multiplier} COMBO!";
if (comboPanel != null) comboPanel.SetActive(true);
if (_comboFade != null) StopCoroutine(_comboFade);
_comboFade = StartCoroutine(FadePanel(comboPanel, 2f));
}
private void OnVaultUnderAttack(TeamVault vault)
{
if (vault.TeamTag != "Player") return;
if (stealProgressPanel != null) stealProgressPanel.SetActive(true);
if (stealProgressText != null) stealProgressText.text = "VAULT UNDER ATTACK!";
}
private void OnVaultDefended(TeamVault vault)
{
if (vault.TeamTag != "Player") return;
if (stealProgressPanel != null) stealProgressPanel.SetActive(false);
}
private void OnCountdownTick(string text)
{
ShowCountdown(text);
}
private void OnPopupMessage(string message, float duration)
{
if (popupText != null) popupText.text = message;
if (popupPanel != null) popupPanel.SetActive(true);
if (_popupFade != null) StopCoroutine(_popupFade);
_popupFade = StartCoroutine(FadePanel(popupPanel, duration));
}
private void OnCharacterSwitched(GameObject oldChar, GameObject newChar)
{
if (newChar == null) return;
if (currentCharacterText != null)
currentCharacterText.text = newChar.name;
// Highlight icon
if (teamCharacterIcons != null)
{
CashSystemManager mgr = CashSystemManager.Instance;
if (mgr != null)
{
var team = mgr.GetTeamCharacters("Player");
int idx = team.IndexOf(newChar);
for (int i = 0; i < teamCharacterIcons.Length; i++)
if (teamCharacterIcons[i] != null)
teamCharacterIcons[i].color = i == idx ? Color.white : new Color(1, 1, 1, 0.5f);
}
}
}
private void OnCarrierKilled(CashCarrier carrier)
{
RefreshCarriedCash();
}
private void OnMatchEnd(string winnerTag)
{
// Could show final score overlay
}
// ─── Helpers ─────────────────────────────────────────────
private void RefreshVaultBars()
{
if (playerVaultText != null) playerVaultText.text = $"${_playerScore:F0}";
if (enemyVaultText != null) enemyVaultText.text = $"${_enemyScore:F0}";
if (playerVaultBar != null) playerVaultBar.value = _playerScore;
if (enemyVaultBar != null) enemyVaultBar.value = _enemyScore;
}
private void RefreshCarriedCash()
{
CashSystemManager mgr = CashSystemManager.Instance;
if (mgr == null) { HideCarried(); return; }
GameObject cur = mgr.CurrentControlledCharacter;
if (cur == null) { HideCarried(); return; }
// Cache CashCarrier — only re-fetch when controlled character changes
if (_cachedCarrierCharacter != cur)
{
_cachedCarrierCharacter = cur;
_cachedCarrier = cur.GetComponent<CashCarrier>();
}
if (_cachedCarrier == null || _cachedCarrier.CarriedCash < 0.01f) { HideCarried(); return; }
if (carriedCashText != null) carriedCashText.text = $"Carrying: ${_cachedCarrier.CarriedCash:F0}";
if (carriedCashPanel != null) carriedCashPanel.SetActive(true);
}
private void HideCarried()
{
if (carriedCashPanel != null) carriedCashPanel.SetActive(false);
}
private void UpdateStealProgress()
{
// Check player vault steal progress (uses EntityRegistry now)
var vaults = EntityRegistry<TeamVault>.GetAll();
TeamVault playerVault = null;
for (int i = 0; i < vaults.Count; i++)
if (vaults[i] != null && vaults[i].TeamTag == "Player") { playerVault = vaults[i]; break; }
if (playerVault != null && playerVault.IsBeingStolen)
{
_stealWasActive = true;
if (stealProgressPanel != null) stealProgressPanel.SetActive(true);
if (stealProgressBar != null) stealProgressBar.value = playerVault.StealProgress;
}
else
{
_stealWasActive = false;
if (stealProgressPanel != null) stealProgressPanel.SetActive(false);
}
}
private IEnumerator FadePanel(GameObject panel, float duration)
{
yield return new WaitForSeconds(duration);
if (panel != null) panel.SetActive(false);
}
// ─── Pickup Screen Effects ───────────────────────────────
private void ShowPickupFlash()
{
if (_pickupFlashCoroutine != null) StopCoroutine(_pickupFlashCoroutine);
// If no flash image assigned, create one at runtime
if (pickupFlashImage == null)
{
CreatePickupFlashImage();
}
if (pickupFlashImage != null)
{
_pickupFlashCoroutine = StartCoroutine(PickupFlashRoutine());
}
}
private void CreatePickupFlashImage()
{
Canvas canvas = GetComponentInParent<Canvas>();
if (canvas == null) canvas = FindObjectOfType<Canvas>();
if (canvas == null) return;
GameObject flashObj = new GameObject("PickupFlash");
flashObj.transform.SetParent(canvas.transform, false);
RectTransform rt = flashObj.AddComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
pickupFlashImage = flashObj.AddComponent<Image>();
pickupFlashImage.color = Color.clear;
pickupFlashImage.raycastTarget = false;
// Make sure it renders on top
flashObj.transform.SetAsLastSibling();
}
private IEnumerator PickupFlashRoutine()
{
pickupFlashImage.color = pickupFlashColor;
float elapsed = 0f;
while (elapsed < pickupFlashDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / pickupFlashDuration;
pickupFlashImage.color = Color.Lerp(pickupFlashColor, Color.clear, t);
yield return null;
}
pickupFlashImage.color = Color.clear;
}
private void ShowPickupAmount(float amount)
{
if (amount <= 0) return;
// If no pickup amount panel, create one dynamically
if (pickupAmountText == null)
{
CreatePickupAmountText();
}
if (pickupAmountText != null)
{
pickupAmountText.text = $"+${amount:F0}";
if (pickupAmountPanel != null) pickupAmountPanel.SetActive(true);
// Auto-hide after a short duration
StartCoroutine(FadePickupAmount());
}
}
private void CreatePickupAmountText()
{
Canvas canvas = GetComponentInParent<Canvas>();
if (canvas == null) canvas = FindObjectOfType<Canvas>();
if (canvas == null) return;
pickupAmountPanel = new GameObject("PickupAmountPanel");
pickupAmountPanel.transform.SetParent(canvas.transform, false);
RectTransform panelRect = pickupAmountPanel.AddComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.6f);
panelRect.anchorMax = new Vector2(0.5f, 0.6f);
panelRect.sizeDelta = new Vector2(700f, 160f);
panelRect.anchoredPosition = Vector2.zero;
GameObject textObj = new GameObject("PickupAmountText");
textObj.transform.SetParent(pickupAmountPanel.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
pickupAmountText = textObj.AddComponent<TextMeshProUGUI>();
pickupAmountText.fontSize = 96;
pickupAmountText.alignment = TextAlignmentOptions.Center;
pickupAmountText.color = new Color(1f, 0.92f, 0.23f); // Bright gold
pickupAmountText.fontStyle = FontStyles.Bold;
pickupAmountText.enableAutoSizing = false;
// Add outline for readability
pickupAmountText.outlineWidth = 0.35f;
pickupAmountText.outlineColor = new Color32(0, 0, 0, 200);
pickupAmountPanel.SetActive(false);
}
private IEnumerator FadePickupAmount()
{
float displayDuration = 1.2f;
float fadeDuration = 0.4f;
if (pickupAmountText == null) yield break;
// Float upward animation
RectTransform rt = pickupAmountPanel.GetComponent<RectTransform>();
Vector2 startPos = new Vector2(0f, 0f);
Vector2 endPos = new Vector2(0f, 50f);
Color startColor = pickupAmountText.color;
float elapsed = 0f;
while (elapsed < displayDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / displayDuration;
// Float upward
if (rt != null)
rt.anchoredPosition = Vector2.Lerp(startPos, endPos, t);
// Fade out in last portion
if (t > 0.6f)
{
float fadeT = (t - 0.6f) / 0.4f;
pickupAmountText.color = new Color(startColor.r, startColor.g, startColor.b, 1f - fadeT);
}
yield return null;
}
// Reset
pickupAmountText.color = startColor;
if (rt != null) rt.anchoredPosition = startPos;
if (pickupAmountPanel != null) pickupAmountPanel.SetActive(false);
}
// ─── Legacy API (called from other scripts) ──────────────
public void UpdateTimerDisplay(float timeRemaining) => OnTimerUpdated(timeRemaining);
public void UpdateVaultDisplay(TeamVault vault)
{
if (vault == null) return;
if (vault.TeamTag == "Player") _playerScore = vault.StoredCash;
else _enemyScore = vault.StoredCash;
_dirtyVault = true;
}
public void UpdateCarriedCashDisplay(CashCarrier carrier) => RefreshCarriedCash();
public void UpdateCurrentCharacter(string characterName, int index)
{
if (currentCharacterText != null) currentCharacterText.text = characterName;
}
public void ShowCountdown(string text)
{
if (countdownText != null)
countdownText.text = text;
if (countdownPanel != null)
countdownPanel.SetActive(!string.IsNullOrEmpty(text));
// Fallback: use timer text for countdown
if (countdownText == null && timerText != null && !string.IsNullOrEmpty(text))
{
if (_originalTimerFontSize < 0) _originalTimerFontSize = timerText.fontSize;
timerText.text = text;
timerText.fontSize = 72;
}
else if (countdownText == null && timerText != null && string.IsNullOrEmpty(text))
{
if (_originalTimerFontSize > 0) timerText.fontSize = _originalTimerFontSize;
}
}
}