using UnityEngine; /// /// CashBag — Collectible cash bag for Cash System mode. /// Neutral gold color when spawned, takes team color on pickup. /// Tracks state: Available → Carried → Dropped → Available/Destroyed. /// Registers with EntityRegistry for zero-allocation queries. /// Fires GameEvents instead of using FindObjectOfType. /// The vault (TeamVault) is the "CashBox" where bags are deposited. /// public class CashBag : MonoBehaviour { public enum BagState { Available, Carried, Dropped } [Header("Cash Settings")] [SerializeField] private float cashValue = 100f; [SerializeField] private float hotDropBonusPercent = 10f; // +10% if picked during hot drop window [Header("Visual Feedback")] [SerializeField] private float rotationSpeed = 30f; [SerializeField] private float bobSpeed = 1f; [SerializeField] private float bobHeight = 0.2f; [SerializeField] private float floatOffset = 0.3f; [Header("Scale")] [SerializeField] private Vector3 targetScale = new Vector3(0.35f, 0.35f, 0.35f); [Header("Drop Settings")] [SerializeField] private float hotDropWindow = 3f; // Seconds after drop where pickup gives bonus [SerializeField] private float dropDespawnTime = 30f; // Despawn dropped bag after this time [SerializeField] private float pickupGracePeriod = 1.5f; // Seconds after spawn/enable before pickup allowed [Header("Visual References")] [SerializeField] private MeshRenderer meshRenderer; [SerializeField] private ParticleSystem idleParticles; [SerializeField] private ParticleSystem pickupBurstEffect; [SerializeField] private ParticleSystem dropImpactEffect; [SerializeField] private GameObject glowObject; // Point light or glow sprite [SerializeField] private GameObject worldIndicator; // "!" icon for dropped bags [Header("Colors")] [SerializeField] private Color neutralColor = new Color(1f, 0.84f, 0f); // Gold [SerializeField] private Color playerTeamColor = new Color(0.13f, 0.59f, 0.95f);// Blue [SerializeField] private Color enemyTeamColor = new Color(0.96f, 0.26f, 0.21f); // Red [SerializeField] private Color droppedPulseColor = new Color(1f, 0.84f, 0f); // Pulsing gold [Header("Glow Settings")] [SerializeField] private float glowLightRange = 8f; [SerializeField] private float glowLightIntensity = 2f; [SerializeField] private Color glowLightColor = new Color(1f, 0.84f, 0f); // Gold [SerializeField] private float glowPulseSpeed = 2f; [SerializeField] private float glowPulseMin = 0.6f; [SerializeField] private float glowPulseMax = 1f; [Header("Audio")] [Tooltip("Sound effect played when this bag is picked up")] [SerializeField] private AudioClip pickupSound; [SerializeField] private float pickupSoundVolume = 3f; // ─── Runtime State ─────────────────────────────────────── private BagState _state = BagState.Available; private CashCarrier _carrier; private Vector3 _spawnPosition; private float _dropTime; private float _despawnTimer; private Collider _collider; private Light _glowLight; private float _enabledTime; // Time.time when this bag was last enabled // Bob animation (randomized phase to desync multiple bags) private float _bobPhase; // ─── Properties ────────────────────────────────────────── public BagState State => _state; public float CashValue => cashValue; public float Value => cashValue; // Alias for AI compatibility public bool IsAvailable => _state == BagState.Available; public bool IsDropped => _state == BagState.Dropped; public bool IsCarried => _state == BagState.Carried; public bool IsHotDrop => _state == BagState.Dropped && (Time.time - _dropTime) < hotDropWindow; public float HotDropBonusPercent => hotDropBonusPercent; public CashCarrier CurrentCarrier => _carrier; public Vector3 SpawnPosition => _spawnPosition; // ─── Lifecycle ─────────────────────────────────────────── private void Awake() { if (meshRenderer == null) meshRenderer = GetComponent(); _collider = GetComponent(); if (_collider != null) _collider.isTrigger = true; _bobPhase = Random.Range(0f, Mathf.PI * 2f); // Desync bobs between bags // Create glow point light if none assigned CreateGlow(); } private void OnEnable() { EntityRegistry.Register(this); _spawnPosition = transform.position; _enabledTime = Time.time; SetAvailable(); } private void Reset() { // Ensure reasonable default scale when added in editor transform.localScale = targetScale; } private void OnDisable() { EntityRegistry.Unregister(this); } private void Update() { switch (_state) { case BagState.Available: AnimateIdle(); break; case BagState.Carried: // Position managed by CashCarrier break; case BagState.Dropped: AnimateDropped(); _despawnTimer += Time.deltaTime; if (_despawnTimer >= dropDespawnTime) Despawn(); break; } } // ─── State Transitions ─────────────────────────────────── /// Reset to neutral available state. public void SetAvailable() { _state = BagState.Available; _carrier = null; _despawnTimer = 0f; if (_collider != null) _collider.enabled = true; if (meshRenderer != null) meshRenderer.enabled = true; gameObject.SetActive(true); // Ensure consistent visual scale transform.localScale = targetScale; SetColor(neutralColor); SetParticles(idleParticles, true); if (worldIndicator != null) worldIndicator.SetActive(false); // Enable glow so players can see the bag from far away SetGlowActive(true); // Show minimap marker when available var marker = GetComponent(); if (marker != null) marker.SetVisible(true); } /// Picked up by a carrier. public void PickUp(CashCarrier carrier) { if (_state == BagState.Carried || carrier == null) return; _state = BagState.Carried; _carrier = carrier; // Disable world presence if (_collider != null) _collider.enabled = false; if (meshRenderer != null) meshRenderer.enabled = false; if (worldIndicator != null) worldIndicator.SetActive(false); SetParticles(idleParticles, false); // Burst pickup effect if (pickupBurstEffect != null) { pickupBurstEffect.transform.position = transform.position; pickupBurstEffect.Play(); } // Play pickup sound effect (play at camera position so volume is consistent) if (pickupSound != null) { Vector3 listenerPos = Camera.main != null ? Camera.main.transform.position : transform.position; AudioSource.PlayClipAtPoint(pickupSound, listenerPos, pickupSoundVolume); } // Set team color Color teamColor = carrier.TeamTag == "Player" ? playerTeamColor : enemyTeamColor; SetColor(teamColor); // Hide glow while carried SetGlowActive(false); // Fire events (replaces FindObjectOfType calls) GameEvents.FireBagPickedUp(this, carrier); GameEvents.FireBagCollected(this); // Hide minimap marker while carried var marker = GetComponent(); if (marker != null) marker.SetVisible(false); } /// Dropped at a world position (carrier died or lost it). public void Drop(Vector3 dropPosition) { _state = BagState.Dropped; _carrier = null; _dropTime = Time.time; _despawnTimer = 0f; // Re-enable world presence transform.position = dropPosition + Vector3.up * 0.5f; if (_collider != null) _collider.enabled = true; if (meshRenderer != null) meshRenderer.enabled = true; SetColor(droppedPulseColor); SetParticles(idleParticles, true); // Re-enable glow when dropped SetGlowActive(true); if (worldIndicator != null) worldIndicator.SetActive(true); // Impact effect if (dropImpactEffect != null) { dropImpactEffect.transform.position = dropPosition; dropImpactEffect.Play(); } GameEvents.FireBagDropped(this, dropPosition); // Show minimap marker when dropped in world var marker = GetComponent(); if (marker != null) marker.SetVisible(true); } /// Remove from play (despawn or deposited). public void Despawn() { _state = BagState.Available; _carrier = null; SetParticles(idleParticles, false); SetGlowActive(false); // Hide from world var marker = GetComponent(); if (marker != null) marker.SetVisible(false); gameObject.SetActive(false); GameEvents.FireBagDespawned(this); // Return to pool if pooled, otherwise destroy var pooled = GetComponent(); if (pooled != null) pooled.ReturnToPool(); else Destroy(gameObject, 0.1f); } // ─── Trigger Detection ─────────────────────────────────── private void OnTriggerEnter(Collider other) { if (_state == BagState.Carried) return; if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected) return; // Grace period: prevent insta-pickup when bag spawns on top of player if (Time.time - _enabledTime < pickupGracePeriod) return; // Use GetComponentInParent because collider may be on child (hand/foot) CashCarrier carrier = other.GetComponentInParent(); if (carrier != null && carrier.CanPickUp()) { PickUp(carrier); carrier.OnBagPickedUp(this); } } // ─── Glow ───────────────────────────────────────────────── private void CreateGlow() { // Use existing glowObject light if assigned in inspector if (glowObject != null) { _glowLight = glowObject.GetComponent(); if (_glowLight != null) return; } // Create a point light child for long-range visibility GameObject lightObj = new GameObject("BagGlow"); lightObj.transform.SetParent(transform, false); lightObj.transform.localPosition = Vector3.up * 0.3f; _glowLight = lightObj.AddComponent(); _glowLight.type = LightType.Point; _glowLight.color = glowLightColor; _glowLight.range = glowLightRange; _glowLight.intensity = glowLightIntensity; _glowLight.shadows = LightShadows.None; _glowLight.renderMode = LightRenderMode.Auto; // Store as glowObject for other methods to reference if (glowObject == null) glowObject = lightObj; } private void SetGlowActive(bool active) { if (_glowLight != null) _glowLight.enabled = active; if (glowObject != null) glowObject.SetActive(active); } private void PulseGlow() { if (_glowLight == null) return; float pulse = Mathf.Lerp(glowPulseMin, glowPulseMax, (Mathf.Sin(Time.time * glowPulseSpeed + _bobPhase) + 1f) * 0.5f); _glowLight.intensity = glowLightIntensity * pulse; } // ─── Animations ────────────────────────────────────────── private void AnimateIdle() { // Rotate transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World); // Bob float y = _spawnPosition.y + floatOffset + Mathf.Sin((Time.time + _bobPhase) * bobSpeed) * bobHeight; transform.position = new Vector3(transform.position.x, y, transform.position.z); // Pulse glow PulseGlow(); } private void AnimateDropped() { // Faster pulse for hot drops float pulseSpeed = IsHotDrop ? bobSpeed * 2f : bobSpeed; float y = transform.position.y; float targetY = _spawnPosition.y + floatOffset + Mathf.Sin((Time.time + _bobPhase) * pulseSpeed) * bobHeight * 0.5f; // Smooth lerp for drop→bob transition transform.position = new Vector3(transform.position.x, Mathf.Lerp(y, targetY, Time.deltaTime * 3f), transform.position.z); // Pulse glow for hot drops if (IsHotDrop && glowObject != null) { float pulse = Mathf.PingPong(Time.time * 3f, 1f); glowObject.transform.localScale = Vector3.one * (1f + pulse * 0.3f); } } // ─── Visual Helpers ────────────────────────────────────── private void SetColor(Color color) { if (meshRenderer != null) { // MaterialPropertyBlock = zero allocation, no material instance created var mpb = new MaterialPropertyBlock(); meshRenderer.GetPropertyBlock(mpb); mpb.SetColor("_BaseColor", color); mpb.SetColor("_EmissionColor", color * 1.2f); // Bright emission for visibility meshRenderer.SetPropertyBlock(mpb); // Enable emission keyword if possible if (meshRenderer.sharedMaterial != null) meshRenderer.sharedMaterial.EnableKeyword("_EMISSION"); } if (glowObject != null) { Light light = glowObject.GetComponent(); if (light != null) light.color = color; } } private void SetParticles(ParticleSystem ps, bool play) { if (ps == null) return; if (play && !ps.isPlaying) ps.Play(); else if (!play && ps.isPlaying) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); } // ─── API ───────────────────────────────────────────────── /// Set the cash value (used by spawner). public void SetCashValue(float value) => cashValue = value; /// Get effective value including hot drop bonus. public float GetEffectiveValue() { if (IsHotDrop) return cashValue * (1f + hotDropBonusPercent / 100f); return cashValue; } #if UNITY_EDITOR private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, 1.5f); UnityEditor.Handles.Label(transform.position + Vector3.up * 1.5f, $"CashBag ${cashValue}\n{_state}"); } #endif }