using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; /// /// CashBoxImportController — Manages cash import to a TeamVault's CashBox. /// Features: /// - Spawns CashBox prefab at vault position with correct transform /// - Countdown window based on import amount /// - Reverse-plays "close" animation to open, then plays forward to close /// - Camera focuses on player and cashbox during import /// - Freezes player movement during import /// - Sound effects for open/close/counting /// /// Attach to the TeamVault or a manager object. /// public class CashBoxImportController : MonoBehaviour { [Header("CashBox Prefab")] [Tooltip("CashBox prefab to spawn at the vault")] [SerializeField] private GameObject cashBoxPrefab; [Header("Spawn Transform (from prefab inspector)")] [SerializeField] private Vector3 spawnOffset = Vector3.zero; [SerializeField] private Vector3 spawnRotation = new Vector3(-90f, 0f, 0f); [SerializeField] private Vector3 spawnScale = new Vector3(60f, 60f, 60f); [Header("Animation Settings")] [Tooltip("Name of the close animation state")] [SerializeField] private string closeAnimationName = "close"; [SerializeField] private float openAnimationDuration = 1f; [SerializeField] private float closeAnimationDuration = 1f; [Header("Import Settings")] [Tooltip("Seconds per $100 imported")] [SerializeField] private float countdownPerHundred = 0.5f; [SerializeField] private float minimumCountdown = 1f; [SerializeField] private float maximumCountdown = 10f; [Header("Camera Settings")] [SerializeField] private float cameraFocusDistance = 5f; [SerializeField] private float cameraFocusHeight = 2f; [SerializeField] private float cameraSmoothSpeed = 5f; [Header("Audio")] [Tooltip("Sound when CashBox opens")] [SerializeField] private AudioClip openSound; [Tooltip("Sound when CashBox closes")] [SerializeField] private AudioClip closeSound; [Tooltip("Looping sound during cash counting")] [SerializeField] private AudioClip countingSound; [Tooltip("Sound when import completes")] [SerializeField] private AudioClip completeSound; [SerializeField] private float soundVolume = 1f; [Header("UI References")] [SerializeField] private GameObject importUIPanel; [SerializeField] private TMP_Text countdownText; [Tooltip("Separate TMP for status label (e.g. 'IMPORTING'). Assign a TextMeshPro in your scene.")] [SerializeField] private TMP_Text statusText; [SerializeField] private TMP_Text amountText; [SerializeField] private Image progressDial; [Tooltip("Optional sprite for the circular dial (e.g. Thick_Dial)")] [SerializeField] private Sprite dialSprite; [Header("Scene References")] [Tooltip("Drag the TimeDial GameObject from the scene here (will use its Image component)")] [SerializeField] private GameObject progressDialObject; // Runtime private GameObject _spawnedCashBox; private Animator _cashBoxAnimator; private AudioSource _audioSource; private TeamVault _vault; private Camera _mainCamera; private Vector3 _originalCameraPosition; private Quaternion _originalCameraRotation; private Transform _originalCameraParent; private bool _isImporting; private CashCarrier _currentCarrier; public bool IsImporting => _isImporting; private void Awake() { _vault = GetComponent(); _audioSource = GetComponent(); if (_audioSource == null) _audioSource = gameObject.AddComponent(); _audioSource.playOnAwake = false; _audioSource.spatialBlend = 0f; // 2D sound for UI feedback _mainCamera = Camera.main; // If a GameObject was provided (e.g. your TimeDial), try to grab its Image component if (progressDial == null && progressDialObject != null) { var img = progressDialObject.GetComponent(); if (img == null) img = progressDialObject.GetComponentInChildren(); if (img != null) { progressDial = img; Debug.Log("[CashBoxImportController] Assigned progressDial from progressDialObject"); } else { Debug.LogWarning("[CashBoxImportController] progressDialObject does not contain an Image component"); } } // If a dial sprite was provided and progressDial exists, apply it if (progressDial != null && dialSprite != null) { progressDial.sprite = dialSprite; progressDial.type = Image.Type.Filled; progressDial.fillMethod = Image.FillMethod.Radial360; progressDial.fillOrigin = (int)Image.Origin360.Top; progressDial.fillClockwise = true; progressDial.fillAmount = 0f; } } private void Start() { // Auto-setup UI if not assigned if (importUIPanel == null) CreateImportUI(); else importUIPanel.SetActive(false); // Auto-spawn CashBox on start if prefab is assigned if (cashBoxPrefab != null && _spawnedCashBox == null) SpawnCashBox(); } /// /// Configure the controller from TeamVault at runtime (when added dynamically). /// public void Configure(GameObject prefab, Vector3 offset, Vector3 rotation, Vector3 scale) { cashBoxPrefab = prefab; spawnOffset = offset; spawnRotation = rotation; spawnScale = scale; } /// /// Spawn the CashBox at vault position. Call this on game start or when needed. /// public void SpawnCashBox() { if (cashBoxPrefab == null) { Debug.LogWarning("[CashBoxImportController] No CashBox prefab assigned!"); return; } if (_spawnedCashBox != null) { Debug.Log("[CashBoxImportController] CashBox already spawned"); return; } // Hide the vault's own visual mesh/renderers so only the CashBox is visible HideVaultVisuals(); // Spawn at vault position with specified transform Vector3 spawnPos = transform.position + spawnOffset; Quaternion spawnRot = Quaternion.Euler(spawnRotation); _spawnedCashBox = Instantiate(cashBoxPrefab, spawnPos, spawnRot, transform); _spawnedCashBox.transform.localScale = spawnScale; _spawnedCashBox.name = "CashBox_" + (_vault != null ? _vault.TeamTag : "Vault"); _cashBoxAnimator = _spawnedCashBox.GetComponent(); if (_cashBoxAnimator == null) _cashBoxAnimator = _spawnedCashBox.GetComponentInChildren(); Debug.Log($"[CashBoxImportController] Spawned CashBox at {spawnPos}, rotation {spawnRotation}, scale {spawnScale}"); } /// /// Disable the vault's own mesh renderers so only the CashBox prefab shows. /// Skips any renderers on the spawned CashBox itself. /// private void HideVaultVisuals() { // Disable all renderers on the vault GameObject (not children that are the CashBox) foreach (var renderer in GetComponents()) { renderer.enabled = false; } // Also disable renderers on existing vault children (the old "vault box") // but skip any previously spawned CashBox for (int i = 0; i < transform.childCount; i++) { Transform child = transform.GetChild(i); if (_spawnedCashBox != null && child.gameObject == _spawnedCashBox) continue; if (child.name.StartsWith("CashBox_")) continue; // Skip our CashBox foreach (var renderer in child.GetComponentsInChildren()) { renderer.enabled = false; Debug.Log($"[CashBoxImportController] Disabled vault visual: {renderer.gameObject.name}"); } } } /// /// Start the import process for a carrier depositing cash. /// public void StartImport(CashCarrier carrier) { if (_isImporting) { Debug.LogWarning("[CashBoxImportController] Import already in progress!"); return; } if (carrier == null || carrier.CarriedCash <= 0) { Debug.LogWarning("[CashBoxImportController] No cash to import!"); return; } // Spawn CashBox if not already if (_spawnedCashBox == null) SpawnCashBox(); if (_spawnedCashBox == null) { Debug.LogError("[CashBoxImportController] Failed to spawn CashBox!"); return; } _currentCarrier = carrier; StartCoroutine(ImportRoutine(carrier)); } private IEnumerator ImportRoutine(CashCarrier carrier) { _isImporting = true; float cashAmount = carrier.CarriedCash; // Calculate countdown based on amount float countdownDuration = Mathf.Clamp( (cashAmount / 100f) * countdownPerHundred, minimumCountdown, maximumCountdown ); Debug.Log($"[CashBoxImportController] Starting import of ${cashAmount:F0}, duration {countdownDuration:F1}s"); // 1. Freeze player movement FreezeCarrier(carrier, true); // 2. Focus camera yield return StartCoroutine(FocusCamera(carrier.transform, _spawnedCashBox.transform)); // 3. Show UI ShowImportUI(cashAmount, countdownDuration); // 4. Play open animation (reverse of close) yield return StartCoroutine(PlayAnimationReverse(closeAnimationName, openAnimationDuration)); PlaySound(openSound); // 5. Countdown with cash counting sound yield return StartCoroutine(CountdownRoutine(cashAmount, countdownDuration)); // 6. Play close animation (forward) yield return StartCoroutine(PlayAnimationForward(closeAnimationName, closeAnimationDuration)); PlaySound(closeSound); // 7. Deposit the cash to vault if (_vault != null && carrier != null) { float deposited = carrier.DepositToVault(_vault); Debug.Log($"[CashBoxImportController] Deposited ${deposited:F0} to vault. Carrier now has ${carrier.CarriedCash:F0}"); } else { Debug.LogError($"[CashBoxImportController] Deposit FAILED — vault: {_vault}, carrier: {carrier}"); } // Force-clear any remaining carried cash display // DepositToVault clears bags, but fire event again as safety GameEvents.FireCashDeposited( carrier != null ? carrier.TeamTag : "Player", cashAmount, 1); PlaySound(completeSound); // 8. Hide UI HideImportUI(); // 9. Restore camera yield return StartCoroutine(RestoreCamera()); // 10. Unfreeze player FreezeCarrier(carrier, false); _isImporting = false; _currentCarrier = null; Debug.Log($"[CashBoxImportController] Import complete!"); } /// /// Play animation in reverse by stepping normalizedTime from 1 to 0. /// private IEnumerator PlayAnimationReverse(string animName, float duration) { if (_cashBoxAnimator == null) yield break; float elapsed = 0f; // Start at end of animation (normalizedTime = 1) _cashBoxAnimator.Play(animName, 0, 1f); _cashBoxAnimator.speed = 0f; // Pause normal playback while (elapsed < duration) { elapsed += Time.deltaTime; float normalizedTime = 1f - (elapsed / duration); // Go from 1 to 0 normalizedTime = Mathf.Clamp01(normalizedTime); // Continuously set the animation time _cashBoxAnimator.Play(animName, 0, normalizedTime); yield return null; } // Ensure we end at time 0 (fully open) _cashBoxAnimator.Play(animName, 0, 0f); _cashBoxAnimator.speed = 1f; // Restore normal speed } /// /// Play animation forward normally. /// private IEnumerator PlayAnimationForward(string animName, float duration) { if (_cashBoxAnimator == null) yield break; // Play from start _cashBoxAnimator.speed = 1f; _cashBoxAnimator.Play(animName, 0, 0f); yield return new WaitForSeconds(duration); } private IEnumerator FocusCamera(Transform playerTransform, Transform boxTransform) { if (_mainCamera == null) yield break; // Store original camera state _originalCameraPosition = _mainCamera.transform.position; _originalCameraRotation = _mainCamera.transform.rotation; _originalCameraParent = _mainCamera.transform.parent; // Detach camera temporarily _mainCamera.transform.SetParent(null); // Calculate target position (between player and box, offset back) Vector3 midPoint = (playerTransform.position + boxTransform.position) / 2f; Vector3 dirToBox = (boxTransform.position - playerTransform.position).normalized; Vector3 sideDir = Vector3.Cross(dirToBox, Vector3.up).normalized; Vector3 targetPos = midPoint + sideDir * cameraFocusDistance + Vector3.up * cameraFocusHeight; Quaternion targetRot = Quaternion.LookRotation(midPoint - targetPos); // Smooth move camera float elapsed = 0f; float focusDuration = 0.5f; while (elapsed < focusDuration) { elapsed += Time.deltaTime; float t = elapsed / focusDuration; t = t * t * (3f - 2f * t); // Smoothstep _mainCamera.transform.position = Vector3.Lerp(_originalCameraPosition, targetPos, t); _mainCamera.transform.rotation = Quaternion.Slerp(_originalCameraRotation, targetRot, t); yield return null; } } private IEnumerator RestoreCamera() { if (_mainCamera == null) yield break; Vector3 currentPos = _mainCamera.transform.position; Quaternion currentRot = _mainCamera.transform.rotation; float elapsed = 0f; float restoreDuration = 0.5f; while (elapsed < restoreDuration) { elapsed += Time.deltaTime; float t = elapsed / restoreDuration; t = t * t * (3f - 2f * t); // Smoothstep _mainCamera.transform.position = Vector3.Lerp(currentPos, _originalCameraPosition, t); _mainCamera.transform.rotation = Quaternion.Slerp(currentRot, _originalCameraRotation, t); yield return null; } // Re-parent camera if (_originalCameraParent != null) _mainCamera.transform.SetParent(_originalCameraParent); } private IEnumerator CountdownRoutine(float totalAmount, float duration) { float elapsed = 0f; // Start counting sound loop if (countingSound != null) { _audioSource.clip = countingSound; _audioSource.loop = true; _audioSource.volume = soundVolume * 0.7f; _audioSource.Play(); } while (elapsed < duration) { elapsed += Time.deltaTime; float progress = elapsed / duration; float remaining = duration - elapsed; float countedAmount = totalAmount * progress; // Update UI UpdateImportUI(remaining, countedAmount, totalAmount, progress); yield return null; } // Stop counting sound if (_audioSource.isPlaying && _audioSource.clip == countingSound) _audioSource.Stop(); // Final UI update UpdateImportUI(0f, totalAmount, totalAmount, 1f); } // Saved component states before freeze (restore exactly on unfreeze) private bool _carrierWasKinematic; private bool _aiWasEnabled; private bool _playerScriptWasEnabled; private bool _navAgentWasEnabled; private bool _navAgentWasStopped; private bool _cashAIWasEnabled; private bool _playerMovementWasEnabled; private void FreezeCarrier(CashCarrier carrier, bool freeze) { if (carrier == null) return; // CharacterAIController var aiController = carrier.GetComponent(); // PlayerScript var playerScript = carrier.GetComponent(); // PlayerMovementBehaviour var playerMovement = carrier.GetComponent(); // NavMeshAgent var navAgent = carrier.GetComponent(); // CashSystemAI var cashAI = carrier.GetComponent(); // Rigidbody var rb = carrier.GetComponent(); if (freeze) { // ─── SAVE original states BEFORE disabling ─── _aiWasEnabled = aiController != null && aiController.enabled; _playerScriptWasEnabled = playerScript != null && playerScript.enabled; _playerMovementWasEnabled = playerMovement != null && playerMovement.enabled; _navAgentWasEnabled = navAgent != null && navAgent.enabled; _navAgentWasStopped = navAgent != null && navAgent.enabled && navAgent.isStopped; _cashAIWasEnabled = cashAI != null && cashAI.enabled; // Disable everything if (aiController != null) aiController.enabled = false; if (playerScript != null) playerScript.enabled = false; if (playerMovement != null) playerMovement.enabled = false; if (cashAI != null) cashAI.enabled = false; if (navAgent != null && navAgent.enabled) navAgent.isStopped = true; if (rb != null) { _carrierWasKinematic = rb.isKinematic; rb.isKinematic = true; rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } } else { // ─── RESTORE exactly the original states ─── // Only re-enable components that were enabled BEFORE freeze if (aiController != null) aiController.enabled = _aiWasEnabled; if (playerScript != null) playerScript.enabled = _playerScriptWasEnabled; if (playerMovement != null) playerMovement.enabled = _playerMovementWasEnabled; if (cashAI != null) cashAI.enabled = _cashAIWasEnabled; if (navAgent != null) { // Only touch the agent if it was enabled before freeze if (_navAgentWasEnabled && navAgent.enabled) navAgent.isStopped = _navAgentWasStopped; } if (rb != null) { rb.isKinematic = _carrierWasKinematic; } } Debug.Log($"[CashBoxImportController] Carrier movement {(freeze ? "frozen" : "restored")}"); } private void PlaySound(AudioClip clip) { if (clip == null || _audioSource == null) return; _audioSource.PlayOneShot(clip, soundVolume); } // ─── UI Management ─────────────────────────────────────── private void CreateImportUI() { // Create a simple runtime UI panel Canvas canvas = FindObjectOfType(); if (canvas == null) { GameObject canvasObj = new GameObject("ImportUICanvas"); canvas = canvasObj.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvasObj.AddComponent(); canvasObj.AddComponent(); } // Create panel importUIPanel = new GameObject("ImportUIPanel"); importUIPanel.transform.SetParent(canvas.transform, false); RectTransform panelRect = importUIPanel.AddComponent(); panelRect.anchorMin = new Vector2(0.5f, 0.5f); panelRect.anchorMax = new Vector2(0.5f, 0.5f); panelRect.sizeDelta = new Vector2(400f, 150f); panelRect.anchoredPosition = new Vector2(0f, 100f); Image panelBg = importUIPanel.AddComponent(); panelBg.color = new Color(0f, 0f, 0f, 0.8f); // Create countdown text GameObject countdownObj = new GameObject("CountdownText"); countdownObj.transform.SetParent(importUIPanel.transform, false); RectTransform countdownRect = countdownObj.AddComponent(); countdownRect.anchorMin = new Vector2(0, 0.6f); countdownRect.anchorMax = new Vector2(1, 1f); countdownRect.offsetMin = Vector2.zero; countdownRect.offsetMax = Vector2.zero; countdownText = countdownObj.AddComponent(); countdownText.text = "IMPORTING..."; countdownText.fontSize = 36; countdownText.alignment = TextAlignmentOptions.Center; countdownText.color = Color.white; // Create amount text GameObject amountObj = new GameObject("AmountText"); amountObj.transform.SetParent(importUIPanel.transform, false); RectTransform amountRect = amountObj.AddComponent(); amountRect.anchorMin = new Vector2(0, 0.3f); amountRect.anchorMax = new Vector2(1, 0.6f); amountRect.offsetMin = Vector2.zero; amountRect.offsetMax = Vector2.zero; amountText = amountObj.AddComponent(); amountText.text = "$0"; amountText.fontSize = 28; amountText.alignment = TextAlignmentOptions.Center; amountText.color = new Color(0.2f, 1f, 0.2f); // Create circular progress dial (radial Image) GameObject dialObj = new GameObject("ProgressDial"); dialObj.transform.SetParent(importUIPanel.transform, false); RectTransform dialRect = dialObj.AddComponent(); dialRect.anchorMin = new Vector2(0.35f, 0.05f); dialRect.anchorMax = new Vector2(0.65f, 0.35f); dialRect.offsetMin = Vector2.zero; dialRect.offsetMax = Vector2.zero; progressDial = dialObj.AddComponent(); if (dialSprite != null) progressDial.sprite = dialSprite; progressDial.type = Image.Type.Filled; progressDial.fillMethod = Image.FillMethod.Radial360; progressDial.fillOrigin = (int)Image.Origin360.Top; progressDial.fillClockwise = true; progressDial.fillAmount = 0f; progressDial.preserveAspect = true; importUIPanel.SetActive(false); } private void ShowImportUI(float totalAmount, float duration) { if (importUIPanel != null) importUIPanel.SetActive(true); UpdateImportUI(duration, 0f, totalAmount, 0f); } private void HideImportUI() { if (importUIPanel != null) importUIPanel.SetActive(false); } private void UpdateImportUI(float remaining, float counted, float total, float progress) { // Timer text: only the seconds number if (countdownText != null) countdownText.text = remaining > 0 ? $"{remaining:F1}" : "0"; // Status text: label on a separate TMP if (statusText != null) statusText.text = remaining > 0 ? "IMPORTING" : "COMPLETE!"; if (amountText != null) amountText.text = $"${counted:F0} / ${total:F0}"; if (progressDial != null) progressDial.fillAmount = Mathf.Clamp01(progress); } // ─── Public API ────────────────────────────────────────── /// /// Check if a carrier can start importing (not already importing). /// public bool CanImport(CashCarrier carrier) { return !_isImporting && carrier != null && carrier.CarriedCash > 0; } /// /// Cancel current import (emergency use only). /// public void CancelImport() { if (!_isImporting) return; StopAllCoroutines(); if (_currentCarrier != null) FreezeCarrier(_currentCarrier, false); HideImportUI(); _isImporting = false; _currentCarrier = null; // Restore camera immediately if (_mainCamera != null && _originalCameraParent != null) { _mainCamera.transform.SetParent(_originalCameraParent); _mainCamera.transform.position = _originalCameraPosition; _mainCamera.transform.rotation = _originalCameraRotation; } Debug.Log("[CashBoxImportController] Import cancelled"); } }