using System.Collections; using System.Collections.Generic; using Invector; // Assuming this is for vDamage, if not used, can be removed using Invector.vCharacterController.AI; using PixelCrushers; // For Saver and SaveSystem using UnityEngine; using UnityEngine.Events; using UnityEngine.AI; // Required for NavMesh namespace Beyond { public class EnemySpawner : Saver { [Header("Spawning Configuration")] public GameObject m_prefab; public int m_enemiesPerSpawnWave = 3; public float m_spawnRadius = 5f; public int m_maxSpawnAttemptsPerEnemy = 10; public float m_navMeshSampleDistance = 1.0f; [Header("Activation & Respawn")] public Transform m_distanceFrom; public float m_spawnCheckInterval = .5f; public float m_distance = 20f; [Tooltip("Maximum number of times a full wave can be (re)spawned. Set to -1 for infinite respawns. This count increments each time a wave is successfully initiated.")] public int m_maxWaveRespawns = -1; public bool m_respawnOnPlayersDeath = true; public LayerMask raycastMask = 1; // Default layer [Header("Events")] public UnityEvent m_OnDead; // Invoked when an enemy spawned by THIS spawner dies public UnityEvent m_onReceivedDamage; // Invoked when an enemy from THIS spawner receives damage private SaveData m_spawnData; private List m_activeEnemies = new List(); // Initialized here private bool _initialWaveCheckPassedThisActivation = false; const float RAY_Y_OFFSET = 30f; // How high above candidate point to start raycast const float GROUND_Y_OFFSET = .1f; // How high above ground to place enemy pivot // Internal class for saving spawner state class SaveData { public int wavesSpawnedCount; public bool waveActive; // Note: _initialWaveCheckPassedThisActivation is transient and reset based on game state, not directly saved. } public override void Awake() { #if ENEMIES_DISABLED && UNITY_EDITOR enabled = false; Debug.LogWarning($"EnemySpawner ({name}): Disabled via ENEMIES_DISABLED directive in Awake. Critical members like m_spawnData will not be initialized by Awake if this return is hit.", this); return; // If this path is taken, m_spawnData, m_activeEnemies, etc., below are NOT initialized by Awake. #endif base.Awake(); // Important: Call Saver's Awake // Initialize critical members m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; // m_activeEnemies is already initialized at declaration, but ensuring it if not: if (m_activeEnemies == null) m_activeEnemies = new List(); _initialWaveCheckPassedThisActivation = false; // Initialize flag } public override void Start() { // Defensive initialization: If Awake() didn't initialize m_spawnData // (e.g., due to ENEMIES_DISABLED causing Awake to return early, // or an exception in base.Awake()), and this component was // subsequently enabled (or was already enabled) causing Start() to run. if (m_spawnData == null) { Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null at the beginning of Start(). Initializing now. This might indicate an issue with Awake's execution path (e.g., ENEMIES_DISABLED directive was met and component re-enabled, or an exception in base.Awake()).", this); m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; // Ensure other members typically initialized in Awake are also safe if (m_activeEnemies == null) { m_activeEnemies = new List(); } _initialWaveCheckPassedThisActivation = false; } base.Start(); // Important: Call Saver's Start // Set up m_distanceFrom if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; else if (Camera.main != null) m_distanceFrom = Camera.main.transform; else { Debug.LogError($"EnemySpawner ({name}): m_distanceFrom could not be set (Player.Instance and Camera.main are null). Disabling spawner.", this); enabled = false; // If disabled here, InvokeRepeating below won't be set up. } // Subscribe to player respawn events if (m_respawnOnPlayersDeath && Player.Instance != null) { var respawner = Player.Instance.GetComponent(); // Assuming PixelCrushers Respawner if (respawner != null) { respawner.m_onRespawned.AddListener(OnPlayerRespawned); } else { Debug.LogWarning($"EnemySpawner ({name}): Player.Instance does not have a Respawner component. m_respawnOnPlayersDeath will not function fully.", this); } } // Start checking for spawn conditions only if the component is still enabled and active if (enabled && gameObject.activeInHierarchy) { InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval); } else if (!enabled) { Debug.Log($"EnemySpawner ({name}): Spawner is disabled at the end of Start(). CheckSpawn will not be invoked repeatedly.", this); } } void OnDestroy() { // Unsubscribe from player respawn events if (m_respawnOnPlayersDeath && Player.Instance != null) { var respawner = Player.Instance.GetComponent(); if (respawner != null) { respawner.m_onRespawned.RemoveListener(OnPlayerRespawned); } } // Clean up listeners from active enemies // Ensure m_activeEnemies is not null before iterating if (m_activeEnemies != null) { foreach (var ai in m_activeEnemies) { if (ai != null) { ai.onDead.RemoveListener(OnEnemyDead); ai.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); } } m_activeEnemies.Clear(); } CancelInvoke("CheckSpawn"); // Ensure InvokeRepeating is stopped } private void OnPlayerRespawned() { if (!m_respawnOnPlayersDeath) return; // Ensure m_spawnData is not null if (m_spawnData == null) { Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in OnPlayerRespawned. Cannot proceed with respawn logic.", this); return; } if (m_activeEnemies == null) m_activeEnemies = new List(); // Should be initialized, but defensive // If a wave was previously active and its enemies are now gone, mark the wave as inactive. if (m_spawnData.waveActive && m_activeEnemies.Count == 0) { m_spawnData.waveActive = false; } _initialWaveCheckPassedThisActivation = false; if (!m_spawnData.waveActive && CanSpawnMoreWaves()) { if (IsPlayerInRange()) { SpawnWave(); if (m_spawnData.waveActive) { _initialWaveCheckPassedThisActivation = true; } } } } public override string RecordData() { // Ensure m_spawnData is not null before serializing if (m_spawnData == null) { Debug.LogWarning($"EnemySpawner ({name}): Attempting to record data, but m_spawnData is null. Returning empty string.", this); // Optionally, initialize it here to a default state if that makes sense for your save system logic // m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; return string.Empty; } return SaveSystem.Serialize(m_spawnData); } public override void ApplyData(string s) { // Ensure m_spawnData is initialized before trying to apply data, // though Awake or Start should have done this. if (m_spawnData == null) { m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null when ApplyData was called. Initialized to default.", this); } if (string.IsNullOrEmpty(s)) return; // No data to apply var data = SaveSystem.Deserialize(s); if (data != null) { m_spawnData = data; if (m_activeEnemies == null) m_activeEnemies = new List(); // Defensive if (m_spawnData.waveActive && m_activeEnemies.Count == 0) { m_spawnData.waveActive = false; } if (m_spawnData.waveActive || m_spawnData.wavesSpawnedCount > 0) { _initialWaveCheckPassedThisActivation = true; } else { _initialWaveCheckPassedThisActivation = false; } } } public bool IsAnyEnemyAlive() { if (m_activeEnemies == null) return false; return m_activeEnemies.Count > 0; } public bool CanSpawnMoreWaves() { if (m_spawnData == null) return false; return m_maxWaveRespawns < 0 || m_spawnData.wavesSpawnedCount < m_maxWaveRespawns; } public bool IsExhausted() { return !CanSpawnMoreWaves() && !IsAnyEnemyAlive(); } public int GetWavesSpawnedCount() { if (m_spawnData != null) { return m_spawnData.wavesSpawnedCount; } Debug.LogWarning($"EnemySpawner ({name}): GetWavesSpawnedCount called but m_spawnData is null. Returning 0.", this); return 0; } public void SpawnWave() { if (m_prefab == null) { Debug.LogError($"EnemySpawner ({name}): m_prefab is not set!", this); return; } // Ensure m_spawnData is not null if (m_spawnData == null) { Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in SpawnWave. Cannot proceed.", this); return; } if (m_activeEnemies == null) m_activeEnemies = new List(); // Defensive if (!CanSpawnMoreWaves()) { return; } if (m_spawnData.waveActive) { return; } int enemiesSuccessfullySpawned = 0; for (int i = 0; i < m_enemiesPerSpawnWave; i++) { Vector3 spawnPosition = Vector3.zero; bool foundValidPosition = false; for (int attempt = 0; attempt < m_maxSpawnAttemptsPerEnemy; attempt++) { Vector2 randomCircleOffset = Random.insideUnitCircle * m_spawnRadius; Vector3 candidateBasePosition = transform.position + new Vector3(randomCircleOffset.x, 0, randomCircleOffset.y); Vector3 raycastStartPos = candidateBasePosition + Vector3.up * RAY_Y_OFFSET; RaycastHit hit; if (Physics.Raycast(raycastStartPos, Vector3.down, out hit, RAY_Y_OFFSET * 2, raycastMask)) { Vector3 potentialSpawnPoint = hit.point + Vector3.up * GROUND_Y_OFFSET; NavMeshHit navHit; if (NavMesh.SamplePosition(potentialSpawnPoint, out navHit, m_navMeshSampleDistance, NavMesh.AllAreas)) { spawnPosition = navHit.position; foundValidPosition = true; break; } } } if (foundValidPosition) { GameObject enemyGO = Instantiate(m_prefab, spawnPosition, transform.rotation, transform); vControlAI spawnedAI = enemyGO.GetComponent(); if (spawnedAI != null) { m_activeEnemies.Add(spawnedAI); spawnedAI.onDead.AddListener(OnEnemyDead); spawnedAI.onReceiveDamage.AddListener(OnEnemyReceiveDamage); enemiesSuccessfullySpawned++; } else { Debug.LogError($"EnemySpawner ({name}): Spawned prefab '{m_prefab.name}' does not have a vControlAI component!", enemyGO); Destroy(enemyGO); } } else { Debug.LogWarning($"EnemySpawner ({name}): Failed to find a valid spawn position for an enemy after {m_maxSpawnAttemptsPerEnemy} attempts.", this); } } if (enemiesSuccessfullySpawned > 0) { m_spawnData.waveActive = true; m_spawnData.wavesSpawnedCount++; Debug.Log($"EnemySpawner ({name}): Spawned wave {m_spawnData.wavesSpawnedCount}/{(m_maxWaveRespawns < 0 ? "infinite" : m_maxWaveRespawns.ToString())} with {enemiesSuccessfullySpawned} enemies.", this); } else { m_spawnData.waveActive = false; Debug.LogWarning($"EnemySpawner ({name}): Attempted to spawn a wave, but no enemies were successfully placed.", this); } } private void OnEnemyReceiveDamage(vDamage damageData) { m_onReceivedDamage?.Invoke(this); } private void OnEnemyDead(GameObject deadEnemyObject) { // Ensure m_spawnData and m_activeEnemies are not null if (m_spawnData == null) { Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in OnEnemyDead.", this); return; } if (m_activeEnemies == null) { Debug.LogError($"EnemySpawner ({name}): m_activeEnemies is null in OnEnemyDead.", this); return; } vControlAI deadAI = deadEnemyObject.GetComponent(); if (deadAI != null && m_activeEnemies.Contains(deadAI)) { deadAI.onDead.RemoveListener(OnEnemyDead); deadAI.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); m_activeEnemies.Remove(deadAI); m_OnDead?.Invoke(this); } if (m_activeEnemies.Count == 0 && m_spawnData.waveActive) { m_spawnData.waveActive = false; Debug.Log($"EnemySpawner ({name}): All enemies in wave died. Wave inactive. Waiting for player respawn or manual reset.", this); } } private bool IsPlayerInRange() { if (m_distanceFrom == null) { if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; else if (Camera.main != null) m_distanceFrom = Camera.main.transform; else { return false; } } return (transform.position - m_distanceFrom.position).sqrMagnitude < m_distance * m_distance; } public void CheckSpawn() // Called by InvokeRepeating { if (!enabled || !gameObject.activeInHierarchy || m_prefab == null) return; // Critical Safety check: If m_spawnData is somehow still null here if (m_spawnData == null) { Debug.LogError($"EnemySpawner ({name}): m_spawnData is unexpectedly null in CheckSpawn! This indicates a severe initialization problem. Cancelling spawn checks for this spawner to prevent further errors.", this); CancelInvoke("CheckSpawn"); // Stop trying to check enabled = false; // Disable the component to be safe return; } // Ensure m_activeEnemies is not null (should be guaranteed by declaration/Awake/Start) if (m_activeEnemies == null) { Debug.LogError($"EnemySpawner ({name}): m_activeEnemies is unexpectedly null in CheckSpawn! Reinitializing. This is unusual.", this); m_activeEnemies = new List(); } if (_initialWaveCheckPassedThisActivation) { return; } // This is the line (or one like it) that likely caused the original NRE if m_spawnData was null if (!m_spawnData.waveActive && CanSpawnMoreWaves() && IsPlayerInRange()) { SpawnWave(); // SpawnWave itself has null checks for m_spawnData now // Check m_spawnData again as SpawnWave might have failed to initialize it if an error occurred there (though unlikely with current SpawnWave structure) if (m_spawnData != null && m_spawnData.waveActive) { _initialWaveCheckPassedThisActivation = true; } } } public void ResetSpawner() { CancelInvoke("CheckSpawn"); // Destroy active enemies if (m_activeEnemies != null) { for (int i = m_activeEnemies.Count - 1; i >= 0; i--) { if (m_activeEnemies[i] != null) { m_activeEnemies[i].onDead.RemoveListener(OnEnemyDead); m_activeEnemies[i].onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); Destroy(m_activeEnemies[i].gameObject); } } m_activeEnemies.Clear(); } else { m_activeEnemies = new List(); // Ensure it's not null for future use } // Reset spawn data if (m_spawnData != null) { m_spawnData.wavesSpawnedCount = 0; m_spawnData.waveActive = false; } else { m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null during ResetSpawner. Initialized to default.", this); } _initialWaveCheckPassedThisActivation = false; if (enabled && gameObject.activeInHierarchy) { if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; else if (Camera.main != null) m_distanceFrom = Camera.main.transform; InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval); Debug.Log($"Spawner ({name}) has been reset. CheckSpawn restarted. Will attempt to spawn when conditions met.", this); } else { Debug.Log($"Spawner ({name}) has been reset but is currently disabled or inactive. CheckSpawn not restarted.", this); } } } }