chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
415
Assets/Scripts/CashSystem/CashBag.cs
Normal file
415
Assets/Scripts/CashSystem/CashBag.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user