chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
685
Assets/Scripts/CashSystem/CashBagSpawner.cs
Normal file
685
Assets/Scripts/CashSystem/CashBagSpawner.cs
Normal file
@@ -0,0 +1,685 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Spawn zone classification for cash bags.
|
||||
/// Contested zones are between team vaults (center of map) — forces team encounters.
|
||||
/// </summary>
|
||||
public enum SpawnZone
|
||||
{
|
||||
Safe, // Near a vault — low risk, low reward
|
||||
Neutral, // Between center and vaults — medium risk
|
||||
Contested // Center of map between vaults — high risk, high reward
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CashBagSpawner — Wave-based spawning of cash bags with object pooling.
|
||||
/// Uses zone-weighted spawning to force team encounters in contested areas.
|
||||
/// </summary>
|
||||
public class CashBagSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Cash Bag Settings")]
|
||||
[SerializeField] private GameObject cashBagPrefab;
|
||||
[SerializeField] private float cashPerBag = 100f;
|
||||
|
||||
[Header("Spawn Limits")]
|
||||
[SerializeField] private int maxBagCount = 15;
|
||||
[SerializeField] private float initialSpawnPercent = 0.60f; // 60% on start
|
||||
|
||||
[Header("Wave Settings")]
|
||||
[SerializeField] private float waveInterval = 30f;
|
||||
[SerializeField] private int bagsPerWave = 3;
|
||||
|
||||
[Header("Spawn Area")]
|
||||
[SerializeField] private Transform[] spawnPoints;
|
||||
[SerializeField] private bool useRandomSpawning = false;
|
||||
[SerializeField] private Vector3 spawnAreaCenter = Vector3.zero;
|
||||
[SerializeField] private Vector3 spawnAreaSize = new Vector3(20f, 0f, 20f);
|
||||
[SerializeField] private float minSpawnDistance = 5f; // Min distance between bags
|
||||
[SerializeField] private float playerAvoidDistance = 5f; // Distance from players
|
||||
[SerializeField] private float vaultAvoidDistance = 6f; // Distance from vaults
|
||||
|
||||
[Header("Zone-Weighted Spawning")]
|
||||
[Tooltip("Chance to spawn in contested center zone (forces team encounters)")]
|
||||
[SerializeField, Range(0f, 1f)] private float contestedZoneWeight = 0.60f;
|
||||
[Tooltip("Chance to spawn in neutral middle zone")]
|
||||
[SerializeField, Range(0f, 1f)] private float neutralZoneWeight = 0.30f;
|
||||
[Tooltip("Chance to spawn in safe zone near vaults")]
|
||||
[SerializeField, Range(0f, 1f)] private float safeZoneWeight = 0.10f;
|
||||
[Tooltip("Value multiplier for contested zone bags")]
|
||||
[SerializeField] private float contestedValueMultiplier = 2.0f;
|
||||
[Tooltip("Value multiplier for neutral zone bags")]
|
||||
[SerializeField] private float neutralValueMultiplier = 1.0f;
|
||||
[Tooltip("Value multiplier for safe zone bags")]
|
||||
[SerializeField] private float safeValueMultiplier = 0.5f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showSpawnGizmos = true;
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────
|
||||
private ObjectPool _pool;
|
||||
private readonly List<CashBag> _activeBags = new List<CashBag>();
|
||||
private readonly HashSet<int> _occupiedSpawnPoints = new HashSet<int>(); // tracks which spawn points have an active bag
|
||||
private readonly Dictionary<int, float> _spawnPointCooldowns = new Dictionary<int, float>(); // prevents same-point reuse
|
||||
private const float SPAWN_POINT_COOLDOWN = 8f; // seconds before a freed point can be reused
|
||||
private int _totalSpawned;
|
||||
private bool _isSpawning;
|
||||
private Coroutine _waveCoroutine;
|
||||
|
||||
// ─── Zone Classification ─────────────────────────────────
|
||||
private readonly List<int> _contestedPoints = new List<int>();
|
||||
private readonly List<int> _neutralPoints = new List<int>();
|
||||
private readonly List<int> _safePoints = new List<int>();
|
||||
private bool _zonesClassified;
|
||||
private Vector3 _mapCenter; // midpoint between vaults — the contested zone center
|
||||
|
||||
public int ActiveBagCount => _activeBags.Count;
|
||||
public int TotalSpawned => _totalSpawned;
|
||||
|
||||
// ─── 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) && !useRandomSpawning)
|
||||
{
|
||||
GameObject[] spawnPointObjects = GameObject.FindGameObjectsWithTag("CashBagSpawn");
|
||||
if (spawnPointObjects.Length > 0)
|
||||
{
|
||||
spawnPoints = new Transform[spawnPointObjects.Length];
|
||||
for (int i = 0; i < spawnPointObjects.Length; i++)
|
||||
spawnPoints[i] = spawnPointObjects[i].transform;
|
||||
}
|
||||
else
|
||||
{
|
||||
useRandomSpawning = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize object pool (pre-warm up to maxBagCount and parent under this transform)
|
||||
if (cashBagPrefab != null)
|
||||
{
|
||||
_pool = new ObjectPool(cashBagPrefab, initialSize: maxBagCount, maxSize: maxBagCount, parent: transform);
|
||||
}
|
||||
|
||||
// Classify spawn points into zones for weighted spawning
|
||||
ClassifySpawnPoints();
|
||||
|
||||
StartSpawning();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
GameEvents.OnBagDespawned += OnBagDespawned;
|
||||
// NOTE: We intentionally do NOT subscribe to OnBagCollected here.
|
||||
// A bag being picked up is NOT the same as a bag leaving the field —
|
||||
// the carrier is still running around with it. We only remove from
|
||||
// _activeBags when the bag is truly gone (despawned/deposited).
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
GameEvents.OnBagDespawned -= OnBagDespawned;
|
||||
}
|
||||
|
||||
// ─── Spawning ────────────────────────────────────────────
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
if (_isSpawning) return;
|
||||
_isSpawning = true;
|
||||
|
||||
int initialCount = Mathf.RoundToInt(maxBagCount * initialSpawnPercent);
|
||||
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
SpawnBag();
|
||||
|
||||
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
|
||||
GameEvents.FirePopupMessage("CASH DROPPED!", 1.5f);
|
||||
|
||||
// Start wave timer
|
||||
_waveCoroutine = StartCoroutine(WaveSpawnRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator WaveSpawnRoutine()
|
||||
{
|
||||
while (_isSpawning)
|
||||
{
|
||||
yield return new WaitForSeconds(waveInterval);
|
||||
if (!_isSpawning) break;
|
||||
|
||||
// Clean up null references
|
||||
_activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);
|
||||
|
||||
int canSpawn = maxBagCount - _activeBags.Count;
|
||||
int toSpawn = Mathf.Min(bagsPerWave, canSpawn);
|
||||
|
||||
if (toSpawn <= 0) continue;
|
||||
|
||||
for (int i = 0; i < toSpawn; i++)
|
||||
{
|
||||
SpawnBag();
|
||||
yield return new WaitForSeconds(0.15f); // Stagger
|
||||
}
|
||||
|
||||
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
|
||||
GameEvents.FirePopupMessage($"+{toSpawn} CASH!", 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnBag()
|
||||
{
|
||||
if (cashBagPrefab == null) return;
|
||||
|
||||
// Determine spawn zone for value multiplier
|
||||
SpawnZone zone = _zonesClassified ? PickWeightedZone() : SpawnZone.Neutral;
|
||||
|
||||
Vector3 spawnPos = _zonesClassified ? GetZoneSpawnPosition(zone) : GetValidSpawnPosition();
|
||||
if (spawnPos == Vector3.zero && !useRandomSpawning) return;
|
||||
|
||||
// Snap to NavMesh
|
||||
if (NavMesh.SamplePosition(spawnPos, out NavMeshHit navHit, 5f, NavMesh.AllAreas))
|
||||
spawnPos = navHit.position + Vector3.up * 0.7f;
|
||||
|
||||
// Use pool if available, fallback to Instantiate
|
||||
GameObject bagObj;
|
||||
if (_pool != null)
|
||||
{
|
||||
bagObj = _pool.Get(spawnPos, Quaternion.identity);
|
||||
if (bagObj == null)
|
||||
{
|
||||
bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
|
||||
}
|
||||
|
||||
CashBag bag = bagObj.GetComponent<CashBag>();
|
||||
if (bag == null) bag = bagObj.AddComponent<CashBag>();
|
||||
|
||||
// Apply zone-based value multiplier — contested bags are worth MORE
|
||||
float zoneMultiplier = GetZoneValueMultiplier(zone);
|
||||
bag.SetCashValue(cashPerBag * zoneMultiplier);
|
||||
_activeBags.Add(bag);
|
||||
_totalSpawned++;
|
||||
|
||||
// Ensure the bag gets a minimap marker
|
||||
if (bag.GetComponent<MinimapObjectMarker>() == null)
|
||||
bag.gameObject.AddComponent<MinimapObjectMarker>();
|
||||
|
||||
GameEvents.FireBagSpawned(bag);
|
||||
}
|
||||
|
||||
// ─── Zone Classification ─────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Classifies each spawn point as Contested, Neutral, or Safe based on
|
||||
/// its distance from the map center (midpoint between team vaults).
|
||||
/// This ensures bags spawn more often in areas where teams will clash.
|
||||
/// </summary>
|
||||
private void ClassifySpawnPoints()
|
||||
{
|
||||
if (spawnPoints == null || spawnPoints.Length == 0 || useRandomSpawning)
|
||||
{
|
||||
_zonesClassified = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_contestedPoints.Clear();
|
||||
_neutralPoints.Clear();
|
||||
_safePoints.Clear();
|
||||
|
||||
// Find vault positions to determine map layout
|
||||
var vaults = EntityRegistry<TeamVault>.GetAll();
|
||||
if (vaults == null || vaults.Count < 2)
|
||||
{
|
||||
// Can't classify without vaults — treat all as neutral
|
||||
for (int i = 0; i < spawnPoints.Length; i++)
|
||||
if (spawnPoints[i] != null)
|
||||
_neutralPoints.Add(i);
|
||||
_zonesClassified = true;
|
||||
_mapCenter = spawnAreaCenter;
|
||||
DevLog.Log("[CashBagSpawner] <2 vaults found, all spawn points classified as Neutral.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate map center (midpoint between vaults — XZ plane)
|
||||
Vector3 v0 = vaults[0].transform.position;
|
||||
Vector3 v1 = vaults[1].transform.position;
|
||||
_mapCenter = (v0 + v1) * 0.5f;
|
||||
_mapCenter.y = 0f; // Normalize to ground
|
||||
|
||||
// Find the maximum distance from center to any spawn point (for normalization)
|
||||
float maxDist = 0f;
|
||||
for (int i = 0; i < spawnPoints.Length; i++)
|
||||
{
|
||||
if (spawnPoints[i] == null) continue;
|
||||
Vector3 flatPos = spawnPoints[i].position;
|
||||
flatPos.y = 0f;
|
||||
float d = Vector3.Distance(flatPos, _mapCenter);
|
||||
if (d > maxDist) maxDist = d;
|
||||
}
|
||||
|
||||
if (maxDist < 1f) maxDist = 1f; // Prevent division by zero
|
||||
|
||||
// Classify: inner 40% = contested, 40-70% = neutral, outer 70%+ = safe
|
||||
for (int i = 0; i < spawnPoints.Length; i++)
|
||||
{
|
||||
if (spawnPoints[i] == null) continue;
|
||||
|
||||
Vector3 flatPos = spawnPoints[i].position;
|
||||
flatPos.y = 0f;
|
||||
float normalizedDist = Vector3.Distance(flatPos, _mapCenter) / maxDist;
|
||||
|
||||
if (normalizedDist < 0.40f)
|
||||
_contestedPoints.Add(i);
|
||||
else if (normalizedDist < 0.70f)
|
||||
_neutralPoints.Add(i);
|
||||
else
|
||||
_safePoints.Add(i);
|
||||
}
|
||||
|
||||
_zonesClassified = true;
|
||||
DevLog.Log($"[CashBagSpawner] Zone classification: " +
|
||||
$"Contested={_contestedPoints.Count}, " +
|
||||
$"Neutral={_neutralPoints.Count}, " +
|
||||
$"Safe={_safePoints.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a spawn zone based on weighted random distribution.
|
||||
/// 60% contested, 30% neutral, 10% safe by default.
|
||||
/// Falls back to any available zone if the chosen zone is empty.
|
||||
/// </summary>
|
||||
private SpawnZone PickWeightedZone()
|
||||
{
|
||||
float r = Random.value;
|
||||
float cumulative = 0f;
|
||||
|
||||
cumulative += contestedZoneWeight;
|
||||
if (r < cumulative && _contestedPoints.Count > 0)
|
||||
return SpawnZone.Contested;
|
||||
|
||||
cumulative += neutralZoneWeight;
|
||||
if (r < cumulative && _neutralPoints.Count > 0)
|
||||
return SpawnZone.Neutral;
|
||||
|
||||
if (_safePoints.Count > 0)
|
||||
return SpawnZone.Safe;
|
||||
|
||||
// Fallback: pick whichever zone has points
|
||||
if (_contestedPoints.Count > 0) return SpawnZone.Contested;
|
||||
if (_neutralPoints.Count > 0) return SpawnZone.Neutral;
|
||||
if (_safePoints.Count > 0) return SpawnZone.Safe;
|
||||
|
||||
return SpawnZone.Neutral; // absolute fallback
|
||||
}
|
||||
|
||||
/// <summary>Returns the value multiplier for a given spawn zone.</summary>
|
||||
private float GetZoneValueMultiplier(SpawnZone zone)
|
||||
{
|
||||
switch (zone)
|
||||
{
|
||||
case SpawnZone.Contested: return contestedValueMultiplier;
|
||||
case SpawnZone.Neutral: return neutralValueMultiplier;
|
||||
case SpawnZone.Safe: return safeValueMultiplier;
|
||||
default: return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the spawn point list for a given zone.</summary>
|
||||
private List<int> GetZonePoints(SpawnZone zone)
|
||||
{
|
||||
switch (zone)
|
||||
{
|
||||
case SpawnZone.Contested: return _contestedPoints;
|
||||
case SpawnZone.Neutral: return _neutralPoints;
|
||||
case SpawnZone.Safe: return _safePoints;
|
||||
default: return _neutralPoints;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Position Validation ─────────────────────────────────
|
||||
|
||||
private Vector3 GetValidSpawnPosition()
|
||||
{
|
||||
return useRandomSpawning ? GetRandomSpawnPosition() : GetSpawnPointPosition();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zone-aware spawn position selection. Picks from the specified zone's spawn points,
|
||||
/// falling back to other zones if all points in the chosen zone are occupied.
|
||||
/// </summary>
|
||||
private Vector3 GetZoneSpawnPosition(SpawnZone preferredZone)
|
||||
{
|
||||
if (!_zonesClassified || useRandomSpawning)
|
||||
return GetValidSpawnPosition();
|
||||
|
||||
// Try preferred zone first
|
||||
Vector3 pos = GetSpawnPointFromZone(GetZonePoints(preferredZone));
|
||||
if (pos != Vector3.zero) return pos;
|
||||
|
||||
// Fallback: try other zones (contested > neutral > safe priority)
|
||||
if (preferredZone != SpawnZone.Contested)
|
||||
{
|
||||
pos = GetSpawnPointFromZone(_contestedPoints);
|
||||
if (pos != Vector3.zero) return pos;
|
||||
}
|
||||
if (preferredZone != SpawnZone.Neutral)
|
||||
{
|
||||
pos = GetSpawnPointFromZone(_neutralPoints);
|
||||
if (pos != Vector3.zero) return pos;
|
||||
}
|
||||
if (preferredZone != SpawnZone.Safe)
|
||||
{
|
||||
pos = GetSpawnPointFromZone(_safePoints);
|
||||
if (pos != Vector3.zero) return pos;
|
||||
}
|
||||
|
||||
// Absolute fallback: original unfiltered method
|
||||
return GetSpawnPointPosition();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a valid spawn position from a specific set of spawn point indices.
|
||||
/// </summary>
|
||||
private Vector3 GetSpawnPointFromZone(List<int> zoneIndices)
|
||||
{
|
||||
if (zoneIndices == null || zoneIndices.Count == 0)
|
||||
return Vector3.zero;
|
||||
|
||||
// Clean up 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 to randomize within zone
|
||||
List<int> shuffled = new List<int>(zoneIndices);
|
||||
for (int i = shuffled.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = Random.Range(0, i + 1);
|
||||
(shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]);
|
||||
}
|
||||
|
||||
// Pick first unoccupied, valid, non-cooldown point
|
||||
foreach (int idx in shuffled)
|
||||
{
|
||||
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
|
||||
if (_occupiedSpawnPoints.Contains(idx)) continue;
|
||||
if (_spawnPointCooldowns.ContainsKey(idx)) continue;
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_occupiedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: allow cooldown points in this zone
|
||||
foreach (int idx in shuffled)
|
||||
{
|
||||
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
|
||||
if (_occupiedSpawnPoints.Contains(idx)) continue;
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_occupiedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
private Vector3 GetSpawnPointPosition()
|
||||
{
|
||||
if (spawnPoints == null || spawnPoints.Length == 0)
|
||||
return Vector3.zero;
|
||||
|
||||
// Clean up 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);
|
||||
|
||||
// Build a list of indices, then shuffle to randomize
|
||||
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 the first unoccupied, valid, non-cooldown spawn point
|
||||
foreach (int idx in indices)
|
||||
{
|
||||
if (spawnPoints[idx] == null) continue;
|
||||
if (_occupiedSpawnPoints.Contains(idx)) continue; // skip occupied
|
||||
if (_spawnPointCooldowns.ContainsKey(idx)) continue; // skip recently used
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_occupiedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: allow cooldown points if nothing else available
|
||||
foreach (int idx in indices)
|
||||
{
|
||||
if (spawnPoints[idx] == null) continue;
|
||||
if (_occupiedSpawnPoints.Contains(idx)) continue;
|
||||
if (IsPositionValid(spawnPoints[idx].position))
|
||||
{
|
||||
_occupiedSpawnPoints.Add(idx);
|
||||
return spawnPoints[idx].position;
|
||||
}
|
||||
}
|
||||
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
private Vector3 GetRandomSpawnPosition()
|
||||
{
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Vector3 randomPos = spawnAreaCenter + new Vector3(
|
||||
Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
|
||||
0.5f,
|
||||
Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
|
||||
);
|
||||
|
||||
if (IsPositionValid(randomPos))
|
||||
return randomPos;
|
||||
}
|
||||
|
||||
return spawnAreaCenter + new Vector3(Random.Range(-5f, 5f), 0.5f, Random.Range(-5f, 5f));
|
||||
}
|
||||
|
||||
private bool IsPositionValid(Vector3 pos)
|
||||
{
|
||||
// Check distance from active bags (using EntityRegistry — zero alloc)
|
||||
var bags = EntityRegistry<CashBag>.GetAll();
|
||||
for (int i = 0; i < bags.Count; i++)
|
||||
{
|
||||
if (bags[i] != null && Vector3.Distance(pos, bags[i].transform.position) < minSpawnDistance)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check distance from all characters (using TeamMember static registry)
|
||||
foreach (TeamMember member in TeamMember.AllMembers)
|
||||
{
|
||||
if (member != null && Vector3.Distance(pos, member.transform.position) < playerAvoidDistance)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check distance from vaults
|
||||
var vaults = EntityRegistry<TeamVault>.GetAll();
|
||||
for (int i = 0; i < vaults.Count; i++)
|
||||
{
|
||||
if (vaults[i] != null && Vector3.Distance(pos, vaults[i].transform.position) < vaultAvoidDistance)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Event Handlers ──────────────────────────────────────
|
||||
|
||||
private void OnBagDespawned(CashBag bag)
|
||||
{
|
||||
_activeBags.Remove(bag);
|
||||
ReleaseSpawnPoint(bag);
|
||||
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
|
||||
|
||||
// Immediately spawn a replacement at a DIFFERENT position
|
||||
// so the field always has bags and they rotate across spawn points
|
||||
if (_isSpawning && _activeBags.Count < maxBagCount)
|
||||
{
|
||||
StartCoroutine(DelayedRespawn(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Respawn a bag after a short delay at a different position.</summary>
|
||||
private IEnumerator DelayedRespawn(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (!_isSpawning) yield break;
|
||||
|
||||
// Clean up null references
|
||||
_activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);
|
||||
|
||||
if (_activeBags.Count < maxBagCount)
|
||||
{
|
||||
SpawnBag();
|
||||
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when a bag is collected (legacy API for compatibility).</summary>
|
||||
public void OnBagCollected(CashBag bag)
|
||||
{
|
||||
// Legacy API: only remove if bag is truly gone (inactive).
|
||||
// Don't remove carried bags — they're still in play.
|
||||
if (bag != null && !bag.gameObject.activeInHierarchy)
|
||||
{
|
||||
if (_activeBags.Contains(bag))
|
||||
_activeBags.Remove(bag);
|
||||
ReleaseSpawnPoint(bag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free the spawn point closest to a removed bag so it can be reused.
|
||||
/// Applies a cooldown so the same point isn't immediately reused.
|
||||
/// </summary>
|
||||
private void ReleaseSpawnPoint(CashBag bag)
|
||||
{
|
||||
if (bag == null || spawnPoints == null) return;
|
||||
|
||||
float bestDist = float.MaxValue;
|
||||
int bestIdx = -1;
|
||||
|
||||
foreach (int idx in _occupiedSpawnPoints)
|
||||
{
|
||||
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
|
||||
float dist = Vector3.Distance(bag.SpawnPosition, spawnPoints[idx].position);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Only release if the bag was reasonably close to the spawn point
|
||||
if (bestIdx >= 0 && bestDist < minSpawnDistance * 2f)
|
||||
{
|
||||
_occupiedSpawnPoints.Remove(bestIdx);
|
||||
// Add cooldown so next spawn goes to a DIFFERENT position
|
||||
_spawnPointCooldowns[bestIdx] = Time.time + SPAWN_POINT_COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
_isSpawning = false;
|
||||
if (_waveCoroutine != null)
|
||||
StopCoroutine(_waveCoroutine);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopSpawning();
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!showSpawnGizmos) return;
|
||||
|
||||
Gizmos.color = new Color(0, 1, 0, 0.3f);
|
||||
Gizmos.DrawCube(spawnAreaCenter, spawnAreaSize);
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawWireCube(spawnAreaCenter, spawnAreaSize);
|
||||
|
||||
if (spawnPoints != null)
|
||||
{
|
||||
for (int i = 0; i < spawnPoints.Length; i++)
|
||||
{
|
||||
if (spawnPoints[i] == null) continue;
|
||||
|
||||
// Color by zone classification
|
||||
if (_zonesClassified)
|
||||
{
|
||||
if (_contestedPoints.Contains(i))
|
||||
Gizmos.color = Color.red; // Contested = RED (danger zone)
|
||||
else if (_neutralPoints.Contains(i))
|
||||
Gizmos.color = Color.yellow; // Neutral = YELLOW
|
||||
else if (_safePoints.Contains(i))
|
||||
Gizmos.color = Color.green; // Safe = GREEN
|
||||
else
|
||||
Gizmos.color = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
}
|
||||
|
||||
Gizmos.DrawSphere(spawnPoints[i].position, 0.5f);
|
||||
}
|
||||
|
||||
// Draw map center (contested zone center)
|
||||
if (_zonesClassified)
|
||||
{
|
||||
Gizmos.color = new Color(1f, 0f, 0f, 0.5f);
|
||||
Gizmos.DrawWireSphere(_mapCenter, 3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user