378 lines
12 KiB
C#
378 lines
12 KiB
C#
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 ───────────────────────────────────────────
|
|
|
|
public static VaultSpawner Instance;
|
|
|
|
private void Awake()
|
|
{
|
|
Instance = this;
|
|
|
|
DontDestroyOnLoad(gameObject);
|
|
}
|
|
|
|
public void NotifyVaultDestroyed()
|
|
{
|
|
StartCoroutine(DelayedRespawn());
|
|
}
|
|
|
|
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;
|
|
|
|
GameObject vaultObj = Instantiate(vaultPrefab, spawnPos, vaultPrefab.transform.rotation);
|
|
//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);
|
|
}
|
|
}
|
|
}
|