chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
520
Assets/Scripts/CashSystem/CashoutStation.cs
Normal file
520
Assets/Scripts/CashSystem/CashoutStation.cs
Normal file
@@ -0,0 +1,520 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// CashoutStation — Deposit point for extraction vaults.
|
||||
///
|
||||
/// FLOW: Carrier deposits vault → 30% instant payout → 45s defense timer for 70% →
|
||||
/// Enemy can contest (pause timer) or hack (10s to steal cashout).
|
||||
///
|
||||
/// Replaces the role of TeamVault as the primary deposit zone in extraction mode.
|
||||
/// TeamVault still exists for backwards compatibility in the old bag-collection mode.
|
||||
///
|
||||
/// Registers with EntityRegistry<CashoutStation> for zero-allocation queries.
|
||||
/// All notifications go through GameEvents.
|
||||
/// </summary>
|
||||
public class CashoutStation : MonoBehaviour
|
||||
{
|
||||
public enum StationState { Idle, CashingOut, Contested, Hacked, Completed }
|
||||
|
||||
[Header("Team Settings")]
|
||||
[SerializeField] private string teamTag = "Player"; // "Player" or "Enemy"
|
||||
[SerializeField] private float storedCash = 0f; // Total team cash (all sources)
|
||||
|
||||
[Header("Cashout Settings")]
|
||||
[SerializeField] private float instantPayoutPercent = 0.30f; // 30% on deposit
|
||||
[SerializeField] private float cashoutDuration = 45f; // 45s to cash out the remaining 70%
|
||||
[SerializeField] private float hackDuration = 10f; // 10s for enemy to steal a cashout
|
||||
[SerializeField] private float contestPauseDelay = 2f; // Seconds before contest pauses timer
|
||||
|
||||
[Header("Interaction")]
|
||||
[SerializeField] private float depositRange = 4f; // Range to deposit vault
|
||||
[SerializeField] private float defenseRadius = 12f; // Area to defend during cashout
|
||||
|
||||
[Header("Visual/Audio")]
|
||||
[SerializeField] private GameObject depositEffect;
|
||||
[SerializeField] private GameObject cashoutActiveEffect;
|
||||
[SerializeField] private GameObject contestedEffect;
|
||||
[SerializeField] private GameObject hackedEffect;
|
||||
[SerializeField] private ParticleSystem idleParticles;
|
||||
[SerializeField] private AudioClip depositSound;
|
||||
[SerializeField] private AudioClip cashoutCompleteSound;
|
||||
[SerializeField] private AudioClip hackedSound;
|
||||
|
||||
[Header("Events (Inspector)")]
|
||||
public UnityEvent<float> OnCashChanged;
|
||||
public UnityEvent OnCashoutStartedLocal;
|
||||
public UnityEvent<float> OnCashoutProgressLocal;
|
||||
public UnityEvent OnCashoutCompletedLocal;
|
||||
public UnityEvent OnContestedLocal;
|
||||
public UnityEvent OnHackedLocal;
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────
|
||||
private StationState _state = StationState.Idle;
|
||||
private float _cashoutProgress; // 0-1
|
||||
private float _cashoutRemainingValue; // The 70% being cashed out
|
||||
private float _hackProgress; // 0-1
|
||||
private string _cashoutOwnerTeamTag; // Team that deposited the vault
|
||||
private Coroutine _cashoutCoroutine;
|
||||
private Coroutine _hackCoroutine;
|
||||
private readonly HashSet<GameObject> _contestingEnemies = new HashSet<GameObject>();
|
||||
private ExtractionVault _currentVault; // Vault being cashed out
|
||||
|
||||
// ─── Properties ──────────────────────────────────────────
|
||||
public float StoredCash => storedCash;
|
||||
public string TeamTag => teamTag;
|
||||
public StationState CurrentState => _state;
|
||||
public float CashoutProgress => _cashoutProgress;
|
||||
public float HackProgress => _hackProgress;
|
||||
public float CashoutRemainingValue => _cashoutRemainingValue;
|
||||
public bool IsCashingOut => _state == StationState.CashingOut || _state == StationState.Contested;
|
||||
public bool IsIdle => _state == StationState.Idle || _state == StationState.Completed;
|
||||
public float DefenseRadius => defenseRadius;
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EntityRegistry<CashoutStation>.Register(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EntityRegistry<CashoutStation>.Unregister(this);
|
||||
StopAllCoroutines();
|
||||
}
|
||||
|
||||
// ─── Trigger Detection ───────────────────────────────────
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
GameObject character = GetCharacterRoot(other);
|
||||
if (character == null) return;
|
||||
|
||||
HealthNew hp = character.GetComponent<HealthNew>();
|
||||
if (hp != null && hp.isDead) return;
|
||||
|
||||
bool isSameTeam = character.CompareTag(teamTag);
|
||||
|
||||
if (isSameTeam)
|
||||
{
|
||||
// Same team: try to deposit a carried vault
|
||||
TryDepositVault(character);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Enemy team: contest or hack an active cashout
|
||||
if (IsCashingOut)
|
||||
{
|
||||
_contestingEnemies.Add(character);
|
||||
if (_contestingEnemies.Count == 1) // First contester
|
||||
OnContestStart(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
// Check if enemy is still alive while contesting
|
||||
if (!IsCashingOut) return;
|
||||
|
||||
GameObject character = GetCharacterRoot(other);
|
||||
if (character == null) return;
|
||||
if (character.CompareTag(teamTag)) return; // Same team, ignore
|
||||
|
||||
if (_contestingEnemies.Contains(character))
|
||||
{
|
||||
HealthNew hp = character.GetComponent<HealthNew>();
|
||||
if (hp != null && hp.isDead)
|
||||
{
|
||||
_contestingEnemies.Remove(character);
|
||||
if (_contestingEnemies.Count == 0)
|
||||
OnContestEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
GameObject character = GetCharacterRoot(other);
|
||||
if (character == null) return;
|
||||
|
||||
if (_contestingEnemies.Remove(character))
|
||||
{
|
||||
if (_contestingEnemies.Count == 0)
|
||||
OnContestEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Deposit ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>Try to deposit a vault carried by this character.</summary>
|
||||
private void TryDepositVault(GameObject character)
|
||||
{
|
||||
// Find if this character is carrying an ExtractionVault
|
||||
var vaults = EntityRegistry<ExtractionVault>.GetAll();
|
||||
ExtractionVault carriedVault = null;
|
||||
|
||||
for (int i = 0; i < vaults.Count; i++)
|
||||
{
|
||||
if (vaults[i] != null && vaults[i].IsCarried && vaults[i].Carrier == character)
|
||||
{
|
||||
carriedVault = vaults[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (carriedVault == null) return;
|
||||
if (!IsIdle) return; // Can't deposit while another cashout is in progress
|
||||
|
||||
DepositVault(carriedVault, character);
|
||||
}
|
||||
|
||||
/// <summary>Deposit a vault and start the cashout timer.</summary>
|
||||
public void DepositVault(ExtractionVault vault, GameObject depositor)
|
||||
{
|
||||
if (vault == null || depositor == null) return;
|
||||
|
||||
// Remove vault from world
|
||||
vault.OnDeposited();
|
||||
_currentVault = vault;
|
||||
_cashoutOwnerTeamTag = depositor.tag;
|
||||
|
||||
// Calculate payouts
|
||||
float remainingValue = vault.RemainingValue; // 70% of total vault value
|
||||
float instantPayout = remainingValue * instantPayoutPercent; // 30% of remaining = 21% of total
|
||||
// Actually the design says 30% instant on deposit, 70% over timer
|
||||
// So: instant = totalValue * 0.30, timer = totalValue * 0.70
|
||||
// But openReward ($1000) was already awarded, so:
|
||||
// remainingValue = totalValue - openReward = $9,000
|
||||
// instant = remainingValue * 0.30 = $2,700
|
||||
// timer = remainingValue * 0.70 = $6,300
|
||||
instantPayout = remainingValue * instantPayoutPercent;
|
||||
_cashoutRemainingValue = remainingValue - instantPayout;
|
||||
|
||||
// Award instant payout
|
||||
storedCash += instantPayout;
|
||||
|
||||
if (depositEffect != null)
|
||||
Instantiate(depositEffect, transform.position + Vector3.up, Quaternion.identity);
|
||||
|
||||
PlaySound(depositSound);
|
||||
|
||||
OnCashChanged?.Invoke(storedCash);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
GameEvents.FireCashoutStarted(this, teamTag, instantPayout);
|
||||
GameEvents.FireKillFeedEntry(depositor.name, "DEPOSITED VAULT", $"+${instantPayout:F0} instant");
|
||||
GameEvents.FirePopupMessage($"CASHOUT STARTED! +${instantPayout:F0}", 2f);
|
||||
|
||||
DevLog.Log($"[CashoutStation] {depositor.name} deposited vault. Instant: ${instantPayout:F0}, Timer: ${_cashoutRemainingValue:F0} over {cashoutDuration}s");
|
||||
|
||||
// Start cashout timer
|
||||
_cashoutCoroutine = StartCoroutine(CashoutRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator CashoutRoutine()
|
||||
{
|
||||
_state = StationState.CashingOut;
|
||||
_cashoutProgress = 0f;
|
||||
|
||||
if (cashoutActiveEffect != null)
|
||||
cashoutActiveEffect.SetActive(true);
|
||||
|
||||
float elapsed = 0f;
|
||||
float totalToCashout = _cashoutRemainingValue;
|
||||
|
||||
while (elapsed < cashoutDuration)
|
||||
{
|
||||
// Pause while contested
|
||||
while (_state == StationState.Contested)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Abort if hacked
|
||||
if (_state == StationState.Hacked)
|
||||
yield break;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
_cashoutProgress = Mathf.Clamp01(elapsed / cashoutDuration);
|
||||
|
||||
// Drip cash over time
|
||||
float cashThisTick = (totalToCashout / cashoutDuration) * Time.deltaTime;
|
||||
storedCash += cashThisTick;
|
||||
|
||||
// Fire progress events (throttled to ~4Hz)
|
||||
if (Time.frameCount % 15 == 0)
|
||||
{
|
||||
GameEvents.FireCashoutProgress(this, _cashoutProgress);
|
||||
OnCashoutProgressLocal?.Invoke(_cashoutProgress);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cashout complete!
|
||||
CompleteCashout();
|
||||
}
|
||||
|
||||
private void CompleteCashout()
|
||||
{
|
||||
_state = StationState.Completed;
|
||||
_cashoutProgress = 1f;
|
||||
|
||||
if (cashoutActiveEffect != null)
|
||||
cashoutActiveEffect.SetActive(false);
|
||||
|
||||
PlaySound(cashoutCompleteSound);
|
||||
|
||||
OnCashoutCompletedLocal?.Invoke();
|
||||
GameEvents.FireCashoutCompleted(this, teamTag, _cashoutRemainingValue);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
GameEvents.FireKillFeedEntry(teamTag, "CASHED OUT", $"${_cashoutRemainingValue:F0}!");
|
||||
GameEvents.FirePopupMessage("CASHOUT COMPLETE!", 3f);
|
||||
|
||||
DevLog.Log($"[CashoutStation] Cashout complete! Total team cash: ${storedCash:F0}");
|
||||
|
||||
// Reset to idle after a short delay
|
||||
StartCoroutine(ResetToIdleDelayed(3f));
|
||||
}
|
||||
|
||||
private IEnumerator ResetToIdleDelayed(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
_state = StationState.Idle;
|
||||
_currentVault = null;
|
||||
_cashoutRemainingValue = 0f;
|
||||
_cashoutProgress = 0f;
|
||||
}
|
||||
|
||||
// ─── Contest / Hack ──────────────────────────────────────
|
||||
|
||||
private void OnContestStart(GameObject contester)
|
||||
{
|
||||
if (_state != StationState.CashingOut) return;
|
||||
|
||||
_state = StationState.Contested;
|
||||
|
||||
if (contestedEffect != null)
|
||||
contestedEffect.SetActive(true);
|
||||
|
||||
OnContestedLocal?.Invoke();
|
||||
GameEvents.FireCashoutContested(this, contester.tag);
|
||||
GameEvents.FirePopupMessage("CASHOUT CONTESTED!", 1.5f);
|
||||
|
||||
DevLog.Log($"[CashoutStation] Cashout contested by {contester.name}!");
|
||||
|
||||
// Start hack timer — if enemy stays long enough, they steal the cashout
|
||||
_hackCoroutine = StartCoroutine(HackRoutine(contester));
|
||||
}
|
||||
|
||||
private void OnContestEnd()
|
||||
{
|
||||
if (_state != StationState.Contested) return;
|
||||
|
||||
// Cancel hack if in progress
|
||||
if (_hackCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_hackCoroutine);
|
||||
_hackCoroutine = null;
|
||||
}
|
||||
_hackProgress = 0f;
|
||||
|
||||
if (contestedEffect != null)
|
||||
contestedEffect.SetActive(false);
|
||||
|
||||
_state = StationState.CashingOut;
|
||||
DevLog.Log($"[CashoutStation] Contest ended, cashout resumed");
|
||||
}
|
||||
|
||||
private IEnumerator HackRoutine(GameObject hacker)
|
||||
{
|
||||
_hackProgress = 0f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < hackDuration)
|
||||
{
|
||||
if (hacker == null) { OnContestEnd(); yield break; }
|
||||
|
||||
HealthNew hp = hacker.GetComponent<HealthNew>();
|
||||
if (hp != null && hp.isDead) { OnContestEnd(); yield break; }
|
||||
|
||||
// Check hacker is still in range
|
||||
float dist = Vector3.Distance(hacker.transform.position, transform.position);
|
||||
if (dist > defenseRadius) { OnContestEnd(); yield break; }
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
_hackProgress = Mathf.Clamp01(elapsed / hackDuration);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Hack successful!
|
||||
CompleteHack(hacker);
|
||||
}
|
||||
|
||||
private void CompleteHack(GameObject hacker)
|
||||
{
|
||||
_state = StationState.Hacked;
|
||||
|
||||
// Stop the cashout coroutine
|
||||
if (_cashoutCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_cashoutCoroutine);
|
||||
_cashoutCoroutine = null;
|
||||
}
|
||||
|
||||
// Calculate stolen amount (remaining uncashed value)
|
||||
float stolen = _cashoutRemainingValue * (1f - _cashoutProgress);
|
||||
|
||||
// Remove from this team
|
||||
storedCash -= stolen;
|
||||
storedCash = Mathf.Max(storedCash, 0f);
|
||||
|
||||
// Award to hacking team
|
||||
string hackerTeamTag = hacker.tag;
|
||||
AwardToTeam(hackerTeamTag, stolen);
|
||||
|
||||
if (hackedEffect != null)
|
||||
hackedEffect.SetActive(true);
|
||||
if (cashoutActiveEffect != null)
|
||||
cashoutActiveEffect.SetActive(false);
|
||||
if (contestedEffect != null)
|
||||
contestedEffect.SetActive(false);
|
||||
|
||||
PlaySound(hackedSound);
|
||||
|
||||
OnHackedLocal?.Invoke();
|
||||
GameEvents.FireCashoutHacked(this, hackerTeamTag, stolen);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
GameEvents.FireScoreChanged(hackerTeamTag, GetTeamCash(hackerTeamTag));
|
||||
GameEvents.FireKillFeedEntry(hacker.name, "HACKED CASHOUT", $"STOLE ${stolen:F0}!");
|
||||
GameEvents.FirePopupMessage($"CASHOUT HACKED! ${stolen:F0} STOLEN!", 3f);
|
||||
|
||||
DevLog.Log($"[CashoutStation] HACKED by {hacker.name}! ${stolen:F0} stolen to {hackerTeamTag}");
|
||||
|
||||
// Reset to idle
|
||||
StartCoroutine(ResetToIdleDelayed(3f));
|
||||
}
|
||||
|
||||
// ─── Direct Cash API ─────────────────────────────────────
|
||||
|
||||
/// <summary>Add cash directly (kill rewards, vault open reward, etc.).</summary>
|
||||
public void AddDirectCash(float amount)
|
||||
{
|
||||
storedCash += amount;
|
||||
OnCashChanged?.Invoke(storedCash);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
}
|
||||
|
||||
/// <summary>Remove cash (team wipe penalty, etc.).</summary>
|
||||
public void RemoveCash(float amount)
|
||||
{
|
||||
storedCash -= amount;
|
||||
storedCash = Mathf.Max(storedCash, 0f);
|
||||
OnCashChanged?.Invoke(storedCash);
|
||||
GameEvents.FireScoreChanged(teamTag, storedCash);
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────
|
||||
|
||||
private void AwardToTeam(string targetTeamTag, float amount)
|
||||
{
|
||||
var stations = EntityRegistry<CashoutStation>.GetAll();
|
||||
for (int i = 0; i < stations.Count; i++)
|
||||
{
|
||||
if (stations[i] != null && stations[i].teamTag == targetTeamTag)
|
||||
{
|
||||
stations[i].AddDirectCash(amount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to TeamVault
|
||||
var vaults = EntityRegistry<TeamVault>.GetAll();
|
||||
for (int i = 0; i < vaults.Count; i++)
|
||||
{
|
||||
if (vaults[i] != null && vaults[i].TeamTag == targetTeamTag)
|
||||
{
|
||||
vaults[i].DepositCash(amount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float GetTeamCash(string targetTeamTag)
|
||||
{
|
||||
var stations = EntityRegistry<CashoutStation>.GetAll();
|
||||
for (int i = 0; i < stations.Count; i++)
|
||||
{
|
||||
if (stations[i] != null && stations[i].teamTag == targetTeamTag)
|
||||
return stations[i].storedCash;
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private GameObject GetCharacterRoot(Collider col)
|
||||
{
|
||||
TeamMember tm = col.GetComponentInParent<TeamMember>();
|
||||
if (tm != null) return tm.gameObject;
|
||||
|
||||
CashCarrier carrier = col.GetComponentInParent<CashCarrier>();
|
||||
if (carrier != null) return carrier.gameObject;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void PlaySound(AudioClip clip)
|
||||
{
|
||||
if (clip == null) return;
|
||||
Vector3 pos = Camera.main != null ? Camera.main.transform.position : transform.position;
|
||||
AudioSource.PlayClipAtPoint(clip, pos, 2f);
|
||||
}
|
||||
|
||||
/// <summary>Get the opposing station via EntityRegistry.</summary>
|
||||
public CashoutStation GetOpposingStation()
|
||||
{
|
||||
var stations = EntityRegistry<CashoutStation>.GetAll();
|
||||
for (int i = 0; i < stations.Count; i++)
|
||||
{
|
||||
if (stations[i] != null && stations[i].teamTag != this.teamTag)
|
||||
return stations[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Gizmos ──────────────────────────────────────────────
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = teamTag == "Player" ? new Color(0, 0.5f, 1f, 0.3f) : new Color(1, 0.2f, 0.2f, 0.3f);
|
||||
Gizmos.DrawSphere(transform.position, 2f);
|
||||
|
||||
Gizmos.color = new Color(1, 1, 0, 0.1f);
|
||||
Gizmos.DrawWireSphere(transform.position, defenseRadius);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
string stateLabel = _state switch
|
||||
{
|
||||
StationState.CashingOut => $"CASHING OUT {_cashoutProgress:P0}",
|
||||
StationState.Contested => $"CONTESTED! Hack:{_hackProgress:P0}",
|
||||
StationState.Hacked => "HACKED!",
|
||||
StationState.Completed => "COMPLETE",
|
||||
_ => "IDLE"
|
||||
};
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"{teamTag} CashoutStation\n${storedCash:F0}\n{stateLabel}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user