diff --git a/Assets/Scenes/Fight_Arena/Fight_Arena.unity b/Assets/Scenes/Fight_Arena/Fight_Arena.unity index 72f0b5ce9..192eb6e6a 100644 --- a/Assets/Scenes/Fight_Arena/Fight_Arena.unity +++ b/Assets/Scenes/Fight_Arena/Fight_Arena.unity @@ -3177,6 +3177,18 @@ Transform: type: 3} m_PrefabInstance: {fileID: 789096691} m_PrefabAsset: {fileID: 0} +--- !u!114 &579328271 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, + type: 3} + m_PrefabInstance: {fileID: 789096691} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!21 &581839214 Material: serializedVersion: 8 @@ -11216,7 +11228,7 @@ PrefabInstance: - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} propertyPath: m_distance - value: 20 + value: 200 objectReference: {fileID: 0} - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} @@ -12619,7 +12631,7 @@ PrefabInstance: - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} propertyPath: m_distance - value: 20 + value: 200 objectReference: {fileID: 0} - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} @@ -12640,6 +12652,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 796152178} + - component: {fileID: 796152179} m_Layer: 0 m_Name: 1= ENEMIES Spawners m_TagString: Untagged @@ -12667,6 +12680,40 @@ Transform: - {fileID: 579328270} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &796152179 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 796152177} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6ce0b24e87d404b299a748ccc1671160, type: 3} + m_Name: + m_EditorClassIdentifier: + spawnerConfigurations: + - spawner: {fileID: 1804595575} + overridePrefab: {fileID: 145182807891766407, guid: e115436bfe06bd447a266ca75621bbdd, + type: 3} + overrideEnemiesPerWave: 3 + - spawner: {fileID: 1238364584} + overridePrefab: {fileID: 8406365487746723040, guid: fcfa2b03d43105e43a8c6b1931305099, + type: 3} + overrideEnemiesPerWave: 5 + - spawner: {fileID: 1491841298} + overridePrefab: {fileID: 3165479696603231053, guid: 5e1c7bd492ab28047bf1e68ef6ae8b9f, + type: 3} + overrideEnemiesPerWave: 4 + - spawner: {fileID: 579328271} + overridePrefab: {fileID: 974380812503253259, guid: fe3f8cbdaa5d03147a0de1c74712d32b, + type: 3} + overrideEnemiesPerWave: 4 + globalOverridePrefab: {fileID: 0} + globalOverrideEnemiesPerWave: -1 + overrideOnStart: 1 + autoEnableSpawners: 1 + disableSpawnersOnStart: 1 --- !u!1 &799294618 GameObject: m_ObjectHideFlags: 0 @@ -13389,7 +13436,7 @@ Material: m_Offset: {x: 0, y: 0} m_Ints: [] m_Floats: - - _AlphaPow: -1.89 + - _AlphaPow: 2 - _BlendMode: 1 - _BumpScale: 1 - _CullMode: 2 @@ -15472,6 +15519,11 @@ PrefabInstance: propertyPath: m_Layer value: 8 objectReference: {fileID: 0} + - target: {fileID: 4460069056851369441, guid: 851e8e61247888340bdec90fc8aa37f5, + type: 3} + propertyPath: m_IsActive + value: 1 + objectReference: {fileID: 0} - target: {fileID: 4460112240072912095, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} propertyPath: m_Layer @@ -16660,12 +16712,12 @@ PrefabInstance: - target: {fileID: 5657452459766331955, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} propertyPath: m_AnchorMax.x - value: 0 + value: 1 objectReference: {fileID: 0} - target: {fileID: 5657452459766331955, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} propertyPath: m_AnchorMax.y - value: 0 + value: 1 objectReference: {fileID: 0} - target: {fileID: 5671630659928648966, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} @@ -16707,6 +16759,11 @@ PrefabInstance: propertyPath: selectedToolbar value: 0 objectReference: {fileID: 0} + - target: {fileID: 5935883485110830931, guid: 851e8e61247888340bdec90fc8aa37f5, + type: 3} + propertyPath: m_IsActive + value: 1 + objectReference: {fileID: 0} - target: {fileID: 6116517633565365760, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} propertyPath: m_Layer @@ -16792,6 +16849,11 @@ PrefabInstance: propertyPath: m_Layer value: 8 objectReference: {fileID: 0} + - target: {fileID: 6783963330927412624, guid: 851e8e61247888340bdec90fc8aa37f5, + type: 3} + propertyPath: m_IsActive + value: 1 + objectReference: {fileID: 0} - target: {fileID: 6786312951323477463, guid: 851e8e61247888340bdec90fc8aa37f5, type: 3} propertyPath: m_IsActive @@ -17704,6 +17766,18 @@ Material: - _SpecColor: {r: 1, g: 1, b: 1, a: 1} m_BuildTextureStacks: [] m_AllowLocking: 1 +--- !u!114 &1238364584 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, + type: 3} + m_PrefabInstance: {fileID: 636119092} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!21 &1242034730 Material: serializedVersion: 8 @@ -18394,7 +18468,7 @@ PrefabInstance: - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} propertyPath: m_distance - value: 20 + value: 200 objectReference: {fileID: 0} - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} @@ -19395,6 +19469,18 @@ Material: - _SpecColor: {r: 1, g: 1, b: 1, a: 1} m_BuildTextureStacks: [] m_AllowLocking: 1 +--- !u!114 &1491841298 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, + type: 3} + m_PrefabInstance: {fileID: 1316222306} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!4 &1516777619 stripped Transform: m_CorrespondingSourceObject: {fileID: 6425420852750441961, guid: 851e8e61247888340bdec90fc8aa37f5, @@ -20885,7 +20971,7 @@ PrefabInstance: - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} propertyPath: m_distance - value: 20 + value: 200 objectReference: {fileID: 0} - target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, type: 3} @@ -20903,6 +20989,18 @@ Transform: type: 3} m_PrefabInstance: {fileID: 1804595573} m_PrefabAsset: {fileID: 0} +--- !u!114 &1804595575 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446, + type: 3} + m_PrefabInstance: {fileID: 1804595573} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1001 &1819555269 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Characters/EnemySpawner.cs b/Assets/Scripts/Characters/EnemySpawner.cs index 4ff676223..6752e5e55 100644 --- a/Assets/Scripts/Characters/EnemySpawner.cs +++ b/Assets/Scripts/Characters/EnemySpawner.cs @@ -1,171 +1,411 @@ using System.Collections; using System.Collections.Generic; -using Invector; +using Invector; // Assuming this is for vDamage, if not used, can be removed using Invector.vCharacterController.AI; -using PixelCrushers; +using PixelCrushers; // For Saver and SaveSystem using UnityEngine; using UnityEngine.Events; +using UnityEngine.AI; // Required for NavMesh namespace Beyond { public class EnemySpawner : Saver { - public GameObject m_prefab; - public Transform m_distanceFrom; - //public bool m_distanceFromMainCam; - public float m_spawnCheckInterval = .5f; - public float m_distance = 20f; - public int m_numToSpawn = -1; - public bool m_respawnOnPlayersDeath = true; - public LayerMask raycastMask = 1; //default - private SaveData m_spawnData; - //private bool m_spawned; - private vControlAI m_spawnedAI; - public UnityEvent m_OnDead; - public UnityEvent m_onReceivedDamage; - class SaveData + [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(); + private bool _initialWaveCheckPassedThisActivation = false; // Flag to control CheckSpawn behavior per activation cycle + + 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; + return; +#endif + base.Awake(); // Important: Call Saver's Awake + m_spawnData = new SaveData + { + wavesSpawnedCount = 0, + waveActive = false + }; + m_activeEnemies = new List(); + _initialWaveCheckPassedThisActivation = false; // Initialize flag + } + + public override void Start() + { + 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; + return; + } + + // 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 + InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval); + } + + 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 + 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; + + // 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; + } + + // Reset the flag to allow CheckSpawn (or direct spawn here) to work for this new "life" + _initialWaveCheckPassedThisActivation = false; + + // Attempt to spawn a new wave if conditions are met + if (!m_spawnData.waveActive && CanSpawnMoreWaves()) + { + if (IsPlayerInRange()) + { + SpawnWave(); + if (m_spawnData.waveActive) // If wave was successfully spawned by this call + { + _initialWaveCheckPassedThisActivation = true; // Mark as done for this cycle + } + } + // else Debug.Log($"Player respawned, but spawner {name} is out of range. No wave spawned immediately by OnPlayerRespawned."); + } + // else if (m_spawnData.waveActive) Debug.Log($"Player respawned, but spawner {name} still has an active wave."); + // else if (!CanSpawnMoreWaves()) Debug.Log($"Player respawned, but spawner {name} has reached its wave limit."); + } + + public override string RecordData() + { + return SaveSystem.Serialize(m_spawnData); + } + + public override void ApplyData(string s) + { + var data = SaveSystem.Deserialize(s); + if (data != null) + { + m_spawnData = data; + // If loaded save indicates a wave was active but no enemies are present (they weren't saved/restored), + // mark wave as inactive. + if (m_spawnData.waveActive && m_activeEnemies.Count == 0) // m_activeEnemies is runtime only + { + m_spawnData.waveActive = false; + } + + // Determine the state of _initialWaveCheckPassedThisActivation based on loaded data + // If a wave was active, or any waves had been spawned, assume the "initial check" for the + // current game session (before this load) effectively passed. + // A Reset or PlayerRespawn will clear this flag if needed for a new cycle. + if (m_spawnData.waveActive || m_spawnData.wavesSpawnedCount > 0) { + _initialWaveCheckPassedThisActivation = true; + } else { + _initialWaveCheckPassedThisActivation = false; + } + } + } + + public bool IsAnyEnemyAlive() + { + 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() { - public int numSpawned; - public bool spawned; + if (m_spawnData != null) + { + return m_spawnData.wavesSpawnedCount; + } + return 0; // Should ideally not happen if Awake ran } - public override void Awake() - { - #if ENEMIES_DISABLED && UNITY_EDITOR - enabled = false; - return; - #endif - base.Awake(); - //m_spawned = false; - m_spawnData = new SaveData(); - m_spawnData.numSpawned = 0; - m_spawnData.spawned = false; - } + public void SpawnWave() + { + if (m_prefab == null) + { + Debug.LogError($"EnemySpawner ({name}): m_prefab is not set!", this); + return; + } - void OnDestroy() - { - if (m_respawnOnPlayersDeath && Player.Instance != null) - { - var respawner = Player.Instance.GetComponent(); - respawner.m_onRespawned.RemoveListener(OnPlayerRespawned); - } - - if (m_spawnedAI) - { - m_spawnedAI.onDead.RemoveListener(OnDead); - } - } + if (!CanSpawnMoreWaves()) + { + // This check is also in CheckSpawn/OnPlayerRespawned, but good for direct calls too + // Debug.Log($"EnemySpawner ({name}): Max wave respawn limit reached or cannot spawn more waves.", this); + return; + } - public override void Start() - { - - base.Start(); - if (Player.Instance != null) - m_distanceFrom = Player.Instance.transform; - else - { - m_distanceFrom = Camera.main.transform; - } + if (m_spawnData.waveActive) + { + // Debug.Log($"EnemySpawner ({name}): Wave already active, not spawning another.", this); + return; + } - if (m_respawnOnPlayersDeath) - { - var respawner = Player.Instance.GetComponent(); - respawner.m_onRespawned.AddListener(OnPlayerRespawned); - } - InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval); - } + int enemiesSuccessfullySpawned = 0; + for (int i = 0; i < m_enemiesPerSpawnWave; i++) + { + Vector3 spawnPosition = Vector3.zero; + bool foundValidPosition = false; - void CheckRespawnCondition() - { - if ((m_numToSpawn < 0 || m_numToSpawn > m_spawnData.numSpawned) && m_spawnedAI == null ) - m_spawnData.spawned = false; - - } - private void OnPlayerRespawned() - { - CheckRespawnCondition(); - } + 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; - public override string RecordData() - { - return SaveSystem.Serialize(m_spawnData); - } + 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; + } + } + } - public override void ApplyData(string s) - { - var data = SaveSystem.Deserialize(s); - if (data != null) - { - m_spawnData = data; - //m_spawned = true; - CheckRespawnCondition(); - } - } + 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); + } + } - public bool SpawnedAndLive() - { - return m_spawnedAI != null; - } + 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 + { + // No enemies were spawned in this attempt, so the wave isn't truly "active". + // And we shouldn't count it towards wavesSpawnedCount if nothing came out. + m_spawnData.waveActive = false; + Debug.LogWarning($"EnemySpawner ({name}): Attempted to spawn a wave, but no enemies were successfully placed.", this); + } + } - public bool SpawnedAndDead() - { - return m_spawnData.spawned && m_spawnedAI == null; - } + private void OnEnemyReceiveDamage(vDamage damageData) // Assuming vDamage from Invector + { + m_onReceivedDamage?.Invoke(this); + } - public void Spawn() - { - if (m_prefab == null) - return; - - Vector3 pos; - const float RAY_OFFSET = 30f; - const float Y_OFFSET = .1f; - pos = transform.position; - pos.y += RAY_OFFSET; - RaycastHit hit = new RaycastHit(); - if (Physics.Raycast(pos, Vector3.down, out hit, 100f, raycastMask)) - { - pos.y -= RAY_OFFSET; - if (pos.y < hit.point.y + Y_OFFSET) - { - pos.y = hit.point.y + Y_OFFSET; - } - } - else - { - Debug.LogError("EnemySpawner error: raycast failed"); - return; - } + private void OnEnemyDead(GameObject deadEnemyObject) + { + 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); + } - //m_spawned = true; - m_spawnData.spawned = true; - m_spawnData.numSpawned++; - var enemy = Instantiate(m_prefab, pos, transform.rotation, transform); - m_spawnedAI = enemy.GetComponent(); - if (m_spawnedAI) - { - m_spawnedAI.onDead.AddListener(OnDead); - m_spawnedAI.onReceiveDamage.AddListener(OnReceiveDamage); - } + if (m_activeEnemies.Count == 0 && m_spawnData.waveActive) + { + m_spawnData.waveActive = false; // Mark wave as inactive + // DO NOT call CheckSpawn() here to prevent immediate respawn. + // DO NOT reset _initialWaveCheckPassedThisActivation here. That's for new cycles (player respawn/reset). + 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) // Should be set in Start, but as a fallback + { + if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; + else if (Camera.main != null) m_distanceFrom = Camera.main.transform; + else + { + // Debug.LogWarning($"EnemySpawner ({name}): Cannot check range, m_distanceFrom is null.", this); + 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; - private void OnReceiveDamage(vDamage arg0) - { - m_onReceivedDamage?.Invoke(this); - } + // If the "initial" spawn for this activation cycle has already happened (or attempted), + // CheckSpawn should not try again until the cycle is reset (by player death or ResetSpawner). + if (_initialWaveCheckPassedThisActivation) + { + // If a wave is active, great, CheckSpawn's job for this cycle is done. + // If no wave is active BUT wavesSpawnedCount > 0, it means the wave spawned by this cycle died. + // In this case, CheckSpawn also shouldn't immediately respawn. + // It waits for OnPlayerRespawned or ResetSpawner to clear the flag. + return; + } - private void OnDead(GameObject arg0) - { - m_spawnedAI = null; - m_OnDead?.Invoke(this); - } + // Try to spawn if: no wave active, can spawn more, player in range, AND initial check hasn't passed. + if (!m_spawnData.waveActive && CanSpawnMoreWaves() && IsPlayerInRange()) + { + SpawnWave(); + if (m_spawnData.waveActive) // If wave was successfully spawned + { + _initialWaveCheckPassedThisActivation = true; // Mark as done for this cycle + } + // If SpawnWave failed to actually spawn anyone (e.g. no valid spots), + // waveActive will be false, and _initialWaveCheckPassedThisActivation will remain false, + // allowing CheckSpawn to try again on the next interval. + } + } - public void CheckSpawn(){ - if (!m_spawnData.spawned && gameObject.activeInHierarchy && (transform.position - m_distanceFrom.position).sqrMagnitude < m_distance * m_distance){ - Spawn(); - } - } - + public void ResetSpawner() + { + CancelInvoke("CheckSpawn"); // Stop existing repeating call + + // Destroy active enemies + 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(); + + // Reset spawn data + if (m_spawnData != null) + { + m_spawnData.wavesSpawnedCount = 0; + m_spawnData.waveActive = false; + } + else // Should not happen if Awake ran + { + m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false }; + } + + _initialWaveCheckPassedThisActivation = false; // Reset the flag for a new cycle + + // Restart InvokeRepeating if spawner is active + if (enabled && gameObject.activeInHierarchy) + { + // Re-initialize m_distanceFrom + 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); + } + } } } \ No newline at end of file diff --git a/Assets/Scripts/Characters/EnemySpawnerManager.cs b/Assets/Scripts/Characters/EnemySpawnerManager.cs new file mode 100644 index 000000000..c9f509f3d --- /dev/null +++ b/Assets/Scripts/Characters/EnemySpawnerManager.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; + +namespace Beyond +{ + public class EnemySpawnerManager : MonoBehaviour + { + [Header("Spawner Configurations")] + [Tooltip("List of spawners to manage and their override settings.")] + public List spawnerConfigurations = new List(); + + [Header("Global Settings (Optional)")] + [Tooltip("If assigned, this prefab will be used for ALL spawners in the list that don't have their own overridePrefab set.")] + public GameObject globalOverridePrefab; + + [Tooltip("If > 0, this will be used for ALL spawners that don't have overrideEnemiesPerWave set. Set to 0 or less to ignore.")] + public int globalOverrideEnemiesPerWave = -1; + public bool overrideOnStart = true; + + public bool autoEnableSpawners = true; + public bool disableSpawnersOnStart = true; + + void Awake() + { + if (disableSpawnersOnStart) + { + // Disable all spawners initially if needed + foreach (var config in spawnerConfigurations) + { + if (config.spawner != null) + { + config.spawner.enabled = false; + } + } + } + ApplySpawnerConfigurations(); + } + + // You could also call this from Start() if you want to ensure all other Awakes have run, + // though for setting properties on other components, Awake() is generally fine. + // void Start() + // { + // ApplySpawnerConfigurations(); + // } + [Button] + public void ApplySpawnerConfigurations() + { + if (spawnerConfigurations == null || spawnerConfigurations.Count == 0) + { + Debug.LogWarning("EnemySpawnerManager: No spawner configurations assigned.", this); + return; + } + + foreach (SpawnerOverrideConfig config in spawnerConfigurations) + { + if (config == null || config.spawner == null) + { + Debug.LogWarning("EnemySpawnerManager: A spawner configuration or its target spawner is null. Skipping.", this); + continue; + } + + EnemySpawner targetSpawner = config.spawner; + + // Apply specific override prefab if set + if (config.overridePrefab != null) + { + targetSpawner.m_prefab = config.overridePrefab; + // Debug.Log($"Manager overriding prefab for {targetSpawner.name} to {config.overridePrefab.name}", this); + } + // Else, if a global override prefab is set, apply that + else if (globalOverridePrefab != null) + { + targetSpawner.m_prefab = globalOverridePrefab; + // Debug.Log($"Manager applying global override prefab to {targetSpawner.name} ({globalOverridePrefab.name})", this); + } + // If neither specific nor global override is set, the spawner uses its own m_prefab. + + // Apply specific override for enemies per wave if set (and > 0) + if (config.overrideEnemiesPerWave > -1) + { + targetSpawner.m_enemiesPerSpawnWave = config.overrideEnemiesPerWave; + // Debug.Log($"Manager overriding enemies per wave for {targetSpawner.name} to {config.overrideEnemiesPerWave}", this); + } + // Else, if a global override for enemies per wave is set (and > 0), apply that + else if (globalOverrideEnemiesPerWave > -1) + { + targetSpawner.m_enemiesPerSpawnWave = globalOverrideEnemiesPerWave; + // Debug.Log($"Manager applying global override enemies per wave to {targetSpawner.name} ({globalOverrideEnemiesPerWave})", this); + } + if (autoEnableSpawners) + { + targetSpawner.enabled = true; // Enable the spawner if it was disabled + } + // If neither specific nor global override is set, the spawner uses its own m_enemiesPerSpawnWave. + + + // --- IMPORTANT --- + // Ensure the spawner hasn't already started its InvokeRepeating for CheckSpawn + // if its settings are being changed. + // One way is to disable the spawner initially and let the manager enable it. + // Or, if spawners might already be active, you might need to CancelInvoke and Restart it. + // For simplicity, let's assume spawners are either configured to not auto-start + // or this manager runs early enough (e.g., via Script Execution Order). + + // If you want to ensure spawners don't start before manager configures them: + // Option A: Set spawners to be disabled by default in the editor, then enable them here. + // targetSpawner.enabled = true; // If they were disabled by default + + // Option B: Make sure EnemySpawnerManager's Awake runs before EnemySpawner's Start. + // You can do this via Edit > Project Settings > Script Execution Order. + // Add EnemySpawnerManager and set it to an earlier value (e.g., -100) + // than the default time or EnemySpawner. + } + + Debug.Log("EnemySpawnerManager: Applied configurations to spawners.", this); + } + + // Optional: Helper to add spawners programmatically if needed + public void AddSpawnerToManage(EnemySpawner spawner, GameObject prefabOverride = null, int enemiesPerWaveOverride = 0) + { + if (spawner == null) return; + + SpawnerOverrideConfig newConfig = new SpawnerOverrideConfig + { + spawner = spawner, + overridePrefab = prefabOverride, + overrideEnemiesPerWave = enemiesPerWaveOverride + }; + spawnerConfigurations.Add(newConfig); + } + + /// + /// Resets all managed spawners to their initial state. + /// This will destroy their current enemies and restart their spawning cycles. + /// + [ContextMenu("Reset All Managed Spawners")] // Adds a right-click option in Inspector for easy testing + [Button] + public void ResetAllManagedSpawners() + { + if (spawnerConfigurations == null || spawnerConfigurations.Count == 0) + { + Debug.LogWarning("EnemySpawnerManager: No spawners configured to reset.", this); + return; + } + + Debug.Log("EnemySpawnerManager: Initiating reset for all managed spawners...", this); + int resetCount = 0; + foreach (SpawnerOverrideConfig config in spawnerConfigurations) + { + if (config != null && config.spawner != null) + { + config.spawner.ResetSpawner(); // Call the spawner's own reset method + resetCount++; + } + } + Debug.Log($"EnemySpawnerManager: Reset command sent to {resetCount} spawners.", this); + } + + } +} \ No newline at end of file diff --git a/Assets/Scripts/Characters/EnemySpawnerManager.cs.meta b/Assets/Scripts/Characters/EnemySpawnerManager.cs.meta new file mode 100644 index 000000000..a3c8602fb --- /dev/null +++ b/Assets/Scripts/Characters/EnemySpawnerManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6ce0b24e87d404b299a748ccc1671160 \ No newline at end of file diff --git a/Assets/Scripts/Characters/KillTrigger.cs b/Assets/Scripts/Characters/KillTrigger.cs index 849c33f2e..fbfc742a6 100644 --- a/Assets/Scripts/Characters/KillTrigger.cs +++ b/Assets/Scripts/Characters/KillTrigger.cs @@ -10,7 +10,7 @@ namespace Beyond public class KillTrigger : Saver { [SerializeField] EnemySpawner[] m_spawners; - + public UnityEvent OnTrigger; [Serializable] @@ -22,9 +22,13 @@ namespace Beyond private SaveData m_data = new SaveData(); void Start() { + if (m_spawners == null) return; // Guard clause foreach (var s in m_spawners) { - s.m_OnDead.AddListener(OnEnemyKilled); + if (s != null) // Good practice to check for null spawners in array + { + s.m_OnDead.AddListener(OnEnemyKilled); + } } } @@ -32,24 +36,44 @@ namespace Beyond { if (m_data.wasTriggered) return; + + if (m_spawners == null) return; // Guard clause + foreach (var spawner in m_spawners) { - if (!spawner.SpawnedAndDead()) - return; + if (spawner == null) continue; // Skip if a spawner in the array is null + + bool hasSpawnedAWave = spawner.GetWavesSpawnedCount() > 0; // We'll need to add this getter to EnemySpawner + bool noEnemiesCurrentlyAlive = !spawner.IsAnyEnemyAlive(); + + if (hasSpawnedAWave && noEnemiesCurrentlyAlive) + { + // This spawner has done its part for *this current engagement* + } + else + { + return; // If ANY spawner doesn't meet this, the trigger condition isn't met + } } OnTrigger?.Invoke(this); m_data.wasTriggered = true; } private void OnEnemyKilled(EnemySpawner arg0) { + // arg0 is the spawner from which an enemy died. + // We need to check all spawners associated with this trigger. CheckSpawners(); } private void OnDestroy() { + if (m_spawners == null) return; // Guard clause foreach (var s in m_spawners) { - s.m_OnDead.RemoveListener(OnEnemyKilled); + if (s != null) // Good practice + { + s.m_OnDead.RemoveListener(OnEnemyKilled); + } } } @@ -63,6 +87,8 @@ namespace Beyond var data = SaveSystem.Deserialize(s); if (data != null) m_data = data; + // Optional: if not triggered, re-check on load in case state was met while inactive + // if (!m_data.wasTriggered) { CheckSpawners(); } } } -} +} \ No newline at end of file diff --git a/Assets/Scripts/Characters/SpawnerOverrideConfig.cs b/Assets/Scripts/Characters/SpawnerOverrideConfig.cs new file mode 100644 index 000000000..370fe7251 --- /dev/null +++ b/Assets/Scripts/Characters/SpawnerOverrideConfig.cs @@ -0,0 +1,21 @@ +// This can be in its own file (e.g., SpawnerConfig.cs) or inside the EnemySpawnerManager.cs file if you prefer. +// For better organization, a separate file is often good. + +using UnityEngine; + +namespace Beyond +{ + [System.Serializable] // Makes it show up in the Inspector when used in a list + public class SpawnerOverrideConfig + { + public EnemySpawner spawner; // Reference to the EnemySpawner instance in the scene + + [Tooltip("If assigned, this prefab will override the spawner's own m_prefab.")] + public GameObject overridePrefab; + + [Tooltip("If > -1, this will override the spawner's m_enemiesPerSpawnWave. Set to 0 or less to use spawner's default.")] + public int overrideEnemiesPerWave = -1; + + // You could add more overrides here if needed, e.g., overrideSpawnRadius, overrideMaxWaveRespawns, etc. + } +} \ No newline at end of file diff --git a/Assets/Scripts/Characters/SpawnerOverrideConfig.cs.meta b/Assets/Scripts/Characters/SpawnerOverrideConfig.cs.meta new file mode 100644 index 000000000..0f6f304ae --- /dev/null +++ b/Assets/Scripts/Characters/SpawnerOverrideConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 40b3ccfac6d0f46f0bb77671c0057209 \ No newline at end of file