Files
2026-06-26 22:41:39 +05:30

416 lines
16 KiB
C#

using UnityEngine;
/// <summary>
/// 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.
/// </summary>
public class CashMag : 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<MeshRenderer>();
_collider = GetComponent<Collider>();
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<CashMag>.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<CashMag>.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 ───────────────────────────────────
/// <summary>Reset to neutral available state.</summary>
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<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(true);
}
/// <summary>Picked up by a carrier.</summary>
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<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(false);
}
/// <summary>Dropped at a world position (carrier died or lost it).</summary>
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<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(true);
}
/// <summary>Remove from play (despawn or deposited).</summary>
public void Despawn()
{
_state = BagState.Available;
_carrier = null;
SetParticles(idleParticles, false);
SetGlowActive(false);
// Hide from world
var marker = GetComponent<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(false);
gameObject.SetActive(false);
GameEvents.FireBagDespawned(this);
// Return to pool if pooled, otherwise destroy
var pooled = GetComponent<PooledObject>();
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<CashCarrier>();
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<Light>();
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<Light>();
_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<Light>();
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 ─────────────────────────────────────────────────
/// <summary>Set the cash value (used by spawner).</summary>
public void SetCashValue(float value) => cashValue = value;
/// <summary>Get effective value including hot drop bonus.</summary>
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
}