using UnityEngine; using UnityEngine.Events; using System.Collections.Generic; /// /// CashCarrier — Attach to characters to track carried cash bags. /// Tracks actual CashBag references (not just float), handles stacking /// speed penalty, combo multiplier on deposit, death drops. /// Uses GameEvents instead of FindObjectOfType. /// public class CashCarrier : MonoBehaviour { [Header("Carry Settings")] [SerializeField] private int maxBags = 3; [Header("Movement Penalty")] [SerializeField] private float singleBagPenalty = 0.15f; // -15% speed per bag [SerializeField] private float multiBagExtraPenalty = 0.10f; // Additional -10% for 2+ bags [SerializeField] private float maxSpeedReduction = 0.40f; // Never slower than 60% speed [Header("Death Drop")] [SerializeField] private float cashDropPercent = 1.0f; // Drop 100% on death (all bags scatter) [SerializeField] private GameObject droppedCashPrefab; // Optional: legacy prefab for dropped cash [Header("Visual Indicators")] [SerializeField] private GameObject carryingIndicator; [SerializeField] private ParticleSystem cashParticles; [Header("Events")] public UnityEvent OnCashChanged; public UnityEvent OnCashDropped; // ─── Runtime ───────────────────────────────────────────── private readonly List _carriedBags = new List(4); private float _legacyCash = 0f; // Backwards compat: cash from old-style AddCash(float) private float _originalSpeed = -1f; private CharacterAIController _aiController; private HealthNew _health; private Animator _animator; private bool _isDead = false; // ─── Combo Tracking ────────────────────────────────────── private float _lastDepositTime; private int _comboCount; private const float COMBO_WINDOW = 10f; // Seconds between deposits to maintain combo // ─── Properties ────────────────────────────────────────── public int BagCount => _carriedBags.Count; public int BoxCount => BagCount; // Legacy alias public float TotalValue => CarriedCash; // Alias for AI compatibility public float CarriedCash { get { float total = _legacyCash; for (int i = 0; i < _carriedBags.Count; i++) if (_carriedBags[i] != null) total += _carriedBags[i].GetEffectiveValue(); return total; } } public bool IsCarrying => _carriedBags.Count > 0 || _legacyCash > 0; public string TeamTag => gameObject.tag; public float SpeedMultiplier { get; private set; } = 1f; public int ComboCount => _comboCount; public List CarriedBags => _carriedBags; public List CarriedBoxes => _carriedBags; // Legacy alias /// True when this character is carrying an ExtractionVault (managed by ExtractionVault, not CashCarrier). public bool IsCarryingVault { get { var vaults = EntityRegistry.GetAll(); for (int i = 0; i < vaults.Count; i++) if (vaults[i] != null && vaults[i].IsCarried && vaults[i].Carrier == gameObject) return true; return false; } } // ─── Lifecycle ─────────────────────────────────────────── private void OnEnable() { EntityRegistry.Register(this); } private void OnDisable() { EntityRegistry.Unregister(this); } private void Start() { if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected) { enabled = false; return; } // ── Explicit reset — guarantees 0 cash on every spawn/reuse ── _carriedBags.Clear(); _legacyCash = 0f; _isDead = false; _aiController = GetComponent(); _health = GetComponent(); _animator = GetComponent(); if (_aiController != null) _originalSpeed = _aiController.GetSpeed(); UpdateVisuals(); OnCashChanged?.Invoke(0f); // Tell listeners we start at zero } private void Update() { // Death check if (_health != null && _health.isDead && !_isDead) { _isDead = true; OnDeath(); } else if (_health != null && !_health.isDead) { _isDead = false; } ApplyMovementPenalty(); } // ─── Bag Management ───────────────────────────────────── /// Can this carrier pick up more bags? public bool CanPickUp() => _carriedBags.Count < maxBags && !_isDead; /// Called by CashBag when picked up. public void OnBagPickedUp(CashMag bag) { if (bag == null || _carriedBags.Contains(bag)) return; _carriedBags.Add(bag); UpdateVisuals(); OnCashChanged?.Invoke(CarriedCash); GameEvents.FireKillFeedEntry(gameObject.name, "picked up", $"Cash (${bag.CashValue})"); } /// Legacy alias for OnBagPickedUp. public void OnBoxPickedUp(CashMag bag) => OnBagPickedUp(bag); /// Drop all carried bags. public void DropAllBags() { if (_carriedBags.Count == 0) return; Vector3 dropPos = transform.position; float totalDropped = 0f; for (int i = _carriedBags.Count - 1; i >= 0; i--) { CashMag bag = _carriedBags[i]; if (bag != null) { // Scatter bags slightly around drop point Vector3 offset = Random.insideUnitSphere * 1.5f; offset.y = 0; bag.Drop(dropPos + offset); totalDropped += bag.CashValue; } } _carriedBags.Clear(); UpdateVisuals(); OnCashDropped?.Invoke(totalDropped); OnCashChanged?.Invoke(CarriedCash); } /// Legacy alias for DropAllBags. public void DropAllBoxes() => DropAllBags(); /// Deposit all to vault. Returns total value deposited. public float DepositToVault(TeamVault vault) { if (vault == null) return 0f; if (_carriedBags.Count == 0 && _legacyCash <= 0) return 0f; // Calculate combo if (Time.time - _lastDepositTime <= COMBO_WINDOW) _comboCount++; else _comboCount = 1; _lastDepositTime = Time.time; float totalValue = 0f; int comboMultiplier = Mathf.Min(_comboCount, 3); // Cap at 3x // Deposit tracked bags for (int i = _carriedBags.Count - 1; i >= 0; i--) { CashMag bag = _carriedBags[i]; if (bag != null) { float value = bag.GetEffectiveValue(); switch (comboMultiplier) { case 2: value *= 1.5f; break; case 3: value *= 2.0f; break; } totalValue += value; bag.Despawn(); } } _carriedBags.Clear(); // Deposit any legacy cash too if (_legacyCash > 0) { float legacyValue = _legacyCash; switch (comboMultiplier) { case 2: legacyValue *= 1.5f; break; case 3: legacyValue *= 2.0f; break; } totalValue += legacyValue; _legacyCash = 0f; } vault.DepositCash(totalValue); UpdateVisuals(); OnCashChanged?.Invoke(0f); // Fire events GameEvents.FireCashDeposited(TeamTag, totalValue, comboMultiplier); if (comboMultiplier > 1) GameEvents.FireComboMultiplier(comboMultiplier); GameEvents.FireKillFeedEntry( gameObject.name, comboMultiplier > 1 ? $"SCORED {comboMultiplier}x" : "scored", $"${totalValue:F0}" ); return totalValue; } // ─── Legacy API (backwards compat with old CashBag/CashSystemAI) ─ /// Add cash directly (legacy float-based API). public void AddCash(float amount) { _legacyCash += amount; UpdateVisuals(); OnCashChanged?.Invoke(CarriedCash); } /// Remove cash amount (legacy API). public float RemoveCash(float amount) { float removed = Mathf.Min(amount, _legacyCash); _legacyCash -= removed; UpdateVisuals(); OnCashChanged?.Invoke(CarriedCash); return removed; } /// Remove all cash and return amount (legacy API). public float RemoveAllCash() { float removed = CarriedCash; _legacyCash = 0f; _carriedBags.Clear(); UpdateVisuals(); OnCashChanged?.Invoke(0f); return removed; } // ─── Death ─────────────────────────────────────────────── private void OnDeath() { // Fire character died for respawn system (always, even if not carrying) GameEvents.FireCharacterDied(gameObject); if (!IsCarrying) return; GameEvents.FireCarrierKilled(this); // Drop tracked bags (scatter around) DropAllBags(); // Drop legacy cash as a physical pickup if (_legacyCash > 0 && droppedCashPrefab != null) { float dropAmount = _legacyCash * cashDropPercent; GameObject dropped = Instantiate(droppedCashPrefab, transform.position + Vector3.up, Quaternion.identity); CashMag droppedBag = dropped.GetComponent(); if (droppedBag != null) droppedBag.SetCashValue(dropAmount); _legacyCash -= dropAmount; OnCashDropped?.Invoke(dropAmount); } } // ─── Movement Penalty ──────────────────────────────────── private void ApplyMovementPenalty() { int count = _carriedBags.Count; if (count == 0 && _legacyCash <= 0) { SpeedMultiplier = 1f; } else if (count == 0) { // Legacy cash — simple penalty float penaltyFactor = Mathf.Clamp01(_legacyCash / 500f); SpeedMultiplier = 1f - (singleBagPenalty * penaltyFactor); } else { // Stacking penalty: -15% per bag, -10% extra for 2+ float penalty = singleBagPenalty * count; if (count >= 2) penalty += multiBagExtraPenalty * (count - 1); penalty = Mathf.Min(penalty, maxSpeedReduction); SpeedMultiplier = 1f - penalty; } // Apply to AI controller (skip if carrying ExtractionVault — vault manages speed directly) if (_aiController != null && _originalSpeed > 0 && !IsCarryingVault) _aiController.SetSpeed(_originalSpeed * SpeedMultiplier); // Apply to animator if (_animator != null) { if (IsCarrying) { float currentSpeed = _animator.GetFloat("Speed"); if (currentSpeed > 0) _animator.speed = SpeedMultiplier; } else { _animator.speed = 1f; } } } // ─── Visuals ───────────────────────────────────────────── private void UpdateVisuals() { bool carrying = IsCarrying; if (carryingIndicator != null) carryingIndicator.SetActive(carrying); if (cashParticles != null) { if (carrying && !cashParticles.isPlaying) cashParticles.Play(); else if (!carrying && cashParticles.isPlaying) cashParticles.Stop(true, ParticleSystemStopBehavior.StopEmitting); } } // ─── Queries ───────────────────────────────────────────── /// Should AI deposit? (has bags or enough cash) public bool ShouldDeposit(float threshold = 200f) { return _carriedBags.Count >= 1 || _legacyCash >= threshold; } /// Check if carrying a specific bag. public bool IsCarryingBag(CashMag bag) => _carriedBags.Contains(bag); /// Legacy alias for IsCarryingBag. public bool IsCarryingBox(CashMag bag) => IsCarryingBag(bag); // ─── Cleanup ───────────────────────────────────────────── private void OnDestroy() { if (_originalSpeed >= 0 && _aiController != null) _aiController.SetSpeed(_originalSpeed); if (_animator != null) _animator.speed = 1f; } }