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(CashBag 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(CashBag 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--)
{
CashBag 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--)
{
CashBag 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);
CashBag 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(CashBag bag) => _carriedBags.Contains(bag);
/// Legacy alias for IsCarryingBag.
public bool IsCarryingBox(CashBag bag) => IsCarryingBag(bag);
// ─── Cleanup ─────────────────────────────────────────────
private void OnDestroy()
{
if (_originalSpeed >= 0 && _aiController != null)
_aiController.SetSpeed(_originalSpeed);
if (_animator != null) _animator.speed = 1f;
}
}