391 lines
13 KiB
C#
391 lines
13 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<float> OnCashChanged;
|
|
public UnityEvent<float> OnCashDropped;
|
|
|
|
// ─── Runtime ─────────────────────────────────────────────
|
|
private readonly List<CashMag> _carriedBags = new List<CashMag>(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<CashMag> CarriedBags => _carriedBags;
|
|
public List<CashMag> CarriedBoxes => _carriedBags; // Legacy alias
|
|
|
|
/// <summary>True when this character is carrying an ExtractionVault (managed by ExtractionVault, not CashCarrier).</summary>
|
|
public bool IsCarryingVault
|
|
{
|
|
get
|
|
{
|
|
var vaults = EntityRegistry<ExtractionVault>.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<CashCarrier>.Register(this);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EntityRegistry<CashCarrier>.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<CharacterAIController>();
|
|
_health = GetComponent<HealthNew>();
|
|
_animator = GetComponent<Animator>();
|
|
|
|
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 ─────────────────────────────────────
|
|
|
|
/// <summary>Can this carrier pick up more bags?</summary>
|
|
public bool CanPickUp() => _carriedBags.Count < maxBags && !_isDead;
|
|
|
|
/// <summary>Called by CashBag when picked up.</summary>
|
|
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})");
|
|
}
|
|
|
|
/// <summary>Legacy alias for OnBagPickedUp.</summary>
|
|
public void OnBoxPickedUp(CashMag bag) => OnBagPickedUp(bag);
|
|
|
|
/// <summary>Drop all carried bags.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Legacy alias for DropAllBags.</summary>
|
|
public void DropAllBoxes() => DropAllBags();
|
|
|
|
/// <summary>Deposit all to vault. Returns total value deposited.</summary>
|
|
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) ─
|
|
|
|
/// <summary>Add cash directly (legacy float-based API).</summary>
|
|
public void AddCash(float amount)
|
|
{
|
|
_legacyCash += amount;
|
|
UpdateVisuals();
|
|
OnCashChanged?.Invoke(CarriedCash);
|
|
}
|
|
|
|
/// <summary>Remove cash amount (legacy API).</summary>
|
|
public float RemoveCash(float amount)
|
|
{
|
|
float removed = Mathf.Min(amount, _legacyCash);
|
|
_legacyCash -= removed;
|
|
UpdateVisuals();
|
|
OnCashChanged?.Invoke(CarriedCash);
|
|
return removed;
|
|
}
|
|
|
|
/// <summary>Remove all cash and return amount (legacy API).</summary>
|
|
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<CashMag>();
|
|
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 ─────────────────────────────────────────────
|
|
|
|
/// <summary>Should AI deposit? (has bags or enough cash)</summary>
|
|
public bool ShouldDeposit(float threshold = 200f)
|
|
{
|
|
return _carriedBags.Count >= 1 || _legacyCash >= threshold;
|
|
}
|
|
|
|
/// <summary>Check if carrying a specific bag.</summary>
|
|
public bool IsCarryingBag(CashMag bag) => _carriedBags.Contains(bag);
|
|
|
|
/// <summary>Legacy alias for IsCarryingBag.</summary>
|
|
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;
|
|
}
|
|
}
|