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);
}
}
}