chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
363
Assets/Scripts/CashSystem/VaultSpawner.cs
Normal file
363
Assets/Scripts/CashSystem/VaultSpawner.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// VaultSpawner — Spawns ExtractionVault objectives at arena points.
|
||||
///
|
||||
/// Simpler than CashBagSpawner: spawns 1-2 vaults at a time at designated
|
||||
/// spawn points spread across the map. When a vault is deposited or despawns,
|
||||
/// a new one spawns at a different location after a delay.
|
||||
///
|
||||
/// Uses the same patterns as CashBagSpawner: NavMesh snap, EntityRegistry,
|
||||
/// GameEvents, ObjectPool.
|
||||
/// </summary>
|
||||
public class VaultSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Vault Settings")]
|
||||
[SerializeField] private GameObject vaultPrefab;
|
||||
[SerializeField] private float vaultValue = 10000f;
|
||||
|
||||
[Header("Spawn Limits")]
|
||||
[SerializeField] private int maxActiveVaults = 2;
|
||||
[SerializeField] private int initialSpawnCount = 1;
|
||||
[SerializeField] private float respawnDelay = 15f; // Seconds before a new vault spawns after one is consumed
|
||||
|
||||
[Header("Spawn Points")]
|
||||
[SerializeField] private Transform[] spawnPoints;
|
||||
[SerializeField] private string spawnPointTag = "VaultSpawn"; // Fallback tag to find spawn points
|
||||
[SerializeField] private float minDistanceBetweenVaults = 30f;
|
||||
[SerializeField] private float playerAvoidDistance = 10f;
|
||||
[SerializeField] private float stationAvoidDistance = 15f; // Don't spawn too close to cashout stations
|
||||
|
||||
[Header("Random Spawning (Fallback)")]
|
||||
[SerializeField] private bool useRandomFallback = true;
|
||||
[SerializeField] private Vector3 spawnAreaCenter = Vector3.zero;
|
||||
[SerializeField] private Vector3 spawnAreaSize = new Vector3(80f, 0f, 80f);
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showGizmos = true;
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────
|
||||
private readonly List<ExtractionVault> _activeVaults = new List<ExtractionVault>(4);
|
||||
private readonly HashSet<int> _usedSpawnPoints = new HashSet<int>();
|
||||
private readonly Dictionary<int, float> _spawnPointCooldowns = new Dictionary<int, float>();
|
||||
private const float SPAWN_POINT_COOLDOWN = 30f;
|
||||
private bool _isSpawning;
|
||||
|
||||
public int ActiveVaultCount => _activeVaults.Count;
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find spawn points by tag if none assigned
|
||||
if (spawnPoints == null || spawnPoints.Length == 0)
|
||||
{
|
||||
GameObject[] objs = GameObject.FindGameObjectsWithTag(spawnPointTag);
|
||||
if (objs.Length > 0)
|
||||
{
|
||||
spawnPoints = new Transform[objs.Length];
|
||||
for (int i = 0; i < objs.Length; i++)
|
||||
spawnPoints[i] = objs[i].transform;
|
||||
DevLog.Log($"[VaultSpawner] Found {objs.Length} spawn points by tag '{spawnPointTag}'");
|
||||
}
|
||||
else if (!useRandomFallback)
|
||||
{
|
||||
DevLog.LogWarning("[VaultSpawner] No spawn points found and random fallback disabled!");
|
||||
}
|
||||
}
|
||||
|
||||
StartSpawning();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
GameEvents.OnCashoutCompleted += OnVaultCashedOut;
|
||||
GameEvents.OnCashoutHacked += OnVaultCashedOut;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
GameEvents.OnCashoutCompleted -= OnVaultCashedOut;
|
||||
GameEvents.OnCashoutHacked -= OnVaultCashedOut;
|
||||
}
|
||||
|
||||
// ─── Spawning ────────────────────────────────────────────
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
if (_isSpawning) return;
|
||||
_isSpawning = true;
|
||||
|
||||
for (int i = 0; i < initialSpawnCount; i++)
|
||||
SpawnVault();
|
||||
|
||||
GameEvents.FirePopupMessage("VAULTS DETECTED!", 2f);
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
_isSpawning = false;
|
||||
}
|
||||
|
||||
private void SpawnVault()
|
||||
{
|
||||
if (vaultPrefab == null)
|
||||
{
|
||||
DevLog.LogWarning("[VaultSpawner] No vault prefab assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPos = GetValidSpawnPosition();
|
||||
if (spawnPos == Vector3.zero)
|
||||
{
|
||||
DevLog.LogWarning("[VaultSpawner] Could not find valid spawn position");
|
||||
return;
|
||||
}
|
||||
|
||||
// Snap to NavMesh
|
||||
NavMeshHit hit;
|
||||
if (NavMesh.SamplePosition(spawnPos, out hit, 10f, NavMesh.AllAreas))
|
||||
spawnPos = hit.position + Vector3.up * 0.7f;
|
||||
|
||||
GameObject vaultObj = Instantiate(vaultPrefab, spawnPos, Quaternion.identity);
|
||||
ExtractionVault vault = vaultObj.GetComponent<ExtractionVault>();
|
||||
if (vault == null)
|
||||
vault = vaultObj.AddComponent<ExtractionVault>();
|
||||
|
||||
_activeVaults.Add(vault);
|
||||
|
||||
// Add minimap marker
|
||||
if (vaultObj.GetComponent<MinimapObjectMarker>() == null)
|
||||
vaultObj.AddComponent<MinimapObjectMarker>();
|
||||
|
||||
GameEvents.FireExtractionVaultSpawned(vault);
|
||||
GameEvents.FireKillFeedEntry("", "NEW VAULT SPAWNED", "");
|
||||
|
||||
DevLog.Log($"[VaultSpawner] Vault spawned at {spawnPos}");
|
||||
}
|
||||
|
||||
// ─── Position Validation ─────────────────────────────────
|
||||
|
||||
private Vector3 GetValidSpawnPosition()
|
||||
{
|
||||
if (spawnPoints != null && spawnPoints.Length > 0)
|
||||
return GetSpawnPointPosition();
|
||||
|
||||
if (useRandomFallback)
|
||||
return GetRandomSpawnPosition();
|
||||
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
private Vector3 GetSpawnPointPosition()
|
||||
{
|
||||
// Clean expired cooldowns
|
||||
List<int> expired = null;
|
||||
foreach (var kvp in _spawnPointCooldowns)
|
||||
{
|
||||
if (Time.time >= kvp.Value)
|
||||
{
|
||||
if (expired == null) expired = new List<int>(4);
|
||||
expired.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
if (expired != null)
|
||||
foreach (int idx in expired)
|
||||
_spawnPointCooldowns.Remove(idx);
|
||||
|
||||
// Shuffle indices
|
||||
List<int> indices = new List<int>(spawnPoints.Length);
|
||||
for (int i = 0; i < spawnPoints.Length; i++)
|
||||
indices.Add(i);
|
||||
for (int i = indices.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = Random.Range(0, i + 1);
|
||||
(indices[i], indices[j]) = (indices[j], indices[i]);
|
||||
}
|
||||
|
||||
// Pick first valid, non-cooldown point
|
||||
foreach (int idx in indices)
|
||||
{
|
||||
if (spawnPoints[idx] == null) continue;
|
||||
if (_usedSpawnPoints.Contains(idx)) continue;
|
||||
if (_spawnPointCooldowns.ContainsKey(idx)) continue;
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_usedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: allow cooldown points
|
||||
foreach (int idx in indices)
|
||||
{
|
||||
if (spawnPoints[idx] == null) continue;
|
||||
if (_usedSpawnPoints.Contains(idx)) continue;
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_usedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
return useRandomFallback ? GetRandomSpawnPosition() : Vector3.zero;
|
||||
}
|
||||
|
||||
private Vector3 GetRandomSpawnPosition()
|
||||
{
|
||||
for (int attempt = 0; attempt < 30; attempt++)
|
||||
{
|
||||
Vector3 pos = spawnAreaCenter + new Vector3(
|
||||
Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
|
||||
0.5f,
|
||||
Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
|
||||
);
|
||||
|
||||
if (IsPositionValid(pos))
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Last resort
|
||||
return spawnAreaCenter + new Vector3(Random.Range(-10f, 10f), 0.5f, Random.Range(-10f, 10f));
|
||||
}
|
||||
|
||||
private bool IsPositionValid(Vector3 pos)
|
||||
{
|
||||
// Check distance from active vaults
|
||||
for (int i = 0; i < _activeVaults.Count; i++)
|
||||
{
|
||||
if (_activeVaults[i] != null && Vector3.Distance(pos, _activeVaults[i].transform.position) < minDistanceBetweenVaults)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check distance from characters
|
||||
foreach (TeamMember member in TeamMember.AllMembers)
|
||||
{
|
||||
if (member != null && Vector3.Distance(pos, member.transform.position) < playerAvoidDistance)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check distance from cashout stations
|
||||
var stations = EntityRegistry<CashoutStation>.GetAll();
|
||||
for (int i = 0; i < stations.Count; i++)
|
||||
{
|
||||
if (stations[i] != null && Vector3.Distance(pos, stations[i].transform.position) < stationAvoidDistance)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Event Handlers ──────────────────────────────────────
|
||||
|
||||
/// <summary>Called when a vault's cashout completes or gets hacked — spawn replacement.</summary>
|
||||
private void OnVaultCashedOut(CashoutStation station, string teamTag, float amount)
|
||||
{
|
||||
// Clean up null/inactive vaults
|
||||
_activeVaults.RemoveAll(v => v == null || !v.gameObject.activeInHierarchy);
|
||||
|
||||
if (_isSpawning && _activeVaults.Count < maxActiveVaults)
|
||||
{
|
||||
StartCoroutine(DelayedRespawn());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayedRespawn()
|
||||
{
|
||||
yield return new WaitForSeconds(respawnDelay);
|
||||
if (!_isSpawning) yield break;
|
||||
|
||||
_activeVaults.RemoveAll(v => v == null || !v.gameObject.activeInHierarchy);
|
||||
|
||||
if (_activeVaults.Count < maxActiveVaults)
|
||||
{
|
||||
SpawnVault();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Periodically check for despawned vaults and replace them.</summary>
|
||||
private void Update()
|
||||
{
|
||||
// Clean up null/inactive vaults every few seconds
|
||||
if (Time.frameCount % 180 == 0)
|
||||
{
|
||||
int before = _activeVaults.Count;
|
||||
_activeVaults.RemoveAll(v => v == null || !v.gameObject.activeInHierarchy);
|
||||
int removed = before - _activeVaults.Count;
|
||||
|
||||
if (removed > 0 && _isSpawning && _activeVaults.Count < maxActiveVaults)
|
||||
{
|
||||
StartCoroutine(DelayedRespawn());
|
||||
}
|
||||
|
||||
// Also release spawn points for inactive vaults
|
||||
ReleaseInactiveSpawnPoints();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseInactiveSpawnPoints()
|
||||
{
|
||||
if (spawnPoints == null) return;
|
||||
|
||||
List<int> toRelease = null;
|
||||
foreach (int idx in _usedSpawnPoints)
|
||||
{
|
||||
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
|
||||
|
||||
bool stillOccupied = false;
|
||||
for (int i = 0; i < _activeVaults.Count; i++)
|
||||
{
|
||||
if (_activeVaults[i] != null &&
|
||||
Vector3.Distance(_activeVaults[i].SpawnPosition, spawnPoints[idx].position) < 5f)
|
||||
{
|
||||
stillOccupied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillOccupied)
|
||||
{
|
||||
if (toRelease == null) toRelease = new List<int>(4);
|
||||
toRelease.Add(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRelease != null)
|
||||
{
|
||||
foreach (int idx in toRelease)
|
||||
{
|
||||
_usedSpawnPoints.Remove(idx);
|
||||
_spawnPointCooldowns[idx] = Time.time + SPAWN_POINT_COOLDOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gizmos ──────────────────────────────────────────────
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Gizmos.color = new Color(1f, 0.84f, 0f, 0.2f);
|
||||
Gizmos.DrawCube(spawnAreaCenter, spawnAreaSize);
|
||||
Gizmos.color = new Color(1f, 0.84f, 0f, 0.5f);
|
||||
Gizmos.DrawWireCube(spawnAreaCenter, spawnAreaSize);
|
||||
|
||||
if (spawnPoints != null)
|
||||
{
|
||||
Gizmos.color = new Color(1f, 0.84f, 0f, 0.8f);
|
||||
foreach (Transform point in spawnPoints)
|
||||
if (point != null)
|
||||
Gizmos.DrawSphere(point.position, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user