using UnityEngine; using UnityEngine.AI; using System.Collections; using System.Collections.Generic; /// /// 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. /// 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 _activeVaults = new List(4); private readonly HashSet _usedSpawnPoints = new HashSet(); private readonly Dictionary _spawnPointCooldowns = new Dictionary(); 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(); if (vault == null) vault = vaultObj.AddComponent(); _activeVaults.Add(vault); // Add minimap marker if (vaultObj.GetComponent() == null) vaultObj.AddComponent(); 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 expired = null; foreach (var kvp in _spawnPointCooldowns) { if (Time.time >= kvp.Value) { if (expired == null) expired = new List(4); expired.Add(kvp.Key); } } if (expired != null) foreach (int idx in expired) _spawnPointCooldowns.Remove(idx); // Shuffle indices List indices = new List(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.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 ────────────────────────────────────── /// Called when a vault's cashout completes or gets hacked — spawn replacement. 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(); } } /// Periodically check for despawned vaults and replace them. 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 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(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); } } }