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