416 lines
16 KiB
C#
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 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<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<CashBag>.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<CashBag>.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
|
|
}
|