using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; using System.Collections.Generic; /// /// 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. /// 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 _feedEntries = new List(); 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(); if (_canvasGroup == null) _canvasGroup = gameObject.AddComponent(); // 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 = ""; } /// Show the HUD (called by CashSystemManager after intro completes). public void ShowHUD() { if (_canvasGroup == null) return; _canvasGroup.alpha = 1f; _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = true; Debug.Log("[CashSystemUI] HUD shown"); } /// /// 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. /// 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(); 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(); 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(); if (importImage != null && stealProgressPanel != null) { var stealPanelImg = stealProgressPanel.GetComponent(); 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(); 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(); 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(CashMag 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(CashMag 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 = $"{killer} {action} {victim}"; _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(); } 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.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(); if (canvas == null) canvas = FindObjectOfType(); if (canvas == null) return; GameObject flashObj = new GameObject("PickupFlash"); flashObj.transform.SetParent(canvas.transform, false); RectTransform rt = flashObj.AddComponent(); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; pickupFlashImage = flashObj.AddComponent(); 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(); if (canvas == null) canvas = FindObjectOfType(); if (canvas == null) return; pickupAmountPanel = new GameObject("PickupAmountPanel"); pickupAmountPanel.transform.SetParent(canvas.transform, false); RectTransform panelRect = pickupAmountPanel.AddComponent(); 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(); textRect.anchorMin = Vector2.zero; textRect.anchorMax = Vector2.one; textRect.offsetMin = Vector2.zero; textRect.offsetMax = Vector2.zero; pickupAmountText = textObj.AddComponent(); 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(); 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; } } }